Components

A component is a plain function, (props, tools) => html`...`, and there are exactly two ways to use one:

// 1. By value: the tag IS the function; no registration, tracked by
//    imports and refactors
html`<${Card} title="Hello" total="${count}" />`

// 2. By name: register once, use anywhere (case-sensitive)
import { setComponents } from 'lemonadejs';
setComponents({ Card, Modal });
html`<Card title="Hello">...</Card>`

No classes, no this, no self, no hook ordering rules. A component used twice is just called twice; there is no identity to manage.

Design context

A few mechanical choices matter for composition:

  • Registration. Components must be findable at render time, by function reference or by registered name. A misspelled or unregistered name should fail loudly, not render empty and silent.
  • Prop types. HTML attributes are strings. Passing an object or a live state across a component boundary requires an escape mechanism; forgetting it produces "[object Object]" at the receiver, silently.
  • Children scope. Content placed between component tags can bind to the parent’s state or the child’s, and the answer must be unambiguous and consistent.

v6 keeps one small rule set for these and makes violations loud.

How LemonadeJS solves it

Two forms, identical semantics. By value (<${Card} />) needs no registration and survives renames. By name (<Card />) reads better in large templates: register with setComponents({ Card }). Names must start with a capital letter and match exactly. An unknown or misspelled name fails at mount with LJS-104 and the registration hint, never an empty render.

The prop rules (all of them):

  • Literal attribute text → string. No guessing, no auto-casting; declared coercion exists, but only where it is declared: in a contract.
  • $passed by reference, untouched. States stay live across the boundary, callbacks stay callable, objects and arrays are never stringified.
  • A bare flagtrue.
  • Props are read-only (frozen in dev). Data flows up through callback props, never by mutating the child. Callback names are lowercase (onsave, onitemclick; see Events).

Snapshot vs live is the caller’s choice, with the same syntax as everywhere else. Pass the state to keep it live; pass its value to snapshot it:

html`<${Card} total="${count}" />`         // live: Card re-renders with count
html`<${Card} total="${count.value}" />`   // snapshot: today's number, frozen

One precision for components inside a re-running expression, a list or a branch: each run re-delivers the plain values, and the engine patches the living instance (new values flow into its prop states; setup never re-runs) instead of rebuilding it. Frozen-at-build applies to the template’s one-time construction; entries in lists stay current. The mechanics, and what still counts as a structural rebuild, are in Lists.

Children are parent-scoped. The nodes between the tags arrive as props.children, already materialized in the parent’s scope, so their $ bind to the parent’s states:

const Card = (props) => html`<div class="card">
    <h3>${props.title}</h3>
    ${props.children}
</div>`;

html`<${Card} title="Inbox">
    <p>${count} unread</p>     <!-- count is the PARENT's state -->
</${Card}>`

Imperative surfaces use the ref convention. The component calls props.ref?.({ open, close }) and the parent holds the api. With a contract, the api becomes a declared, verified surface that also flows through React refs automatically.

Named or scoped slot patterns have no direct v6 equivalent. The answer is a render prop: pass a function returning a view. This is a convention, not a syntax.

In practice

A context menu is a stack of Modals. The Studio contextmenu does not implement positioning, auto-adjusting or layering: every open menu level is a headerless, auto-adjusting Modal, and the stack is an array mapped to views:

const levelView = (lvl) => html`<${Modal} key="${lvl}" header="${false}"
    position="absolute" top="${lvl.top}" left="${lvl.left}" focus="${false}" autoadjust>
    <ul class="lm-contextmenu-list" role="menu">${lvl.options.map(itemView)}</ul>
</${Modal}>`;

return html`<div class="lm-contextmenu">
    ${() => levels.value.map((lvl, li) => levelView(lvl, li))}
</div>`;

The list re-runs every time a submenu opens or closes, and the surviving Modal entries are patched in place, not rebuilt. key="${lvl}" keys each level by its own object, and the engine flows changed values into the living instances (Lists is the full story). This matters here for a visible reason: a rebuilt Modal would re-run auto-adjust and move the parent menu when a submenu opens. An earlier version of this component worked around the rebuild with a WeakMap view cache; live patching made the cache unnecessary and the workaround was deleted from the source.

Any block can live in a datagrid cell. The datagrid’s column render is a render prop returning a string or an html view. From the shipped demo, a working Switch inside a cell of a 100,000-row grid:

{
    name: 'active', title: 'Active', align: 'center',
    render: (value, row) =>
        html`<${Switch} checked="${!!value}" size="small"
            onchange="${(on) => { row.active = on; rows.touch(); }}" />`,
}

The cell view composes a full contract component; the onchange callback mutates the caller’s own row and notifies. The state model and the composition model are the same model.

Reference

import { setComponents } from 'lemonadejs';
setComponents({ Card, Modal });   // by name: capitalized, exact match, LJS-104 if unknown
html`<${Card} ... />`             // by value: no registration, same props handling

// prop rules
title="Hello"             // → string
total="${count}"          // → by reference: state stays LIVE
total="${count.value}"    // → snapshot of today's value
disabled                  // → true
onsave="${(v) => ...}"    // → callback prop (lowercase, LJS-305)

// children and api
<${Card}>...</${Card}>            // → props.children, bound to the PARENT scope
props.ref?.({ open, close })      // expose an imperative api to the caller
  • Props are frozen in dev; flow data up with callbacks.
  • Components are by-value plain functions: no this, no instance registry, nothing to clean up that unmount does not already own.
  • When a component becomes a product, published, consumed by other stacks or other agents, add a contract.