Template Syntax
Everything inside a component root that is not the component script is its template. It is ordinary HTML plus a small, fixed set of directives — no JSX, no custom event syntax, no invented attributes.
Interpolation
Wrap a JavaScript expression in braces to use it in a text node or an attribute value. A pure binding evaluates to the raw value; mixed text interpolates into a string.
<p>Signed in as {user.name}</p>
<input value="{query}" />
<div class="card {isActive ? 'active' : ''}"></div>
<button disabled="{count >= 10}">Increment</button>
| Array | Joined without commas. Nested arrays flatten recursively. |
| boolean / null / undefined | Render as an empty string — which is what makes conditional rendering read cleanly. |
| object / function / symbol | Render as an empty string and log [PP-WARN] Invalid template child. |
Lists with pp-for
pp-for goes on a <template> element and nowhere else.
The supported forms are item in items and (item, index) in items.
Give repeated elements a stable, unique key.
<ul>
<template pp-for="(todo, index) in todos">
<li key="{todo.id}">
{index + 1}. {todo.title}
<button onclick="removeTodo(todo.id)">Remove</button>
</li>
</template>
</ul>
<script>
const [todos, setTodos] = pp.state([
{ id: 1, title: 'First task' },
{ id: 2, title: 'Second task' },
]);
function removeTodo(id) {
setTodos(todos.filter((todo) => todo.id !== id));
}
</script>
Any iterable works
Arrays, Set, Map, NodeList, generators. null and
undefined render an empty list; a non-iterable value renders an empty list and warns.
Handlers survive rerenders
Event handlers inside a loop are rewritten so loop variables still resolve after the list diffs. Duplicate keys degrade the diff and cause unstable DOM reuse.
key attribute, so the same markup works with or without
pp-for.
Reactive inline CSS with pp-style
Reserve the plain style attribute for fully static CSS. The moment a declaration contains an interpolated
expression, move it to pp-style. Editors and CSS tooling treat a brace inside
style as invalid CSS and report errors such as “property value expected”;
pp-style keeps the source clean and is merged into style at compile time.
<div style="width: {progress}%"></div>
<div pp-style="width: {progress}%"></div>
pp-styleis removed from the rendered markup and merged intostyle.- When both are present, the static
stylecontent is kept first and thepp-styledeclarations are appended after it. - Write CSS declaration text, not a JavaScript object. Leading semicolons are trimmed before merging.
<p style="font-weight: 700" pp-style="color: {tone};">Status</p>
<button pp-style="gap: {isCompact ? '0.25rem' : '0.5rem'}; opacity: {isDisabled ? 0.6 : 1};">
Save
</button>
Dynamic attributes with pp-spread
Spread one object expression into attributes. The value must be a spread expression and must evaluate to an object.
<button pp-spread="{...buttonAttrs}" hidden="{isLoading}">Save</button>
<script>
const buttonAttrs = {
class: 'btn btn-primary',
'aria-label': 'save',
};
const isLoading = false;
</script>
Use real attribute names
Keys are emitted verbatim, so write aria-label,
data-id, class, style.
null and undefined values are omitted.
Handlers are dropped silently
Keys matching on* and keys that are not valid attribute names are
removed without warning. Known booleans are emitted bare when true and omitted when false.
Events
Use native on* attributes. The value can be raw code or an expression wrapped in braces.
Hyphenated forms such as on-click are not events — on a component boundary they become a
onClick prop instead.
<button onclick="setCount(count + 1)">Increment</button>
<input oninput="setQuery(event.target.value)" />
<form onsubmit="save(event)">...</form>
The runtime injects event, e,
$event, target,
currentTarget and el into every handler.
Handlers are rebound after DOM morphing, so never assume a one-time binding.
Forms
Controlled
valuebindings work through normal interpolation.checkeddrives checkboxes and radios in both directions.<select>has dedicated support, including multiple selects.<textarea value="...">is normalised at compile time.
Uncontrolled
defaultvaluefor inputs, textareas and selects.defaultcheckedfor checkboxes and radios.- Defaults are restored on
form.reset()for managed inputs. - Mixing controlled
checkedwithdefaultcheckedon one element logs a mode-switch warning.
data-pp-select-value,
data-pp-default-value, data-pp-default-checked,
data-pp-input-value, data-pp-checked-value.
The runtime writes and reads those itself.
Passing props across a boundary
Child props come from DOM attributes. Author them in kebab-case; the runtime hydrates them as camelCase
on pp.props. An empty attribute becomes the boolean true.
| Template attribute | Runtime prop |
|---|---|
| as-child | asChild |
| close-on-escape-key | closeOnEscapeKey |
| selected-date | selectedDate |
| on-date-select | onDateSelect |
Pure prop expressions and mixed strings are both evaluated in the parent scope, so a callback such as
on-date-select can hand a child the parent's own setter.
Directive reference
The complete set. Everything else in a template is ordinary HTML.
| Attribute | Goes on | Purpose |
|---|---|---|
| pp-for | <template> only |
Repeat the content for each item of an iterable. |
| key | a repeated element | Identity for keyed diffing. Plain HTML attribute; make it stable and unique. |
| pp-style | any element | Reactive inline CSS, merged into style at compile time. |
| pp-spread | any element | Spread one object expression into attributes. |
| pp-ref | any element | Bind the node to a pp.ref(...) object or a callback ref. |
| pp-ref-forward | a component boundary | Resolve a parent pp-ref through to the child's real root. |
| on* | any element | Native event attributes. Rebound after every DOM morph. |
| value / checked | form controls | Controlled bindings, including multi-selects and textareas. |
| defaultvalue / defaultchecked | form controls | Uncontrolled defaults, restored on form.reset(). |
| pp-spa="false" | a single <a> |
Opt that one link out of SPA interception. See pp-spa. |
pp-component is injected by Prisma PHP and identifies the instance — never author it.
The runtime also writes internal data-pp-* attributes for refs and form controls; those are implementation
detail, not authoring surface.