Thanks to the previous part, the application is beautiful, but there are no interesting interactions.
Once the deletion is implemented, the application will look like this:
When clicking the Delete
button, a spinner appears and then the list is refreshed.
Server
First of all, add the delete
function in the PersonRepository
(in Shared/Models.fs
):
type PersonRepository = { getAll: unit -> Async<Person list> delete: int -> Async<unit> }
It takes a Person
‘s id in parameter and returns nothing when deletion is done.
In the implementation (in Server/Server.fs
), the persons
value is immutable but the delete
function has to modify it. So the first thing to do is to make persons
mutable with the mutable
keyword:
let mutable persons = generator.Generate(10) |> List.ofSeq
persons
is now a mutable value but its type is immutable. In C#, to remove an item in a List, a simple persons.Remove(person)
does the job, but there is no Remove function in the F# list. Removing an item in an immutable list can be done by filtering:
delete = fun id -> async { do! Async.Sleep 2000 persons <- persons |> List.filter (fun p -> p.id <> id) }
The new value of persons
is all the persons
value without the Person with the id asked for deletion. It is a little bit counter-intuitive when you come from a “side-effect-ful” language like C#, Java… but we will see a much more counter-intuitive approach later.
One last thing, the delete
code is good but it doesn’t work because all the ids are random and not unique.
Instead of having a generator that generates random data for every field, a better approach is to have a function that generates a Person depending on an id:
let generate id = Faker<Person>("fr") .CustomInstantiator(fun f -> { id = id firstName = f.Name.FirstName() lastName = f.Name.LastName() address = { number = f.Random.Number(0, 100) street = f.Address.StreetName() postalCode = f.Address.ZipCode() city = f.Address.City() } }) .Generate()
Replace the initialization of persons
:
let initialNumberOfPersons = 10 let mutable persons = [1..initialNumberOfPersons] |> List.map generate
Now all the ids are unique and the code stays clear. You can test it with Postman:
Server part is done, you can now delete persons.
GitHub of this step here.
Client
Simple implementation
This implementation will not involve a busy indicator.
To remove a person, a new “delete” button will appear on the main list. When hitting it, the page reloads and the list appears without the deleted person.
In Persons/Types.fs
, add a new Msg
| Delete of Person
In Persons/State.fs
, change update
to this:
let update (msg : Msg) (model : Model) : Model * Cmd<Msg> = let model' = match msg with | Loading -> { message = "Loading" ; persons = [] } | Loaded (Error _) -> { message = "Error while loading persons" ; persons = [] } | Loaded (Ok persons) -> { message = "" ; persons = persons } | _ -> model let cmd = match msg with | Loading -> Cmd.ofAsync Server.api.getAll () (Ok >> Loaded) (Error >> Loaded) | Delete p -> Cmd.ofAsync Server.api.delete p.id (fun _ -> Loading) (Error >> Loaded) | _ -> Cmd.none model', cmd
Now, init
can be refactored to this:
let init () : Model * Cmd<Msg> = let model = { message = "Loading" persons = [] } model, Cmd.ofMsg Loading
The previous call to Server.api.getAll
is now handled by update
.
In the Persons/View.fs
, a new column with a button should be added:
let personHeader = tr [] [ th [] [ str "Id" ] th [] [ str "First name" ] th [] [ str "Last name" ] th [] [ str "Address" ] th [] [] ] let personLine dispatch p = tr [] [ td [] [ p.id |> string |> str ] td [] [ str p.firstName ] td [] [ str p.lastName ] td [] [ str (Address.toString p.address) ] td [] [ Button.a [ Button.OnClick (fun _ -> dispatch (Delete p)) ] [ str "delete"] ] ]
personLine
now takes a dispatch
argument which is called on the Button.OnClick
to launch the Delete
command.
Don’t forget to update personsTable
and containerBox
to compile.
let personsTable dispatch persons = let lines = persons |> List.map (personLine dispatch) Table.table [ Table.IsHoverable ] [ thead [] [ personHeader ] tbody [] lines ] let containerBox (model : Model) (dispatch : Msg -> unit) = let content = if System.String.IsNullOrEmpty(model.message) |> not then str model.message else personsTable dispatch model.persons Box.box' [ ] [ Field.div [ Field.IsGrouped ] [ content ] ]
GitHub of this step here.
Implement busy
When deleting a Person
, a spinner should appear in the clicked button, so each Person
should have the information for its busy state.
This change will have a huge impact on the types and in the update part.
First of all, the model should change to something like this:
type PersonWithState = { person: Person isBusy: bool } module PersonWithState = let create p = { person = p ; isBusy = false } let markAsBusy p ps = if ps.person = p then { ps with isBusy = true } else ps type Model = { message: string persons: PersonWithState list }
But the main change is in the Msg
:
type Msg = | Loading | Loaded of Result<Person list, exn> | Deleting of Person | Deleted of Result<Person, exn>
The Delete
message is now replaced by two distinct messages: Deleting
and Deleted
. The first one is dispatched when the user clicks on the button. The second one is the result of the api call. It’s the same pattern as the Loading
/Loaded
.
The Persons/State.fs
should change too, especially the update
function. cmd
changes a little bit:
let cmd = match msg with | Loading -> Cmd.ofAsync Server.api.getAll () (Ok >> Loaded) (Error >> Loaded) | Deleting p -> Cmd.ofAsync Server.api.delete p.id (fun _ -> Deleted (Ok p)) (Error >> Deleted) | _ -> Cmd.none
Server.api.delete
is now called in the Deleting
and the ok/error functions call Deleted
instead of Loaded
.
model
changes are bigger. The Loaded
part needs some changes due to PersonWithState
:
| Loaded (Ok persons) -> { message = "" persons = persons |> List.map PersonWithState.create }
And the Deleting
/Deleted
part:
| Deleting p -> { model with persons = model.persons |> List.map (PersonWithState.markAsBusy p) } | Deleted (Error _) -> { message = "Error while deleting person" ; persons = [] } | Deleted (Ok p) -> { model with persons = model.persons |> List.filter (fun ps -> ps.person <> p) }
When Deleting
, the person to be deleted must be marked as busy. With a mutable logic, a isBusy
setter should be called to mark a person as deleting. This is where the functional paradigm can be strange when you come from a “side-effect-ful” world. In a pure functional approach, this modification is done via a map
with a function that returns the modified value, but only for the element that needs a change.
Here, it’s PersonWithState.markAsBusy
and it returns:
– a new PersonWithState
with the isBusy
set to true
for the marked person p
– the current value for the other persons
let markAsBusy p ps = if ps.person = p then { ps with isBusy = true } else ps
When the deletion is done, the new persons
is just the previous value without the person deleted, thanks to filter
(like in the Server
part).
In the Persons/Views.fs
, the only thing to change is personLine
:
let personLine dispatch ps = let p = ps.person tr [] [ td [] [ p.id |> string |> str ] td [] [ str p.firstName ] td [] [ str p.lastName ] td [] [ str (Address.toString p.address) ] td [] [ Button.a [ Button.IsLoading ps.isBusy Button.OnClick (fun _ -> dispatch (Deleting p)) ] [ str "delete"] ] ]
Button.IsLoading ps.isBusy
shows the spinner in the button. dispatch
now calls Deleting
instead of Delete
and that’s all \o/
Addendum 26/08/2018: As giuliohome said in the comment, Button.IsLoading
is a Fulma value. It will set a css class on the button to show the spinner. This Fulma IsLoading
can be found also in Control
, Input
, Form
TextArea
and Select
.
GitHub of this step here.
Refactor
There is a little refactoring to do in order to help reducing the number of entries in Msg
. The load part is very similar to the delete part, there is a message for the action in progress (Loading
, Deleting
) and another one for the result (Loaded
, Deleted
) which returns a Result
. All these states are the same concept, it’s like an asynchronous Result
.
Let’s add a AsyncResult.fs
in the Client
folder:
module AsyncResult open Elmish type AsyncResult<'InProgress, 'Done> = | InProgress of 'InProgress | Done of 'Done | Error of exn let inline ofAsyncCmd task arg toMsg = Cmd.ofAsync task arg (Done >> toMsg) (Error >> toMsg) let inline ofAsyncCmdWithMap task arg toMsg map = Cmd.ofAsync task (map arg) (fun _ -> Done arg |> toMsg) (Error >> toMsg)
… and add it in the Client.fsproj
, just before Server.fs
:
<Compile Include="AsyncResult.fs" />
AsyncResult
needs two types, the argument type when it’s in progress and another one for the return type. It can be either in progress, done or in error.
There are also some helpers to create a Cmd
:
– ofAsyncCmd
is used when the argument for the InProgress
and for the Done
is the same type.
– ofAsyncCmdWithMap
is used when you want to map the argument for the InProgress
.
Note: In all the files you have to modify, don’t forget to import AsyncResult
module:
open AsyncResult
In Persons/Types.fs
,
type Msg = | Loading | Loaded of Result<Person list, exn> | Deleting of Person | Deleted of Result<Person, exn>
is replaced by
type Msg = | Load of AsyncResult<unit, Person list> | Delete of AsyncResult<Person, Person>
The type in Loading
and in Deleting
becomes the first type in the AsyncResult
, and the first type in the Result
of Loaded
and Deleted
becomes the second type in the AsyncResult
.
Persons/State.fs
is modified; init
changes its last line by:
model, Cmd.ofMsg <| Load (InProgress ())
update
is quite huge and thanks to this refactoring, it is easier to explode it into smaller, specialized functions. Here is the refactored version:
let updateLoadModel result = match result with | InProgress _ -> { message = "Loading" ; persons = [] } | Error _ -> { message = "Error while loading persons" ; persons = [] } | Done persons -> { message = "" persons = persons |> List.map PersonWithState.create } let updateDeleteModel model result = match result with | InProgress p -> { model with persons = model.persons |> List.map (PersonWithState.markAsBusy p) } | Error _ -> { message = "Error while deleting person" ; persons = [] } | Done p -> { model with persons = model.persons |> List.filter (fun ps -> ps.person <> p) } let update (msg : Msg) (model : Model) : Model * Cmd<Msg> = let model' = match msg with | Load r -> updateLoadModel r | Delete r -> updateDeleteModel model r let cmd = match msg with | Load (InProgress _) -> AsyncResult.ofAsyncCmd Server.api.getAll () Load | Delete (InProgress p) -> AsyncResult.ofAsyncCmdWithMap Server.api.delete p Delete Person.getId | _ -> Cmd.none model', cmd
As said before, 2 new functions appear: updateLoadModel
and updateDeleteModel
to simplify update
.
cmd
now uses the AsyncResult.ofAsyncCmd…
functions.
Note that for the Delete
part, p
is a Person
but Server.api.delete
asks for an int
as argument. That’s why ofAsyncCmdWithMap
is used with a Person.getId
. Also, you have to add it in the Shared/Models.fs
, just after the Person
type:
module Person = let getId p = p.id
Finally, Persons/Views.fs
need to be fixed in personLine
:
open AsyncResult // … Button.OnClick (fun _ -> dispatch (Delete (InProgress p))) ]
That’s all for the delete part!
GitHub of this step here.
Next step: creating a new person with page navigation.
The loading indicator is a cool idea! Is it worth nothing that it comes from Fulma and it is the FP translation of Bulma `is-loading` class and it is applicable to any control in general, right?
(O.T. Do we have anything like that in WPF? )
LikeLike
Thank you giuliohome for the suggestion, I edited this post to add a little bit explanation on `IsLoading`. It seems to work with any control bit I didn’t try.
In WPF, there is no existing control in the standard WPF library to do this kind of thing. But Telerik has a RadBusyIndicator which is doing the spinner part on any control. If you don’t want Telerik dll, it is a very simple custom control you can do (it is just a ContentControl with a IsBusy dependency property bound to 2 elements).
LikeLiked by 1 person