Last week we introduced RemoteData, added a Loading Indicator, and added the ability to search our Users' data table. This week we'll add server-side sorting and some better charts.

First, though, let's look at the solution to last week's exercise.

Exercise Solution

The exercise last week was to take what we'd done with the Users view: indication that we were loading data to the table (by extending the RemoteData type a bit), and the ability to search it - and replicate it across the rest of our resources.

Here's how I solved it.

Loading Indication

We've already modeled our data using our RemotePersistentPaginated type. All that's left is to update each of the views to show the loading indication (which we signify by adding an overlay to the tables, presently) to the remaining views. This just involves adding a loading class to a div that wraps our tables, and showing the previous data when we are mid-request.

Before we do that though, let's clean up the View.Users module a tiny bit, extracting out the renderTable function that's presently inside of a let into a top-level function:

module View.Users exposing (view, header)
-- ...
renderTable : Model -> Paginated User -> Html Msg
renderTable model paginatedUsers =
    Table.table
        [ Options.css "width" "100%"
        , Elevation.e2
        ]
        [ Table.thead []
            [ Table.th [] []
            , Table.th
                (thOptions UserName model)
                [ text "Name" ]
            , Table.th [] [ text "Position" ]
            , Table.th [] [ text "Email" ]
            , Table.th [] [ text "Today" ]
            , Table.th [] [ text "Last 7 days" ]
            , Table.th [] [ text "Projects" ]
            , Table.th [] [ text "Open Tasks" ]
            , Table.th []
                [ Textfield.render Mdl
                    [ 0, 4 ]
                    model.mdl
                    [ Textfield.label "Search"
                    , Textfield.floatingLabel
                    , Textfield.text'
                    , Textfield.value model.usersModel.userSearchQuery
                    , Textfield.onInput <| UserMsg' << SetUserSearchQuery
                    , onEnter <| UserMsg' <| FetchUsers <| "/users?q=" ++ model.usersModel.userSearchQuery
                    ]
                ]
            ]
        , Table.tbody []
            (List.indexedMap (viewUserRow model) paginatedUsers.items)
        , Table.tfoot []
            [ Html.td [ colspan 999, class "mdl-data-table__cell--non-numeric" ]
                [ PaginatedTable.paginationData [ 0, 3 ] (UserMsg' << FetchUsers) model paginatedUsers ]
            ]
        ]


usersTable : Model -> Html Msg
usersTable model =
    case model.usersModel.users.current of
        NotAsked ->
            text "Initialising..."

        Loading ->
            case model.usersModel.users.previous of
                Nothing ->
                    text "Loading..."

                Just previousUsers ->
                    div [ class "loading" ]
                        [ renderTable model previousUsers
                        ]

        Failure err ->
            case model.usersModel.users.previous of
                Nothing ->
                    text <| "There was a problem fetching the users: " ++ toString err

                Just previousUsers ->
                    div []
                        [ text <| "There was a problem fetching the users: " ++ toString err
                        , renderTable model previousUsers
                        ]

        Success paginatedUsers ->
            renderTable model paginatedUsers

Something to note here is that we're actually passing the model around far too frequently. This is a bit of a crutch of mine that I use when I'm prototyping, but we're well past that stage and I'm still doing it. I'll be fixing this up in some later commits to this branch, but I don't think it's particularly useful for me to show here. If you're interested, just check out the commit history for the after_episode_028.1 tag and the later commits should cover it.

Anyway, we'll keep moving on and implement essentially this same thing for Organizations:

module View.Organizations exposing (view, header)
-- ...
renderTable : Model -> Paginated Organization -> Html Msg
renderTable model paginatedOrganizations =
    Table.table
        [ Options.css "width" "100%"
        , Elevation.e2
        ]
        [ Table.thead []
            [ Table.th
                (thOptions OrganizationName model)
                [ text "Name" ]
            , Table.th [] [ text "Actions" ]
            ]
        , Table.tbody []
            (List.indexedMap (organizationRow model) paginatedOrganizations.items)
        , Table.tfoot []
            [ Html.td [ colspan 999, class "mdl-data-table__cell--non-numeric" ]
                [ PaginatedTable.paginationData [ 7, 3 ] (OrganizationMsg' << FetchOrganizations) model paginatedOrganizations ]
            ]
        ]


organizationsTable : Model -> Html Msg
organizationsTable model =
    case model.organizationsModel.organizations.current of
        NotAsked ->
            text "Initialising..."

        Loading ->
            case model.organizationsModel.organizations.previous of
                Nothing ->
                    text "Loading..."

                Just previousOrganizations ->
                    div [ class "loading" ]
                        [ renderTable model previousOrganizations
                        ]

        Failure err ->
            case model.organizationsModel.organizations.previous of
                Nothing ->
                    text <| "There was a problem fetching the organizations: " ++ toString err

                Just previousOrganizations ->
                    div []
                        [ text <| "There was a problem fetching the organizations: " ++ toString err
                        , renderTable model previousOrganizations
                        ]

        Success paginatedOrganizations ->
            renderTable model paginatedOrganizations

And we'll do it for Projects:

module View.Projects exposing (view, header)
-- ...
import Types exposing (Project, ProjectSortableField(..), Sorted(..), Paginated)
-- ...
renderTable : Model -> Paginated Project -> Html Msg
renderTable model paginatedProjects =
    Table.table
        [ Options.css "width" "100%"
        , Elevation.e2
        ]
        [ Table.thead []
            [ Table.th
                (thOptions ProjectName model)
                [ text "Name" ]
            , Table.th [] [ text "Actions" ]
            ]
        , Table.tbody []
            (List.indexedMap (projectRow model) paginatedProjects.items)
        , Table.tfoot []
            [ Html.td [ colspan 999, class "mdl-data-table__cell--non-numeric" ]
                [ PaginatedTable.paginationData [ 3, 3 ] (ProjectMsg' << FetchProjects) model paginatedProjects ]
            ]
        ]


projectsTable : Model -> Html Msg
projectsTable model =
    case model.projectsModel.projects.current of
        NotAsked ->
            text "Initialising..."

        Loading ->
            case model.projectsModel.projects.previous of
                Nothing ->
                    text "Loading..."

                Just previousProjects ->
                    div [ class "loading" ]
                        [ renderTable model previousProjects
                        ]

        Failure err ->
            case model.projectsModel.projects.previous of
                Nothing ->
                    text <| "There was a problem fetching the Projeccts: " ++ toString err

                Just previousProjects ->
                    div []
                        [ text <| "There was a problem fetching the Projects: " ++ toString err
                        , renderTable model previousProjects
                        ]

        Success paginatedProjects ->
            renderTable model paginatedProjects

Alright, so that's got our nice loading indication when we're flipping through pages. Next, let's add the ability to search the other endpoints.

Searching

For the record, here's how I've added searching to the other two endpoints. This is Elixir code - it's outside of the scope of this topic of course, but maybe worth seeing anyway. At any rate, if you add that bit of code those endpoints will support search.

Next, we move on to the Elm bits. We need to track the search query and add the appropriate message to the Msg types and Update to support updating that string.

First, we'll add the necessary parts to the Model:

module Model exposing (Model, UsersModel, ProjectsModel, OrganizationsModel, initialModel)
-- ...
type alias ProjectsModel =
    { -- ...
    , projectSearchQuery : String
    }


type alias OrganizationsModel =
    { -- ...
    , organizationSearchQuery : String
    }


initialModel : Maybe Route.Location -> Model
initialModel location =
    { -- ...
    , projectsModel =
        { -- ...
        , projectSearchQuery = ""
        }
    , organizationsModel =
        { -- ...
        , organizationSearchQuery = ""
        }
    }

Then we'll add the Msgs to support updating this field in both sub-models:

module Msg exposing (Msg(..), UserMsg(..), ProjectMsg(..), OrganizationMsg(..), LoginMsg(..))
-- ...
type ProjectMsg
    -- ...
    | SetProjectSearchQuery String


type OrganizationMsg
    -- ...
    | SetOrganizationSearchQuery String

We'll handle it in the Update:

module Update exposing (update)
-- ...
updateProjectMsg : Model -> ProjectMsg -> ProjectsModel -> ( ProjectsModel, Cmd Msg, Maybe ( String, String ) )
updateProjectMsg model msg projectsModel =
    case msg of
        -- ...
        SetProjectSearchQuery query ->
            ( { projectsModel | projectSearchQuery = query }
            , Cmd.none
            , Nothing
            )


updateOrganizationMsg : Model -> OrganizationMsg -> OrganizationsModel -> ( OrganizationsModel, Cmd Msg, Maybe ( String, String ) )
updateOrganizationMsg model msg organizationsModel =
    case msg of
        -- ...
        SetOrganizationSearchQuery query ->
            ( { organizationsModel | organizationSearchQuery = query }
            , Cmd.none
            , Nothing
            )

Now we can track the query and update it from each of the views, if we had the same sort of search field that the User view offers. We can add it easily enough:

module View.Projects exposing (view, header)
-- ...
import Material.Textfield as Textfield
import Util exposing (onEnter)
-- ...
renderTable : Model -> Paginated Project -> Html Msg
renderTable model paginatedProjects =
    Table.table
        [ Options.css "width" "100%"
        , Elevation.e2
        ]
        [ Table.thead []
            [ Table.th
                (thOptions ProjectName model)
                [ text "Name" ]
            , Table.th []
                [ Textfield.render Mdl
                    [ 3, 4 ]
                    model.mdl
                    [ Textfield.label "Search"
                    , Textfield.floatingLabel
                    , Textfield.text'
                    , Textfield.value model.projectsModel.projectSearchQuery
                    , Textfield.onInput <| ProjectMsg' << SetProjectSearchQuery
                    , onEnter <| ProjectMsg' <| FetchProjects <| "/projects?q=" ++ model.projectsModel.projectSearchQuery
                    ]
                ]
            ]
        , Table.tbody []
            (List.indexedMap (projectRow model) paginatedProjects.items)
        , Table.tfoot []
            [ Html.td [ colspan 999, class "mdl-data-table__cell--non-numeric" ]
                [ PaginatedTable.paginationData [ 3, 3 ] (ProjectMsg' << FetchProjects) model paginatedProjects ]
            ]
        ]

And to Organizations:

module View.Organizations exposing (view, header)
-- ...
import Material.Textfield as Textfield
import Util exposing (onEnter)
-- ...
renderTable : Model -> Paginated Organization -> Html Msg
renderTable model paginatedOrganizations =
    Table.table
        [ Options.css "width" "100%"
        , Elevation.e2
        ]
        [ Table.thead []
            [ Table.th
                (thOptions OrganizationName model)
                [ text "Name" ]
            , Table.th []
                [ Textfield.render Mdl
                    [ 7, 4 ]
                    model.mdl
                    [ Textfield.label "Search"
                    , Textfield.floatingLabel
                    , Textfield.text'
                    , Textfield.value model.organizationsModel.organizationSearchQuery
                    , Textfield.onInput <| OrganizationMsg' << SetOrganizationSearchQuery
                    , onEnter <| OrganizationMsg' <| FetchOrganizations <| "/organizations?q=" ++ model.organizationsModel.organizationSearchQuery
                    ]
                ]
            ]
        , Table.tbody []
            (List.indexedMap (organizationRow model) paginatedOrganizations.items)
        , Table.tfoot []
            [ Html.td [ colspan 999, class "mdl-data-table__cell--non-numeric" ]
                [ PaginatedTable.paginationData [ 7, 3 ] (OrganizationMsg' << FetchOrganizations) model paginatedOrganizations ]
            ]
        ]

But shouldn't we generalize this stuff?

Oh yeah, so this was not so great to be honest - we should really generalize it, to avoid adding a bunch of boilerplate...at least that's what I thought when I started, and honestly when I sent this episode out.

However, right now our data model is a bit unrealistically homogenous (every resource just has a name field at present on the backend, out of some mixture of laziness and eagerness to skip that part). Consequently, I'd like to push that off for a bit longer. I at least wanted to push it past the point where we stop passing the model into every function, which I'll take care of as an addendum to this episode. So we'll get there, maybe, but not yet!

At any rate, feel free to look after the resources section to see an addendum where I remove the Model from the View.Users module. For now, though, on to readings to prepare for the week.

Preparatory Readings

We'll be adding server-side sorting this week. The plan is to provide a sort parameter of some sort to the backend. We've already got the concept of a union type for each resource, so I'm sure we'll use that to derive the query we send to the backend to handle that. Spend some time thinking about how that will end up working.

We'll also be swapping the elm-charts library for gampleman/elm-visualization. I've got a good feeling about it, since it's based on D3 and I'm a big fan of D3.

Go ahead and read up on that library and check out the examples in elm-reactor to get a feel for how it's used.

Resources

Addendum - Don't Pass the Model Around So Much, Maybe?

This is not exactly part of the episode but I felt it needed to be done so I figured I might as well clean up some of the functions to be more explicit about their dependencies - depending on the entire model everywhere is a bit lazy. It works, but it feels bad and seems to encourage ma to make poor decisions. We can fix it!

I'm going to focus this discussion on the Users view, but in the final commit I'll have applied the lessons from it to the other index views as well at least.

We'll tighten up the dependencies 'from the inside out' - tackling our innermost functions first, then working our way up to the top as far as we feel like we'd like to go.

thOptions isn't needed presently

So we've got these thOptions functions floating around that we were using to handle sorting the tables in the browser. Now we'll need the server to do this but haven't yet implemented it, so let's just get rid of that one.

Remove model from switchViewButton

We'd been using the model in the switchViewButton function, so we'll just pull out the two parts of it that we need in the function one level up the chain and be explicit.

Remove model from addUserButton

Next, looking at the addUserButton function it becomes clear that we just need the Material.Model so we'll fix that one.

Same deal for editButton and deleteButton

These two buttons also just used the Material.Model so let's just pass that along instead.

Up the chain, to viewUserRow

Now we can stop passing the model into viewUserRow since none of the functions that it calls require it any longer.

Looking at the table

So if we look at the renderTable function, we still require the model in one place - the paginationData function. This is going to require us to make changes to all of the other table views as well, but that's not really a big deal. We'll change that function to no longer require the model, and stop requiring it in the renderTable function as well

Finished?

Alright, so this gets the model out of most of the functions...but let's take it as far as we can here, shall we? Here's what I came up with.

That doesn't quite have everything sorted yet. We still have the Model being used in our header function, because it's required for the defaultHeaderWithNavigation function. As it turns out, we don't even use that parameter in that function, which is shameful, but it's an example of the sort of thing that this exercise can find for you. So I can stop requiring it, and get that sorted out throughout the other places I use it.

This finally allowed me to remove the last use of Model in View.Users.