Template
Stamp out HTML templates at runtime with real data.
Depends on: wcp-expression-parser
Defining a template
A <wcp-template> with an id holds a <template> (the markup, plus an optional per-instance script) and an optional <style> (shared and scoped across all instances).
<wcp-template id="user-card"> <template> <!-- runs once per instance, see "Instance scripts" below --> <script type="module"> // ... </script>
<!-- markup, with placeholders and special attributes --> <div>...</div> </template>
<!-- scoped to instances of this template, see "Scoped styles" below --> <style> /* ... */ </style></wcp-template>A basic example:
<wcp-template id="user-card"> <template> <script type="module"> const { refs, props } = TemplateHost; refs.signoutBtn.addEventListener("click", () => { console.log("sign out", props.id); }); </script>
<div class="card"> <h2>[[ name ]]</h2> <p>[[ admin ? "Admin" : "Member" ]]</p> <button template-ref="signoutBtn">Sign out</button> </div> </template>
<style> .card { padding: 1em; } </style></wcp-template>Scoped styles
The <style> tag is declared once per <wcp-template> definition, not duplicated per instance. Internally, its rules are wrapped in a selector that matches only instances created from this template, using CSS nesting.
<wcp-template id="user-card"> <template>...</template>
<style> .card { padding: 1em; } .card:hover { box-shadow: 0 0 0 1px var(--accent); } </style></wcp-template>Warning
Because every rule ends up nested, anything that isn’t valid inside a nested CSS block won’t work here, most notably @keyframes, @font-face, @import, and @charset. Put those in a regular, top-level stylesheet instead.
Instance scripts
A <script> inside <template> runs once per rendered instance, with access to TemplateHost, the <wcp-template> element this instance is rendered inside.
<template> <script type="module"> const { refs, props, onDisconnect } = TemplateHost;
const onClick = () => console.log("clicked", props.name); refs.signoutBtn.addEventListener("click", onClick);
const unsubscribe = onDisconnect(() => { refs.signoutBtn.removeEventListener("click", onClick); }); </script> ...</template>TemplateHost is the host element itself, so TemplateHost.remove() safely removes the instance from the DOM.
Warning
Module scripts (type="module") get their own scope per instance, so TemplateHost is just a normal binding, safe to use in closures, setTimeout, async callbacks, etc.
Classic scripts (no type="module") all share the same global scope, so TemplateHost can’t be a normal variable: it’s added to the global scope, the script runs synchronously, and it’s then removed. This means TemplateHost is only valid during the script’s initial, synchronous execution. Capture what you need into a local variable first:
<script> { const host = TemplateHost; // capture now host.refs.signoutBtn.addEventListener("click", () => { host.render({ loggedOut: true }); // safe, uses the captured reference }); // referencing TemplateHost directly here, e.g. inside a setTimeout, would fail }</script>Using a template
In HTML
Place a <wcp-template use="id"> where you want the output to appear. The rendered markup is injected inside it on connect. When any data attribute is later changed, the rendered content is replaced with a fresh render.
Pass data via prop-* attributes or json-data.
| Attribute | Example | Behavior |
|---|---|---|
prop-name | prop-name="Alice" | Plain string |
prop-name:json | prop-scores:json="[1,2,3]" | Parsed as JSON |
prop-name:js | prop-admin:js="x > 1" | Evaluated as JS |
json-data | json-data='{"name":"Alice"}' | Full data object |
Dot notation sets nested keys (e.g. prop-config.theme="dark"). When combining both, json-data is applied first and prop-* is merged on top.
<wcp-template use="user-card" prop-name="Alice" prop-admin:json="true"></wcp-template>Warning
HTML attribute names are case-insensitive. Browsers automatically lowercase them, so prop-camelCase becomes prop-camelcase. If your template expects a camelCase property, passing it through an attribute will change the property name and may break data mapping.
In JavaScript
Call .create(data) on the template element. It returns a <wcp-template> element; append it to the DOM to render it.
const template = document.querySelector("#user-card");const instance = template.create(user);
// renders on connectlist.append(instance);
// Re-render using the mutated propsinstance.render({ name: "Bob" });
// `refs` is available once renderedinstance.refs.signoutBtn.addEventListener("click", () => console.log("sign out"));Placeholders
Written as [[ expression ]], evaluated against the data object. Prefix with \ to escape.
The placeholder can appear in HTML attribute values, textContent except for style and script tags.
<span>[[ name ?? "Anonymous" ]]</span><span>[[ isAdmin ? "Admin" : "Guest" ]]</span><span>[[ flags.active ? profile.label : profile.fallback ]]</span>Tip
Resolve to null to remove an attribute entirely. E.g. disabled="[[ disabled ? '' : null ]]".
Special attributes
template-ref
Makes an element accessible via the refs object, both from .create() and from TemplateHost inside instance scripts.
<button template-ref="deleteBtn">Delete</button>template-remove
Removes the element when the expression is true.
<div template-remove="[[ archived ]]">...</div>template-for
Repeats the element once per entry of a collection. Syntax: indexVar, valueVar of propertyVar.
<li template-for="i, tag of tags">[[ i ]]: [[ tag ]]</li>You can skip either indexVar or valueVar, e.g.
<li template-for=",tag of tags">[[ tag ]]</li><li template-for="i of tags">[[ i ]]</li>propertyVar can be a placeholder or expression, e.g. template-for="i, item of [[ items ?? [] ]].
It works with anything that has .entries(), arrays, Maps, Sets, plain objects, and typed arrays, as well as strings and numbers:
- String:
indexVaris the character index,valueVaris the character. - Number: iterates from
0up to (but not including) the number;indexVarandvalueVarare both the current index. - Map / Set / Object / typed array:
indexVarandvalueVarcome from.entries()(key and value).
Security
Expressions are evaluated by a purpose-built parser, not eval or new Function. Output is applied via textContent and setAttribute, never innerHTML.
That said, attributes like href, src, and event handlers can still be vectors if fed untrusted input. Only pass data you control, or sanitize first.
API References
Methods
onDisconnect()
For rendered instances only - To subscribe to disconnect events.
Type: (callback: () => void) => () => void
render()
For rendered instances only - To force re-render optionally with new props.
Type: (properties?: DataType) => void
create()
Creates a new template instance with the given data.
Type: (properties: Record<string, unknown>) => Template
Properties
refs
For rendered instances only - Template elements indexed by their template-ref name.
Accessors: Get, Set
Type: Record<string, Element>
Default: {}
props
For rendered instances only - Props provided to the template during rendering.
Accessors: Get, Set
Type: Record<string, unknown>
Default: {}
Attributes
"use"
Allows to use defined template by its id.
Type: boolean
Slots
default
The template content.