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}.
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.