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 want | Use | Result |
|---|---|---|
| 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 isV001. It may stand before or after the uses that name it. Itsidis 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 staticclass. Any other attribute isV002, 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, andto="#email"naming one isB005.
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'sfocus="#name__input", a stylesheet rule#email__input { … }, andplay'sfocus:#email__inputall name the copy's element. The host sees the scoped id too: thesourceof an event isemail__input, and a host that routes by the part before the last__getsemail. - Inside the copy, write the id as the template has it. In a template,
focus="#no"on a modal means thenobutton of the same copy, and each copy resolves it to its own:del__nofor<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
idwhen 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__inputbelow. - 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 isV011, as is an alias written twice in onewith. - The target may not exist yet. An input in the copy creates the path when you type. The use reports one
B003for the missing path, and nothing more from the copy (see a missingwithpath). - Nested uses compose. In
parts/forms.tuibelow,addressreadsa.streetand passes it on asftofield; the input ends up bound toorder.billing.street.value. - A copy sees the aliases in scope where its use stands. A use inside a list row or an
eachcontainer can read the row's alias without being given it, and awithpath can start with it:with="j.status as s". The template then depends on where it is used, so give a reusable template awithfor everything it reads. - An inner
eachcan reuse the name of a use-site alias. In the template,each="g.items as r"shadows therof the surrounding list while the copy is built, andgstill reads throughwith="r.group as g". An innereachalias that repeats awithalias, though, isV011at 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 containereach, 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 oneB002at the use, not one per row. - A static
classon a use is added to every top-level element of the copy, after the classes the template wrote and before itsclass:NAMEguards. 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.
| Code | When |
|---|---|
V019 | a <use> without template, or one that names no template; or one that closes a cycle |
V021 | the limits: elements, source bytes, nesting depth |
V011 | a with that is malformed, repeats an alias, or uses one already in scope; an inner each alias equal to a with alias |
V012 | a <template> without id; a <use> without id whose template has ids |
V013 | text directly in a <template>; content in a <use> |
V001, V002, V004 | a template in the wrong place; an attribute a <use> does not take; a duplicate template or use id |
B002, B003 | a missing if/hidden path at a use; a missing with path or a missing path in a copy |
B005 | a 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
withpaths 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
withpath that is missing is oneB003at the use, a warning (an error under--strict), as above forphone. 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, andclass:NAMEguards read through it arenullif the member is missing, without aB002:if="f.error"is how a form field shows an error only when the host has written one, andmissing.tuiabove does not reportf.error. A{path}or abindis stillB003when 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") isB002when 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, andpreview --watchknow about fragments and copies.- The language server checks a fragment through the documents that include it and follows
template=and scoped ids across files. playanddumpin 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.tuionce and copy its components with<use>.
Widgets
Tuimark's built-in widgets - list, input, checkbox, select, button, progress and spinner, sparkline, table, tabs, hints and modal - with rendered examples, their events and the keys they take.
The kit
A copy-paste component kit for Tuimark - one file of templates you include, design tokens and k- component classes, small documents that show each of the 21 components, and a gallery app that shows them all.