-
Notifications
You must be signed in to change notification settings - Fork 0
Data Binding
TesseraUI supports dynamic content through a TesseraModel object passed at render time. Templates can read model values with {{ }} expressions, loop with v-for, and show/hide elements with v-if and v-show.
Build a model from a flat key-value map:
TesseraModel model = TesseraModel.of(Map.of(
"player.name", player.getName().getString(),
"player.level", String.valueOf(playerLevel),
"player.health", String.valueOf((int) player.getHealth()),
"items.count", String.valueOf(items.size())
));All values are strings. Numeric comparisons in expressions work on the string representation.
Use TesseraModel.EMPTY when your template has no dynamic content.
Reference model values directly:
<label>Hello, {{ player.name }}!</label>
<label>Level: {{ player.level }}</label><label>{{ player.name }} — Level {{ player.level }}</label><label>{{ items.count == 0 ? "Empty" : items.count + " items" }}</label>Supported operators: ==, !=, >, <, >=, <=.
String literals in ternary conditions must also be quoted:
<!-- ✓ Correct -->
<label>{{ player.rank == 'admin' ? "Administrator" : "Player" }}</label>
<!-- ✗ Wrong — looks up model key "admin", not the string -->
<label>{{ player.rank == admin ? "Administrator" : "Player" }}</label><label>{{ items.count == 0 ? t:ui.mymod.empty : items.count + " " + t:ui.mymod.items }}</label>Use : (with spaces) as the ternary separator when a t: key is involved.
Repeat a block for each item in a list.
Pass the count and per-item fields:
TesseraModel model = TesseraModel.of(Map.of(
"items", String.valueOf(items.size()), // count
"item.name.0", items.get(0).getName(),
"item.rarity.0", items.get(0).getRarity(),
"item.name.1", items.get(1).getName(),
"item.rarity.1", items.get(1).getRarity()
));<col v-for="item in items">
<label>{{ item.name }}</label>
<badge>{{ item.rarity }}</badge>
</col>item is the loop variable. Inside the template, {{ item.name }} resolves to item.name.<index> from the model.
The same rule applies inside <virtual-list v-for="item in items">: row data must still be keyed with the loop variable prefix (item.name.0, item.action.0, ...), and row templates should use the loop variable prefix ({{ item.name }}, onclick="{{ item.action }}").
For compatibility with early 1.0 templates, <virtual-list> also accepts bare row keys such as {{ name }}. Prefer the prefixed form because it matches regular v-for and avoids ambiguity in nested templates.
Inside <virtual-list> rows, bindings that cannot be resolved as row data fall back to the parent model. This lets global values such as {{ s.items }} or {{ screen.title }} be shared without copying them onto every row.
<col v-for="slot in hotbar">
<label>{{ slot.name }}</label>
<label>× {{ slot.count }}</label>
</col>Completely removes the element and its layout space when the expression is falsy:
<row v-if="player.isAdmin">
<button onclick="openAdmin">Admin panel</button>
</row>The element is not rendered and takes no space in the layout when the condition is false.
Expressions can be written bare or wrapped in {{ }} — both forms are equivalent:
<!-- Bare expression (recommended) -->
<label v-if="items.count > 0">{{ items.count }} items available</label>
<!-- Wrapped — also works -->
<label v-if="{{ items.count > 0 }}">{{ items.count }} items available</label><!-- Numeric: both sides resolved from model or as literals -->
<badge v-if="player.level >= 10">Expert</badge>
<!-- String equality: quote the literal with single or double quotes -->
<badge v-if="player.rank == 'admin'">Admin</badge>
<badge v-if="player.rank != 'guest'">Member</badge>
<!-- Two model keys compared -->
<label v-if="score == highScore">New record!</label>
<!-- Truthy check (true / 1 / yes / on) -->
<row v-if="player.isOnline">● Online</row>Note: Always quote string literals with
'...'or"...". An unquoted value likeplayer.rank == adminlooks up the keyadminin the model, not the string"admin". If the key is absent, it is treated as an empty string.
Missing keys: A key that is not in the model resolves to
""(empty string), not0. Somissing == 0isfalse, nottrue.
Keeps the element in the layout (space is preserved) but hides it:
<label v-show="player.isOnline">● Online</label>
<!-- Same expression syntax as v-if: bare or wrapped, string literals quoted -->
<label v-show="status == 'active'">Active</label>Use v-show when you want to avoid layout shifts on state changes. Use v-if when the element should never reserve space.
Bindings work in attributes too:
<img src="{{ player.avatarPath }}"/>
<item-slot item="{{ player.heldItem }}"/>
<input value="{{ config.defaultName }}"/>If an attribute binding cannot be resolved, the literal {{ ... }} text remains in the attribute. For handler attributes such as onclick="{{ actionKey }}", TesseraUI logs a warning because the button would otherwise be silently inert.
Templates are rebuilt from scratch when you call TesseraTemplateRenderer.build(...). To keep input text, cursor, selection, and focus across rebuilds:
- Give every stateful
<input>/<textarea>a stableid. - Keep one
TesseraRenderContextfield on your screen. - Pass that context into the renderer.
<input id="search" value="{{ search.default }}" oninput="onSearch"/>private final TesseraRenderContext renderContext = new TesseraRenderContext();
root = TesseraTemplateRenderer.build(
TesseraTemplate.load("yourmod:ui/search"),
model,
clickHandlers,
inputHandlers,
renderContext,
x, y, w, h
);If a context is provided and an <input> or <textarea> has no id, TesseraUI logs a warning because that widget cannot persist state.
Map<String, String> data = new HashMap<>();
data.put("entries", String.valueOf(entries.size()));
for (int i = 0; i < entries.size(); i++) {
data.put("entry.name." + i, entries.get(i).getName());
data.put("entry.count." + i, String.valueOf(entries.get(i).getCount()));
}
TesseraModel model = TesseraModel.of(data);For dynamic resolution:
TesseraModel model = key -> {
if (key.equals("time")) return LocalTime.now().toString();
return null;
};