/Client.Formlet

Primary LanguageF#Apache License 2.0Apache-2.0

FSharp.Client.Formlet

See my blog post on why I consider Formlets to a very important idea: https://mrange.wordpress.com/2014/10/26/building-forms-fast-using-formlets/

The major inspiration for this library comes from WebSharper (https://github.com/intellifactory/websharper).

WebSharper demonstrated how powerful a, so called, Formlet can be to represent an interactive form.

WebSharper impressed so much that we think Formlets are the current best way to represent interactive forms in F#.

Formlets are awesome because:

  • They are functionally idiomatic, that means they feel right in a functional language such as F#
  • They are endlessly composable. That means any formlet can be combined with any other formlet to produce a new formlet.
  • Using formlets it's trivial to create highly interactive forms.

Example

// Defines how to input personal information
let person =
    formlet {
        let! firstName  = LabeledText       "First name"    "John"
        let! lastName   = LabeledText       "Last name"     "Doe"
        let! birthDate  = LabeledDateTime   "Birth date"    None
        return firstName, lastName, birthDate
    }
    |> Enhance.WithLegend "Person"

// Defines how to address information
let address =
    formlet {
        let! street = LabeledText       "Street"    ""
        let! zip    = LabeledText       "Zip"       ""
        return street, zip
    }
    |> Enhance.WithLegend "Address info"

// Combines the personal and address info formlets and adds an error summary
//  Two formlets combined results in a new formlet which in turn can be combined
//  And so on...
let f =
    formlet {
        let! firstName, lastName, birthDate = person
        let! address                        = address
        return firstName, lastName, address,
    }
    |> Enhance.WithErrorSummary

This defines a form like below:

While this is rather static what's really great with Formlets is the ease how you can build Forms that react to different input. The simplest way would be an if expression

let f =
    formlet {
        let! country    = LabeledText "Country" "SWEDEN"
        let! orgNo      = LabeledText "Org no"  ""
        let! mva        =
            // In Norway a company needs to have an MVA number in addition to the OrgNo
            if country = "NORWAY" then
                LabeledText "MVA" ""
            else
                Formlet.Return "N/A"

        return country, orgNo, mva
    }

If country is "NORWAY" the MVA input textbox is shown, otherwise not.

A slightly more interesting example involves the Option input:

let empty =
    formlet {
        return "", None
    }

let sweden =
    formlet {
        let! orgNo = LabeledText "Org no" ""
        return orgNo, None
    }

let norway =
    formlet {
        let! orgNo  = LabeledText "Org no" ""
        let! mva    = LabeledText "MVA" ""
        return orgNo, Some mva
    }

let companyInfo =
    let options = LabeledOption "Country" empty [|"Sweden", sweden; "Norway", norway|]
    formlet {
        let! name       = LabeledText   "Name"      ""
        // The user selects an option that evaluates into a formlet
        let! country    = options
        // This invokes the selected formlet presenting the user a different
        // form depending on the selected option
        // This is just mind-boggling cool and succinct
        let! orgNo, mva = country
        return name, orgNo, mva
    }
    |> Enhance.WithLegend "Company info"

Extensibility

It's very important to have an extensibility story and FSharp.Client.Formlet is designed to not be locked into to a special platform.

Obviously the various adaptations are tied to a platform but the core is agnostic.

In addition, it shall be possible for a user of FSharp.Client.Formlet to extend with custom formlets that combines like the other formlets.

It's preferable if creating a formlet isn't overly hard.

Example of the basic text input formlet

/// The visual element of the text input formlet
type InputTextElement(initialText : string) as this =
    inherit TextBox()

    let mutable text                = initialText

    do
        this.Text   <- initialText
        this.Margin <- DefaultMargin

    member val ChangeNotifier = EmptyChangeNotification with get, set

    override this.OnLostFocus(e) =
        base.OnLostFocus(e)

        if text <> this.Text then
            text <- this.Text

            this.ChangeNotifier ()

/// The text input Formlet
let Text initialText : Formlet<FormletContext, UIElement, string> =
    let eval (fc,cl,ft : FormletTree<UIElement>) =
        let e =
            match ft with
            | Element (:? InputTextElement as e) -> e
            | _ ->
                InputTextElement(initialText)
        e.ChangeNotifier <- cl
        (FormletResult.Success e.Text), Element (e :> UIElement)

    FormletMonad.New eval