html
! Elements described, compared, and changed where they differ. Web only. ! ! A Node is a description: a tag, its attributes and its children, held as ! an ordinary value. [mount] builds it once and remembers it; [update] ! compares a new description against the remembered one and touches only the ! parts that differ. ! ! ``kite ignore ! use std/html ! ! fn row(app: App, r: Row) -> html.Node { ! return html.keyed("\(r.id)", html.el("tr", [], [ ! html.txt("td", [html.class("num")], "\(r.id)"), ! html.txt("td", [html.click(|e: dom.Event| { open(app, r.id) })], r.name), ! ])) ! } ! ! var view = html.mount(body, map(rows, row)) ! html.update(view, map(next, row)) ! ` ! ! **Two constructors, not one per tag.** el and txt take the tag as a ! string, so this is a page of code rather than a hundred and ten ! near-identical functions, and a tag added to HTML next year works the day it ! ships. A mistyped tag becomes a <flase> in the document rather than a ! compile error, which is the price. ! ! **What a press does is written where the control is.** [click] and its ! neighbours are attributes like any other, so the handler sits in the same ! expression as the class and the label — and it closes over whatever is in ! scope there, which is normally the thing the press is about. ! ! The alternative, and what this module used to force, is a data-action ! naming the control and one delegated listener elsewhere that matches on the ! name. That works and stays readable at about forty controls. At a hundred ! and fifty it stops: nothing checks that a name written on a button is a name ! anybody matches, so a control can draw, take the press, and do nothing — ! silently, and in numbers. It also loses the closure, so everything the ! handler needs has to be encoded into data- attributes and parsed back. ! Both failures are unrepresentable here. ! ! A listener is attached **once per element and event**, at the moment that ! element first describes a handler for it. A repaint replaces the stored ! closure rather than the listener, so re-describing a tree costs no ! addEventListener calls at all, and an element keeps its focus, its scroll ! position and its listeners across every update that reuses it. ! ! **Give list items a key.** Without one, children are matched by position, so ! a list that reorders rewrites the text of every element it moved past. With ! one, the element itself moves and its contents are left alone — which is the ! difference between a sort touching a hundred elements and five hundred. ! ! Class names pass through exactly as written. A stylesheet somebody else ! wrote is the thing styling this. ! ! A Node that carries a handler is not Share`, because a closure that ! reaches a host value is not. Nothing here was ever going to cross to another ! isolate — a description exists to become elements in one document — so the ! marker is noted rather than worked around.
AttrNodeMountedattrclassiddataonclickinputchangesubmitkeydownfocus_infocus_outtexteltxtemptykeyedmountupdatedeparted
Attr
struct Attr
One attribute, or one handler.
Both, in one type, because both are things written in the same list about the same element — which is how they read at the call site and how JSX, where most readers meet the idea, spells them too. run is what tells them apart: an ordinary attribute has none, and a handler's name is the event rather than an attribute name.
name: strvalue: strrun: Option<fn(dom.Event)>— What to run whennamehappens here. Absent for a real attribute.
Node
struct Node
A described element, or a run of text.
A text node is a Node whose tag is empty. One type rather than two keeps [Node] the only child list there is.
tag: strbody: strkey: strattrs: [Attr]kids: [Node]
Mounted
struct Mounted
What was built, kept so the next description has something to compare against.
Held by the caller rather than in a table inside this module: a page with three islands has three of these, and nothing here has to know how many there are or when one goes away.
into: dom.Elementvar live: [Live]var gone: [str]— The keys of the children the last [update] removed. Read it with [departed].
attr
pub fn attr(name: str, value: str) -> Attr
class
pub fn class(names: str) -> Attr
The class attribute, taken whole — the way it is written everywhere else.
id
pub fn id(name: str) -> Attr
data
pub fn data(name: str, value: str) -> Attr
A data- attribute. The prefix is added here so a caller writes the name.
on
pub fn on(event: str, run: fn(dom.Event)) -> Attr
Run this when event happens on this element.
click
pub fn click(run: fn(dom.Event)) -> Attr
input
pub fn input(run: fn(dom.Event)) -> Attr
Every keystroke in a field, as it is typed. dom.event_value is what it now says.
change
pub fn change(run: fn(dom.Event)) -> Attr
A field that has finished changing: a select, a checkbox, a blurred text box.
submit
pub fn submit(run: fn(dom.Event)) -> Attr
A form being submitted. Almost always paired with dom.prevent_default, because the browser's own submit navigates away from the program.
keydown
pub fn keydown(run: fn(dom.Event)) -> Attr
focus_in
pub fn focus_in(run: fn(dom.Event)) -> Attr
focus_out
pub fn focus_out(run: fn(dom.Event)) -> Attr
text
pub fn text(body: str) -> Node
A run of text.
el
pub fn el(tag: str, attrs: [Attr], kids: [Node]) -> Node
An element with children.
txt
pub fn txt(tag: str, attrs: [Attr], body: str) -> Node
An element whose only child is text.
empty
pub fn empty(tag: str, attrs: [Attr]) -> Node
An element with nothing in it: an <input>, an <img>.
keyed
pub fn keyed(key: str, node: Node) -> Node
The same node, identified.
Two nodes with the same key are the same thing in a new position. Without a key an element is identified by where it sits, which is right for a fixed layout and wrong for a list.
mount
pub fn mount(into: dom.Element, nodes: [Node]) -> (Mounted, error)
Build a description inside into, replacing whatever was there.
update
pub fn update(var view: Mounted, nodes: [Node]) -> error
Compare a new description against what is there, and change the difference.
departed
pub fn departed(view: Mounted) -> [str]
The keys whose elements left the document during the last [update].
State kept per row — what a useState inside a list item would have been, which here is a {str: T} in the program's own store — has to be dropped when the row goes, and this is the only place that knows which rows went. Without it the map grows for the life of the program and the pruning is hand-written against a list the caller has to re-derive.
let err = html.update(view, rows)
for key in html.departed(view) {
forget_draft(store, key)
}
Every key in a removed subtree is reported, not just its root: a keyed row takes its keyed children with it. The list is replaced by each update, including one that removed nothing.