Skip to content

Forms

Represent each field with its own signal, editable value, or draft. A record of reactive fields lets each edit update only the readers of that field.

A form as a record of signals

A record of reactive fields lets each edit update the readers of that field alone.

A two-field person form

Both fields use drafts so that upstream reloads preserve local edits. The helpers collect a snapshot, reset both fields in a batch, and report whether either has been edited.

type Person = { Name: string; Age: int }

type PersonForm =
    {
        Name: Editable<string>
        Age: Editable<int>
    }

module PersonForm =
    let create (person: Signal<Person>) =
        {
            Name = createDraft (fun _ -> person.Value.Name)
            Age = createDraft (fun _ -> person.Value.Age)
        }

    let snapshot (form: PersonForm) : Person =
        { Name = form.Name.Value; Age = form.Age.Value }

    let reset (form: PersonForm) =
        batch (fun () ->
            form.Name.Reset ()
            form.Age.Reset ())

    let isDirty (form: PersonForm) =
        form.Name.IsEdited || form.Age.IsEdited
  • A field is a Signal<'T> when the form owns the value, and an Editable<'T> when it comes from upstream.
  • snapshot and a whole-record set are written by hand once per type. Put a multi-field set in a batch, so an effect that reads several fields runs once.
  • isDirty is tracked: a memo or an effect over it wakes when any field's edit starts or ends.
Compare with a whole-record signal

Holding the whole record in one signal and reading fields through selectors costs a record copy and a selector run per field on each write. See Migrating from Elmish.

Edit this page