#
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.
let myNames = ListModel.FromSeq ["John"; "Ana"]
#
ListModel.Create
Creates a ListModel using a given function to determine the key of an item.
type Person = { Username: string; Name: string }
let myPeople =
ListModel.Create (fun p -> p.Username)
[
{ Username = "johnny87"; Name = "John" }
{ Username = "theana12"; Name = "Ana" }
]
(The examples on this page use the above Person
type and myPeople
model.)
#
Modifying ListModels
Once you have a ListModel, you can modify its contents in a number of ways, see below.
#
listModel.Add
Inserts an item into the model. If there is already an item with the same key, this item is replaced.
myPeople.Add { Username = "mynameissam"; Name = "Sam" }
// myPeople now contains John, Ana and Sam.
myPeople.Add { Username = "johnny87"; Name = "Johnny" }
// myPeople now contains Johnny, Ana and Sam.
#
listModel.RemoveByKey
Removes the item from the model that has the given key. If there is no such item, then nothing happens.
myPeople.RemoveByKey "theana12"
// myPeople now contains John.
myPeople.RemoveByKey "chloe94"
// myPeople now contains John.
#
listModel.Remove
Removes the item from the model that has the same key as the given item. It is effectively equivalent to listModel.RemoveByKey(getKey x)
, where getKey
is the key function passed to ListModel.Create
and x
is the argument to Remove
.
myPeople.Remove { Username = "theana12"; Name = "Another Ana" }
// myPeople now contains John.
#
listModel.Set
Sets the entire contents of the model, discarding the previous contents.
myPeople.Set [
{ Username = "chloe94"; Name = "Chloe" };
{ Username = "a13x"; Name = "Alex" }
]
// myPeople now contains Chloe, Alex.
#
listModel.Clear
Removes all items from the model.
myPeople.Clear()
// myPeople now contains no items.
#
listModel.UpdateBy
Updates the item with the given key. If the function returns None or the item is not found, nothing is done.
myPeople.UpdateBy (fun u -> Some { u with Name = "The Real Ana" }) "theana12"
// myPeople now contains John, The Real Ana.
myPeople.UpdateBy (fun u -> None) "johnny87"
// myPeople now contains John, The Real Ana.
#
listModel.UpdateAll
Updates all the items of the model. If the function returns None, the corresponding item is unchanged.
myPeople.UpdateAll (fun u ->
if u.Username.Contains "ana" then
Some { u with Name = "The Real Ana" }
else
None)
// myPeople now contains John, The Real Ana.
#
listModel.Lens
Creates an Var<'T>
that does not have its own separate storage, but is bound to the value for a given key.
let john : Var<Person> = myPeople.Lens "johnny87"
#
listModel.LensInto
Creates an Var<'T>
that does not have its own separate storage, but is bound to a part of the value for a given key.
let varJohnsName : Var<string> =
myPeople.LensInto "johnny87" (fun p -> p.Name) (fun p n -> { p with Name = n })
// The following input field edits John's name directly in the listModel.
let editJohnsName = Doc.Input [] varJohnsName
#
Reactively observing ListModels
The main purpose for using a ListModel is to be able to reactively observe it.
#
listModel.View
Gives a View<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.
open WebSharper.UI.Client
open WebSharper.UI.Html
let myPeopleList =
myPeople.View
|> Doc.BindView (fun people ->
ul [] [
people
|> Seq.map (fun p -> li [] [text p.Name])
|> Doc.Concat
]
)
#
listModel.ViewState
Is equivalent to View
, except that it returns a View<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 choose View
if you need to store the resulting seq
separately.
#
listModel.Map
Reactively maps a function on each item. It is similar to the View.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 to View.MapSeqCachedBy
using the ListModel's key function.
let myDoc =
myPeople.Map(fun p ->
Console.Log p.Username
p [] [text p.Name]
)
|> Doc.BindView Doc.Concat
|> Doc.RunAppend JS.Document.Body
// Logs johnny87, theana12
// Displays John, Ana
// We add an item with a key that doesn't exist yet,
// so the mapping function is called for it and the result is added.
myPeople.Add { Username = "mynameissam"; Name = "Sam" }
// Logs mynameissam
// Displays John, Ana, Sam
// We change the value for an existing key,
// so this change is ignored by Map.
myPeople.Add { Username = "johnny87"; Name = "Johnny" }
// Logs nothing, since no key has been added
// Displays John, Ana, Sam (unchanged)
#
Map(f: 'K -> View<'T> -> 'V)
Additionally observes changes to individual items that are updated. It is equivalent to View.MapSeqCachedViewBy
using the ListModel's key function.
myPeople.Map(fun k vp ->
Console.Log k
p [] [text (vp.V.Name)]
)
|> Doc.BindView Doc.Concat
|> Doc.RunAppend JS.Document.Body
// Logs johnny87, theana12
// Displays John, Ana
// We add an item with a key that doesn't exist yet,
// so the mapping function is called for it and the result is added.
myPeople.Add { Username = "mynameissam"; Name = "Sam" }
// Logs mynameissam
// Displays John, Ana, Sam
// We change the value for an existing key,
// so the mapping function is not called again
// but the View's value is updated.
myPeople.Add { Username = "johnny87"; Name = "Johnny" }
// Here we changed the value for an existing key
// Logs nothing, since no key has been added
// Displays Johnny, Ana, Sam (changed!)
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 second Map
method above, except that it passes an Var<'T>
instead of a View<'T>
. This makes it possible to edit list items within the mapping function.
let myDoc =
myPeople.MapLens(fun k vp ->
label [] [
text (vp.V.Username + ": ")
Doc.InputV [] vp.V.Name
]
)
|> Doc.BindView Doc.Concat
#
listModel.Doc
Is similar to Map
, but the function must return a Doc
and the resulting Docs are concatenated. It is similar to the Doc.BindSeqCached
family of functions.
#
listModel.DocLens
Like MapLens
but concatenating the resulting Docs.
#
listModel.TryFindByKeyAsView
Gives a View on the item that has the given key, or None
if it is absent.
let showJohn =
myPeople.TryFindByKeyAsView("johnny87")
|> Doc.BindView (function
| None -> text "He is not here."
| Some u -> text (sprintf "He is here, and his name is %s." u.Name)
)
#
listModel.FindByKeyAsView
Is equivalent to TryFindByKeyAsView
, 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):
View.Map Option.isSome (listModel.TryFindByKeyAsView(k))