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.
| Engine | Tested version | Compatibility notes |
| Handlebars | 4.7.9 | Uses the engine's normal escaping. |
| Eta | 4.6.0 | Uses escaped interpolation. |
| EJS | 6.0.1 | Uses escaped interpolation. |
| Mustache | 4.2.0 | Uses the engine's normal escaping. |
| Nunjucks | 3.2.4 | Compatibility fixture enables autoescape. |
| Pug | 3.0.4 | Uses escaped interpolation. |
| KitaJS HTML | 4.2.13 | Compatibility 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.
| Parameter | Type | Default | Description |
|---|
parentElement | Element | — | Parent that owns the rendered root. |
element | Element | — | Existing root to adopt; an attached root may be updated in place without a template when update() returns true. |
model | object | — | 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. |
batchUpdates | boolean | false | Coalesce 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.
| Parameter | Type | Default | Description |
|---|
values | Record<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.
| Parameter | Type | Default | Description |
|---|
value | string | — | 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.