Lists and keyed rows
A list renders one row per item of an array read from the instance. Rows are keyed: when the array changes, an item whose key is still present keeps its row, its DOM nodes and its state, and moves to its new position. New keys create rows; missing keys dispose theirs.
The samples use the opens from Templates and bindings.
forEachFixed: a row per item
type Todo = { Id: int; Label: Signal<string>; Done: Signal<bool> }
let todoList =
Component.template (fun _ (todos: Signal<Todo array>) -> todos) (
ul (Attrs.className "todos") {
forEachFixed (fun (todo: Todo) -> todo.Id) (fun (todos: Signal<_ array>) -> todos.Value) (
li (Attrs.className (fun todo -> if todo.Done.Value then "done" else "open")) {
from (fun todo -> todo.Label)
}
)
}
)
forEachFixed key items row takes the key of an item, the reader of the items, and the row view. The row's instance is the item the row was created for. A later item with the same key keeps the row and its original item; a row follows changes through the signals the item holds (Label and Done above).
forEach: a row that follows its item
type Score = { Player: string; Points: int }
let scoreboard =
Component.template (fun _ (scores: Signal<Score array>) -> scores) (
table () {
tbody () {
forEach (fun (score: Score) -> score.Player) (fun (scores: Signal<_ array>) -> scores.Value) (
tr () {
td () { fun (row: unit -> Score) -> (row ()).Player }
td () { fun (row: unit -> Score) -> string (row ()).Points }
}
)
}
}
)
With forEach the row's instance is a reader, unit -> 'item, that returns the current item for the row's key. Writing a new array with { Player = "ada"; Points = 4 } in place of { Player = "ada"; Points = 3 } keeps Ada's row and re-runs its bindings. Use forEach for immutable items, and forEachFixed for items that hold their own signals.
Keys
- Keys are compared with
=. A key function must be pure. - An item keeps its key while it is in the list; to change an item's key, replace the item.
- Duplicate keys fail before any row changes.
- The items reader is tracked: the list re-runs when a signal it read changes.
The analyzers report this at build time: RAN021.
Rows and the outer instance
A row's bindings receive the row's instance only. A row reads the enclosing instance's state through the item itself, or through a signal or memo that the item or the definition holds. A read of the outer instance inside a row fails to compile (FS0041 through a text yield, FS0193 through a part).
The annotations in the samples follow the inference rule: the items reader reads a Signal, a class, and its parameter carries the annotation; the key lambda is annotated where the item's labels are also declared by a later record.
Server HTML
A list renders between region markers:
<ul class="todos"><!--[--><li class="done">Write</li><li class="open">Ship</li><!--]--></ul>
Hydration adopts the server's rows in order. A client list whose item count or row structure differs re-renders that list alone and reports the mismatch.
The explicit form
each key items row takes a row definition whose props are unit -> 'item:
let scoreRow =
Component.define (fun _ (row: unit -> Score) -> row) (fun t ->
t.tr
[||]
[|
t.td [||] [| t.text (fun (row: unit -> Score) -> (row ()).Player) |]
t.td [||] [| t.text (fun (row: unit -> Score) -> string (row ()).Points) |]
|])
let scoreboardExplicit =
Component.define (fun _ (scores: Signal<Score array>) -> scores) (fun t ->
t.table
[||]
[|
t.tbody
[||]
[| t.each (fun (score: Score) -> score.Player) (fun (scores: Signal<Score array>) -> scores.Value) scoreRow |]
|])
forEach key items row is each key items over a row definition whose setup returns the reader; forEachFixed is the same with a setup that calls the reader once. A row definition passed to each can be a full component with its own setup, which runs once per row.
For collection state itself (keyed projections, editable keyed sources and change readers) see Collections.