Fun with dependent formlets

Joel Bjornson

Joel Bjornson

Jun 18, 2010

Reading time:

6 mins

Share via:

To give some hints of what you can do with the WebSharper formlet library, the objective of the following exercise is to create a simple spread-sheet like widget based on formlets.

In short, we want to create a grid for inputting integer values and add a dynamically updated column displaying the sums of all rows along with a row for displaying the sums of all columns.

At first we define a general combinator for constructing a generic grid formlet:

[<JavaScript>]
let Grid rows cols formlet =
    List.init rows (fun _ ->
        List.init cols (fun _ ->
            formlet
        )
        |> Formlet.Sequence
        |> Formlet.Horizontal
    )
    |> Formlet.Sequence

The function accepts two parameters for specifying the number of rows and columns and an inner formlet constructor. The resulting formlet produces values of type list of list of T, where T is the value type of the inner formlet. For the spread-sheet widget we want to be able to input integer values, why the following helper function comes in handy:

[<JavaScript>]
let IntField (n: option<int>) =
    let initVal =
        match n with
        | Some v -> string v
        | None -> ""
    Controls.Input initVal
    |> Validator.IsInt "Only integer values allowed"
    |> Enhance.WithValidationFrame
    |> Formlet.MapResult (fun res ->
        match res with
        | Success vl -> int vl
        | _          -> 0
        |> Success
    )

The IntField is just a Controls.Input with integer validation mapped with a function for translating non integer values to 0. In this way, forms instantiated using IntField will never enter a failing state. Here is an example of a grid with 7 rows and 4 columns used with IntField as the inner formlet constructor:

Grid 7 4 (IntField None)

Next step is to enhance the grid formlet with an extra column for displaying the summed values of each row. The following combinator creates a dependent formlet, holding the extra grid column:

[<JavaScript>]
let WithRowSums grid  =
    // Composing elements horizontally
    let horizontal (e1 : Element) (e2 : Element) =
        Table [TBody [TR [TD [e1]; TD [e2]]]]
    // Creates a dependent formlet containing the row sums
    Formlet.BindWith horizontal grid (fun rows ->
        // Sum each row
        rows
        |> List.map (fun row ->
            Some (List.sum row)
            |> IntField
        )
        |> Formlet.Sequence
        |> Formlet.Map (fun rowSums ->
            List.zip rows rowSums
            |> List.map (fun (cells,sum) -> cells @ [sum])
        )
        |> Formlet.MapElement (fun el ->
            // Wrap with a css class
            Span [Class "sum"] -< [el]
        )
    )

To lay out the two form components (the grid and the extra column) horizontally, Formlet.BindWith applied with a function (horizontal) for specifying the composition is used.

In a similar fashion a new combinator, enhancing the grid with an extra row containing the column sums may be defined. This time the default formlet builder is utilized since it will compose the visual elements vertically and we want to display the additional row beneath the grid:

[<JavaScript>]
let WithColumnSums grid  =
    // The grid enhanced with an extra sum row
    Formlet.Do {
        let! rows = grid
        return!
            // Sum each column
            rows
            |> List.reduce (fun sum row ->
                List.zip sum row
                |> List.map (fun (x,y) -> x + y)
            )
            |> List.map (Some >> IntField)
            |> Formlet.Sequence
            |> Formlet.Map (fun row -> rows @ [row])
            |> Formlet.Horizontal
            |> Formlet.MapElement (fun el ->
                Span [Class "sum"] -< [el]
            )
    }

The only thing remaining now is to compose the previously defined combinators into a nice grid enhanced with row and column sums:

[<JavaScript>]
let GridWithColumnAndRowSums () =
    IntField None
    |> Grid 7 4 
    |> WithColumnSum
    |> WithRowSum

Here is an example of GridWithColumnAndRowSums in action:

Note that every time a value in the grid is changed the dependent cells containing the summed values are updated immediately.

Read more from

Can’t find what you were looking for? Drop us a line.

Joel Bjornson
Found a typo?

This blog post is hosted on GitHub here. Feel free to file a ticket or send a PR.

Newsletter

We will not spam you or give your details to anyone.