# LeanUI templates LeanUI views are values of type `Html Msg`. They can be built with the smart constructors of `LeanUI.Html` (`el`, `text`, `Attr.*`), or written as JSX-style templates: ```lean module public import LeanUI meta import LeanUI.Syntax -- templates are compile-time only public section open LeanUI def item (t : Todo) (editing : Bool) : Html Msg :=
  • .toggle t.id} />
  • ``` A template is an ordinary Lean term. It is type-checked like the code it stands for, it can appear wherever an `Html Msg` can, and Lean expressions go inside `{…}`. Contents: [setup and cost](#setup-and-cost) · [elements](#elements) · [attributes](#attributes) · [events](#events) · [children and text](#children-and-text) · [fragments](#fragments) · [components](#components) · [namespaces](#namespaces-svg-and-mathml) · [where templates can appear](#where-templates-can-appear) · [errors](#errors-and-warnings) · [grammar](#grammar) · [limitations](#limitations) · [how it works](#how-it-works) ## Setup and cost Add `meta import LeanUI.Syntax` to a module that writes templates. Nothing else is needed: the syntax is active in that module only (modules importing it do not see it, and need their own `meta import` to write templates). The import is `meta` because templates cost nothing at run time. `LeanUI.Syntax` and the Lean compiler it is built on run while your module compiles and are never linked into the program. A template expands to the constructors you would write by hand (`Html.elem`, `Html.text`, `Attr.attr`, `Attr.prop`, `Attr.key`, the `Attr.on*` helpers, array literals, `toString`); every decision that depends on the type of an interpolated value is made by the elaborator, not by a type class at run time. The example above expands to ```lean Html.elem Ns.html "li" #[Attr.key (toString t.id), Attr.attr "class" (("todo" ++ if t.done then " completed" else "") ++ if editing then " editing" else "")] #[Html.elem Ns.html "input" #[Attr.attr "class" "toggle", Attr.attr "type" "checkbox", Attr.prop "checked" (PropValue.bool t.done), Attr.onCheck fun _ => Msg.toggle t.id] #[], Html.elem Ns.html "label" #[Attr.onDblClick (Msg.edit t.id)] #[Html.text t.title], Html.elem Ns.html "button" (let xs := #[Attr.attr "class" "destroy", Attr.onClick (Msg.remove t.id)]; if editing then xs.push (Attr.attr "disabled" "") else xs) #[]] ``` Measured on `examples/Counter.lean`, which is written as a template: - the generated C is the same as for the hand-written view except for the numbering of constants; its runtime initializer calls `Init` and `LeanUI` only, and the file mentions no `Lean.*` and no `LeanUI.Syntax` declaration (checked by `leanui-syntax-tests`); - the wasm64 build is 1,034,481 bytes (hand-written: 1,034,487), and the browser test `tests/browser/counter.mjs` sends the same 7 flushes, 279 commands and 1,250 bytes in Chromium and Firefox; - the native test binary that uses templates links no compiler code (about 1.9 MB; a runtime `import Lean` would add about 100 MB). ## Elements ```lean
    …
    -- element with children -- self-closing -- custom elements (with a dash) are fine ``` - The `<` must touch the tag name: `, found `. - Void elements (`area base br col embed hr img input link meta source track wbr`) cannot have children: write `
    `, ``. - Tag names are case-sensitive and are emitted as written (SVG needs `linearGradient`, `foreignObject`, …). A name starting with an uppercase letter is a [component](#components). - An unknown lowercase HTML element without a dash gets a warning (`` is probably a typo); so does an unknown SVG or MathML element inside ``/``. Use an explicit prefix (``) to opt out. ## Attributes Attribute names may contain letters, digits, `-`, `_` and `:` (`data-id`, `aria-label`, `xlink:href`, `accept-charset`). Lean keywords are fine as names (`for`, `open`, `default`, `in`). | Template | Becomes | |---|---| | `name="text"` | `Attr.attr "name" "text"` (a Lean string literal, with Lean escapes) | | `name={e}` | `Attr.attr "name" s`, `s` the string form of `e` (below) | | `name` | `Attr.attr "name" ""` (a boolean attribute that is present) | | `disabled={b}` (HTML boolean attributes) | `Attr.attr "disabled" ""` when `b`, nothing otherwise | | `value={e}`, `checked={b}`, `selected={b}`, `indeterminate={b}`, `muted={b}` | `Attr.prop …` (live form state, below) | | `.name={e}` | `Attr.prop "name" v` (a DOM property) | | `.name="text"` / `.name` | `Attr.prop "name" (.str "text")` / `Attr.prop "name" (.bool true)` | | `name?={o}` | `Attr.attr "name" …` when `o = some v`, nothing for `none` | | `class="a b"`, `class={e}`, `class:x={b}`, `class:x` | one `class` attribute (below) | | `style="…"`, `style={e}`, `style:p={v}`, `style:p="v"` | one `style` attribute (below) | | `key="k"`, `key={k}` | `Attr.key …` (reconciliation key) | | `onClick={m}` … `on:event={h}` | listeners, see [events](#events) | | `{...attrs}` | splices an `Array (Attr Msg)`, a `List`, or an `Option` | | `if={c}` | the element is a child only when `c` (see [conditional children](#conditional-children)) | Attributes keep their source order (except that the `class` and `style` parts are gathered into one attribute at the position of the first of them). **String forms of values.** In `name={e}`, `key={e}`, `style:p={e}`: | type of `e` | string | |---|---| | `String` | `e` itself | | `Nat`, `Int`, `Float`, `Char`, `UInt*`, … | `toString e` (`7`, `-3`) | | `Bool` | `"true"` / `"false"` (right for `aria-*`, `draggable`, `spellcheck`, `contenteditable`) | | any type with a `ToString` instance | `toString e`: this is the extension point for your own types | | `Option _` | error: use `name?={e}` (or `e.getD …`) | | `List`, `Array`, `Html` | error | The conversion is chosen while the template compiles; for a `String` no code is added at all. A value whose type is not known yet (say `i` in `#[1, 2].map fun i =>
  • `) waits until Lean has decided it (`Nat` there). **Boolean attributes.** For HTML's boolean attributes (`allowfullscreen async autofocus autoplay controls default defer disabled formnovalidate hidden inert ismap itemscope loop multiple nomodule novalidate open playsinline readonly required reversed`), `{b}` decides whether the attribute is present, as HTML requires (`disabled="false"` would disable). Without a value they are present. **Live form state.** `value={…}`, `checked={…}`, `selected={…}`, `indeterminate={…}` and `muted={…}` set the DOM *property*. An attribute only sets the initial value, so an input whose attribute follows the model stops following it once the user edits it. With a string literal (`value="x"`, `checked`) they stay attributes: the initial state, as in HTML. Server-side rendering prints properties as attributes, so the page is right before any wasm runs. **Properties.** `.name={e}` sets any DOM property: a `String` becomes `PropValue.str`, a `Bool` `PropValue.bool`, a `Float` `PropValue.num`, a `PropValue` is used as is, other `ToString` values become strings (`.tabIndex={2}`). **Optional attributes.** `title?={hint?}` adds `title` only for `some`; the payload is converted like any value (`data-n?={count?}` with `count? : Option Nat`). **Classes.** ```lean
  • ``` For an either/or, use `{if c then else }`; for an optional child, `{...o.map view}`. ## Fragments `<>…` is an `Array (Html Msg)`. As a child it is spliced in place. ```lean def pair (k v : String) : Array (Html Msg) := <>

    {k}
    {v}
    <>
    a
    1
    -- two children of
    {...entries.map fun (k, v) => <>
    {k}
    {v}
    }
    -- an array of fragments is flattened ``` ## Components A tag that starts with an uppercase letter calls a function. Attributes become named arguments, and the children become the `children` argument: ```lean def Card (title : String) (children : Array (Html Msg) := #[]) (featured := false) : Html Msg :=

    {title}

    {...children}
    -- Card (title := "Hello") (featured := true) (children := #[…])

    body

    -- Card (title := t) -- qualified names ``` - `"text"` passes a string, `{e}` passes `e`, a bare name passes `true`; - `key={k}` puts the key on the element the component returns (so components can be keyed list items); - `if={c}` works as for elements; - a component whose result is an `Array (Html Msg)` (a fragment) is spliced where it is used; - arguments must be valid Lean identifiers; attribute splices are not supported on components. A component with its own message type is embedded with `Html.map`: ```lean
    {( : Html Child.Msg).map Msg.child}
    ``` `
    ` is rejected with a hint (the name would otherwise resolve to the `Div` class), and so is a name that is not a function returning `Html`. ## Namespaces: SVG and MathML Elements inside `` are created with the SVG namespace and elements inside `` with MathML's, automatically. `` switches its children back to HTML. ```lean

    HTML again

    Euler: e=2.718

    ``` A template inside `{…}` starts fresh, but SVG-only and MathML-only tags (`circle`, `path`, `g`, `rect`, `text`, `mi`, `mrow`, …) always get their namespace, so `{...pts.map fun p => }` is right. The names shared by HTML and SVG (`a`, `title`, `script`, `style`) follow the lexical context; force one with a prefix: ``, ``, ``. ## Where templates can appear Anywhere a term can, including as a function argument (`pure
    `, `Html.map f `, `xs.push
  • `), as `fun` and `match` bodies, in `if` branches and array literals. **After a `let`.** A template at the start of a line right after `let x := v` is read by Lean as `v < …` (`<` is also less-than, and a binary operator continues an expression across lines). End the `let` with `;`, or parenthesize the template, or use `where`: ```lean def view (m : Model) : Html Msg := let left := count m; {left} left ``` Without the `;` the error says exactly this. **Ordinary code is unaffected:** `a < b`, `a
  • a
  • ` | `expected , found ` | | `
    ` | `expected , found ; is a void element: write ` | | `
    x` | `expected
    , found ` | | `

    a > b

    ` | `'>' cannot appear in JSX text; write > or {">"}` | | text at column 0 inside a template | `template text starts in column 0 here; indent it (or, if this is Lean code, an element above is missing its closing tag)` | | `let n := 1` + newline + `

    …

    ` | `this template starts a line, but < is also less-than, …; end that line with ; or put the template in parentheses` | Elaboration errors and warnings (selection; `tests/Syntax.lean` checks the exact text of each): unknown or misspelled event shorthands, inline JavaScript handlers, wrong value forms (`onClick="x"`, `class:done="yes"`, `key` without a value, `title?="x"`), duplicates (attribute, `class`, `key`, two listeners for one event), `Option` values without `?`, collections where one child is expected, `Bool` children, values without `ToString`, void elements with children, `
    `, non-components, `if` outside a child or without a condition, React attribute names, unknown elements and unknown character references (warnings). ## Grammar ``` element ::= '<' NAME attr* '/>' | '<' NAME attr* '>' child* '' | '<' '>' child* '' -- fragment attr ::= NAME '=' STRING | NAME '=' '{' term '}' | NAME | '{...' term '}' child ::= element | TEXT | '{' term '}' | '{...' term '}' | '{' '}' NAME ::= [A-Za-z_.] [A-Za-z0-9_.:|?-]* -- `.prop`, `on:x`, `x?`, `onClick|prevent` TEXT ::= [^{}<>]+ ``` `<` must be followed immediately by the name (or `>`), with no space. ## Limitations - After `let x := v`, a template on the next line needs `;` (see [above](#where-templates-can-appear)); the error says so. - A Lean comment can only stand where a child starts; text cannot begin with `--` or `/-` (write `{"--"}`), and template text may not start in column 0. - Inside `{…}`, the tags shared by HTML and SVG (`a`, `title`, `script`, `style`) default to HTML; use `svg:a`. - A child whose type is unknown until after the template (bare numerals) is taken to be a view. - As a function argument, `<` followed by a complete opening tag is a template: `f ` is never ordinary Lean (`<` and `>` do not chain), so no comparison is affected. - Templates do not change what the runtime does: a `