Something went wrong

Thank you for being patient! We're working hard on resolving the issue

Overview - Lona Docs Log in

Row Templates

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.

What an alias row looks like

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.

Why templates exist

  • Compact wire format. One alias row instead of a four-row subtree.
  • Schema-stable mutation. Switching cities is setProp("locationId", ...) — the user doesn't reparent rows.
  • Server-thin rendering contract. The server doesn't know the renderer shape; it ships the alias and lets the client expand it.
  • Versioning. A template's child shape can evolve (add a renderer slot, rename a preset) without rewriting any persisted alias rows.

The factory contract

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

Built-in templates

TemplatePropsPurpose
weatherlocationIdSheet-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
taskauthIdTasks from a connected provider
task-listauthIdA single task list (used internally by task)
datalookupKeyData series with preset renderers (Garmin, Whoop, etc.)
timezonetzTimezone 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)

Adding an alias row

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" },
  },
});

Updating props

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.

See also

  • Rows — row roles and the alias variant
  • Row Roles & Facades — how aliases relate to source / list / data-source / data-renderer roles
  • Reactive Data — how cell streams flow into expanded template children