Reactive programming
WebSharper.UI's reactive layer helps represent user inputs and other time-varying values, and define how they depend on one another.
Vars
Reactive values that are directly set by code or by user interaction are represented by values of type Var<'T>
. 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.
The following are available from WebSharper.UI.Client
:
-
Doc.InputType.Text
creates an<input>
element with given attributes that is bound to aVar<string>
.With the above code, once
myInput
has been inserted in the document, gettingvarText.Value
will at any point reflect what the user has entered, and setting it will edit the input. -
Doc.InputType.Int
andDoc.InputType.Float
create an<input type="number">
bound to aVar<CheckedInput<_>>
of the corresponding type (int
orfloat
).CheckedInput
provides access to the validity and actual user input, it is defined as follows: -
Doc.InputType.IntUnchecked
andDoc.InputType.FloatUnchecked
create an<input type="number">
bound to aVar<_>
of the corresponding type (int
orfloat
). They do not check for the validity of the user's input, which can cause wonky interactions. We recommend usingDoc.InputType.Int
orDoc.InputType.Float
instead. -
Doc.InputType.TextArea
creates a<textarea>
element bound to aVar<string>
. -
Doc.InputType.Password
creates an<input type="password">
element bound to aVar<string>
. -
Doc.InputType.CheckBox
creates an<input type="checkbox">
element bound to aVar<bool>
. -
Doc.InputType.CheckBoxGroup
also creates an<input type="checkbox">
, but instead of associating it with a simpleVar<bool>
, it associates it with a specific'T
in aVar<list<'T>>
. If the box is checked, then the element is added to the list, otherwise it is removed.Result:
Plus varColor is bound to contain the list of ticked checkboxes.
-
Doc.InputType.Select
creates a dropdown<select>
given a list of values to select from. The label of every<option>
is determined by the given print function for the associated value.Result:
Plus varColor is bound to contain the selected color.
-
Doc.InputType.SelectDyn
is similar toDoc.InputType.Select
, but it takes aView<list<'T>>
instead of a static list of values. This means that the dropdown can change dynamically based on the contents of the View. -
Doc.InputType.SelectOptional
andDoc.InputType.SelectDynOptional
allow for selecting no values. -
Doc.InputType.SelectMultiple
andDoc.InputType.SelectMultipleDyn
allow for selecting multiple values. -
Doc.InputType.Radio
creates an<input type="radio">
given a value, which sets the givenVar
to that value when it is selected.Result:
Plus varColor is bound to contain the selected color.
More variants available in the Doc.InputType
module are: Color
, Date
, DateTimeLocal
, Email
, File
, Month
, Range
, Search
, Tel
, Time
, Url
, Week
, .
These all correspond to the HTML <input>
element of the same type.
Views
The full power of WebSharper.UI's reactive layer comes with View
s. A View<'T>
is a time-varying value computed from Vars and from other Views. At any point in time the view has a certain value of type 'T
.
One thing important to note is that the value of a View is not computed unless it is needed. For example, if you use View.Map
, the function passed to it will only be called if the result is needed. It will only be run while the resulting View is included in the document using one of these methods. This means that you generally don't have to worry about expensive computations being performed unnecessarily. However it also means that you should avoid relying on side-effects performed in functions like View.Map
.
In pseudo-code below, [[x]]
notation is used to denote the value of the View x
at every point in time, so that [[x]]
= [[y]]
means that the two views x
and y
are observationally equivalent.
Note that several of the functions below can be used more concisely using the V shorthand.
Creating and combining Views
The first and main way to get a View is using the View
property of Var<'T>
. This retrieves a View that tracks the current value of the Var.
You can create Views using the following functions and combinators from the View
module:
-
View.Const
creates a View whose value is always the same. -
View.ConstAnyc
is similar toConst
, but is initialized asynchronously. Until the async returns, the resulting View is uninitialized. -
View.Map
takes an existing View and maps its value through a function. -
View.Map2
takes two existing Views and map their value through a function.Similarly,
View.Map3
takes three existing Views and map their value through a function. -
View.MapAsync
is similar toView.Map
but maps through an asynchronous function.An important property here is that this combinator saves work by abandoning requests. That is, if the input view changes faster than we can asynchronously convert it, the output view will not propagate change until it obtains a valid latest value. In such a system, intermediate results are thus discarded.
Similarly,
View.MapAsync2
maps two existing Views through an asynchronous function. -
View.Apply
takes a View of a function and a View of its argument type, and combines them to create a View of its return type.While Views of functions may seem like a rare occurrence, they are actually useful together with
View.Const
in a pattern that can lift a function of any number N of arguments into an equivalent ofView.MapN
.
Inserting Views in the Doc
Once you have created a View to represent your dynamic content, here are the various ways to include it in a Doc:
-
textView
is a reactive counterpart totext
, which creates a text node from aView<string>
. -
Doc.BindView
maps a View into a dynamic Doc. -
Doc.EmbedView
unwraps aView<Doc>
into a Doc. It is equivalent toDoc.BindView id
. -
attr.*Dyn
is a reactive equivalent to the correspondingattr.*
, creating an attribute from aView<string>
.For example, the following sets the background of the input element based on the user input value:
-
attr.*DynPred
is similar toattr.*Dyn
, but it takes an extraView<bool>
. When this View is true, the attribute is set (and dynamically updated as withattr.*Dyn
), and when it is false, the attribute is removed.
Mapping Views on sequences
Applications often deal with varying collections of data. This means using a View of a sequence: a value of type View<seq<T>>
, View<list<T>>
or View<T[]>
. In this situation, it can be sub-optimal to use Map
or Doc
to render it: the whole sequence will be re-computed even when a single item has changed.
The SeqCached
family of functions fixes this issue. These functions map a View of a sequence to either a new View<seq<U>>
(functions View.MapSeqCached*
and method .MapSeqCached()
) or to a Doc
(functions Doc.BindSeqCached
and method .DocSeqCached()
) but avoid re-mapping items that haven't changed.
There are different versions of these functions, which differ in how they decide that an item "hasn't changed".
-
View.MapSeqCached : ('T -> 'V) -> View<seq<'T>> -> View<seq<'V>>
uses standard F# equality to check items. -
View.MapSeqCachedBy : ('T -> 'K) -> ('T -> 'V) -> View<seq<'T>> -> View<seq<'V>>
uses the given key function to check items. This means that if an item is added whose key is already present, the corresponding returned item is not changed. So you should only use this when items are intended to be added or removed, but not changed. -
`View.MapSeqCachedViewBy : ('T -> 'K) -> ('K -> View<'T> -> 'V) -> View<seq<'V>> covers the situation where items are identified by a key function and can be updated. Instead of passing the item's value to the mapping function, it passes a View of it, so you can react to the changes.
Each of these View.MapSeqCached*
functions has a corresponding Doc.BindSeqCached*
:
Doc.BindSeqCached : ('T -> #Doc) -> View<seq<'T>> -> Doc
Doc.BindSeqCachedBy : ('T -> 'K) -> ('T -> #Doc) -> View<seq<'T>> -> Doc
Doc.BindSeqCachedViewBy : ('T -> 'K) -> ('K -> View<'T> -> #Doc) -> View<seq<'T>> -> Doc
These functions map each item of the sequence to a Doc and then concatenates them. They are basically equivalent to passing the result of the corresponding View.MapSeqCached*
to Doc.BindView Doc.Concat
, like we did in the examples above.
Finally, all of the above functions are also available as extension methods on the View<seq<'T>>
type. .MapSeqCached()
overloads correspond to View.MapSeqCached*
functions, and .DocSeqCached()
overloads correspond to Doc.BindSeqCached*
functions.
Vars and lensing
The Var<'T>
type is actually an abstract class, this makes it possible to create instances with an implementation different from Var.Create
. The main example of this are lenses.
In WebSharper.UI, a lens is a Var
without its own storage cell that "focuses" on a sub-part of an existing Var
. For example, given the following:
You might want to create a form that allows entering the first and last name separately. For this, you need two Var<string>
s that directly observe and alter the FirstName
and LastName
fields of the value stored in varPerson
. This is exactly what a lens does.
To create a lens, you need to pass a getter and a setter function. The getter is called when the lens needs to know its current value, and extracts it from the parent Var
's current value. The setter is called when setting the value of the lens; it receives the current value of the parent Var
and the new value of the lens, and returns the new value of the parent Var
.
Automatic lenses
In the specific case of records, you can use LensAuto
to create lenses more concisely. This method only takes the getter, and is able to generate the corresponding setter during compilation.
You can be even more concise when using Doc.InputType.Text
and family thanks to the V shorthand.
The V Shorthand
Mapping reactive values from their model to a value that you want to display can be greatly simplified using the V shorthand. This shorthand revolves around passing calls to the property view.V
to a number of supporting functions.
Views and V
When an expression containing a call to view.V
is passed as argument to one of the supporting functions, it is converted to a call to View.Map
on this view, and the resulting expression is used in a way relevant to the supporting function.
The simplest supporting function is called V
, and it simply returns the view expression.
You can use arbitrarily complex expressions:
Other supporting functions use the resulting View in different ways:
-
text
passes the resulting View totextView
. -
attr.*
attribute creation functions pass the resulting View to the correspondingattr.*Dyn
. -
Attr.Style
passes the resulting View toAttr.DynamicStyle
.
Calling .V
outside of one of the above supporting functions is a compile error. There is one exception: if view
is a View<Doc>
, then view.V
is equivalent to Doc.EmbedView view
.
Vars and V
Vars also have a .V
property. When used with one of the above supporting functions, it is equivalent to .View.V
.
Additionally, var.V
can be used as a shorthand for lenses. .V
is a shorthand for .LensAuto
when passed to the following supporting functions:
-
Lens
simply creates a lensed Var. -
Doc.InputType.TextV
,Doc.InputType.TextAreaV
,Doc.InputType.PasswordV
-
Doc.InputType.IntV
,Doc.InputType.IntUncheckedV
-
Doc.InputType.FloatV
,Doc.InputType.FloatUncheckedV
ListModels
ListModel<'K, 'T>
is a convenient type to store an observable collection of items of type 'T
. Items can be accessed using an identifier, or key, of type 'K
.
ListModels are to dictionaries as Vars are to refs: a type with similar capabilities, but with the additional capability to be reactively observed, and therefore to have your UI automatically change according to changes in the stored content.
Creating ListModels
You can create ListModels with the following functions:
-
ListModel.FromSeq
creates a ListModel where items are their own key. -
ListModel.Create
creates a ListModel using a given function to determine the key of an item.
Every following example will assume the above Person
type and myPeopleColl
model.
Modifying ListModels
Once you have a ListModel, you can modify its contents like so:
-
listModel.Add
inserts an item into the model. If there is already an item with the same key, this item is replaced. -
listModel.RemoveByKey
removes the item from the model that has the given key. If there is no such item, then nothing happens. -
listModel.Remove
removes the item from the model that has the same key as the given item. It is effectively equivalent tolistModel.RemoveByKey(getKey x)
, wheregetKey
is the key function passed toListModel.Create
andx
is the argument toRemove
. -
listModel.Set
sets the entire contents of the model, discarding the previous contents. -
listModel.Clear
removes all items from the model. -
listModel.UpdateBy
updates the item with the given key. If the function returns None or the item is not found, nothing is done. -
listModel.UpdateAll
updates all the items of the model. If the function returns None, the corresponding item is unchanged. -
listModel.Lens
creates anVar<'T>
that does not have its own separate storage, but is bound to the value for a given key. -
listModel.LensInto
creates anVar<'T>
that does not have its own separate storage, but is bound to a part of the value for a given key. See lenses for more information.
Reactively observing ListModels
The main purpose for using a ListModel is to be able to reactively observe it. Here are the ways to do so:
-
listModel.View
gives aView<seq<'T>>
that reacts to changes to the model. The following example creates an HTML list of people which is automatically updated based on the contents of the model. -
listModel.ViewState
is equivalent toView
, except that it returns aView<ListModelState<'T>>
. Here are the differences:ViewState
provides better performance.ListModelState<'T>
implementsseq<'T>
, but it additionally provides indexing and length of the sequence.- However, a
ViewState
is only valid until the next change to the model.
As a summary, it is generally better to use
ViewState
. You only need to chooseView
if you need to store the resultingseq
separately. -
listModel.Map
reactively maps a function on each item. It is similar to theView.MapSeqCached
family of functions: it is optimized so that the mapping function is not called again on every item when the content changes, but only on changed items. There are two variants:-
Map(f: 'T -> 'V)
assumes that the item with a given key does not change. It is equivalent toView.MapSeqCachedBy
using the ListModel's key function. -
Map(f: 'K -> View<'T> -> 'V)
additionally observes changes to individual items that are updated. It is equivalent toView.MapSeqCachedViewBy
using the ListModel's key function.
Note that in both cases, only the current state is kept in memory: if you remove an item and insert it again, the function will be called again.
-
-
listModel.MapLens
is similar to the secondMap
method above, except that it passes anVar<'T>
instead of aView<'T>
. This makes it possible to edit list items within the mapping function. -
listModel.Doc
is similar toMap
, but the function must return aDoc
and the resulting Docs are concatenated. It is similar to theDoc.BindSeqCached
family of functions. -
listModel.DocLens
, similarly, is likeMapLens
but concatenating the resulting Docs. -
listModel.TryFindByKeyAsView
gives a View on the item that has the given key, orNone
if it is absent. -
listModel.FindByKeyAsView
is equivalent toTryFindByKeyAsView
, except that when there is no item with the given key, an exception is thrown. -
listModel.ContainsKeyAsView
gives a View on whether there is an item with the given key. It is equivalent to (but more optimized than):