This week we'll introduce authentication to our API and handle that in knewter/time-tracker, as well as add a couple of niceies to the UI. Today we'll provide a few resources to prepare for the week's work, but first let's address the exercise from last week.

Exercise Solution

Last week's exercise was to wire up validations to the remaining forms, as well as perhaps refactor a bit. Let's see how I tackled it.

I've tagged my starting point as before_episode_023.1.

Some refactoring first because I got the heebies

I started out working on the Edit view for users but as soon as I started looking at it the awfulness of the code overwhelmed me. In general I'm not really a fan of refactoring before you need it, but here I could see that I would immediately need something and figured I might as well do the refactor first.

Introducing a FormWithErrors type

The first thing I want to do is add an alias for the 2-tuple containing (APIFieldErrors, Form String User):

type alias FormWithErrors a =
    ( Form String a, Maybe APIFieldErrors )


type alias Model =
    { -- ...
    , newUserForm : FormWithErrors User
    -- ...
    }

Now that's a bit nicer. Here's the commit, for reference.

Introducing a Validators module

I'd like to collect all of my validators together, so I'll introduce a Validators module next:

module Model exposing (Model, initialModel)

-- ...
-- I was able to remove the Form.Validate import from this modle, which is a win.
import Validators
-- ...
initialModel : Maybe Route.Location -> Model
initialModel location =
    { -- ...
    , newUserForm = ( Form.initial [] Validators.validateNewUser, Nothing )
    -- ...
    }
module Validators exposing (validateNewUser)

import Form.Validate exposing (Validation, form1, get, string)
import Types exposing (User, APIFieldErrors)


validateNewUser : Validation String User
validateNewUser =
    form1 (User Nothing)
        (get "name" string)

With that I cleaned up the Model a bit, which is also nice. Here's this commit.

Breaking the users part of the model out - or not

I think in general you should keep as flat of a model as possible. However, at this point each of our resources is adding four fields to our model. I was going to refactor this out but decided that it's not actually causing me any grief yet so I'd skip it. This is a good thing to do if you can - don't just refactor things for the sake of doing it.

I'm nearly certain this will happen eventually, but I will try to wait until I have some more realistic resources built out before I do it.

Extract our tagger

We have a few lines in our view that look like this:

-- ...
   , Textfield.onInput <| UserMsg' << NewUserFormMsg << (Form.Field.Text >> Form.Input name.path)
   , Textfield.onFocus <| UserMsg' << NewUserFormMsg <| Form.Focus name.path
   , Textfield.onBlur <| UserMsg' << NewUserFormMsg <| Form.Blur name.path
-- ...

That's a lot of duplication. This is the tag we use for anything that isn't a top level message in this view. Normally I might use Html.App.map but instead I'll just do the dumb thing and refactor the composition into a function:

module View.Users.New exposing (view)
-- ...
nameField : Model -> Html Msg
nameField model =
    let
        -- ...
    in
        Textfield.render Mdl
            [ 1, 0 ]
            model.mdl
            ([ -- ...
             , Textfield.onInput <| tagged << (Form.Field.Text >> Form.Input name.path)
             , Textfield.onFocus <| tagged <| Form.Focus name.path
             , Textfield.onBlur <| tagged <| Form.Blur name.path
             ]
                ++ conditionalProperties
            )


submitButton : Model -> Html Msg
submitButton model =
    Button.render Mdl
        [ 1, 1 ]
        model.mdl
        [ -- ...
        , Button.onClick <| tagged Form.Submit
        ]
        [ text "Submit" ]
-- ...
tagged : Form.Msg -> Msg
tagged =
    UserMsg' << NewUserFormMsg

This seems to work out nicely so far for me. My attempts to use Html.App.map didn't pan out immediately as you saw in the pairing video with Luke if you watched it, so I just went for a dumb refactor rather than spending any time on it.

nameField is the worst

Our nameField function right now is:

nameField : Model -> Html Msg
nameField model =
    let
        rawName =
            Form.getFieldAsString "name" (fst model.newUserForm)
                |> Debug.log "rawName"

        apiErrors =
            (snd model.newUserForm)

        name =
            case apiErrors of
                Nothing ->
                    rawName

                Just errorDict ->
                    case ( rawName.isDirty, rawName.liveError /= Nothing ) of
                        ( True, True ) ->
                            rawName

                        ( False, True ) ->
                            rawName

                        ( True, False ) ->
                            rawName

                        ( _, False ) ->
                            case Dict.get "name" errorDict of
                                Nothing ->
                                    rawName

                                Just errorList ->
                                    { rawName | liveError = Just <| Form.Error.CustomError (String.join ", " errorList) }


        conditionalProperties =
            case name.liveError of
                Just error ->
                    case error of
                        Form.Error.InvalidString ->
                            [ Textfield.error "Cannot be blank" ]

                        Form.Error.Empty ->
                            [ Textfield.error "Cannot be blank" ]

                        Form.Error.CustomError errString ->
                            [ Textfield.error errString ]

                        _ ->
                            [ Textfield.error <| toString error ]

                Nothing ->
                    []
    in
        Textfield.render Mdl
            [ 1, 0 ]
            model.mdl
            ([ Textfield.label "Name"
             , Textfield.floatingLabel
             , Textfield.text'
             , Textfield.value <| Maybe.withDefault "" name.value
             , Textfield.onInput <| tagged << (Form.Field.Text >> Form.Input name.path)
             , Textfield.onFocus <| tagged <| Form.Focus name.path
             , Textfield.onBlur <| tagged <| Form.Blur name.path
             ]
                ++ conditionalProperties
            )

I'm going to suggest that 66 lines of code is pretty dumb for a single field. Maybe we don't want to deal with that at scale. Let's at least figure out where we can move the complexity to. Let's introduce an OurForm module and move conditionalProperties there. Also it's got a bad name so let's rename it:

module OurForm exposing (errorMessageTranslator)

import Material.Textfield as Textfield
import Form.Error


errorMessageTranslator field =
    case field.liveError of
        Just error ->
            case error of
                Form.Error.InvalidString ->
                    [ Textfield.error "Cannot be blank" ]

                Form.Error.Empty ->
                    [ Textfield.error "Cannot be blank" ]

                Form.Error.CustomError errString ->
                    [ Textfield.error errString ]

                _ ->
                    [ Textfield.error <| toString error ]

        Nothing ->
            []

Here's this commit.

That's still dumb though

Yeah we called this errorMessageTranslator because tha'ts what it does, but it also produces a Textfield.error and that's...weird. Let's just produce a Maybe String and if there's an error, we can merge it into the view itself:

module OurForm exposing (errorMessagesForTextfield)

import Material.Textfield as Textfield
import Form.Error
import Form exposing (FieldState)


errorMessageTranslator : FieldState String String -> Maybe String
errorMessageTranslator field =
    case field.liveError of
        Just error ->
            case error of
                Form.Error.InvalidString ->
                    Just "Cannot be blank"

                Form.Error.Empty ->
                    Just "Cannot be blank"

                Form.Error.CustomError errString ->
                    Just errString

                _ ->
                    Just <| toString error

        Nothing ->
            Nothing


errorMessagesForTextfield : FieldState String String -> List (Textfield.Property a)
errorMessagesForTextfield field =
    case errorMessageTranslator field of
        Just errorString ->
            [ Textfield.error errorString ]

        Nothing ->
            []

Here's this commit.

handleAPIErrors

We still have a lot of code in the view for handling the API Errors. Let's pull that out:

module OurForm exposing (errorMessagesForTextfield, handleAPIErrors)
-- ...
handleAPIErrors : Maybe APIFieldErrors -> FieldState String String -> FieldState String String
handleAPIErrors apiErrors field =
    case apiErrors of
        Nothing ->
            field

        Just errorDict ->
            case ( field.isDirty, field.liveError /= Nothing ) of
                ( True, True ) ->
                    field

                ( False, True ) ->
                    field

                ( True, False ) ->
                    field

                ( _, False ) ->
                    case Dict.get "name" errorDict of
                        Nothing ->
                            field

                        Just errorList ->
                            { field | liveError = Just <| Form.Error.CustomError (String.join ", " errorList) }

And now our view is getting nicer. Here's what was once a 66-line monstrosity:

nameField : Model -> Html Msg
nameField model =
    let
        ( form, apiErrors ) =
            model.newUserForm

        name =
            Form.getFieldAsString "name" form
                |> OurForm.handleAPIErrors apiErrors
    in
        Textfield.render Mdl
            [ 1, 0 ]
            model.mdl
            ([ Textfield.label "Name"
             , Textfield.floatingLabel
             , Textfield.text'
             , Textfield.value <| Maybe.withDefault "" name.value
             , Textfield.onInput <| tagged << (Form.Field.Text >> Form.Input name.path)
             , Textfield.onFocus <| tagged <| Form.Focus name.path
             , Textfield.onBlur <| tagged <| Form.Blur name.path
             ]
                ++ OurForm.errorMessagesForTextfield name
            )

Now it's mostly just dealing with the stuff we have to deal with. I can work with this. Now we can move on to the other views.

Here's this commit.

Organizations - New

OK so next I implemented the new organization form with validation. this required a bit of work and I don't want to spam this with very similar content, so I won't show it all, but here are the most important bit.

Our form changes are easy:

module View.Organizations.New exposing (view)

-- ...
import Form exposing (Form)
import Form.Field
import Form.Input
import Form.Error
import OurForm
-- ...
nameField : Model -> Html Msg
nameField model =
    let
        ( form, apiErrors ) =
            model.newOrganizationForm

        name =
            Form.getFieldAsString "name" form
                |> OurForm.handleAPIErrors apiErrors
    in
        Textfield.render Mdl
            [ 1, 0 ]
            model.mdl
            ([ Textfield.label "Name"
             , Textfield.floatingLabel
             , Textfield.text'
             , Textfield.value <| Maybe.withDefault "" name.value
             , Textfield.onInput <| tagged << (Form.Field.Text >> Form.Input name.path)
             , Textfield.onFocus <| tagged <| Form.Focus name.path
             , Textfield.onBlur <| tagged <| Form.Blur name.path
             ]
                ++ OurForm.errorMessagesForTextfield name
            )


submitButton : Model -> Html Msg
submitButton model =
    Button.render Mdl
        [ 6, 1 ]
        model.mdl
        [ Button.raised
        , Button.ripple
        , Button.colored
        , Button.onClick <| tagged Form.Submit
        ]
        [ text "Submit" ]


cancelButton : Model -> Html Msg
cancelButton model =
    Button.render Mdl
        [ 6, 2 ]
        model.mdl
        [ Button.ripple
        , Button.onClick <| NavigateTo <| Just Organizations
        , Options.css "margin-left" "1rem"
        ]
        [ text "Cancel" ]


tagged : Form.Msg -> Msg
tagged =
    OrganizationMsg' << NewOrganizationFormMsg

There were a lot of other changes necessary as well, among them adding uniqueness constraints to the backend and abstracting out the api error message decoding. Here's that last bit:

module Update exposing (update)
-- ...
updateUserMsg : UserMsg -> Model -> ( Model, Cmd Msg )
updateUserMsg msg model =
    case msg of
        -- ...
        CreateUserFailed error ->
            { model | newUserForm = ( fst model.newUserForm, Just (decodeError error) ) }
                ! []
                |> andLog "Create User failed" (toString <| decodeError error)
-- ...
updateOrganizationMsg : OrganizationMsg -> Model -> ( Model, Cmd Msg )
updateOrganizationMsg msg model =
    case msg of
        -- ...
        CreateOrganizationFailed error ->
            { model | newOrganizationForm = ( fst model.newOrganizationForm, Just (decodeError error) ) }
                ! []
                |> andLog "Create Organization failed" (toString <| decodeError error)
-- ...
decodeError : OurHttp.Error -> APIFieldErrors
decodeError error =
    case error of
        BadResponse code text value ->
            case value of
                Text responseBody ->
                    JD.decodeString Decoders.apiFieldErrorsDecoder (Debug.log "r" responseBody)
                        |> Result.withDefault
                            ((Debug.log <|
                                "derp didn't get an api field errors decodable response back, instead got "
                                    ++ responseBody
                             )
                                Dict.empty
                            )

                e ->
                    Dict.empty
                        |> (Debug.log <|
                                "this is a blob how did that happen?: "
                                    ++ (toString error)
                           )

        e ->
            Dict.empty
                |> (Debug.log <|
                        "Something other than a BadResponse: "
                            ++ (toString e)
                   )

For all the other changes, here's this commit.

Projects - New

Nothing exciting here comparatively...

Here's this commit.

Editing!

Here's the final code after this episode if you're just wanting to see what's up.

So we've got everything working for our New forms, but editing is still broken and awful. How are we going to handle that? I'm not sure yet, and I think if I introduced it this episode might be too long. If you implemented editing, I'd love to see it and I'd be glad to merge a commit and add you to the contributors list on the repo, or kibitz on the implementation, or whatever. Failing that, I'll definitely get to it soon...but this is already a pretty big episode. Let's move on to something else this week:

Preparatory Readings

This week we're going to add authentication to our project. This means our API will want to know about users and only let a logged-in user do things. It also means that in some sense we're going to have to keep around an API token to make these API calls with.

Here's a post from Auth0 on handling authentication. I thought it was very well written.

We're also going to implement a cards view for our users list, so if you've forgotten about how cards work in elm-mdl you can check out that documentation as well.

Resources