Skip to content

Latest commit

 

History

History
205 lines (149 loc) · 5.34 KB

html.md

File metadata and controls

205 lines (149 loc) · 5.34 KB

html Template Engine

A lightweight HTML template engine with update capabilities. Creates templates that can be re-rendered with new data without recreating DOM nodes.

Usage

import { html } from '@relax.js/core/html';

// Create a template
const userCard = html`
    <div class="user-card">
        <h2>{{name}}</h2>
        <p>{{email}}</p>
    </div>
`;

// Render with data
const result = userCard({ name: 'John', email: 'john@example.com' });
container.appendChild(result.fragment);

// Update with new data (no DOM recreation)
result.update({ name: 'Jane', email: 'jane@example.com' });

Features

Mustache Bindings

Use {{property}} syntax to bind context properties to text content or attributes:

const template = html`
    <a href="{{url}}" class="{{linkClass}}">{{linkText}}</a>
`;

Template Literal Substitutions

Use ${} for static values or functions:

const staticClass = 'container';
const template = html`<div class="${staticClass}">{{content}}</div>`;

Event Handlers

Bind event handlers with context:

const row = html`
    <tr>
        <td>{{name}}</td>
        <td>
            <button onclick=${function() { this.onEdit(this.id); }}>Edit</button>
        </td>
    </tr>
`;

const result = row({
    id: 42,
    name: 'Item',
    onEdit(id) { console.log('Edit:', id); }
});

Context Functions

Call methods from the context object:

const template = html`<div>{{formatPrice}}</div>`;

const result = template({
    price: 99.99,
    formatPrice() {
        return `$${this.price.toFixed(2)}`;
    }
});

Function Arguments

Pass arguments to context functions using the pipe syntax:

const template = html`<div>{{greet|name}}</div>`;

const result = template({
    name: 'John',
    greet(name) { return `Hello, ${name}!`; }
});
// Result: <div>Hello, John!</div>

The value after | is resolved from the context and passed to the function. Multiple arguments use comma separation:

const template = html`<div>{{format|name,title}}</div>`;

const result = template({
    name: 'John',
    title: 'Developer',
    format(name, title) { return `${name} - ${title}`; }
});

For value transformations using pipes (like uppercase, currency), use compileTemplate instead.

Checking before running

npx @relax.js/core check verifies every {{name}} and {{fn|args}} against the object the template is bound with, and reports a misspelled name with file and line. No annotation needed; the type comes from the bind call. See Checking templates.

API

html (tagged template literal)

function html(
    templateStrings: TemplateStringsArray,
    ...substitutions: any[]
): (context: any) => RenderTemplate

Creates a template function that accepts a context object and returns a RenderTemplate.

RenderTemplate

interface RenderTemplate {
    fragment: DocumentFragment;
    update(context: any): void;
}
  • fragment: The rendered DOM fragment. Add to the DOM once.
  • update(context): Re-renders with new data without recreating DOM nodes.

Design

This template engine is designed for single-use updateable templates:

  1. Create the template once
  2. Render and add fragment to the DOM
  3. Call update() to push changes to the existing nodes

Step 2 happens once. fragment is a DocumentFragment, so adding it to the DOM moves its nodes out and leaves it empty. The bindings keep pointing at those nodes, which is what makes update() work.

Calling the template function again does not give you a second instance. It returns the same, now empty, fragment and rebinds the nodes already on screen, so the first instance changes to the new data and nothing is added. Nothing reports this:

const card = html`<p>{{name}}</p>`;
host.appendChild(card({ name: 'Alice' }).fragment);
host.appendChild(card({ name: 'Bob' }).fragment); // adds nothing, Alice now reads Bob

Evaluate the tagged literal again for a second instance:

const card = (name: string) => html`<p>{{name}}</p>`({ name });
host.appendChild(card('Alice').fragment);
host.appendChild(card('Bob').fragment);

compileTemplate returns an element rather than a fragment, so its content survives being added to the DOM. It renders a collection through loop= rather than through multiple instances.

Property vs Attribute Binding

The engine automatically detects when to set element properties vs attributes:

// Sets input.value property (not attribute)
const input = html`<input value="{{val}}">`;

// Sets data-id attribute
const div = html`<div data-id="{{id}}"></div>`;

Custom Elements

Custom elements are automatically upgraded when encountered in templates.

Limitations

The html template is optimized for simple, updateable templates. For advanced features, use compileTemplate:

Feature html compileTemplate
Mustache bindings Yes Yes
Event handlers Yes (onclick=${fn}) Yes (r-click="handler(args)")
In-place updates Yes Yes
Nested paths (user.name) No Yes
Loops (loop="item in items") No Yes
Conditionals (if, unless) No Yes
Pipe transformations No Yes
Function calls with args Yes (via |) Yes