</> 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-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.

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-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.