SAFE Part 2: Delete

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:

Delete with busy

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:

Postman delete

Postman getAll

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 ] ]

Delete without busy

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.

Delete with busy

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.

5 thoughts on “SAFE Part 2: Delete”

  1. 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? )

    Like

    1. 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).

      Liked by 1 person

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.