Skip to content

Validation

Validation is the parsers. A field's parser runs on every change and returns the value or a message; the form is valid when every parser returns Ok. What the view decides is when to show the messages.

When errors show

The view's Validation sets the default for every field:

  • ValidateOnSubmit: nothing until the user submits with errors. Then every error shows.
  • ValidateOnBlur: also as soon as the user leaves a field.
  • ValidateOnChange: also as soon as the user changes a field.

Form.validateOn overrides the mode for one field or a sub-form: a username field can check on change while the rest waits for submit.

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

let username = Var.create ""
let bio = Var.create ""

let form =
    Form.succeed (fun username bio -> username, bio)
    |> Form.append (
        TextField.create "username"
        |> TextField.withLabel "Username (checked as you type)"
        |> Field.create
            username
            (fun value ->
                if value.Length < 3 then
                    Error "At least 3 characters"
                else
                    Ok value
            )
        |> Form.textField
        |> Form.validateOn ValidateOnChange
    )
    |> Form.append (
        TextField.create "bio"
        |> TextField.withLabel "Bio (checked on submit)"
        |> Field.create
            bio
            (fun value ->
                if value.Length > 20 then
                    Error "20 characters at most"
                else
                    Ok value
            )
        |> Form.textField
    )

let state = Var.create View.Idle

Html.mount
    "app"
    (Form.View.asHtml
        {
            OnSubmit = fun _ -> state.Value <- View.Success "Saved"
            State = state
            ErrorVisibility = View.errorVisibility ()
            Action = View.Action.SubmitOnly "Save"
            Validation = ValidateOnSubmit
        }
        form)

Checks across fields

A parser is a tracked read: any Var it reads becomes a dependency, and the field is re-checked when that Var changes. The repeat-password field in Your first form reads password.Value and that is all the wiring there is.

Errors from the server

Some errors are only known after a request: the address is taken, the coupon expired. Form.withExternalError takes a tracked read that returns the message while it applies. The form fails and the message shows on the field right away, whatever the validation mode.

let taken: Var<string option> = Var.create None

emailField
|> Form.withExternalError (fun () ->
    match taken.Value with
    | Some address when address = email.Value -> Some "This address is taken"
    | _ -> None
)

Comparing with the current value makes the error go away as soon as the user changes the field.

Asynchronous checks

Form.validateAsync runs a check on the field's parsed value some milliseconds after the user stopped typing. While it runs the field is marked as validating; a rejection shows as an error; submitting waits for a pending check.

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

let username = Var.create ""

let form =
    TextField.create "username"
    |> TextField.withLabel "Username"
    |> Field.create username Ok
    |> Form.textField
    |> Form.validateAsync
        300
        (fun value ->
            promise {
                do! Promise.sleep 600

                return
                    if value = "admin" then
                        Error "This name is taken"
                    else
                        Ok()
            }
        )

let state = Var.create View.Idle

Html.mount
    "app"
    (Form.View.asHtml
        {
            OnSubmit = fun name -> state.Value <- View.Success $"Registered %s{name}"
            State = state
            ErrorVisibility = View.errorVisibility ()
            Action = View.Action.SubmitOnly "Register"
            Validation = ValidateOnSubmit
        }
        form)

Type admin. The check starts 300 ms after the last keystroke and answers 600 ms later. A result that arrives after the value changed again is ignored.

Hiding the errors again

Once a submit has failed, every field shows its error until it is fixed. When the form starts over, for a new entry or after a reset, that state has to be cleared too: keep the ErrorVisibility you gave the view and call Reset() on it. It forgets the failed submit and which fields were left.

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

let name = Var.create ""
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"
                                    on.click (fun _ ->
                                        Form.reset form
                                        errors.Reset()
                                        state.Value <- View.Idle
                                    )
                                    Html.text "Start over"
                                ]
                            Html.button
                                [
                                    attr.type' "submit"
                                    attr.className "rf-button rf-button--primary"
                                    Html.text "Save"
                                ]
                        ]
                )
            Validation = ValidateOnBlur
        }
        form)

Press Save with the field empty: the error shows. Press Start over: the field is cleared and the error is gone. Without errors.Reset() the field would be cleared but still marked as failed, and "This field is required" would stay.

The "Saved" line after a successful submit is the view's State, which is your Var; the button sets it back to Idle for the same reason.

Edit this page