Thank you for being patient! We're working hard on resolving the issue
Row templates expand a single alias row into a tree of typed child rows on hydration. They're how a user-friendly "add a weather row" or "add a calendar" gesture turns into the underlying data-source / renderer / logs rows the rendering layer expects — without the user (or the wire format) having to spell out the full shape.
An alias row is a row whose _type = "alias" and whose attributes carry a
template name plus optional props:
{
"id": "r_weather_perth",
"type": "alias",
"label": "Perth",
"attributes": {
"template": "weather",
"props": { "locationId": "2063523" }
}
}
On the wire, that's all that's stored. At hydrate time, the registered
template factory expands props into the full child tree:
alias template=weather
│ props.locationId=2063523
├─ data-source lookupKey=:~weather:2063523
│ │ attrs={dtype:"obj", lid:2063523}
│ └─ data-renderer type=weather
└─ logs lookupKey=:~weather:2063523:logs
attrs.logType=weather
Updating props.locationId flips the lookup keys on the data-source and
logs children in place — the renderer child's identity is preserved,
no orphan rows. Deleting the alias deletes its expansion.
The wire shape stays one row regardless of how the template grows.
setProp("locationId", ...)
— the user doesn't reparent rows.Each template ships as a BuiltinAliasFactory:
export interface BuiltinAliasFactory {
readonly templateName: string;
readonly template: {
props?: Record<string, "string">;
rule: { kind: "static"; children: (props: AliasProps) => ChildSpec[] };
dynamicTail?: DynamicTail; // for templates with cell-driven children
activeLayout?: (timeScale: TimeScale) => string;
};
}
props — the prop schema; alias rows whose attributes don't satisfy
it fail to hydrate.rule.children — pure function from props to child specs. Runs on
every hydrate and on every patchAliasProps call.dynamicTail — optional cell-stream-driven extension; e.g. the
task-list template appends one child per provider task list.activeLayout — picks a layout family per ambient time scale (the
calendar template uses this to switch between timeline[column] for
day/week and grid for month).Pure-static factories (no app-layer dependency) ship from
tento/tento-lona-js/sheets/src/builtin-alias-factories.ts. Templates that need an
app-layer closure (a config provider, a preferences LiveData) live with
their dependency in tento/tento-lona-js/src/preferences/.
| Template | Props | Purpose |
|---|---|---|
weather | locationId | Sheet-specific weather row (per-row location) |
preferences-default-weather | (none) | User's default weather row (location from prefs; app-layer) |
calendar | (none) | Calendar with events + connected calendar list |
task | authId | Tasks from a connected provider |
task-list | authId | A single task list (used internally by task) |
data | lookupKey | Data series with preset renderers (Garmin, Whoop, etc.) |
timezone | tz | Timezone legend for calendars |
calendar-allday | (none) | All-day strip; peer of calendar, auto-linked in the header |
calendar-list | (none) | List of connected calendars (app-layer) |
preferences-secondary-timezones | (none) | User-pref-driven timezone strip (app-layer) |
The high-level helper is RowsClient.linkRow:
await client.rows.linkRow(sheetKey, ":~lona:weather");
The :~lona:<x> lookup keys are server-materialized alias rows, not
a client-side name-match. Each canonical key has its own server-side
route (lona-rows/src/integrations/lona/<x>.rs) that decides which
template to materialize the alias against. The wire row that lands in
the sheet carries attributes.template explicitly — clients just
hydrate it like any other alias.
Verified example (from lona-rows/src/integrations/lona/weather.rs):
fn lookup_key(_: &(), _: &()) -> String {
":~lona:weather".to_string()
}
fn attributes(_: &(), _: &()) -> Option<serde_json::Value> {
Some(serde_json::json!({
"template": "preferences-default-weather"
}))
}
So :~lona:weather does not resolve to the weather template by
name — it materializes with attributes.template = "preferences-default-weather". The mapping is server-authoritative;
the canonical list of :~lona:<x> keys lives under
lona-rows/src/integrations/lona/.
For custom alias rows, build the row spec directly client-side:
sheet.addRow({
type: "alias",
attributes: {
template: "weather",
props: { locationId: "2063523" },
},
});
const row = sheet.row({ rowLabel: "Perth" });
row?.alias?.setProp("locationId", "2193733"); // → Auckland
The hydration tree rebuilds in place: lookup keys flip on the affected data-source children, renderer identity is preserved, no orphan rows.