Skip to content

Templates

Templates are tagged-template literals — the html`` tag. No JSX, no compiler; they are just JavaScript.

Bindings

Interpolate signals and values directly. Text bindings are escaped by default. Bind attributes with name="${value}" and events with @event=${handler}.

save-button.ts
import { component, signal, html } from '@nisli/core';

component('save-button', () => {
  const busy = signal(false);
  const label = signal('Save');

  return html`
    <button
      class="btn"
      disabled=${busy}
      @click=${() => (label.value = 'Saved')}
    >${label}</button>
  `;
});

Lists

each() renders a keyed list that reconciles efficiently. It takes the items signal, a key function, and a template that receives each item as a signal. Inside that template, wrap each field read in a computed()${computed(() => item.value.name)} — so a change to one item updates only that leaf binding. Reading item.value.name bare would subscribe the list's reconciler to the per-item signal, re-reconciling the whole list on every item change.

import { signal, computed, html, each } from '@nisli/core';

const items = signal([
  { id: 1, name: 'Signals' },
  { id: 2, name: 'Templates' },
]);

export const list = html`<ul>
  ${each(
    items,
    (item) => item.id,
    // Bind a LEAF computed, not item.value.name directly: a bare read would
    // subscribe the list's reconciler effect to the per-item signal, so the
    // whole list re-reconciles when one item changes.
    (item) => html`<li>${computed(() => item.value.name)}</li>`,
  )}
</ul>`;

Conditionals

when() renders a template while a condition is truthy (pass a signal to stay reactive).

import { signal, html, when } from '@nisli/core';

const open = signal(true);

export const panel = html`${when(open, () => html`<p>Now you see me.</p>`)}`;

Compose other components by calling their factory inside a template — ${Button({ children: 'Save' })}. Custom elements are always used via their factory, never as raw tags.