tuimark

Templates and composition

Reuse markup in a Tuimark document - split it into files with include and fragment, repeat a piece with template and use, scope its ids, bind each copy to its own data with with, and read the diagnostics and tool output for what a copy produced.

A document that grows has two problems: it gets long, and the same few lines appear again and again. The current language (a <tui> without version) has a tool for each, and both are expanded when the document is read, so nothing about them exists at run time. The dump, the keymap, focus, and the events see the expanded document, as if you had written every copy by hand.

You wantUseResult
to split a long document by the pieces a person would name<include> and <fragment>the contents of a file placed where the include stands
to repeat a piece, each time with its own data<template> and <use> (this page)a copy of the template at every <use>, with scoped ids and its data behind an alias

The two combine: a fragment can hold templates, so a team keeps its form fields in one file and every document includes it. A pinned document (one that writes version="1", "2", or "3") has none of these tags; they are V001, "requires a <tui> without version".

A template and its uses

A <template id="…"> holds a piece of markup. It is never drawn by itself. Each <use template="…"/> places a copy of it where the use stands:

<tui>
  <style>
    .label { bold: true; }
    .invalid { color: red; }
  </style>
  <template id="field">
    <col>
      <text class="label" class:invalid="f.error">{f.label}</text>
      <input id="input" bind="f.value" placeholder="{f.hint}" on:submit="submit"/>
      <text class="invalid" if="f.error">{f.error}</text>
    </col>
  </template>
  <keymap>
    <bind keys="ctrl+s" action="save" label="save"/>
    <bind keys="ctrl+n" action="focus" to="#email__input" label="email"/>
  </keymap>
  <screen id="main" focus="#name__input">
    <col gap="1" pad="1">
      <text>Hello, {form.name.value}</text>
      <use id="name" template="field" with="form.name as f"/>
      <use id="email" template="field" with="form.email as f"/>
      <hints/>
    </col>
  </screen>
</tui>

field has a label, an input, and an error line, and the form uses it twice. Each <use> names its template, gives the copy an id, and says which data the copy reads with with. Both fields come out of the same markup:


 Hello,

 Name
 your name

 Email
 ada@
 not an email

 ctrl+s save  ctrl+n email
  • Where a template stands. As a direct child of <tui> or <screen>, in the document or in a fragment it includes; anywhere else is V001. It may stand before or after the uses that name it. Its id is required (V012) and unique among templates (V004).
  • What it holds. Any elements, one or several. Not <tui>, <screen>, <style>, <keymap>, <bind>, <fragment>, or another <template> at any depth (V001), and no loose text (V013: wrap it in a <text>). A template can hold <use> elements of other templates, so a template can build on a smaller one.
  • What a <use> takes. template (required), id, with, if, hidden, and a static class. Any other attribute is V002, and the message says where it goes instead: on the template's element, or as a class. A <use> takes no content, because there are no slots (V013).
  • What a copy is. The same elements as the template, built where the use stands, checked as if you had written them there. A <use> is not a node: it takes no space in the layout, and to="#email" naming one is B005.

The first <use> in the example is name, and the second is email. Type into the first field, move to the second with ctrl+n, type, submit, and save:


 Hello, Ada

 Name
 Ada

 Email
 ada@x
 not an email

 ctrl+s save  ctrl+n email
$ tuimark play field.tui --data field.json --cols 40 --rows 12 --input "text:Ada ctrl+n text:x enter ctrl+s"
4 submit email__input {} "ada@x"
5 save email__input {} "ada@x"

The heading reads Hello, Ada because the first copy's input wrote what you typed to form.name.value. The events say which copy fired them: email__input. Both of those follow from the two sections below.

Scoped ids

Two copies of one template cannot both have an input called input, so a copy's ids are prefixed with the id of its use and two underscores: the input of <use id="email"> is email__input. This is the same instance__part shape the kit uses for its ids, and it is what your own references use:

$ tuimark dump field.tui --data field.json --cols 40 --rows 12
=== nodes (those with an id) ===
main          screen  40x12 @(0,0)
name__input   input   38x1 @(1,4) focus
email__input  input   38x1 @(1,7)
  • Outside the copy, write the scoped id. The keymap row <bind keys="ctrl+n" action="focus" to="#email__input"/>, the screen's focus="#name__input", a stylesheet rule #email__input { … }, and play's focus:#email__input all name the copy's element. The host sees the scoped id too: the source of an event is email__input, and a host that routes by the part before the last __ gets email.
  • Inside the copy, write the id as the template has it. In a template, focus="#no" on a modal means the no button of the same copy, and each copy resolves it to its own: del__no for <use id="del">.
  • A reference to the unscoped id from outside is B005, and the message names the one you want:
<tui>
  <template id="confirm">
    <modal id="dlg" open="c.open">
      <button id="no" label="No" on:click="no"/>
    </modal>
  </template>
  <keymap>
    <bind keys="n" action="focus" to="#no"/>
  </keymap>
  <screen id="main">
    <use id="del" template="confirm" with="ui.del as c"/>
  </screen>
</tui>
$ tuimark validate ref-bad.tui
error B005 ref-bad.tui:8:5 /keymap/bind to="#no" refers to an id that does not exist; ids in a <template> are scoped by their <use>: #del__no
  • A use needs an id when its template has ids (V012: the message lists them and the scoped form they take). A template with no ids can be used without one.
  • Nesting adds a prefix. A template that uses another one scopes the inner ids by both uses: billing__street__input below.
  • A use's id is an id of the document, so it must not repeat another (V004). A copy's ids never clash with another copy's.

A confirmation dialog is a good template, because its ids, the modal's focus, and its keys all stay inside the copy. Here the keymap row reaches the copy's modal by its scoped id, and y, tab, and enter act on it:

<tui>
  <template id="confirm">
    <modal id="dlg" open="c.open" focus="#no">
      <text>{c.question}</text>
      <row height="1" gap="1">
        <button id="yes" label="Yes" on:click="yes"/>
        <button id="no" label="No" on:click="no"/>
      </row>
    </modal>
  </template>
  <keymap>
    <bind keys="y" action="yes_pressed" when="#del__dlg" label="yes"/>
  </keymap>
  <screen id="main">
    <button id="open" label="Delete…" on:click="ask"/>
    <use id="del" template="confirm" with="ui.del as c"/>
  </screen>
</tui>
$ tuimark play confirm.tui --data confirm.json --cols 30 --rows 8 --input "y tab enter"
1 yes_pressed del__no {} null
3 yes del__yes {} null

The y row fired with del__no as the source, the button the modal's focus="#no" selected; then tab and enter clicked del__yes. Pressing esc would close the modal, because open="c.open" is a path the runtime can write through the alias.

What with does

with="path as alias" makes alias stand for path inside the copy. Every path in the copy that starts with the alias goes to that path in the store: {f.label} reads form.email.label, and bind="f.value" reads and writes form.email.value. That is why the typed text above reached form.name.value. Nothing is copied; the alias is a name for the path, so an input in a copy edits the same data a {form.name.value} anywhere else shows.

  • Several clauses are separated by exactly a comma and a space: with="form.one as x, form.two as y". Anything else is V011, as is an alias written twice in one with.
  • The target may not exist yet. An input in the copy creates the path when you type. The use reports one B003 for the missing path, and nothing more from the copy (see a missing with path).
  • Nested uses compose. In parts/forms.tui below, address reads a.street and passes it on as f to field; the input ends up bound to order.billing.street.value.
  • A copy sees the aliases in scope where its use stands. A use inside a list row or an each container can read the row's alias without being given it, and a with path can start with it: with="j.status as s". The template then depends on where it is used, so give a reusable template a with for everything it reads.
  • An inner each can reuse the name of a use-site alias. In the template, each="g.items as r" shadows the r of the surrounding list while the copy is built, and g still reads through with="r.group as g". An inner each alias that repeats a with alias, though, is V011 at that element. The copy is still placed.

Templates in a fragment

The two files of this example are the usual split. parts/forms.tui holds the templates; the document includes it and uses them twice:

<tui>
  <include src="parts/forms.tui"/>
  <screen id="main" focus="#billing__street__input">
    <col gap="1" pad="1">
      <use id="billing" template="address" with="order.billing as a"/>
      <use id="shipping" template="address" with="order.shipping as a" if="order.ship_elsewhere"/>
    </col>
  </screen>
</tui>

 ╭─ Billing ────────────────────╮
 │Street                        │
 │1 Main St                     │
 │ZIP                           │
 │94105                         │
 ╰──────────────────────────────╯

 ╭─ Shipping ───────────────────╮
 │Street                        │
 │                              │
 │ZIP                           │
 │                              │
 ╰──────────────────────────────╯

$ tuimark dump app.tui --data data.json --cols 34 --rows 16
=== nodes (those with an id) ===
main                     screen  34x16 @(0,0)
billing__street__input   input   30x1 @(2,3) focus
billing__zip__input      input   30x1 @(2,5)
shipping__street__input  input   30x1 @(2,10)
shipping__zip__input     input   30x1 @(2,12)

shipping has an if, which is the next section. A fragment that holds templates is checked on its own with only the checks its elements give themselves, because the data and the uses are in the documents that include it.

$ tuimark validate parts/forms.tui
ok (fragment checked on its own)

Guards and classes on a use

if and hidden on a <use> decide whether its copy exists in the frame, and they are read where the use stands: the with of the same use is not in scope for them (if="s.urgent" would read a path the use itself defines, which is B002), but an each alias is. The same use in a list row is decided once per row.

<tui>
  <style>
    .badge { reverse: true; }
    .hot { bold: true; }
  </style>
  <template id="badge">
    <text class="badge" class:hot="s.urgent">{s.label}</text>
  </template>
  <screen id="main" focus="#jobs">
    <list id="jobs" each="jobs as j" key="j.id">
      <item>
        <row gap="1">
          <text>{j.name}</text>
          <use template="badge" with="j.status as s" if="show_badges" class="cell"/>
        </row>
      </item>
    </list>
  </screen>
</tui>

The badge exists only while show_badges is truthy. A copy that is not placed has no ids, no focus, and no events, as if the use were not written:

build
deploy
lint
build ok
deploy failed
lint skipped
  • Per placement. The guard of a use in a list <item>, in the template of a container each, or in a table's row (below) is evaluated for each row, with the row's alias in scope. A guard whose path is missing is one B002 at the use, not one per row.
  • A static class on a use is added to every top-level element of the copy, after the classes the template wrote and before its class:NAME guards. Style it like any class:
// $ tuimark play rows.tui --data rows.json --cols 24 --rows 3 --input set:show_badges=true --format json
// excerpt: the nodes that have classes
[
  {"tag":"text","x":6,"y":0,"classes":["badge","cell"]},
  {"tag":"text","x":7,"y":1,"classes":["badge","cell","hot"]},
  {"tag":"text","x":5,"y":2,"classes":["badge","cell"]}
]

The badge comes from the template, cell from the use, and hot from the template's class:hot="s.urgent" guard, true for the deploy row only.

Tables

A <column> holds its cell as text and cannot hold a <use> (V016). To reuse a column, make the whole <column> the template, and put the <use> where the column would stand, directly in the <table>. The template's <item> row template can be placed the same way.

<tui>
  <style>
    .hot { bold: true; }
  </style>
  <template id="status">
    <column title="Status" width="8" class:hot="s.urgent">{s.label}</column>
  </template>
  <screen id="main" focus="#jobs">
    <table id="jobs" each="jobs as j" key="j.id" height="4">
      <column title="Job" width="1fr">{j.name}</column>
      <use template="status" with="j.status as s" if="show_status"/>
    </table>
  </screen>
</tui>
Job             Status
build           ok
deploy          failed
lint            skipped

A with path that starts with the table's row alias (j.status as s) is read for every row. The use's if and hidden stay per table: setting show_status to false takes the column away for all rows, header included:

Job
build
deploy
lint

Seeing what a copy produced

The expanded document is what every tool shows, so a copy is checked the way any element is. Three places tell you where it came from.

The dump. A copy's nodes appear in the node list with their scoped ids, as in the lists above. tuimark play and dump need no special flag.

inspect. Besides the cascade, tuimark inspect ends with the element a node was built from and the uses and includes that placed it, innermost first:

$ tuimark inspect app.tui --data data.json --cols 34 --rows 16 --id shipping__zip__input
…
source parts/forms.tui:5:7
via <use id="shipping__zip" template="field"> parts/forms.tui:11:7
via <use id="shipping" template="address"> app.tui:6:7

The input shipping__zip__input was written at parts/forms.tui:5:7, placed by the zip use inside address (line 11), which the shipping use placed (app.tui, line 6). With --json the same information is the source member:

// $ tuimark inspect field.tui --data field.json --cols 40 --rows 12 --id email__input --json
// excerpt: "source"
{
  "source": {
    "file": "field.tui",
    "line": 9,
    "col": 7,
    "via": [
      {
        "use": "email",
        "template": "field",
        "file": "field.tui",
        "line": 21,
        "col": 7
      }
    ]
  }
}

A node the document writes itself has "via": [], and a document that writes version has no source at all. A use without an id is listed as <use template="…">.

ir. tuimark ir keeps the composition as written: a use node holds the copy it placed, in the template's own words (f.value, {f.label}), with the scoped id and a from member, FILE:LINE of the element the node came from. Read the aliases through the with of the enclosing use:

// $ tuimark ir field.tui
// excerpt: the <use id="email"> node and what it placed
{
  "kind": "use",
  "id": "email",
  "attrs": {
    "template": "field",
    "with": "form.email as f"
  },
  "children": [
    {
      "kind": "col",
      "from": "field.tui:7",
      "children": [
        {
          "kind": "text",
          "classes": [
            "label"
          ],
          "attrs": {
            "class:invalid": "f.error"
          },
          "text": "{f.label}",
          "from": "field.tui:8"
        },
        {
          "kind": "input",
          "id": "email__input",
          "attrs": {
            "placeholder": "{f.hint}"
          },
          "bind": "f.value",
          "from": "field.tui:9"
        },
        {
          "kind": "text",
          "classes": [
            "invalid"
          ],
          "if": "f.error",
          "text": "{f.error}",
          "from": "field.tui:10"
        }
      ]
    }
  ]
}

from is on every node a use or an include placed, and absent on a node the document wrote in its own file.

Diagnostics

The codes a template can raise, with the page's examples where there is one. The diagnostics reference has the full text.

CodeWhen
V019a <use> without template, or one that names no template; or one that closes a cycle
V021the limits: elements, source bytes, nesting depth
V011a with that is malformed, repeats an alias, or uses one already in scope; an inner each alias equal to a with alias
V012a <template> without id; a <use> without id whose template has ids
V013text directly in a <template>; content in a <use>
V001, V002, V004a template in the wrong place; an attribute a <use> does not take; a duplicate template or use id
B002, B003a missing if/hidden path at a use; a missing with path or a missing path in a copy
B005a to=, when=, or focus= that names a use, or names an id only the template writes

Cycles

A template that reaches itself through the uses in its body would never stop expanding, so each use on the cycle is V019, and none of them expands. A reference to a template that does not exist is V019 too:

<tui>
  <template id="menu">
    <col>
      <text>menu</text>
      <use template="submenu"/>
    </col>
  </template>
  <template id="submenu"><use template="menu"/></template>
  <template id="self"><use template="self"/></template>
  <screen id="main">
    <use template="menu"/>
    <use template="nope"/>
  </screen>
</tui>
$ tuimark validate cycle.tui
error V019 cycle.tui:5:7 /template/col/use <use template="submenu"> in <template id="menu"> makes a cycle of templates: menu → submenu → menu
error V019 cycle.tui:8:26 /template/use <use template="menu"> in <template id="submenu"> makes a cycle of templates: submenu → menu → submenu
error V019 cycle.tui:9:23 /template/use <use template="self"> in <template id="self"> makes a cycle of templates: self → self
error V019 cycle.tui:12:5 /screen/use <use template="nope"> names no <template>

Each message names a simple cycle through the use, starting from its own template, so the one you read is the loop that use closes. A long one is cut: up to 8 templates are named whole, and a longer cycle shows its first 8, then … (N more), then the template it returns to. A cycle of ten templates reports ten uses, each starting at its own template:

$ tuimark validate long-cycle.tui
error V019 long-cycle.tui:2:21 /template/use <use template="t1"> in <template id="t0"> makes a cycle of templates: t0 → t1 → t2 → t3 → t4 → t5 → t6 → t7 → … (2 more) → t0
… (9 more lines)

A cycle is reported whether or not any use reaches it. One that only copies meet (possible once the template definitions' own V021 has stopped a body) is reported once, at the use that closes it, however many copies meet it.

Limits

Because a copy of a copy of a copy grows fast, expansion has a budget, shared with <include>. A document that would pass any of these is V021:

  • 100000 elements inserted by includes and uses, counted once per copy, at every depth;
  • 16 MiB of placed source, counting the fragments and templates copied, the scoped-id prefixes, and the with paths substituted into each copy;
  • 256 levels of nesting in the resulting tree, counting each <include> and <use> as a level.

The use that would go over inserts nothing, and no later include or use expands, so the rest of the document is still reported. Five uses of a template in each of seven templates, one inside another, is 5⁷ leaves:

<tui>
  <template id="t0"><use template="t1"/><use template="t1"/><use template="t1"/><use template="t1"/><use template="t1"/></template>
  <template id="t1"><use template="t2"/><use template="t2"/><use template="t2"/><use template="t2"/><use template="t2"/></template>
  <template id="t2"><use template="t3"/><use template="t3"/><use template="t3"/><use template="t3"/><use template="t3"/></template>
  <template id="t3"><use template="t4"/><use template="t4"/><use template="t4"/><use template="t4"/><use template="t4"/></template>
  <template id="t4"><use template="t5"/><use template="t5"/><use template="t5"/><use template="t5"/><use template="t5"/></template>
  <template id="t5"><use template="t6"/><use template="t6"/><use template="t6"/><use template="t6"/><use template="t6"/></template>
  <template id="t6"><use template="t7"/><use template="t7"/><use template="t7"/><use template="t7"/><use template="t7"/></template>
  <template id="t7"><spacer/></template>
  <screen id="main"><use template="t0"/></screen>
</tui>
$ tuimark validate fan.tui
error V021 fan.tui:8:81 /screen/use use template="t7" exceeds the expansion limit (100000 elements or 16 MiB of placed source): it inserts nothing, and no later include or use expands

The error is at the use that went over, t7 in t6, with the line and column of that use. A fragment included inside a <template> has a budget of its own, so a template whose include would fan out is reported even when no use copies it: the message ends exceeds the expansion limit for template definitions, and the template is checked without that include.

Messages about a copy

An element that a use placed is written once in the template and exists once per copy, so a diagnostic about it is reported at the template's line and has to say which copy it means. There are two forms.

A path that went through a with alias ends with the path it stands for and the use that gave it: (P via <use …>). In the form below the template reads f.help and the use email made that form.email.help; the phone use has no form.phone, so the use is the problem, and the copy says nothing more:

<tui>
  <template id="field">
    <col>
      <text class:invalid="f.error">{f.label}</text>
      <input id="input" bind="f.value"/>
      <text if="f.error">{f.error}</text>
      <text>{f.help}</text>
    </col>
  </template>
  <screen id="main">
    <use id="email" template="field" with="form.email as f"/>
    <use id="phone" template="field" with="form.phone as f"/>
  </screen>
</tui>
$ tuimark validate missing.tui --data missing.json
warning B003 missing.tui:7:7 /screen/col/text bind path "f.help" is missing (form.email.help via <use id="email">)
warning B003 missing.tui:12:5 /screen/use with path "form.phone" (alias f) is missing
ok

A use with an id is named by it (<use id="email">). A use without one is named by its template (<use template="badge">), and when the path starts with an each alias, which reads the same in every row, the message adds where the use is written, so you can find it: at FILE:LINE:COL.

$ tuimark validate rows.tui --data rows-bad.json
warning B003 rows.tui:7:5 /screen/list/item/row/text bind path "s.label" is missing (j.status.label via <use template="badge"> at rows.tui:14:11)
ok

Any other diagnostic about a copy (a V012 at a copied box, a store flag that is missing) ends with , placed by <use …> at FILE:LINE:COL, naming the use that placed the element, and its file, line, and column. A use inside another template is named by its scoped id (<use id="top__in">), and its location is the line where it is written in the template. An element placed by an <include> ends with , placed by <include> at FILE:LINE:COL in the same way.

A missing with path and optional flags

  • A with path that is missing is one B003 at the use, a warning (an error under --strict), as above for phone. The copy's own paths are not reported, because each one would say the same thing, so you fix the one line. The copy is still placed, and an input in it still writes the path.
  • A flag in the copy is optional. When the alias stands for an object, if, hidden, disabled, open, and class:NAME guards read through it are null if the member is missing, without a B002: if="f.error" is how a form field shows an error only when the host has written one, and missing.tui above does not report f.error. A {path} or a bind is still B003 when missing. When the alias stands for something that is not an object, every flag reports.
  • A store flag is an ordinary one. A path that does not start with an alias (hidden="debug") is B002 when missing, once per copy: , placed by <use id="email"> at ….

Long ids and paths

A message quotes at most 64 bytes of an id or a path, cut at a character boundary and followed by …, so one very long name cannot make a diagnostic unreadable. Both the path in the copy and the path it stands for are cut, and so is a template name:

$ tuimark validate long.tui --data long.json
warning B003 long.tui:3:5 /screen/text bind path "f.settings.profile.preferences.notifications.email.digest.freque…" is missing (user.settings.profile.preferences.notifications.email.digest.fre… via <use id="u">)
error V019 long.tui:7:5 /screen/use <use template="another_template_whose_name_is_longer_than_sixty_four_bytes_so_n…"> names no <template>

--json carries the message as printed. The full names are in the source.

Tools and templates

  • validate, fmt, inspect, run, and preview --watch know about fragments and copies.
  • The language server checks a fragment through the documents that include it and follows template= and scoped ids across files.
  • play and dump in a test see the expanded document, so a golden does not change when you move a piece into a fragment, as long as the expansion is the same.
  • The kit is a fragment of templates: include kit/kit.tui once and copy its components with <use>.
Edit on GitHub

Last updated on

On this page