</> Ultima

Template DSL reference

The whole reactive vocabulary of ReactiveComponent, in one place. Any path used in data-if, data-else-if or as a data-class value accepts a leading ! to negate it (e.g. data-if="!loading").

AttributeWhereSyntaxEffect
data-bindany element{"attribute":"path"}Two-way attribute/property binding
data-classany element{"class":"boolean.path"}Toggles CSS classes
data-styleany element{"css-property":"path"}Sets inline style properties
data-onany element{"event":"method"}Calls this.method(event, scope) when the event fires
{{ path }}text nodesHello {{ user.name }}Text interpolation (one-way); undefined/null render as empty, objects as JSON
data-refany element"name"Exposes the element on this.refs.name
data-if<template>"path"Starts a conditional chain
data-else-if<template>"path"Continues the conditional chain
data-else<template>(no value)Unconditional fallback
data-for<template>"item in path [: key]"Repeats the content for each item of an array

data-bind

Attribute or property binding, both ways. In the data → DOM direction, the value read from the path is applied following a sequence of special cases — textContent, checked (for checkboxes), value, datetime-local fields — before falling back to a generic setAttribute.

<input data-bind='{"value":"name"}'>
<input type="checkbox" data-bind='{"checked":"active"}'>
<span data-bind='{"textContent":"name"}'></span>

In the DOM → data direction, any element with data-bind listens for the native change event and writes the value it reads back to the corresponding path — that's how an <input> becomes a two-way binding, with no extra syntax.

The same syntax composes nested reactive components: data-bind='{"data-state":"path"}' on a <child-component> passes a whole object down into it, both ways. A light-DOM child renders its own content with its own state: the parent only binds the child element itself, not what's inside it.

Boolean properties (disabled, checked, hidden, required, …) are driven through the property, so false really means off. A null or undefined value clears the attribute (or empties value and textContent) instead of printing "undefined". In a list of primitives (n in nums), binding to the item variable writes back into that slot of the array.

Safety. Values from state are never turned into code: event handler attributes (on*) and srcdoc are never bound, and URL attributes (href, src, action, …) refuse javascript:, vbscript: and data:text/html values (a warning is logged). Malformed JSON in data-bind/data-class/data-style or in data-state is reported with console.error and ignored.

data-class

<div data-class='{"highlight":"active", "error":"!valid"}'></div>

Toggles each class via classList.toggle, based on the (boolean, or negated with !) value of the corresponding path.

data-style

<div data-style='{"border-color":"statusColor"}'></div>

Sets an inline style property from the path's value. If the value is undefined/null, the property is removed.

data-on

<button data-on='{"click":"save"}'>Save</button>

<template data-for="task in tasks : id">
  <button data-on='{"click":"remove"}'>x</button>
</template>

// in the component class
remove(event, scope) {
    this.state = { ...this.state, tasks: this.state.tasks.filter(t => t.id !== scope.task.id) };
}

Maps native event names to methods of the component (never to arbitrary code). The method receives the event and the element's read scope: state, computed values and, inside a data-for, the loop variables (scope.task above). A name that isn't a method is reported with console.error. One listener per element and event is created for the element's whole life, so re-rendering never stacks handlers, and keyed list items always call the handler with their current item.

Text interpolation — {{ path }}

<p>Hello {{ user.name }}, you have {{ count }} items</p>
<template data-for="t in tasks"><li>{{ t.title }}</li></template>

Inside any text node, {{ path }} is replaced by the value at that path (same path syntax as the other bindings, including the leading ! and the data-for variables). It is one-way and the value is always inserted as text, never as HTML. It is not evaluated inside <script>, <style>, <textarea> or in an element whose data-bind sets textContent/innerHTML. Only paths are supported, not arbitrary expressions: use computed() for those.

data-ref

<input data-ref="searchField">

// after rendering:
this.refs.searchField.focus();

Exposes the element on this.refs.name. If more than one element uses the same name (common inside a list), this.refs.name becomes an array.

Conditionals — data-if / data-else-if / data-else

<template data-if="loading">
  <p>Loading...</p>
</template>
<template data-else-if="error">
  <p>Something went wrong.</p>
</template>
<template data-else>
  <p>All good.</p>
</template>

A chain is a sequence of consecutive sibling <template> elements, starting with data-if, with zero or more data-else-if, optionally ending in a single data-else. The first truthy path wins.

If the winning branch is the same as in the previous render, the content is not destroyed and recreated — it's just reprocessed in place. This matters a lot when there's a keyed data-for inside the branch: item identity survives renders in which the condition didn't change.

Lists — data-for

<template data-for="task in tasks : id">
  <li data-bind='{"textContent":"task.title"}'></li>
</template>

Syntax: "item in path"; optionally "item, index in path" (also exposes the index); and a " : field" suffix that declares the items' identity key.

With a usable key — explicit, or inferred when every item has a uuid or id — the list does keyed reconciliation: the DOM nodes of an item whose key is still present are reused and only repositioned, not recreated. Without a key (or with a missing/duplicate key in a given pass), the whole list is rebuilt on every render.