Manual HTML Directives

Directives

elements man html/directives Read as markdown

The 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>