Skip to content

Data Binding

curveo edited this page May 12, 2026 · 5 revisions

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.


TesseraModel

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.


Interpolation {{ }}

Reference model values directly:

<label>Hello, {{ player.name }}!</label>
<label>Level: {{ player.level }}</label>

Concatenation

<label>{{ player.name }} — Level {{ player.level }}</label>

Ternary expression

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

Translation in expression

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


Loops — v-for

Repeat a block for each item in a list.

Model side

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()
));

Template side

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

Nested access

<col v-for="slot in hotbar">
  <label>{{ slot.name }}</label>
  <label>× {{ slot.count }}</label>
</col>

Conditional rendering — v-if

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>

Supported comparisons

<!-- 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 like player.rank == admin looks up the key admin in 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), not 0. So missing == 0 is false, not true.


Conditional visibility — v-show

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.


Attribute binding

Bindings work in attributes too:

<img src="{{ player.avatarPath }}"/>
<item-slot item="{{ player.heldItem }}"/>
<input value="{{ config.defaultName }}"/>

Model helpers

Building from a list

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);

Lambda model

For dynamic resolution:

TesseraModel model = key -> {
    if (key.equals("time")) return LocalTime.now().toString();
    return null;
};

Clone this wiki locally