Simple reactive scenarios with WebSharper

Adam Granicz

Adam Granicz

Dec 31, 2016

Reading time:

12 mins

Share via:

(This post is part of F# Advent 2016 - Happy 2017!)

One of the most fundamental design considerations any developer must deal with is handling change. In this article, we are primarily concerned with client-side state and changes to it. Change can be brought about by various external factors (user input such as mouse or keyboard events, server push messages, etc.) or by means internal to the application itself.

For a long time, handling client-side changes was tied to an object-oriented, event-driven programming style that required a user-initiated action/event connected to an UI control "instance". You may remember adding event handlers for buttons that triggered an action and possibly a change in the state of the user interface by refreshing relevant controls.

While the range of events to which event handlers can be attached typically grows with the maturity (and complexity) of the encompassing UI framework (ASP.NET, WinForms, WPF, etc.), they themselves will never be enough to handle change as we would like to.

Two-way binding with reactive variables

WebSharper long introduced UI.Next1 to supersede WebSharper.Html2 as its main DOM construction library.

Buried deep in the WebSharper documentation, you can find a comprehensive tutorial3 on how to use UI.Next for basic two-way data binding with reactive markup. The drill is pretty simple: next to composing with ordinary HTML combinators, you can use various HTML input controls whose values are automatically synched with a reactive variable (typically of type Var<string>).

Here we have a plain input box bound to v:

open WebSharper
open WebSharper.JavaScript
open WebSharper.UI.Next
open WebSharper.UI.Next.Html
open WebSharper.UI.Next.Client
open WebSharper.UI.Next.Notation

[<JavaScript>]
module SimpleInput =

    let Main =
        let v = Var.Create ""
        div [
            Doc.Input [] v
        ]
        |> Doc.RunById "main"

You can now update the input box easily by writing to v. Consider the addition of a simple Clear button:

    ...
    div [
        buttonAttr [on.click (fun _ _ -> v := "")] [text "Clear"]
        Doc.Input [] v
    ]

The fun starts when there is dependent markup in your page computed from the value of the input box. Say, you wanted to echo what's typed into the input box, using all caps:

    ...
    div [
        buttonAttr [on.click (fun _ _ -> v := "")] [text "Clear"]
        Doc.Input [] v
        p [textView (v.View.Map (fun s -> s.ToUpper()))]
    ]

Here, v.View returns the current value of v, and textView converts it to an HTML text node. You can also react to keyboard and mouse input (API4) equally easily:

    div [
        ...
        p [textView (Input.Mouse.Position.Map (fun (x,y) -> sprintf "%d:%d" y x))]
    ]

To sum up the basics a somewhat more elaborate live snippet is below:

List models

Now that you can gather and manipulate user input via reactive variables, and reflect computed/derived values in reactive markup, you can also bind composite data in your web pages; and this is where the real fun begins. All we need is Models (API5), and in particular ListModels (API6).

ListModels associate values with keys in a time-varying collection. This association can be implicit, or as in most cases explicit. Below is a ListModel that stores simple names - here the names themselves act as their own key (note the id function used as the value->key map):

let names = ListModel.Create id ["John"; "James"]
names.Add "Jonathan"

Example - System messages

We are only scratching the surface here, but assume we want to display a set of system messages, each coming with one of the usual Info/Warning/Error flavor. For added complexity, we also want an Add and a Remove button to play with test data.

namespace Samples

open WebSharper
open WebSharper.JavaScript
open WebSharper.UI.Next
open WebSharper.UI.Next.Html
open WebSharper.UI.Next.Client
open WebSharper.UI.Next.Notation

[<JavaScript>]
module SimpleInput =
    let counter = ref 0
    let removedCounter = ref 0
    let GetNextCounter() = incr counter; !counter
    
    type MessageType = Info | Warning | Error
    type Message =
        {
            Id: int
            Title: string
            Type: MessageType
        }
        
    let RandomMessage() =
        let id = GetNextCounter()
        {
            Id = id
            Title = sprintf "Message #%d" id
            Type = if id % 3 = 0 then Info elif id % 3 = 1 then Warning else Error
        }
        
    let Main =
        let messages = ListModel.Create (fun msg -> msg.Id) [RandomMessage()]

        div [
            buttonAttr [
                on.click (fun _ _ -> messages.Add(RandomMessage()))
            ] [text "Add"]

            buttonAttr [
                on.click (fun _ _ ->
                    if !removedCounter < !counter then
                        incr removedCounter
                        messages.RemoveByKey(!removedCounter)
                )
            ] [text "Remove"]
            
            messages.View.DocSeqCached (fun msg ->
                let ty, bg =
                    match msg.Type with
                    | Info -> "info", "bg-success"
                    | Warning -> "warning", "bg-warning"
                    | Error -> "error", "bg-danger"
                pAttr [attr.``class`` bg] [text (ty + ": " + msg.Title)]
            )
        ]
        |> Doc.RunById "main"

Here, messages is initialized with a random system message, and messages.View.DocSeqCached is a fancy way of reflecting each message to markup - and note we use Bootstrap class names, so be sure to include the main Bootstrap CSS in the template you are serving for your application. A live snippet is below:

Example - Grouping system messages

Suppose that in the previous example, we want to group the system messages based on their type. We can introduce a helper function to filter the subset we are interested in:

    ...
    let Main =
        let messages = ListModel.Create (fun msg -> msg.Id) [RandomMessage()]

        let filter pred =
            messages.View.DocSeqCached (fun msg ->
                if pred msg then
                    let ty, bg =
                        match msg.Type with
                        | Info -> "info", "bg-success"
                        | Warning -> "warning", "bg-warning"
                        | Error -> "error", "bg-danger"
                    pAttr [attr.``class`` bg] [text (ty + ": " + msg.Title)] :> Doc
                else
                    Doc.Empty
            )
		...

Using filter, we can compute the info/warning/error set separately and display them after each other:

        let infos    = filter (fun msg -> match msg.Type with | Info    -> true | _ -> false)
        let warnings = filter (fun msg -> match msg.Type with | Warning -> true | _ -> false)
        let errors   = filter (fun msg -> match msg.Type with | Error   -> true | _ -> false)

        div [
            buttonAttr [
                on.click (fun _ _ -> messages.Add(RandomMessage()))
            ] [text "Add"]

            buttonAttr [
                on.click (fun _ _ ->
                    if !removedCounter < !counter then
                        incr removedCounter
                        messages.RemoveByKey(!removedCounter)
                )
            ] [text "Remove"]
        
            h3 [text "Info-level messages"]
            infos
            h3 [text "Warnings"]
            warnings
            h3 [text "Errors"]
            errors
        ]
        |> Doc.RunById "main"

So with a few lines adjustment we have message grouping under control - feel free to play with the live snippet below:

Conclusion

In this brief article, we looked at a few basic reactive scenarios with WebSharper and saw how UI.Next makes it easy to work with two-way binding, reactive markup, and aggregate client-side models. In upcoming articles, I will further examine ListModels and their more advanced capabilities, including client-side and client-server persistence.

Happy 2017 and happy coding!


  1. This link has been updated to point to the WebSharper.UI repository.

  2. This link has been updated to point to the WebSharper.Html repository.

  3. This link has been updated to point to the latest WebSharper.UI documentation page.

  4. This link has been updated to the latest version.

  5. This link has been updated to the latest version.

  6. This link has been updated to the latest version.

Read more from

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

Newsletter

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