Skip to content

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.

Edit this page