Skip to content

Saving and loading

Every field remembers a baseline: the value it had when the form was built. Comparing with it gives a dirty flag; writing it back is a reset; moving it is a save. Each piece below adds one button to the same profile form.

Knowing when there is something to save

Form.isDirty form () is true while any field differs from its baseline. It is a tracked read, so a button whose disabled reads it follows the fields. The submit button is drawn by hand here, with View.Action.Custom, to give it that attribute.

open Fable.Ripple
open Fable.Ripple.Dom
open Fable.Ripple.Form
open Fable.Ripple.Form.Plain

let name = Var.create "Ada"

let form =
    TextField.create "name"
    |> TextField.withLabel "Name"
    |> Field.create name Ok
    |> Form.textField

let state = Var.create View.Idle

Html.mount
    "app"
    (Form.View.asHtml
        {
            OnSubmit = fun name -> state.Value <- View.Success $"Saved %s{name}"
            State = state
            ErrorVisibility = View.errorVisibility ()
            Action =
                View.Action.Custom(fun _ ->
                    Html.div
                        [
                            attr.className "rf-actions"
                            Html.button
                                [
                                    attr.type' "submit"
                                    attr.className "rf-button rf-button--primary"
                                    attr.disabled (fun () -> not (Form.isDirty form ()))
                                    Html.text "Save"
                                ]
                        ]
                )
            Validation = ValidateOnSubmit
        }
        form)

Save is disabled. Change the name: it lights up. Type Ada back: it goes off again, because the value equals the baseline.

Going back

Form.reset form writes every baseline back. Errors that were showing stay shown until the error visibility is reset too, so the two calls go together.

open Fable.Ripple
open Fable.Ripple.Dom
open Fable.Ripple.Form
open Fable.Ripple.Form.Plain

let name = Var.create "Ada"
let errors = View.errorVisibility ()

let form =
    TextField.create "name"
    |> TextField.withLabel "Name"
    |> Field.create
        name
        (fun value ->
            if value.Length < 2 then
                Error "At least 2 characters"
            else
                Ok value
        )
    |> Form.textField

let state = Var.create View.Idle

Html.mount
    "app"
    (Form.View.asHtml
        {
            OnSubmit = fun name -> state.Value <- View.Success $"Saved %s{name}"
            State = state
            ErrorVisibility = errors
            Action =
                View.Action.Custom(fun _ ->
                    Html.div
                        [
                            attr.className "rf-actions"
                            Html.button
                                [
                                    attr.type' "button"
                                    attr.className "rf-button"
                                    attr.disabled (fun () -> not (Form.isDirty form ()))
                                    on.click (fun _ ->
                                        Form.reset form
                                        errors.Reset()
                                        state.Value <- View.Idle
                                    )
                                    Html.text "Reset"
                                ]
                            Html.button
                                [
                                    attr.type' "submit"
                                    attr.className "rf-button rf-button--primary"
                                    attr.disabled (fun () -> not (Form.isDirty form ()))
                                    Html.text "Save"
                                ]
                        ]
                )
            Validation = ValidateOnBlur
        }
        form)

Clear the name and leave the field: the error shows and Reset lights up. Press it: Ada is back and the error is gone.

Saving

Saving is not the library's business; what it needs to know is that the current values are now the baseline. Form.commit form does that. Call it once the save succeeded, and the form is clean again with the new values.

open Fable.Ripple
open Fable.Ripple.Dom
open Fable.Ripple.Form
open Fable.Ripple.Form.Plain

let name = Var.create "Ada"

let form =
    TextField.create "name"
    |> TextField.withLabel "Name"
    |> Field.create name Ok
    |> Form.textField

let state = Var.create View.Idle

Html.mount
    "app"
    (Html.div
        [
            Form.View.asHtml
                {
                    OnSubmit =
                        fun name ->
                            // send it to the server here
                            Form.commit form
                            state.Value <- View.Success $"Saved %s{name}"
                    State = state
                    ErrorVisibility = View.errorVisibility ()
                    Action =
                        View.Action.Custom(fun _ ->
                            Html.div
                                [
                                    attr.className "rf-actions"
                                    Html.button
                                        [
                                            attr.type' "submit"
                                            attr.className "rf-button rf-button--primary"
                                            attr.disabled (fun () -> not (Form.isDirty form ()))
                                            Html.text "Save"
                                        ]
                                ]
                        )
                    Validation = ValidateOnSubmit
                }
                form
            Html.p
                [
                    Html.text (fun () ->
                        if Form.isDirty form () then
                            "Unsaved changes"
                        else
                            "Everything saved"
                    )
                ]
        ])

Change the name and press Save: the line under the form goes back to "Everything saved" and the button goes off, with the new name in the field.

Loading a record

To fill the form from a record, say a profile fetched from the server, describe once how the record maps to the Vars. FormValues is that pair of functions. Form.load values form record then writes every field in one batch and commits, so the loaded record is the new baseline.

open Fable.Ripple
open Fable.Ripple.Dom
open Fable.Ripple.Form
open Fable.Ripple.Form.Plain

type Profile =
    {
        Name: string
        City: string
    }

let name = Var.create ""
let city = Var.create ""

let form =
    Form.succeed (fun name city ->
        {
            Name = name
            City = city
        }
    )
    |> Form.append (
        TextField.create "name"
        |> TextField.withLabel "Name"
        |> Field.create name Ok
        |> Form.textField
    )
    |> Form.append (
        TextField.create "city"
        |> TextField.withLabel "City"
        |> Field.create city Ok
        |> Form.textField
    )

let values: FormValues<Profile> =
    {
        Snapshot =
            fun () ->
                {
                    Name = name.Value
                    City = city.Value
                }
        Load =
            fun profile ->
                name.Value <- profile.Name
                city.Value <- profile.City
    }

let state = Var.create View.Idle

Html.mount
    "app"
    (Html.div
        [
            Html.button
                [
                    attr.className "rf-button"
                    on.click (fun _ ->
                        Form.load
                            values
                            form
                            {
                                Name = "Grace"
                                City = "New York"
                            }
                    )
                    Html.text "Load Grace"
                ]
            Form.View.asHtml
                {
                    OnSubmit =
                        fun profile ->
                            state.Value <-
                                View.Success $"Saved %s{profile.Name} in %s{profile.City}"
                    State = state
                    ErrorVisibility = View.errorVisibility ()
                    Action = View.Action.SubmitOnly "Save"
                    Validation = ValidateOnSubmit
                }
                form
            Html.p
                [
                    Html.text (fun () ->
                        if Form.isDirty form () then
                            "Unsaved changes"
                        else
                            "Everything saved"
                    )
                ]
        ])

Press Load Grace: both fields fill and the form is clean. Edit a field: unsaved. Form.snapshot values is the other direction, the record as it is typed, for a draft or a preview.

Writing a Var from outside

The form owns nothing. Any code can write a field's Var; the field, its parser, every check that reads it and the dirty flag follow. Form.load is exactly that, for every field at once, followed by a commit.

open Fable.Ripple
open Fable.Ripple.Dom
open Fable.Ripple.Form
open Fable.Ripple.Form.Plain

let name = Var.create "Ada"

let form =
    TextField.create "name"
    |> TextField.withLabel "Name"
    |> Field.create name Ok
    |> Form.textField

Html.mount
    "app"
    (Html.div
        [
            Html.button
                [
                    attr.className "rf-button"
                    on.click (fun _ -> name.Value <- name.Value.ToUpper())
                    Html.text "Upper case"
                ]
            Form.View.asHtml
                {
                    OnSubmit = ignore
                    State = Var.create View.Idle
                    ErrorVisibility = View.errorVisibility ()
                    Action = View.Action.Custom(fun _ -> Html.none)
                    Validation = ValidateOnSubmit
                }
                form
            Html.p
                [
                    Html.text (fun () ->
                        if Form.isDirty form () then
                            "Changed"
                        else
                            "Unchanged"
                    )
                ]
        ])
Edit this page