From JavaScript to Functional Web Development (part 2)

If you haven’t read it yet, you can read the first part here!

When I was 12 or so I really liked an MMO browser game. I tried to recreate my own version of that game for my friends and myself. In the process I really wanted to show off everyone’s stats so I wrote a high score table. Today we’re going to recreate this table here.

Back then it worked with an SQL database, but now we’re going to make it so it’ll allow us to add new entries on the same website.

First we just create our WebSharper Single Page Application, I’ll name my project “HighScoreTable”. We’ll only work in the Client.fs and the index.html . Let’s start with the index.html so we’ll have our base for the template in F#. From the original project we don’t need to modify anything from the <head> tag. Our code goes into the <body> .

To do this we need some new information about WebSharper. We’ve already seen how to work with static F# generated elements, but this time we’ll learn how to use the templates. With templates we are able to write our F# code once and then we only have to modify the html file and there’ll be no need for an F# re-build.

What do we need? We need a table for our high score table, some input fields for our new entry and a button to add our new entry. We will also need some new tags we haven’t used yet: 
-ws-var : Binds a reactive variable to our input fields values’
-ws-children-template : Creates a template for us, but it throws away the tagged DOM element
-ws-template : Creates a template keeping the tagged DOM element
-ws-replace : Replaces the DOM element with our given element(s)
-ws-hole : Fills the DOM element with our given element(s)
-ws-onclick : Assigns an function to the element’s “onclick” action

But what is a template exactly? With templates we can define html blocks and use (and re-use) them in our result html file. For Example:

<div ws-template="LoginTemplate">
<input ws-var="name" value="Username" />
<input type="password" ws-var="password" />
<button ws-onclick="login">Login</button>
</div>

We could reference this Login template and place it anywhere in our code from F# (since we tagged it with ws-template the <div> is part of the template). It would have a reactive variable bound to “Name” and “Password”, and a function bound to the “Login” button. From our F# code we’re able to access this and we can create a Doc from this template which later can be used to fill a ws-replace for example. This might seem strange at first, but you’ll understand everything by the end of this post.


Index.html

Since we want to achieve the html modification results without F# re-build, we have to write a little bit more in our index.html . Let’s start with our main template, I’m going to call it “Main”:

<div ws-children-template="Main">
...
</div>

The table we’re going to use is simple, contains a static thead and a dynamic tbody . To create our dynamic body we are going to use another template. This template will serve as the table row for each individual entry. In this example we’ll call this template “Row”:

<tr ws-template="Row">
<td ws-hole="Name"></td>
<td ws-hole="Level"></td>
<td ws-hole="Gold"></td>
<td ws-hole="Score"></td>
</tr>

We have to have this template between our <tbody></tbody tags since it’s a <tr></tr> template. If we have it outside of the table it will be thrown away.

Only one thing’s left from our index.html and that’s the new entry form. Since we’re using four attributes for each row, we have to have four input fields and a button:

<div>
<input ws-var="Name" />
<input type="number" ws-var="Level" />
<input type="number" ws-var="Gold" />
<input type="number" ws-var="Score" />
<button ws-onclick="Add">Add</button>
</div>

With this added we’re almost done with the html file. All we need is a placeholder for our template and a nice title. For example a <div> with an id="main" would serve well as a placeholder:

<h1>My high score table</h1>
<div id="main"></div>

This is our index.html with the table’s static part added:


Client.fs

First of all we have to access our html template from the fs file. We can do this by defining a new type for it:

type IndexTemplate = Template<"index.html", ClientLoad.FromDocument>

Using this we’re able to create our template by using IndexTemplate.Main() and IndexTemplate.Row() since we named our templates “Main” and “Row”.

Just as we did for the html file, let’s see what we need in the F# file:
- A record type for our entries
- A list with our entries
- Four reactive variables for the new entry
- A function to add our new entry
- Filling the templates

I will call our entry record “Entry” and will give the above defined properties:

type Entry =
{Name: string;
Level: int;
Gold: int;
Score: int}

To make the code more readable and the type easy to use, we can define a Create function:

static member Create name level gold score =
{
Name = name
Level = level
Gold = gold
Score = score
}

For the list we should use WebSharper’s ListModel since we can use the data from this list dynamically and that’s just what we need here. We’re going to create this ListModel from a seq with the ListModel.FromSeq function and give a default element to the list:

let Players =
ListModel.FromSeq [
Entry.Create "Name" 1 25 200
]

With this we have everything we need to fill our table row template. We’re going to start with a simple function to convert our entry to a table row:

let row entry =
IndexTemplate.Row()
.Name(entry.Name)
.Level(string entry.Level)
.Gold(string entry.Gold)
.Score(string entry.Score)
.Doc()

Now we have to go through our list and map this row function to every element, then create a Doc from it to use it in our <tbody>’s ws-hole .
This requires more than one function, but I’ll explain everything:

let data =
Players.View.Doc(fun lm ->
lm
|> Seq.sortByDescending (fun t ->
t.Score
)
|> Seq.map row
|> Doc.Concat
)

First we create a View from our list. This means we’ll get the current data from the list at all time. Then as the tbody requires we convert our list to a Doc. To do this we give a function that gets a ListModel as a parameter and results in a Doc . We name this ListModel “lm” then we apply a sort on it (since it’s a high score table, we need some kind of sorting). This sort happens by the Seq.sortByDescending function. This function needs another function to know what field to sort by. In this example we set it to sort by the player’s Score. Now we have a sorted seq with the players’ data. Next step is to convert it to a table Doc. We’re lucky and already have the function that does the exact same thing: row . All we have to do with this is to map this function on our list. We’re almost done, but now we have a list in a list, but we only want a list in out tbody . To solve this we just call Doc.Concat to make a single list from the list of lists. With this we’re done with our table.

We still need four reactive variable to store our new entry data. Let’s create these with Var.Create :

let newEntryName = Var.Create "Name"
let newEntryLevel = Var.Create 0
let newEntryGold = Var.Create 0
let newEntryScore = Var.Create 0

At this point we’re almost done. The last thing we need is the new entry in our table and constructing the template. Our function to add a new entry to the list is small enough to leave it in the template declaration as a lambda function.

As I mentioned before we can use our template with the IndexTemplate.Main() and we can access every field we marked with a ws-*="Something" as .Something() . What do we have to do here?
- Fill the ws-hole in our tbody
- Link every input field with its reactive variable
- Write our add function to the button with ws-onlick="Add" (We can add a new entry to our Players ListModel with the Add function)

IndexTemplate.Main()
.Data(data)
.Name(newEntryName)
.Level(newEntryLevel)
.Gold(newEntryGold)
.Score(newEntryScore)
.Add(fun _ ->
Entry.Create (newEntryName.Value) (newEntryLevel.Value) (newEntryGold.Value) (newEntryScore.Value)
|> Players.Add
|> ignore
)

We’re almost done! The only thing we have to do is the finalize our template, create a Doc from it with the .Doc() function and as we did it at our first project, place this in our <div> with the id="main" . We do that with calling the Doc.RunById "main" function on our template.

Let’s see the whole Client.fs in its final form:

The result table website

Of course this doesn’t look fancy or professional, but with our F# code done we only have to modify the index.html with the template and the styles and it’ll work without the need of re-building the whole project. Just modify the index.html and hit refresh in your browser.

If you want to check out this website, or want to play with the code, you can do it on Try WebSharper. Following this link you can see the result of the code we wrote and modify it to see the changes in your browser!


Be sure to leave a 💚, it helps me know what stories you want to read, and leave a comment if you have any question! Hope you had fun reading the post and found your love for F# and for WebSharper.