Skip to content

Template

Minified: 4.89 KBGzip: 2.25 KBBrotli: 1.99 KBZstd: 2.32 KB

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.

AttributeExampleBehavior
prop-nameprop-name="Alice"Plain string
prop-name:jsonprop-scores:json="[1,2,3]"Parsed as JSON
prop-name:jsprop-admin:js="x > 1"Evaluated as JS
json-datajson-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 connect
list.append(instance);
// Re-render using the mutated props
instance.render({ name: "Bob" });
// `refs` is available once rendered
instance.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: indexVar is the character index, valueVar is the character.
  • Number: iterates from 0 up to (but not including) the number; indexVar and valueVar are both the current index.
  • Map / Set / Object / typed array: indexVar and valueVar come 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.