WebSharper documentation
WebSharper Forms

Introduction

The WebSharper.Forms library, available on NuGet, provides a high-level abstraction for working with web forms and constructing interactive user interfaces. It can declaratively describe user data input such as forms, including data validation and feedback. It allows for a separation of concerns between the data model and the rendering of the form, while the two are connected in a type-safe manner.

Additional benefits are composability and reusability of form components, as well as the ability to create dynamic forms that adapt to user input, i.e. the type and appearance of input fields in part of the form dynamically depends on user input in previous fields.

Using forms is easier than it looks. Users might find the type signatures complex, so this guide explains the meaning of all elements that compose a form. In addition, introductory examples will be presented to get the user acquainted with WebSharper.Forms.

WebSharper.Forms is based on WebSharper UI. Therefore, familiarity with concepts such as Var, View and the Doc type for HTML is necessary to work with it.

See the bottom of this page for a complete working example.

A simple form

Programming with reactive forms has two distinct steps:

  • Defining your form, i.e. defining the fields that compose the result, how they are composed, and what validation must be run on them.

  • Rendering your form, i.e. creating the markup that will be used and connecting the input fields with the reactive values created in the first step.

Defining a form

In this step we create a value of type Form<'T, 'R> where:

  • 'T is the type returned by the form.

  • 'R is the type of the render builder. It will always have the following shape:

    (arg1 -> arg2 -> ... -> argn -> 'b) -> 'b

    which means that the view function from the second step will take arguments arg1 ... argn and return whatever type of markup element we want.

Let's create a reactive form to input data about a pet. We will need the species and the name of the pet. First, let's define the corresponding types:

type Species =
    | Cat | Dog | Piglet
 
    [<JavaScript>]
    override this.ToString() =
        match this with
        | Cat -> "cat"
        | Dog -> "dog"
        | Piglet -> "piglet"
 
type Pet = { species : Species; name : string }

Then, let's define the form itself:

let PetForm (init: Pet) =
    Form.Return (fun s n -> { species = s; name = n })
    <*> Form.Yield init.species
    <*> (Form.Yield init.name
        |> Validation.IsNotEmpty "Please enter the pet's name.")

Let's break this down:

  • Form.Yield creates a singular reactive value that we can later render, with an initial value.
  • The <*> operator (an alias for Form.Apply) is used to apply a function to the values of multiple forms by chaining. In this case, it applies the function fun s n -> { species = s; name = n } to the values of the two forms that follow it.
  • Form.Return creates a form that returns a value of type Pet when the function is applied to the values of the two forms.
  • Validation can be applied at any level (singular or composed values). Here, a built-in validator is applied to the name field, which checks that it is not empty. If it is empty, an error message will be shown. Validation checks are performed when the form is evaluated (on every change or on submit, see later).

So in total, the PetForm function describes a functionality of a form with two input fields and how they are initialized from and composed into a single Pet value. This functionality is reusable and can be rendered in different ways.

However, this function is still generic, which will allow the type system to plug in the actual type of the rendered form. In this way, the form composition itself are not tied to a specific rendering implementation, like WebSharper.UI's Doc.

We have now defined how a species and a name should be composed into a Pet, and how the name should be verified. Time to define how to render the corresponding reactive form.

Rendering a form

The form we defined has the following type:

val PetForm : Pet ->
                Form<Pet,
                       (Var<Species> ->
                        Var<string> ->
                        'b) -> 'b>

The first type argument to our form is Pet, as expected, since that's what we want to return. The second type argument has the shape described previously: it takes as argument a function from several arguments (two Vars), and calls it with the appropriate Vars to obtain the rendered document.

let RenderPet species name =
    Doc.Concat [
        label [] [Doc.InputType.Radio [] Cat species; text (string Cat)]
        label [] [Doc.InputType.Radio [] Dog species; text (string Dog)]
        label [] [Doc.InputType.Radio [] Piglet species; text (string Piglet)]
        Doc.InputType.Text [] name
    ]

Here, species has type Var<Species>, and name has type Var<string>. So the type of RenderPet corresponds to the argument of the second type parameter of PetForm, with 'b specialized to Doc.

The functions Doc.InputType.Radio and Doc.InputType.Input come from WebSharper.UI, and create elements whose value is always synchronized with the Var they receive. Here their attributes are empty, but you are free to enhance them with styling, for example:

Doc.InputType.Text [attr.class "pet-name"] name

In order to use RenderPet to render the pet form, we use Form.Render:

let PetFormUI =
    PetForm { species = Cat; name = "Fluffy" }
    |> Form.Render RenderPet

We now have a value PetFormUI : Doc that we can integrate directly into our HTML markup. It will display a radio list and a text input field, and update the resulting Pet value according to user input in these two fields.

Note that right now, we are not doing anything with this resulting Pet. The simplest way to do so is using Form.Run, which calls a function every time the value is changed.

let PetFormUI =
    PetForm { species = Cat; name = "Fluffy" }
    |> Form.Run (fun animal ->
        JavaScript.Alert (
            "Your " + string animal.species +
            " is called " + animal.name))
    |> Form.Render RenderPet

More complex forms

Submit button

The above PetFormUI is not very user friendly: it triggers (and shows an alert window) every time the user inputs a character. Let's fix this by adding a submit button to the underlying form.

let PetFormWithSubmit (init: Pet) =
    Form.Return (fun s n -> { species = s; name = n })
    <*> Form.Yield init.species
    <*> (Form.Yield init.name
        |> Validation.IsNotEmpty "Please enter the pet's name.")
    |> Form.WithSubmit

Now PetFormWithSubmit only triggers a new return value when the user submits the form. A new value of type Submitter<Pet> is passed to the view function, and rendering it is just as simple:

let RenderPet species name submitter =
    div [] [
        label [] [Doc.InputType.Radio [] Cat species; text (string Cat)]
        label [] [Doc.InputType.Radio [] Dog species; text (string Dog)]
        label [] [Doc.InputType.Radio [] Piglet species; text (string Piglet)]
        Doc.InputType.Text [] name
        Doc.Button "Submit" [] submitter.Trigger
    ]

If you want the submit button to be grayed out when the input is invalid (i.e. in our case, when the name field is empty), use Doc.ButtonValidate instead.

Displaying values and error messages

We have already seen Form.Run; but another common action to do with the result value is to display it. You can get the result from the View property on the submitter. It has a value of the following type:

type Result<'T> =
    | Success of 'T
    | Failure of ErrorMessage list

where ErrorMessage has a Text field containing the text message. Here is an example:

let RenderPetWithSubmit species name submit =
    div [
        submit.View
        |> View.Map (function
            | Success pet ->
                Doc.Concat [
                    span [] [text ("Your " + string pet.species + " is called ")]
                    b [] [text pet.name]
                ]
            | Failure errors ->
                Doc.Concat [
                    for error in errors do
                        yield b [attr.style "color:red"] [text error.Text] :> _
                ])
        |> Doc.EmbedView
    ]

Note that we've been showing the result after submission. If you want to use the live value as it is input by the user, either to display it or for some other purpose, it is available as submit.Input.

Forms for collections

Let's make this form more complex by asking the user about their own name and a list of their pets. They will be able to add, remove and reorder pets in the form.

Here is the final data we want to collect:

type Person =
    {
        firstName: string
        lastName: string
        pets: seq<Pet>
    }

Defining a form for this type is relatively straightforward using a function from the Form.Many* family:

let PersonForm (init: Person) =
    Form.Return (fun first last pets ->
        { firstName = first; lastName = last; pets = pets })
    <*> (Form.Yield init.firstName
        |> Validation.Is Validation.NotEmpty "Please enter your first name.")
    <*> (Form.Yield init.lastName
        |> Validation.Is Validation.NotEmpty "Please enter your last name.")
    <*> Form.Many init.pets { species = Cat; name = "" } PetForm
    |> Form.WithSubmit

The function Form.Many takes three arguments:

  • The initial collection of values, of type seq<Pet>.

  • The value of type Pet with which the new sub-form should be initialized when the user inserts a new pet.

  • A function taking an initial Pet value and returning the Form<Pet, _> that will be shown for each pet.

It returns a form whose value is a sequence of Pets, and adds an argument to the render function of type Form.Many.CollectionWithDefault<'T, 'V, 'W>. The type 'T is the type of items in the collection, and 'V -> 'W is the type of the render builder for a single item. This is how you render such a stream:

let RenderPerson (firstName: Var<string>)
                 (lastName: Var<string>)
                 (pets: Form.Many.CollectionWithDefault<Pet,_,_>)
                 (submit: Submitter<Result<_>>) =
    div [] [
        div [] [Doc.InputType.Text [] firstName]
        div [] [Doc.InputType.Text [] lastName]
        pets.Render (fun ops species name ->
            div [] [
                RenderPet species name
                Doc.ButtonValidate "Move up" [] ops.MoveUp
                Doc.ButtonValidate "Move down" [] ops.MoveDown
                Doc.Button "Delete" [] ops.Delete
            ])
        Doc.Button "Add a pet" [] pets.Add
        Doc.ButtonValidate "Submit" [] submit
    ]

The function passed to pets.Render is called once for every new item in the collection, and defines how this individual item should be rendered. It takes as arguments:

  • A value of type Form.Many.ItemOperations, named ops here. This value has members that allow to move the current item up or down in the collection, or to delete it.

  • The arguments of the render function for the item rendering form, ie. the form that was passed as the third argument to Form.Many.

CollectionWithDefault also contains a callback called Add that adds a new pet at the end of the collection.

Localized errors

We have seen how to show all the errors together from submit. But in many cases it is useful to show the error associated with a given field next to that field. For that purpose, the type View<Result<'T>> has an extension method Through that takes a Var or a Form, and returns a new View<Result<'T>> whose value is the same as the original one, except on failure, only error messages associated with the given Var or Form are kept. For example, the following shows the error messages associated with firstName:

submit.View.Through firstName
|> View.Map (function
    | Success _ -> Doc.Empty
    | Failure errors ->
        Doc.Concat [
            for error in errors do
                yield b [] [attr.style "color:red"] [text error.Text] :> _
        ]
)
|> Doc.EmbedView

Complete example

Here is now the complete example, showcasing all the elements described in this tutorial.

On this page