Browse documentation

Template internals

RenderEvent: any UI can participate

Choose the output boundary that matches the renderer you already have.

A search page can contain a canvas produced by a chart library and a server-rendered article preview produced by another serializer. Neither producer needs to become a Typed template interpreter. They need a small output boundary that says what representation they already own.

This is the starting point for library authors. Application authors can compose normal templates using renderable values. Here the question is how already-produced UI participates without surrendering its native identity or hiding its lifetime.

Choose output by what the producer actually has

Producer ownsOutput valueWhat the consumer receives
Native DOM node or rangeDomRenderEventthose exact objects
Correctly serialized HTMLHtmlRenderEventtrusted ordered string chunks
Application text/dataordinary interpolationcontext-escaped data

The producing Fx supplies sequencing, failures, required services, and cancellation. Constructing an event value does none of those things by itself.

import { Fx } from "@typed/fx";
import { DomRenderEvent, HtmlRenderEvent } from "@typed/template/RenderEvent";

export const browserOutput = Fx.sync(() => {
  const canvas = document.createElement("canvas");
  canvas.width = 320;
  canvas.height = 120;
  return DomRenderEvent(canvas);
});

export const serverOutput = Fx.fromIterable([
  HtmlRenderEvent("<article><h2>Saved article</h2>", false),
  HtmlRenderEvent("<p>A serializer-owned preview.</p></article>", true),
]);

The browser value is lazy: each run creates its canvas. The server example contains constant serializer-owned markup and marks the terminal chunk. Those Fx values carry different update semantics despite sharing the RenderEvent union.

Preserve the foreign renderer’s actual update model

An editor commonly creates one host, then updates its own descendants in place. Its adapter should usually emit that host once and keep the editor’s work scoped. Recreating the host for every domain change would discard the identity the integration is supposed to retain.

A producer that truly replaces its root can emit replacement objects. The containing Typed range may insert, move, or remove those represented objects; the producer still owns internal descendants, resource teardown, and its application state. It does not gain authority over the surrounding page merely because its output can be placed there.

For HTML, emissions are chunks of one finite serialization, not successive full replacement views. The consumer must preserve order and completion. Serializing a live node and parsing it again cannot preserve that node’s listeners, selection, or identity.

Compose at the smallest useful boundary

import { Fx } from "@typed/fx";
import { html } from "@typed/template";
import { DomRenderEvent } from "@typed/template/RenderEvent";

const chart = Fx.sync(() => DomRenderEvent(document.createElement("canvas")));
export const page = html`<main>
  <h1>Saved-article activity</h1>
  <section aria-label="Activity chart">${chart}</section>
</main>`;

No wrapper protocol or assumed foreign.mount() return type appears here. The adapter models the foreign API it actually has. When setup acquires an editor, observer, or timer, use scoped Effects. component adds a child Scope and lifts returned output such as a DomRenderEvent; Fx.gen is sufficient for yielded setup returning an Fx. When no setup is needed, a direct template or Fx value is sufficient.

The inferred E and R channels remain part of the caller’s program. Do not erase a foreign initialization error behind an untracked Promise or start a hidden fiber that outlives the host. Using DomRenderEvent develops a complete scoped browser adapter.

Make trust explicit at the HTML boundary

Ordinary text belongs in interpolation:

import { html } from "@typed/template";

const note = "<strong>This is saved text, not authored markup.</strong>";
export const preview = html`<p>${note}</p>`;

Wrapping note in HtmlRenderEvent would make an unsupported claim that a serializer already owns its HTML interpretation. The constructor performs no sanitization. Keep application data on the escaped path; use branded HTML only for an actual serializer with a defined format/trust contract. See Using HtmlRenderEvent for terminal chunks and nested serialization.

Decide whether you need an output adapter or an interpreter

If the library produces existing output, stop at RenderEvent. If it only needs to measure or configure a particular template element, a scoped ref is smaller still. Implement RenderTemplate only when the library must interpret template literals and their parts for a target.

Test the boundary accordingly: exact node identity and teardown for DOM, escaped/trusted ownership and ordered completion for HTML, and propagated errors/cancellation for both. Public isDomRenderEvent and isHtmlRenderEvent guards distinguish the representations; avoid structural checks on arbitrary toString objects.

The next library steps are multi-node output for ranges, or the compilation pipeline for a genuine interpreter.