Fable.Modulo


Fable.Modulo

This is a Fable library designed to help with the HTML forms boilerplate code, inspired by Fable.Form

Introduction

Handling an HTML form requires a lot of boilerplate code, even more when the form is part of an Elmish application, since:

  1. you need at least one attribute for each form's field in your application's model
  2. you need one message for each field with related handling code
  3. you have to handle each field's view individually

Fable.Modulo helps you by providing a set of helpers with a balanced trade-off between ease of develpment and ability of customization.

Fable.Modulo is based on Fable.React and it doesn't require or assume any other dependency: you are free to use your favourite CSS framework or react-based library.

Getting started

This example is a step-by-step guide to create a working Elmish application ran by Fable, however there is no need to use Elmish nor the fable template: the only required dependency (other than of course Fable.Core) is Fable.React.

You will need .NET 5 or above.

  1. install the fable template:

    dotnet new --install Fable.Template
    
  2. create a new fable project:

    dotnet new fable -o fable-modulo-example -n fable-modulo-example
    
  3. install the nuget packages:

    dotnet add src/App.fsproj package Fable.React
    dotnet add src/App.fsproj package Fable.Elmish
    dotnet add src/App.fsproj package Fable.Elmish.React
    dotnet add src/App.fsproj package Fable.Modulo
    
  4. add the npm dependencies:

    npm add react react-dom
    
  5. replace the content of the file public/index.html with the following code:

    <!doctype html>
    <html>
    <head>
    <title>Fable.Modulo</title>
    <meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="shortcut icon" href="fable.ico" />
    </head>
    <body>
    <div id="react-main"></div>
    <script src="bundle.js"></script>
    </body>
    </html>
    
  6. replace the content of the file src/App.fs with the following code:

    module App
    
    open System
    open Elmish
    open Elmish.React
    open Fable.React
    open Fable.React.Props
    open Fable.Modulo
    
    type Field<'t> = FormFieldModel<FormModel, 't>
    and FormModel =
        {
            String : Field<string>
            FloatOption : Field<float option>
            Checkbox : Field<bool>
            ChoiceOptional : Field<{| Key : int; Value : string |} option>
        }
    
    type Model =
        {
            Form : FormModel
            FormValidationError : string option
        }
    
    type Msg = 
        | UpdateForm of FormModel
        | ValidateForm
    
    let transformValidation x =
        x |> Option.map (fun e -> e |> List.map(fun (k, v) -> sprintf "%s: %s" k v) |> String.concat ", ")
        
    let init _ =
        let form = 
            Auto.initForm {
                String = input {
                    value ""
                    label "A field taking a string"
                }
                FloatOption = input {
                    error "please fill me"
                    label "A float optional value" 
                    validator (fun _ v -> match v with | None -> Ok None | Some v when v < 100.0 -> Ok (Some v) | _ -> Error "The value should be < 100")
                    placeholder "please insert a value < 100"
                    tooltip "This is a float input"
                }
                Checkbox = checkbox {
                    value false
                    label "A checkbox"
                }
                ChoiceOptional = select {
                    value None
                    values (Helpers.optionValues [{|Key = 1; Value = "option 1"|}; {|Key = 2; Value = "option 2"|}])
                    value_label (fun x -> x |> Option.map (fun x -> x.Value) |> Option.defaultValue "<no choice>")
                    label "Choice optional"
                }
    
            }
    
        let model =
            {
                Form = form
                FormValidationError = validate form |> transformValidation
            }
        model, Cmd.none
    
    let update msg model =
        match msg with
        | UpdateForm newForm -> {model with Form = newForm}, Cmd.none
        | ValidateForm ->
            {model with FormValidationError = validate model.Form |> transformValidation}, Cmd.none
    
    let view model dispatch =
        section [] [
            h3 [] [str "Fable.Modulo example"]
            
            div [Style [Display DisplayOptions.Grid; GridTemplateColumns "1fr 1fr"]] [
                div [] [
                    div [] [
                        fieldset [] [
                            legend [] [str "Form"]
    
                            Auto.View.basicForm model.Form (UpdateForm >> dispatch) [
                                button [OnClick (fun e -> dispatch ValidateForm; e.preventDefault())] [str "Validate"]
                            ]
                        ]
                    ]
                ]
    
                div [] [
                    div [] [
                        match model.FormValidationError with
                        | None -> str "The form is valid"
                        | Some e -> sprintf "Error: %s" e |> str
                    ]
                ]
            ]
        ]
    
    Program.mkProgram init update view
    |> Program.withConsoleTrace
    |> Program.withReactSynchronous "react-main"
    |> Program.run
    

Let's see the relevant code

Form Model

A form is represented by a plain record type in Fable.Modulo, each field of the record should be of type FormFieldModel:

type Field<'t> = FormFieldModel<FormModel, 't>
and FormModel =
    {
        String : Field<string>
        FloatOption : Field<float option>
        Checkbox : Field<bool>
        ChoiceOptional : Field<{| Key : int; Value : string |} option>
    }

Here a type abbreviation is used to reduce the boilerplate. As you can see FormFieldModel is a generic type that accepts two arguments:

  1. the type of the enclosing form (the record this field belongs to)
  2. the type of the underlying value: there is no restriction on it, for instance I used an anonymous record wrapped in an Option in the example above.

Refer to the modeling section for further informations.

WARNING: if you are using the "automatic" mode don't forget to call Auto.initForm on the record that represents your form: this call will create the updater functions.

Message

A single message is used for the state change of every field in the form:

type Msg = 
    | UpdateForm of FormModel

In the update function you should put all the form's logic: validation, computation of derived fields, update of other pieces of the model and so on. Since you get the whole form in every dispatched message the internal consistency is automatically ensured; on the other hand it is not easy to identify the field that caused the message, but since you have both the old and the new form models it should be easy to check what changed.

View

The example uses the View.basicForm function:

Auto.View.basicForm model.Form (UpdateForm >> dispatch) [
    button [OnClick (fun e -> dispatch ValidateForm; e.preventDefault())] [str "Validate"]
]

It generates automatically an html form element by reflecting the model.Form object and dispatching an UpdateForm message for each underlying field's state update. Refer to the view section for informations about the customization of the html output.

Modeling

The main building block of Fable.Modulo modeling is the type FormFieldModel, which wraps three more types:

Every type implements the IFormFieldModel; some informations are common to the three types, such as: - underlying value - layout data (label, tooltip etc.) - validation function

Other informations are specific, for instance: - FormInputModel needs to keep track of the text displayed in the input element and a parser function to transform the text to the underlying value - FormSelectModel needs the list of allowed values, a function to transform an underlying value to a displayed text and so on

Underlying value

The underlying value is represented by the type:

Result<'t, string>

The value can be Error, for instance if you have an integer field and the user typed some alphabetic character or if the field has a custom validator attached.

The type of the value for a FormCheckboxModel is constrained to bool, but for the other kind of fields it can be every f# type including records and discriminated unions.

Updater

A central concept in Fable.Modulo is the updater: it is a function that takes a field and a form and returns an updated form

FormInputModel<'f, 't> -> 'f -> 'f

For instance a raw FormInputModel could be created explicitly by using FormInputModel.create:

FormInputModel.create "" (Ok "") Parsers.string Formatters.string (fun item form -> {form with MyField = item})

The line above creates a new field with:

The updater function is just boilerplate and if you use the function Auto.initForm it gets created automatically for every field in the record.

Thw workflow when the use changes the content of an input element for a FormInputModel is the following: 1. the field's underlying value is updated by the result of the parser function called on the input element's text 1. if a validator function is available, the new value is validated and the field's underlying value is update accordingly 1. the updater function is called with the new field 1. the final callback (for instance in the example above UpdateForm >> dispatch) gets called with the updated form

View

Since Fable.Modulo doesn't have dependecies other than Fable.React you can provide your own implementation for the view and use only the modeling part of the library or you can use one of the provided helpers.

Manual implementation

The project placed in Examples/Plain shows how you can implement your own view:

  1. completely on your own, for instance:

    Fable.React.Standard.input [
        Value model.Form.String.Text
        OnChange (View.FormInputModel.onChange model.Form model.Form.String (UpdateForm >> dispatch))
        AutoFocus true
    ]
    

    this creates a plain Fable.React element that displays the text stored in the field named String in the form model. The function FormInputModel.onChange provides the glue between the react element and the form model

  2. with basic helpers, for instance:

    View.basicField model.Form model.Form.Custom (UpdateForm >> dispatch)
    View.basicSelect model.Form model.Form.Choice (UpdateForm >> dispatch)
    

    the first one creates a react element equivalent to the one created manually above; the second one create an html <select> and related <option>s. No layout is assumed.

Automatic implementation

In order to further reduce the boilerplate code Fable.Modulo provides the View module that uses reflection to generate react elements automatically from the model record.

The basicForm helper creates a form with one <div> for each form field; each div contains a <label> followed by the field itself (depending on the field model type). You can customize the look by implementing css rules for the classes listed in Classes. If you need a different structure you could fieldsBase and arrange the returned Fields as you please.

The module Bulma implements an automatic form and applies the structure and styling from the bulma framework.

namespace System
module String from Microsoft.FSharp.Core
<summary>Functional programming operators for string processing. Further string operations are available via the member functions on strings and other functionality in <a href="http://msdn2.microsoft.com/en-us/library/system.string.aspx">System.String</a> and <a href="http://msdn2.microsoft.com/library/system.text.regularexpressions.aspx">System.Text.RegularExpressions</a> types. </summary>
<category>Strings and Text</category>
Multiple items
val string: value: 'T -> string
<summary>Converts the argument to a string using <c>ToString</c>.</summary>
<remarks>For standard integer and floating point values the and any type that implements <c>IFormattable</c><c>ToString</c> conversion uses <c>CultureInfo.InvariantCulture</c>. </remarks>
<param name="value">The input value.</param>
<returns>The converted string.</returns>
<example id="string-example"><code lang="fsharp"></code></example>


--------------------
type string = System.String
<summary>An abbreviation for the CLI type <see cref="T:System.String" />.</summary>
<category>Basic Types</category>
Multiple items
val float: value: 'T -> float (requires member op_Explicit)
<summary>Converts the argument to 64-bit float. This is a direct conversion for all primitive numeric types. For strings, the input is converted using <c>Double.Parse()</c> with InvariantCulture settings. Otherwise the operation requires an appropriate static conversion method on the input type.</summary>
<param name="value">The input value.</param>
<returns>The converted float</returns>
<example id="float-example"><code lang="fsharp"></code></example>


--------------------
[<Struct>] type float = System.Double
<summary>An abbreviation for the CLI type <see cref="T:System.Double" />.</summary>
<category>Basic Types</category>


--------------------
type float<'Measure> = float
<summary>The type of double-precision floating point numbers, annotated with a unit of measure. The unit of measure is erased in compiled code and when values of this type are analyzed using reflection. The type is representationally equivalent to <see cref="T:System.Double" />.</summary>
<category index="6">Basic Types with Units of Measure</category>
type 'T option = Option<'T>
<summary>The type of optional values. When used from other CLI languages the empty option is the <c>null</c> value. </summary>
<remarks>Use the constructors <c>Some</c> and <c>None</c> to create values of this type. Use the values in the <c>Option</c> module to manipulate values of this type, or pattern match against the values directly. 'None' values will appear as the value <c>null</c> to other CLI languages. Instance methods on this type will appear as static methods to other CLI languages due to the use of <c>null</c> as a value representation.</remarks>
<category index="3">Options</category>
[<Struct>] type bool = System.Boolean
<summary>An abbreviation for the CLI type <see cref="T:System.Boolean" />.</summary>
<category>Basic Types</category>
Multiple items
val int: value: 'T -> int (requires member op_Explicit)
<summary>Converts the argument to signed 32-bit integer. This is a direct conversion for all primitive numeric types. For strings, the input is converted using <c>Int32.Parse()</c> with InvariantCulture settings. Otherwise the operation requires an appropriate static conversion method on the input type.</summary>
<param name="value">The input value.</param>
<returns>The converted int</returns>
<example id="int-example"><code lang="fsharp"></code></example>


--------------------
[<Struct>] type int = int32
<summary>An abbreviation for the CLI type <see cref="T:System.Int32" />.</summary>
<category>Basic Types</category>


--------------------
type int<'Measure> = int
<summary>The type of 32-bit signed integer numbers, annotated with a unit of measure. The unit of measure is erased in compiled code and when values of this type are analyzed using reflection. The type is representationally equivalent to <see cref="T:System.Int32" />.</summary>
<category>Basic Types with Units of Measure</category>
module Option from Microsoft.FSharp.Core
<summary>Contains operations for working with options.</summary>
<category>Options</category>
val map: mapping: ('T -> 'U) -> option: 'T option -> 'U option
<summary><c>map f inp</c> evaluates to <c>match inp with None -&gt; None | Some x -&gt; Some (f x)</c>.</summary>
<param name="mapping">A function to apply to the option value.</param>
<param name="option">The input option.</param>
<returns>An option of the input value after applying the mapping function, or None if the input is None.</returns>
<example id="map-1"><code lang="fsharp"> None |&gt; Option.map (fun x -&gt; x * 2) // evaluates to None Some 42 |&gt; Option.map (fun x -&gt; x * 2) // evaluates to Some 84 </code></example>
Multiple items
module List from Microsoft.FSharp.Collections
<summary>Contains operations for working with values of type <see cref="T:Microsoft.FSharp.Collections.list`1" />.</summary>
<namespacedoc><summary>Operations for collections such as lists, arrays, sets, maps and sequences. See also <a href="https://docs.microsoft.com/dotnet/fsharp/language-reference/fsharp-collection-types">F# Collection Types</a> in the F# Language Guide. </summary></namespacedoc>


--------------------
type List<'T> = | op_Nil | op_ColonColon of Head: 'T * Tail: 'T list interface IReadOnlyList<'T> interface IReadOnlyCollection<'T> interface IEnumerable interface IEnumerable<'T> member GetReverseIndex: rank: int * offset: int -> int member GetSlice: startIndex: int option * endIndex: int option -> 'T list static member Cons: head: 'T * tail: 'T list -> 'T list member Head: 'T member IsEmpty: bool member Item: index: int -> 'T with get ...
<summary>The type of immutable singly-linked lists.</summary>
<remarks>Use the constructors <c>[]</c> and <c>::</c> (infix) to create values of this type, or the notation <c>[1;2;3]</c>. Use the values in the <c>List</c> module to manipulate values of this type, or pattern match against the values directly. </remarks>
<exclude />
val map: mapping: ('T -> 'U) -> list: 'T list -> 'U list
<summary>Builds a new collection whose elements are the results of applying the given function to each of the elements of the collection.</summary>
<param name="mapping">The function to transform elements from the input list.</param>
<param name="list">The input list.</param>
<returns>The list of transformed elements.</returns>
<example id="map-1"><code lang="fsharp"> let inputs = [ "a"; "bbb"; "cc" ] inputs |&gt; List.map (fun x -&gt; x.Length) </code> Evaluates to <c>[ 1; 3; 2 ]</c></example>
val sprintf: format: Printf.StringFormat<'T> -> 'T
<summary>Print to a string using the given format.</summary>
<param name="format">The formatter.</param>
<returns>The formatted result.</returns>
<example>See <c>Printf.sprintf</c> (link: <see cref="M:Microsoft.FSharp.Core.PrintfModule.PrintFormatToStringThen``1" />) for examples.</example>
val concat: sep: string -> strings: seq<string> -> string
<summary>Returns a new string made by concatenating the given strings with separator <c>sep</c>, that is <c>a1 + sep + ... + sep + aN</c>.</summary>
<param name="sep">The separator string to be inserted between the strings of the input sequence.</param>
<param name="strings">The sequence of strings to be concatenated.</param>
<returns>A new string consisting of the concatenated strings separated by the separation string.</returns>
<exception cref="T:System.ArgumentNullException">Thrown when <c>strings</c> is null.</exception>
<example id="concat-1"><code lang="fsharp"> let input1 = ["Stefan"; "says:"; "Hello"; "there!"] input1 |&gt; String.concat " " // evaluates "Stefan says: Hello there!" let input2 = [0..9] |&gt; List.map string input2 |&gt; String.concat "" // evaluates "0123456789" input2 |&gt; String.concat ", " // evaluates "0, 1, 2, 3, 4, 5, 6, 7, 8, 9" let input3 = ["No comma"] input3 |&gt; String.concat "," // evaluates "No comma" </code></example>
union case Option.None: Option<'T>
<summary>The representation of "No value"</summary>
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
<summary> Represents an OK or a Successful result. The code succeeded with a value of 'T. </summary>
union case Option.Some: Value: 'T -> Option<'T>
<summary>The representation of "Value of type 'T"</summary>
<param name="Value">The input value.</param>
<returns>An option representing the value.</returns>
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
<summary> Represents an Error or a Failure. The code failed with a value of 'TError representing what went wrong. </summary>
val defaultValue: value: 'T -> option: 'T option -> 'T
<summary>Gets the value of the option if the option is <c>Some</c>, otherwise returns the specified default value.</summary>
<param name="value">The specified default value.</param>
<param name="option">The input option.</param>
<returns>The option if the option is Some, else the default value.</returns>
<remarks>Identical to the built-in <see cref="defaultArg" /> operator, except with the arguments swapped.</remarks>
<example id="defaultValue-1"><code lang="fsharp"> (99, None) ||&gt; Option.defaultValue // evaluates to 99 (99, Some 42) ||&gt; Option.defaultValue // evaluates to 42 </code></example>
Multiple items
module Result from Microsoft.FSharp.Core
<summary>Contains operations for working with values of type <see cref="T:Microsoft.FSharp.Core.FSharpResult`2" />.</summary>
<category>Choices and Results</category>


--------------------
[<Struct>] type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
<summary>Helper type for error handling without exceptions.</summary>
<category>Choices and Results</category>