Runtime Primitives

ferro-json-ui ships a small JavaScript runtime that is inlined into every page rendered by DefaultLayout and DashboardLayout. Most of the runtime is internal: it powers behaviors emitted by components (popover menus, tabs, dismissible toasts, sidebar, modals) and the attributes those components use are implementation details.

A small subset of the runtime is a public contract: DOM attributes the runtime recognizes on hand-authored or component-output HTML. This page documents that subset.

data-lazy-hero

Opts a <video> element into intersection-driven preload promotion. When the element approaches the viewport, the runtime sets preload="auto" and calls <video>.load() so the first frame is ready by the time the user reaches the element.

Contract

AttributeRequiredDefaultDescription
data-lazy-heroyesOpt-in marker. The element must also have preload="none".
data-lazy-hero-marginno200px 0pxPer-element rootMargin for the IntersectionObserver. Any CSS-margin shorthand the IntersectionObserver constructor accepts.
data-lazy-hero-promotedno (runtime sets it)absentIdempotency marker. The runtime sets this to "1" after promotion; re-running the primitive on the same element is a no-op.

Selector

The runtime matches video[preload="none"][data-lazy-hero]:not([data-lazy-hero-promoted]). Three consequences:

  • <video> without preload="none" is ignored. The runtime does not override an author-tuned preload value.
  • Non-<video> elements with data-lazy-hero are ignored. The promote action (flip preload, call .load()) is video-specific.
  • Already-promoted elements are excluded by the :not(...) clause.

Usage

<!-- Above-the-fold: load eagerly -->
<video preload="auto" poster="/posters/hero-0.jpg" muted playsinline>
  <source src="/assets/hero-0.mp4" type="video/mp4">
</video>

<!-- Below-the-fold: lazy-promote on viewport approach (default 200px lead) -->
<video preload="none" data-lazy-hero poster="/posters/hero-1.jpg" muted playsinline>
  <source src="/assets/hero-1.mp4" type="video/mp4">
</video>

<!-- Below-the-fold with a larger lead time (slower-loading hero) -->
<video preload="none" data-lazy-hero data-lazy-hero-margin="400px 0px"
       poster="/posters/hero-2.jpg" muted playsinline>
  <source src="/assets/hero-2.mp4" type="video/mp4">
</video>

Observer cardinality

Elements are grouped by their resolved data-lazy-hero-margin value. The runtime constructs one IntersectionObserver per distinct margin value, with all elements sharing that value fanned out to it. A page where every hero uses the default has exactly one observer; a page mixing defaults with one override has two observers.

Browser support

The primitive depends on the IntersectionObserver API. On environments without it (rare; some legacy embedded WebViews, certain test harnesses with minimal DOM polyfills), the runtime silently no-ops and the videos behave exactly as authored.

Lifecycle

The runtime scans the DOM once, when DOMContentLoaded fires. Elements inserted into the DOM after that point are not observed. Pages that render heroes server-side as part of the initial HTML — the intended use case — are unaffected.

Performance, not access control

data-lazy-hero defers a fetch the browser would otherwise issue at page load. It does not prevent a fetch. Once the element approaches the viewport, the runtime initiates the fetch. Do not use this attribute to gate paid content or otherwise restrict resource access — that is an access-control concern, not a performance concern.

data-live-fragment / data-channel

Emitted by the LiveFragment builtin on its container <div>. The runtime opens one shared WebSocket to /_ferro/ws per page, subscribes to each declared channel, and swaps the container's innerHTML when a fragment event arrives for the matching channel.

Contract

AttributeSet byDescription
data-live-fragmentLiveFragment rendererOpt-in marker; selects the container for WebSocket subscription
data-channelLiveFragment rendererSubscription key — "projection.{name}.{key}" where name and key are HTML-escaped by the server

Channel format

projection.{projection_name}.{projection_key} — matches the channel the server publishes on via ferro-projection. Both segments are HTML-escaped server-side; channel values are server-controlled and not user-injectable.

Subscribe + swap

The runtime:

  1. Collects all [data-live-fragment] containers on DOMContentLoaded and builds a channelMap keyed by data-channel.
  2. Opens one shared WebSocket to /_ferro/ws.
  3. On open, sends { "type": "subscribe", "channel": "..." } for each channel.
  4. On message, matches { "type": "event", "event": "fragment", "channel": "...", "data": { "html": "..." } } and sets target.innerHTML = msg.data.html.

No WASM, no client-side reactive state, no eval.

Limitations

  • One LiveFragment element per unique channel per page (first container wins for duplicate channels).
  • No automatic reconnect on WebSocket error.
  • No list or collection reconciliation — the entire container HTML is replaced on each delta. This is an explicit non-goal; one binding pattern is supported.

Elements inserted into the DOM after DOMContentLoaded are not observed. The LiveFragment builtin always renders its container in the initial server HTML.