Creating a reactive Single-Page App in F# with WebSharper UI.Next and Bootstrap – Part 2

Hello again! This is the second post on our attempt to make a dynamically editable Bootstrap navbar using WebSharper’s UI.Next library. If you haven’t done so yet, I recommend you start by reading the first part here where I cover the basic of the application (reactive Single-Page App) and the different HTML files used. AS a reminder, here’s how the final application looks like:

(click on the .gif to enlarge)
Let’s jump right back where we left off and go over the F# logic.

Client.fs

I’m going to divide this file into smaller parts to make it easier to follow. Here’s the first part:

namespace UI.Next.Demo

open WebSharper
open WebSharper.UI.Next
open WebSharper.UI.Next.Client
open WebSharper.UI.Next.Html

[<JavaScript>]
module Client =
    
    type NavbarSampleTemplate = Templating.Template<"navbar_sample.html">
    type BsNavbarTemplate = Templating.Template<"bootstrap_navbar.html">

    type Tab = { Name: string; Url: string; IsActive: Var<bool> }

    let sampleTabs = [
        {Name = "First tab"; Url = "/#"; IsActive = Var.Create true }
        {Name = "Second tab"; Url = "/#"; IsActive = Var.Create false }
    ]

    let Main =
 // ... to be continued

Except for the bunch of necessary open statements and the creation of a new Client module that will transpile to JavaScript thanks to WebSharper’s [<JavaScript>] attribute, we create:

  • 2 types based on our HTML templates above, NavbarSampleTemplate and BsNavbarTemplate.
  • 1 type called Tab that will contain the Name and Url of new items we will be adding to our navbar, as well as a reactive flag IsActive to determine if the item is active or not.
  • Finally, we create a list of 2 tabs that will so we can populate our navbar when the page is launched.

Let’s continue with the Main function:

let Main =
        
	// Reactive variables
	let brand = Var.Create "Play With UI.Next!"
	let newTabName = Var.Create ""
	let newTabUrl = Var.Create ""
	let tabs = ListModel.Create (fun t -> t.Name) sampleTabs

We declare all the reactive variables that we will be using in our app. As you can see, a reactive variable of type String can be declared as follows:

let myReactiveVariable = Var.Create "Some string"

UI.Next also allows us to create reactive collections by using ListModel.Create. The line below instantiate a reactive collection that contains the 2 tabs that we declared in the previous snippet:

let tabs = ListModel.Create (fun t -> t.Name) sampleTabs

We will use this reactive collection to dynamically update the navbar when a new item is being added to it. In the next snippet, we declare a small function called mapTab that allows to map a given Tab instance to its HTML template defined by BsNavbarTemplate:

/// create a new tab (name and url) from the HTML template.
let mapTab tab = 
    BsNavbarTemplate.Tab.Doc(
        Name = View.Const tab.Name,
        Url = View.Const tab.Url,
        ActiveClass = Attr.DynamicClass "active" tab.IsActive.View id,
        SetActive = fun _ _ ->
            tabs.Iter (fun t -> if t.Name <> tab.Name then Var.Set t.IsActive false)
            Var.Set tab.IsActive true
    )

The function returns a Doc, meaning the result can be rendered as HTML. I used View.Const to map the name and URL of the tab to the $!{Name} and $!{Url} text holes respectively. Const means that the value will be static and won’t be observed over time (once the tab is added to the navbar, you cannot edit its properties anymore).

The very useful Attr.DynamicClass function allows us to dynamically add or remove the class=”active” css attribute to the HTML content generated. When tab.IsActive equals true, this results in:

<li class="active">...</li>

However when tab.IsActive equals false, the following HTML fragment is generated instead:

<li>...</li>

This is very convenient as we don’t have to explicitly update the HTML by ourselves when a different navbar item is clicked.

The HTML type provider in action!

There is one extra concept worth mentioning here: both NavbarSampleTemplate and BsNavbarTemplate are creating using something that we call a type provider in F#. This means that the properties Name, Url, ActiveClass and SetActive that you see in the snippet below are actually extracted from the HTML template at compile time, and therefore provide additional type-safety to our program. Let’s see it in action in the .gif below:

(click on the .gif to enlarge)

That’s right! After we change the HTML attribute from ActiveClass to ActiveClasssss, the compiler throws an error saying that it doesn’t know of the ActiveClass property in F#! No more typing issues that you might (will) experience in other languages. Type-safety for the win!

Putting everything together

We’re almost there! The last bit of F# code will cover:

  • how to instantiate the initial navbar.
  • how to dynamically add a new item to the navbar.
  • how  to wire up the whole page and run it under <div id=”main”/> in our index.html file.

Instantiating the navbar

I created a separate initNavbar function for that:

/// instantiate the navbar.
let initNavbar = 
    BsNavbarTemplate.Doc(
        Brand = brand.View,
        Tabs = [
            tabs.View |> Doc.BindSeqCached mapTab
        ]
    )

In this function, the reactive collection of navbar items (called tabs) is mapped to our Tabs data hole (data-hole=”Tabs” in bootstrap_navbar.html). To do that, we use the UI.Next’s Doc.BindSeqCached function that converts a collection to a Doc type. I am not quite sure why the function contains the Cached keyword though. This is something I need to look at.

Adding a new item to the navbar

This part is straightforward. We create a new item as inactive based on the name and URL text inputs shown below:

We then add the item to the tabs collection and reset both text inputs using the Var.Set function. This is shown in the snippet below:

/// add a new tab to the navbar and reset the input.
let addNewTab () =
    tabs.Add { Name = newTabName.Value; Url = newTabUrl.Value; IsActive = Var.Create false }
    Var.Set newTabName ""
    Var.Set newTabUrl ""

Running all components under the main div element

The last part is also easy to understand. We map all the elements we created to the template declared in navbar_sample.html. We then run the whole thing under the main div element:

// Wire up the whole page and run it under the 'main' div.
NavbarSampleTemplate.Doc(
    Brand = brand,
    NavBar = [initNavbar],
    NewTabName = newTabName,
    NewTabUrl = newTabUrl,
    CreateNewTab = fun _ _ -> addNewTab()
)
|> Doc.RunById "main"

Here again we can benefit from the HTML type provider that we described above. The last line allows us to inject the whole Doc under the main HTML element, thanks to the Doc.RunById function provided by WebSharper. Voila!

Wrapping up

That’s it for the second part of this mini-series. We managed to create a bootstrap navbar that we can dynamically update. The whole thing was wrapped into a Single-Page app with reactive elements.

Putting this together took me longer than I’d expected, mainly because the topic was new to me (I am more of a back-end developer myself, remember!) and I got a little lost in the documentation and the various APIs for manipulating ListModel reactive collections.

Also, most if not all examples featured on UI.Next’s samples page were creating HTML structures in F# directly, instead of reading it from real HTML templates, like I did here.

I feel however that the HTML template approach is cleaner when you understand it well, and this gives more freedom to your designers to tweak the visuals of the page independently from the F# logic.

And most importantly, this was a lot of fun and I didn’t have to write a single line of JavaScript! What else can I ask for?

Thanks for reading and until the next one!

2 thoughts on “Creating a reactive Single-Page App in F# with WebSharper UI.Next and Bootstrap – Part 2

  1. Pingback: Creating a reactive Single-Page App in F# with WebSharper UI.Next and Bootstrap – Part 1 – Youenn Bouglouan

  2. Pingback: dotnetomaniak.pl

Leave a Reply

Your email address will not be published. Required fields are marked *