Directives
elements man html/directives Read as markdownThe e:-prefixed attributes: e:for, e:if / e:elseif / e:else, and
e:switch / e:case / e:default.
Directives are e:-prefixed attributes. They compose with normal attributes on
the same element. On an element with both e:if and e:for, the e:if is
evaluated first: a false e:if means the element does not render at all, and a
true e:if allows e:for to iterate. It is all-or-nothing for the whole loop.
e:if cannot filter individual rows. It decides whether the loop runs, not
which items it runs over. To show a subset, filter the iterable, as below. Do
not render every row and hide some with a class or display: none: the rows
are still there, in the DOM and in the accessibility tree, and counts taken
from the list will be wrong.
e:for
Iterates over any iterable. The result is reactive: updates patch the DOM in place at the row level.
<li e:for={user of users}>{user.name}</li>
<li e:for={[index, user] of users.entries()}>{index}: {user.name}</li>
<li e:for={key in lookup}>{key}: {lookup[key]}</li>
Filtering and shaping. The right-hand side is an expression, so filter, sort, or reshape the rows there. The loop re-evaluates the expression when anything it read changes, and reconciles the new row set against the old one by key, so a row present in both keeps its dom.
<!-- All / Active / Done over a LiveView, live in every browser -->
<button onclick={() => view.filter = "active"}>Active</button>
<li e:for={todo of todos.filter((t) => matches(t, view.filter))}>
{todo.title}
</li>
A helper works the same way. A function that builds its own array from the
rows, feed(messages) say, is live because the loop's evaluation runs the
helper, and the helper iterated the view. Whatever the expression reads on the
way, a tab, a search string, the view itself, becomes something the loop
follows.
LiveView supports filter, sort, map, find, at, length, and the
rest of the non-mutating Array surface directly, and each returns a plain
array. todos.filter(...) is the whole answer for a list already on screen;
nothing is re-fetched. (recipes/search-filter is the other case: a
server-side re-query for a result set too large to hold in the browser.)
Grouping is Map.groupBy with e:key, since a [key, rows] entry has no
id:
<section e:for={[day, msgs] of Map.groupBy(messages, dayOf)} e:key={([day]) => day}>
<h3>{day}</h3>
<li e:for={m of msgs}>{m.body}</li>
</section>
for...of and for...in work the same as in JavaScript: of iterates arrays
and other iterables; in iterates object keys, string characters, and similar.
The Elements runtime patches for...of at the row level. for...in re-renders
the whole list when the right side changes.
By default rows are keyed by the id field on each item. Provide an id and
the runtime diffs by it, patching only the rows that changed. Without an id,
rows key by object identity, so replacing the array with fresh objects (say, a
refetch) re-renders every row.
Use e:key to supply your own key function when a row has no id, or when the
stable key isn't a field on the row, such as iterating Object.entries,
composite keys, and so on. It takes the iteration value and returns a string or
number:
<li e:for={[index, item] of Object.entries(items)} e:key={([index, item]) => item.id}>
{index} - {item.description}
</li>
The key function's parameter is the e:for iteration value, typed the same as
the loop binding, so a wrong destructure or a missing field is a compile error.
e:key pairs with e:for on the same element (either order).
e:if / e:elseif / e:else
<div e:if={status === "loading"}>loading</div>
<div e:elseif={status === "error"}>error: {error.message}</div>
<div e:else>ready</div>
Branches must be siblings. Whitespace between them is allowed.
e:switch / e:case / e:default
Parent holds the expression, children are cases.
<div e:switch={status}>
<span e:case="active">active</span>
<span e:case="paused">paused</span>
<span e:default>unknown</span>
</div>