Browse documentation

Template rendering

DOM scalar parts and attributes

See which exact DOM location each scalar template interpolation owns and what one push changes.

Suppose the query state is correct in a log but the search field appears stale. There are several possible breaks: the input event read the wrong field, state was not published, the render was stopped, a queue hasn’t applied the change, or another writer overwrote the native field. Debugging is easier when each step has a concrete target.

This guide follows the editing loop from template authoring. The syntax’s set/clear rules live in scalar bindings; this page uses them to locate a real update failure.

Start from one complete feedback loop

import { RefSubject } from "@typed/fx";
import { component } from "@typed/ui/Component";
import { html } from "@typed/template";
import * as EventHandler from "@typed/template/EventHandler";

export const Search = component(function* () {
  const query = yield* RefSubject.make("");
  const readInput = EventHandler.make((event: Event) =>
    RefSubject.set(query, (event.currentTarget as HTMLInputElement).value),
  );
  return html`<label>
    Search articles
    <input type="search" .value=${query} oninput=${readInput} />
    <output>${query}</output>
  </label>`;
});

The browser changes the edit property, then dispatches input. The handler reads that property and sets the subject. Two subscribers receive the value: the input’s .value part and the output’s text part. A render queue can determine when the native writes happen. The component generator does not restart during that sequence.

Know why a later write can be direct

At setup, parsed part paths identify concrete targets in the cloned or adopted structure. The renderer captures those targets in updaters and subscribes their inputs. It does not run a document selector after every emission or search a component tree for the matching field.

An attribute updater retains its attribute/element target; a property updater retains its element and property name. A text-only updater writes its text target. A structural child hole instead owns a bounded range that can contain multiple nodes. Do not treat replacing nested output as the same operation as assigning a string to one field.

Use observations to choose the next boundary

ObservationInspect next
The input visibly changed but the subject didn’thandler registration, currentTarget, and the state operation
Subject changed but neither consumer didwhether rendering is still running and whether work is queued
getAttribute("value") is unchangedread input.value; this template owns the property
Output changed but input object was replacedparent switching, root output, or a changing collection key
Field changes then immediately revertsanother writer or another state publication
Events stop after a spread changewhether the handler entry was removed and finalized

An attribute MutationObserver cannot detect every property assignment. Inspect the native property as well, and use a DOM breakpoint on the actual suspect element rather than observing the entire page. Count producer publications and applied changes separately; queue coalescing can make those counts differ while retaining the latest value.

Draw the cooperation boundary around fields

Typed can own the edit property while an analytics helper owns a distinct data attribute and an animation library owns separate class tokens. It cannot merge simultaneous writes to .value or recover its captured input after another owner replaces the label’s innerHTML.

The same rule explains why a spread is not one opaque assignment. Each accepted entry has its own field and lifetime; class/data entries additionally track local collections. Use spread lifetimes and class contributions when the suspected conflict is inside those collections.

Keep the test proportional to the failure

For a property binding, assert its native value and stable element identity after a real state change. For an event, assert dispatch/cancellation and cleanup. For a queue, assert the chosen scheduling condition rather than sleeping a guessed duration. For structural replacement, test the owned range and child lifetimes.

A passing text assertion proves only that text. It does not prove a selection survived, another owner’s class remained, or a removed listener stopped running. Move to Direct updates, local reconciliation when the interaction actually changes a collection or nested range.