#WebSharper’s super power with Vars
Using WebSharper allows you to develop both the client side and the server side of a web application in C# or F#, with client to server communication abstracted as RPC calls. There’s no need to handle low-level concerns like XMLHttpRequest or data serialisation. There’s great Javascript interoperability of course, allowing to use javascript libraries from your F# code.
But still, what I enjoy most when developing a WebSharper app is its reactive layer built on Vars and Views. From Websharper’s documentation:
Vars are similar to F# ref<'T> in that they store a value of type 'T
that you can get or set using the Value property.
But they can additionally be reactively observed or two-way bound to
HTML input elements.
A View
has a value computed from Var
s and/or other View
s, with its value continuously kept up to date. All HTML elements using a View
are updated when the View
’s value changes. View
s are deeply integrated in WebSharper’s UI library, making it trivial to have dynamic user interfaces.
Introductory example
Here’s a minimal example to grasp the concept of Var
s and View
s. It defines a Var<string>
, a Var
containing a string
, by initiating it with the string "a"
.
It also defines html content with a button, which when clicked will append an additional a
to the string stored in the accumulator
.
Finally,we display the accumulator value in the page.
// Define a Var with the value "a"
let accumulator = Var.Create "a"
div
[]
[
button
[
on.click
(fun el ev ->
accumulator.Set (accumulator.Value + "a"))
]
[ text "Append 'a'" ]
br [] []
span
[]
[textView accumulator.View ]
]
You’ll notice there’s no code handling the update of the value displayed. We include the view in the output, and WebSharper hanles all updates for us.
This is a toy example illustrating how things work, but let’s now look at a real world example.
Real world example
I wanted to toggle the display of an element in the page when a button was clicked. To give an idea of the conciseness and approachability of the code, here it is (we will examine it below):
let linkableVisible = Var.Create false
let linkableVisibilityClass =
linkableVisible.View
|> View.Map
(fun isVisible ->
if isVisible then
"d-block"
else
"d-none"
)
let buttonText =
linkableVisible.View
|> View.Map (fun visible->
if visible then
"Hide element"
else
"Show element"
)
div []
[
button
[ attr.``class`` "btn-primary"
on.clickView
linkableVisible.View
(fun _ev _el isVisible->
linkableVisible.Set (not isVisible))
]
[ buttonText.V
]
div [attr.classDyn linkableVisibilityClass ]
[
div [ ]
[
text "toggled element"
]
]
]
We define one Var
named linkableVisible
, holding a booling value indicating if the element is currently visible. This is the Var
on which the solution will be built.
We hide the element by giving it the CSS class d-none
from Bootstrap. So what we want is map the value of linkableVisible
to "d-none"
(resp. "d-block"
) if its value is false
(resp. true
). But of course, when the value of linkableVisible
is changed, we want the CSS class to be updated accordingly. This indicates the need of defining a View
derived from linkableVisible
.
This is how it is done:
let linkableVisibilityClass =
// Take the view of linkableVisible
linkableVisible.View
// Map is to the css class
|> View.Map
// This function gets the current value of linkableVisible
(fun isVisible ->
if isVisible then
"d-block"
else
"d-none"
)
Similarly, we want the button text to be dependent of the visibility of the toggled element. If the element is visible (linkableVisible
is true
), we want the button text to be Hide element
, and if the element is hidden, the button text should be Show element
. Of course the button text should update accordingly, which again suggests the use of a view. And indeed here again we map a view of linkableVisible
to a text view with the right value with this code:
let buttonText =
linkableVisible.View
|> View.Map (fun visible->
if visible then
t.tt "Hide element"
else
t.tt "Show element"
)
And that’s all there is to do on the logic side. We now just have to integrate these views in the html generation. Let’s first take a look at the button:
button
[ attr.``class`` "btn-primary"
on.clickView
linkableVisible.View
(fun _ev _el isVisible->
linkableVisible.Set (not isVisible))
]
[ buttonText.V
]
We set the class btn-primary
provided by Bootstrap, and set the click handler, which simply toggles the boolean value present in the Var
linkableVisible
.
Setting the Var
’s new value is the only time our code touches the Var
, all other code is based on View
s. We never read the value from the Var
itself, but access it through its View
.
Including the toggle element is done as easily:
div [attr.classDyn linkableVisibilityClass ]
[
div [ ]
[
text "toggled element"
]
]
WebSharper provides the helper attr.classDyn
to set the class string attribute of an element, taking a View<string>
and setting its value as the class of said element. When the value of the View
changes, so does the class of the element in the DOM.
You can run this code online here and its real world application here.
Going further
You might say, toggling an element visibility is good, but it means it has to be present in the DOM. What if I don’t want to include it in the DOM right away, because it is the result of an RPC call to get information from the server? Well we can easily build further on what we have. Let’s look at the changes needed.
We first define a function that will issue the RPC and get information from the server and return a Doc
based on it. We don’t issue an RPC here to make it runnable on try.websharper.com, but as it is async it replicates the situation correctly with the Async.Sleep
.
let callRPCAndReturnDoc() = async {
do! Async.Sleep 300
return div [] [text "my info from server"]
}
At initial page render time, we won’t have a doc available to include in the DOM, so we create a Var
containing an option type. When we have the Doc
built with data from the server, we will store it in this Var
:
let linkableDocOption:Var<Option<Doc>> = Var.Create None
In the HTML, we have to include a Doc
anyway, so we build a View
from this Var
, mapping a None
value to Doc.Empty
which can be included in the HTML document:
let linkableDocView =
linkableDocOption.View
|> View.Map(
function
| None -> Doc.Empty
| Some doc -> doc
)
Our onclick handler will now have to act according to 2 criteria: the visibility and the availability of the Doc
build with the result of the RPC:
- visible -> hide the element, whatever the Doc status
-
hidden,
linkableDocOption
is None -> issue RPC and show element -
hidden,
linkableDocOption
is Some -> show element.
But to achieve this, we need a view giving the visibility and RPC status. We map to a View<bool,bool>
(a pair of boolean values):
let isVisibleIsRetrieved =
// Map2's mapping function takes 2 arguments, corresponding to the current values
// of the 2 views also passed as arguments to Map2
View.Map2 (fun visible (doc:Option<Doc>) -> (visible,doc.IsSome))
linkableVisible.View
linkableDocOption.View
With this view defined, writing our click handler follows its description nearly literally:
let displayLinkableTable (visible,retrieved) = async {
match visible,retrieved with
// If visible, just hide it. It must have been retrieved at that point.
| true, _ ->
linkableVisible.Set (not visible)
// if it was alredy retrieved (by a previous click on the button), just make it visible again
| false, true ->
linkableVisible.Set (not visible)
| false, false ->
// Call the thunk we got to setup the linkable table, we get a Doc value
let! docBuiltFromRPC = callRPCAndReturnDoc()
// Update the Vars accordingly
linkableDocOption.Set (Some docBuiltFromRPC)
linkableVisible.Set (not visible)
}
Updates to the HTML to be displayed are minimal. Update the click handler to use isVisibleIsRetrieved
, and start the Async
to issue the RPC:
[ on.clickView
isVisibleIsRetrieved
(fun _ev _el isVisibleIsRetrieved->
(displayLinkableTable isVisibleIsRetrieved)|> Async.StartImmediate)
]
and include the linkableDoc
view in the HTML:
div [ ]
[
linkableDocView.V
]
It uses the V
shortcut easing the use of Views in the Doc
building functions.
And that’s all there is. You can run this code online here. You’ll notice that the first time you click on the button the element is shown with a small delay, because the RPC was issued (simulated here by the Async.Sleep
). Subsequent displays of the element are immediate though.
The real world application of this can be found here.
The changes needed to issue the RPC at first display are minimal, but bring a big improvement. And if you want to improve the solution further it can be done easily:
-
want to display a spinner while the RPC is done? Replace
Doc.Empty
by the spinner. -
want to execute the RPC every time the element is shown? This is easily done by updating slightly
displayLinkableTable
. And displaying the spinner can be done by changinglinkableDocOption
before issuing the RPC. -
want to issue the RPC only if the previous RPC was issued more than 10 seconds away? Put the time of the last RPC call in a
Var
and use it to decide if an RPC should be issued.
Possibilities are endless, all while keeping the code maintainable and staying in F#. What more is there to ask? :-)