Rendering and lifecycle

white-label-view

Render new output or adopt existing DOM through explicit browser or server lifecycles, with first-party tagged HTML and application-owned renderer choice.

Install and requirements

  • Node.js ^22.18.0 or >=24.11.0 for installation/development/server rendering
  • EventTarget-compatible models for observable model binding
  • Browser DOM only for the default browser entrypoint
  • No DOM globals required by white-label-view/server
import View from 'white-label-view';
import View from 'white-label-view/server';
import {html, attributes, unsafeHTML} from 'white-label-view/html';

Rendering contract

Browser View templates must resolve to exactly one DOM element, one tagged HTML value, or one compatible single-root HTML string. Invalid or multi-root output throws TypeError without replacing the last successful root.

Server View accepts tagged HTML values or compatible HTML strings and stores the most recently rendered HTML for toString(). It does not emulate DOM nodes, mounting, delegated events, focus, or animation frames.

import {html} from 'white-label-view/html';

const view = new View({
    parentElement: document.querySelector('main'),
    model,
    template: data => html`<section>${data.name}</section>`
}).initialize();

Browser lifecycle

initialize() performs a synchronous render. A successful mount/replacement establishes model binding, calls addListeners(), then afterMount(). Before replacement or destruction, removeListeners() runs and owned children are cleaned up.

An existing attached element can be adopted without a client template. View calls update(element, data) before requiring template output; return true to handle the render in place, or false to fall back to the configured template.

A View can also own both its rendered markup and the events bound to that markup. Delegated listeners registered through this.delegated are cleared when the root is replaced or the View is destroyed.

import {Model} from 'white-label-model';
import View from 'white-label-view';
import {html} from 'white-label-view/html';

const model = new Model({open: false});

class MenuView extends View {
    handleClick = () => model.update({open: !model.get().open});

    override addListeners() {
        this.delegated.on('click', '[data-menu-toggle]', this.handleClick);
        return this;
    }
}

const view = new MenuView({
    parentElement: document.querySelector('main')!,
    model,
    template(data) {
        const state = data as {open: boolean};
        return html`<section>
            <button type="button" data-menu-toggle>Toggle menu</button>
            <p>${state.open ? 'Open' : 'Closed'}</p>
        </section>`;
    }
}).initialize();

Model binding and batching

A model is treated as observable when it exposes addEventListener() and removeEventListener(). If it exposes get(), View passes get() to the template; otherwise it passes the model itself.

batchUpdates: true coalesces model-triggered browser renders into one requestAnimationFrame. Manual render() stays synchronous. Server rendering is always synchronous.

Tagged HTML and trust

The first-party html tagged template escapes ordinary interpolated text and quoted attribute values. attributes() validates attribute names, rejects intrinsic on* handlers and srcdoc, and escapes attribute values.

Tagged HTML and attribute helper values are recognized through runtime-owned identity that application code cannot reproduce with a marker-shaped object. unsafeHTML() is an explicit caller-owned trust boundary for markup that has already been independently trusted or sanitized. HTML escaping is not a URL, CSS, or application-policy sanitizer, so applications still validate context-sensitive values.

const template = data => html`<section>${data.name}</section>`;

Template engines

White Label View includes a small first-party tagged HTML template helper and otherwise stays renderer-agnostic. Applications that prefer a third-party renderer install, configure, and call it themselves; View does not bundle, configure, detect, or adapt third-party engines.

First-party tagged HTML

html escapes ordinary interpolated text and quoted attribute values. Use attributes() for conditional/opening-tag attributes and reserve unsafeHTML() for content that has already been independently trusted or sanitized.

import {html} from 'white-label-view/html';

const template = data => html`<article class="card">
    <h2>${data.title}</h2>
</article>`;

Tested third-party engines

Compatibility CI installs each renderer only in the test environment, wires it through the normal template(data) contract, and verifies its synchronous HTML output with both browser View and white-label-view/server. These engines are not White Label runtime dependencies.

EngineTested versionCompatibility notes
Handlebars4.7.9Uses the engine's normal escaping.
Eta4.6.0Uses escaped interpolation.
EJS6.0.1Uses escaped interpolation.
Mustache4.2.0Uses the engine's normal escaping.
Nunjucks3.2.4Compatibility fixture enables autoescape.
Pug3.0.4Uses escaped interpolation.
KitaJS HTML4.2.13Compatibility fixture marks dynamic child text safe; child expressions are otherwise unescaped by default.

Bring your own renderer

A renderer only needs to produce a value View already accepts: one DOM element in the browser, or an HTML string for browser/server rendering. Install and configure the renderer in the application, then call it from the View template.

npm install <your-template-engine>

const view = new View({
    parentElement,
    template: data => renderTemplate(data)
});

When a third-party renderer returns a string, that string is an application-owned trust boundary. Follow that renderer's escaping and trusted-markup rules before returning the result to View.

Serverless rendering

Use white-label-view/server when a function should return HTML. It requires no DOM globals and can turn request-scoped Model state into an HTML string before the invocation returns.

Create server View instances per request when they own request data or subscriptions. Browser View remains a separate progressive-enhancement layer and should not be emulated inside the serverless function.

Browser constructor settings

new View(settings?)

Create a browser View.

ParameterTypeDefaultDescription
parentElementElement—Parent that owns the rendered root.
elementElement—Existing root to adopt; an attached root may be updated in place without a template when update() returns true.
modelobject—Data source; observable only when it exposes addEventListener/removeEventListener.
template(data) => string | Node | HTMLMarkup—Optional render function; required when an in-place update does not handle the current attached root.
update(element, data) => boolean—Optional in-place update hook; true means the current root handled the render without template replacement.
batchUpdatesbooleanfalseCoalesce model-driven renders into an animation frame.
Returns
A View instance.
Runtime
Browser

Browser View API

update(element, data)

Handle an attached root in place before template rendering; returning true allows an adopted View to render without a client template.

Returns
boolean; true means handled, false falls back to normal template rendering when configured.
Runtime
Browser

setModel(model?)

Move model binding and synchronously render current data.

Returns
The same View instance.
Runtime
Browser

initialize()

Perform the initial synchronous render and lifecycle setup.

Returns
The same View instance.
Runtime
Browser

requestRender()

Render now or queue one animation-frame render when batching is enabled.

Returns
The same View instance.
Runtime
Browser

addChild(child)

Give this View cleanup ownership of another View without automatically mounting it.

Returns
The parent View.
Throws / rejects
TypeError for ownership cycles or a child already owned elsewhere.
Runtime
Browser

releaseChild(child)

Relinquish cleanup ownership without destroying the child.

Returns
The parent View.
Runtime
Browser

destroy()

Release listeners, binding, queued work, owned children, and the DOM root.

Returns
The same View instance; it may be initialized again.
Runtime
Browser

initializeModelBinding()

Subscribe once to an EventTarget-compatible model's change event.

Returns
undefined.
Runtime
Browser

destroyModelBinding()

Release the owned EventTarget model subscription and cancel queued rendering.

Returns
undefined.
Runtime
Browser

addListeners()

Subclass hook called after root installation.

Returns
The same View instance by default.
Runtime
Browser

removeListeners()

Subclass hook called before root replacement or destruction.

Returns
The same View instance by default.
Runtime
Browser

afterMount()

Subclass hook called after insertion/adoption and listener setup.

Returns
The same View instance by default.
Runtime
Browser

delegate(scope?)

Create a caller-owned delegated-event registry scoped to an element or the current root.

Returns
DelegatedEvents registry.
Runtime
Browser

render()

Synchronously adopt/update an attached root or mount, preserve, or replace template output.

Returns
The same View instance.
Throws / rejects
TypeError when required template output does not resolve to exactly one browser element.
Runtime
Browser

DelegatedEvents API

delegated.on(type, selector, callback, options?)

Register a native delegated event listener. once is consumed only by a matching event.

Returns
The same DelegatedEvents registry.
Runtime
Browser

delegated.off(type, selector?, callback?, options?)

Remove matching delegated registrations, optionally scoped by selector/callback/capture.

Returns
The same DelegatedEvents registry.
Runtime
Browser

delegated.clear()

Remove every native and abort listener owned by the registry.

Returns
The same DelegatedEvents registry.
Runtime
Browser

Server View API

setModel(model?)

Move model binding and synchronously render current data.

Returns
The same server View.
Runtime
Node.js / DOM-free

initialize()

Render current state and initialize the server lifecycle.

Returns
The same server View.
Runtime
Node.js / DOM-free

addChild(child)

Register child cleanup ownership.

Returns
The parent server View.
Throws / rejects
TypeError for ownership cycles or conflicting ownership.
Runtime
Node.js / DOM-free

releaseChild(child)

Release ownership without destroying the child.

Returns
The parent server View.
Runtime
Node.js / DOM-free

initializeModelBinding()

Subscribe to EventTarget-compatible model changes.

Returns
The same server View.
Runtime
Node.js / DOM-free

destroyModelBinding()

Release the EventTarget model subscription after first resetting internal binding state, so the server View remains reusable even if external listener removal throws.

Returns
The same server View.
Runtime
Node.js / DOM-free

render()

Synchronously render the template into stored HTML.

Returns
The same server View.
Throws / rejects
TypeError for output that is neither a string nor White Label HTMLMarkup.
Runtime
Node.js / DOM-free

toString()

Read the most recently rendered HTML.

Returns
HTML string, or an empty string before/after output is cleared.
Runtime
Node.js / DOM-free

destroy()

Destroy children, release subscriptions, and clear stored output.

Returns
The same server View.
Runtime
Node.js / DOM-free

Tagged HTML API

html`...`

Render a tagged HTML template with escaped interpolations and runtime-owned trusted output.

Returns
HTMLMarkup.
Runtime
Browser + Node.js

attributes(values)

Render validated, escaped HTML attributes for interpolation at an opening-tag attribute boundary.

ParameterTypeDefaultDescription
valuesRecord<string, unknown>—Attribute names and values; on* names and srcdoc are rejected.
Returns
Attribute markup accepted only by html opening-tag interpolation.
Runtime
Browser + Node.js

unsafeHTML(value)

Mark caller-owned, already trusted or sanitized HTML for insertion without escaping.

ParameterTypeDefaultDescription
valuestring—Trusted HTML string.
Returns
HTMLMarkup.
Runtime
Browser + Node.js
  • Never pass untrusted user content directly.
  • unsafeHTML() is an explicit trust boundary, not a sanitizer.

isHTMLMarkup(value)

Identify tagged output created by the active White Label View package instance. Marker-shaped objects and output from a separate installed copy are not implicitly trusted.

Returns
Type-predicate boolean.
Runtime
Browser + Node.js

HTMLMarkup

TypeScript contract for trusted tagged output created by the active White Label View package instance.

Returns
Type-only public contract with a readonly value and string-coercion methods.
Runtime
TypeScript
  • Do not construct marker-shaped objects and assume they are trusted; runtime acceptance requires package-instance-owned identity.

TypeScript

View.Settings and View.Model expose the EventTarget-based browser/server model contracts.

Template data is unknown by design; application code should narrow domain data before accessing fields. white-label-view/html exports the HTMLMarkup type for tagged output.

Security and trust boundaries

The html tag escapes ordinary dynamic text and quoted attribute values. attributes() validates names, rejects on* event-handler attributes and srcdoc, and escapes rendered attribute values. Tagged HTML and attribute values use package-instance-owned identity so arbitrary objects cannot forge trusted markup.

HTML escaping is not a URL or CSS sanitizer. Validate context-sensitive values separately. unsafeHTML(), application-owned HTML-string templates, third-party renderer output, and strings crossing package-copy boundaries are explicit caller-trusted inputs.

Design boundaries

View owns rendering mechanics and rendering lifecycle, not application state, routing, networking, CSS, sanitization policy, or application-wide events.

The browser entrypoint can adopt and update an existing root explicitly; it does not provide automatic hydration or claim to reconcile arbitrary server DOM with browser state.