Intro

I recently killed a whole day trying to figure out how to implement navigation within an Elm web application. I finally figured it out due to some helpful blogs and GitHub repos. In summary, I learned that the key to performing navigation using Elm’s Navigation package is by referencing the Location record of the Navigation module.
 
I come from a native app development background. As a result, I was not clear on what a SPA really meant and why the Navigation package was tightly coupled to it.

My current understanding is that navigation within Elm apps tend to rely on SPAs (Single Page Applications). Thus, I believe within a SPA application, we are actually manipulating the HTML DOM to display various UIs for the end-user.
 

Configuration

Several procedures enabled me to finally get navigation to work on an app

First, let’s install Elm’s navigation package:

elm-package install elm-lang/navigation

Second, let’s import the Navigation module that our code will need to reference:

import Navigation exposing (..)

Next, let’s establish the following code for our main Elm page that enables navigation:

main =
    Navigation.program UrlChange
        { init = model
        , view = view
        , update = update
        , subscriptions = (\_ -> Sub.none)
        }

 

The code above is for bootstrapping the web app to run. Observe that we invoke the function Navigation.program and supply the URLChange message as an argument.

The URLChange definition can be found here:

type Msg = UrlChange Navigation.Location

 
Once the above items have been coded, we can then define our model type:

type alias Model =
   { currentRoute : Navigation.Location }

 

Observe that our model will now maintain navigation context information. In this case, we are storing the location (i.e. URL data) within our model.

We can then initialize our model with the following function:

model : Navigation.Location -> ( Model, Cmd Msg )
model location =

( { currentRoute = location } , Cmd.none )

 
If we look at the annotation of this function, we will observe that the function has a Navigation.Location parameter.

Thus, as noted earlier, we actually called this function with the following code that we defined earlier:

main =

Navigation.program UrlChange

{ init = model

, view = view

, update = update

, subscriptions = (\_ -> Sub.none)

}

 

The init function is assigned the result of the model function.

Our update function is defined as follows:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
   case msg of
      UrlChange location ->
         ( { model | currentRoute = location }, Cmd.none )

Whenever the URL changes, the URLChange message will be dispatched and the update function will return a new model with the latest URL that our view function will need for rendering.

This now leads us to our view function which is the following:

view : Model -> Html Msg
view model =
    let
        routePath =
            fromUrlHash model.currentRoute.hash
    in
        case routePath of
            [] ->
                homePage model

            [ "home" ] ->
                homePage model

            [ "contributor", id ] ->
                Html.map Contributor <| Contributor.view <| Contributor.Model (Id "") [] [] [] []

            _ ->
                notFoundPage

 
Our view function simply returns html. However, in order for the appropriate html to be retuned, our view function needs to parse the URL that our model references as seen above.

The following functions return html:

homePage : Model -> Html Msg
homePage model =
    div []
        [ header []
            [ label [] [ text "Nikeza" ]
            , model |> loginUI
            ]
        , div [] [ contributorsUI ]
        , footer [ class "copyright" ]
            [ label [] [ text "(c)2017" ]
            , a [ href "" ] [ text "GitHub" ]
            ]
        ]

notFoundPage : Html Msg
notFoundPage =
    div [] [ text "Not Found" ]

 
At last, we define our URL parsing code as follows:

-- NAVIGATION

type alias RoutePath =
   List String

fromUrlHash : String -> RoutePath
fromUrlHash urlHash = 
   urlHash |> String.split "/" |> List.drop 1

 

Conclusion

In conclusion, I attempted to document my understanding of Elm’s Navigation package. I learned that the key to performing navigation using this package is by referencing the Location record of the Navigation module. A complete example can be found on GitHub.

Reference the following guide for installing the unit test package.

Here’s a couple of unit tests:

module HelloTest exposing (..)

import Controls.Login as Login exposing (Model)
import Home exposing (..)
import Test exposing (..)
import Expect


suite : Test
suite =
    describe "Login module"
        [ test "runtime.tryLogin succeeds with valid credentials" <|
            \_ ->
                let
                    ( login, runtime ) =
                        ( Login.Model "test" "test" False, Home.runtime )

                    result =
                        runtime.tryLogin login
                in
                    Expect.equal result.loggedIn True
        , test "runtime.tryLogin fails with invalid credentials" <|
            \_ ->
                let
                    ( login, runtime ) =
                        ( Login.Model "test" "invalid_password" False, Home.runtime )

                    result =
                        runtime.tryLogin login
                in
                    Expect.equal result.loggedIn False
        ]

Appendix

Here’s some of the test dependencies below.

Home.elm

module Home exposing (..)

import Domain.Core exposing (..)
import Controls.Login as Login exposing (..)
import Tests.TestAPI as TestAPI exposing (tryLogin)
import Services.Server as Services exposing (tryLogin)
import Html exposing (..)
import Html.Attributes exposing (..)


main =
    Html.beginnerProgram
        { model = model
        , update = update
        , view = view
        }



-- CONFIGURATION


configuration : Configuration
configuration =
    Isolation


type Configuration
    = Integration
    | Isolation


type alias Dependencies =
    { tryLogin : Loginfunction }


runtime : Dependencies
runtime =
    case configuration of
        Integration ->
            Dependencies Services.tryLogin

        Isolation ->
            Dependencies TestAPI.tryLogin



-- MODEL


type alias Model =
    { videos : List Video
    , articles : List Article
    , login : Login.Model
    }


model : Model
model =
    { videos = [], articles = [], login = Login.model }


init : ( Model, Cmd Msg )
init =
    ( model, Cmd.none )



-- UPDATE


type Msg
    = Video Video
    | Article Article
    | Submitter Submitter
    | Search String
    | Register
    | OnLogin Login.Msg


update : Msg -> Model -> Model
update msg model =
    case msg of
        Video v ->
            model

        Article v ->
            model

        Submitter v ->
            model

        Search v ->
            model

        Register ->
            model

        OnLogin subMsg ->
            case subMsg of
                Login.Attempt v ->
                    let
                        latest =
                            Login.update subMsg model.login
                    in
                        { model | login = runtime.tryLogin latest }

                Login.UserInput _ ->
                    { model | login = Login.update subMsg model.login }

                Login.PasswordInput _ ->
                    { model | login = Login.update subMsg model.login }



-- VIEW


view : Model -> Html Msg
view model =
    div []
        [ header []
            [ label [] [ text "Nikeza" ]
            , model |> sessionUI
            ]
        , footer [ class "copyright" ]
            [ label [] [ text "(c)2017" ]
            , a [ href "" ] [ text "GitHub" ]
            ]
        ]


sessionUI : Model -> Html Msg
sessionUI model =
    let
        loggedIn =
            model.login.loggedIn

        welcome =
            p [] [ text <| "Welcome " ++ model.login.username ++ "!" ]

        signout =
            a [ href "" ] [ label [] [ text "Signout" ] ]
    in
        if (not loggedIn) then
            Html.map OnLogin <| Login.view model.login
        else
            div [ class "signin" ] [ welcome, signout ]

TestAPI.elm

module Tests.TestAPI exposing (..)

import Controls.Login as Login exposing (Model)


tryLogin : Login.Model -> Login.Model
tryLogin credentials =
    let
        successful =
            String.toLower credentials.username == "test" && String.toLower credentials.password == "test"
    in
        if successful then
            { username = credentials.username, password = credentials.password, loggedIn = True }
        else
            { username = credentials.username, password = credentials.password, loggedIn = False }

Core.elm

module Domain.Core exposing (..)

import Controls.Login as Login exposing (Model)

...

type alias Loginfunction =
    Login.Model -> Login.Model


tryLogin : Loginfunction -> String -> String -> Login.Model
tryLogin loginf username password =
    loginf <| Login.Model username password False

Login.elm

module Controls.Login exposing (..)

import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)


-- MODEL


type alias Model =
    { username : String
    , password : String
    , loggedIn : Bool
    }


model : Model
model =
    Model "" "" False



-- UPDATE


type Msg
    = UserInput String
    | PasswordInput String
    | Attempt ( String, String )


update : Msg -> Model -> Model
update msg model =
    case msg of
        UserInput v ->
            { model | username = v }

        PasswordInput v ->
            { model | password = v }

        Attempt ( username, password ) ->
            { model | username = username, password = password }



-- VIEW


view : Model -> Html Msg
view model =
    div []
        [ input [ class "signin", type_ "submit", value "Signin", onClick <| Attempt ( model.username, model.password ) ] []
        , input [ class "signin", type_ "password", placeholder "password", onInput PasswordInput, value model.password ] []
        , input [ class "signin", type_ "text", placeholder "username", onInput UserInput, value model.username ] []
        ]

Intro

In this post, I will attempt to document my understanding of how to do Dependency Injection within Elm.

Dependency Injection is a technique used to provide a set of dependencies that a system requires at runtime. This technique is especially advantageous when testing that the system complies with business rules without having to provide all of the dependencies that a production environment may require.

Function Interface

To support Dependency Injection within a functional programming paradigm, we can create a type to represent the interface to a function. To accomplish this, we need to specify the signature of the arbitrary function.


Here’s a type called LoginFunction within the Domain.Core module along with its signature:

type alias Loginfunction =
    Login.Model -> Login.Model

We can then use this type as an interface to a function. Hence, we can pass a function as an argument to the tryLogin function as well as other parameters.

Here’s an example:

tryLogin : Login.Loginfunction -> String -> String -> Login.Model
tryLogin loginf username password =
    loginf <| Login.Model username password False

Test API

The TestAPI is a server library that is designed to support testing by executing client requests. These requests can be for serving up some arbitrary value to the client or by executing some arbitrary operation that adheres to an interface that the client is required to comply with.

Here’s the implementation of the TestAPI’s tryLogin function:

module Tests.TestAPI exposing (..)

import Controls.Login as Login exposing (Model)

tryLogin : Login.Model -> Login.Model
tryLogin credentials =
    let
        successful =
            String.toLower credentials.username == "test" && String.toLower credentials.password == "test"
    in
        if successful then
            { username = credentials.username, password = credentials.password, loggedIn = True }
        else
            { username = credentials.username, password = credentials.password, loggedIn = False }

Registering Dependencies

We can engineer our webpage to have the capability to run in either isolation or integration mode. Isolation mode means that our webpage will not rely on system integration (i.e. web server) to function. Hence, our webpage is its own self-contained system. Integration mode, on the other hand, means that our system does require external systems in order for the webpage to be functional.

Here’s some of the code to do it:

import Domain.Core exposing (..)
import Controls.Login as Login exposing (..)
import Tests.TestAPI as TestAPI exposing (tryLogin)
import Services.Server as Services exposing (tryLogin)
...
-- CONFIGURATION

configuration : Configuration
configuration =
    Isolation -- SET CONFIGURATION HERE ! ! !

type Configuration
    = Integration
    | Isolation

type alias Dependencies =
    { tryLogin : Login.Loginfunction }

runtime : Dependencies
runtime =
    case configuration of
        Integration ->
            Dependencies Services.tryLogin

        Isolation ->
            Dependencies TestAPI.tryLogin

The client code was updated with the following call to the tryLogin function:

        OnLogin subMsg ->
            case subMsg of
                Login.Attempt v ->
                    let
                        latest =
                            Login.update subMsg model.login
                    in
                        { model | login = runtime.tryLogin latest }

Conclusion

In conclusion, I attempted to document my journey of learning Elm by exploring Dependency Injection. The code can be found on GitHub.

Appendix

Below, are the modules that I implemented.

Home.elm

module Home exposing (..)

import Domain.Core exposing (..)
import Controls.Login as Login exposing (..)
import Tests.TestAPI as TestAPI exposing (tryLogin)
import Services.Server as Services exposing (tryLogin)
import Html exposing (..)
import Html.Attributes exposing (..)

main =
    Html.beginnerProgram
        { model = model
        , update = update
        , view = view
        }

-- CONFIGURATION

configuration : Configuration
configuration =
    Isolation

type Configuration
    = Integration
    | Isolation

type alias Dependencies =
    { tryLogin : Login.Loginfunction }

runtime : Dependencies
runtime =
    case configuration of
        Integration ->
            Dependencies Services.tryLogin

        Isolation ->
            Dependencies TestAPI.tryLogin

-- MODEL

type alias Model =
    { videos : List Video
    , articles : List Article
    , login : Login.Model
    }

model : Model
model =
    { videos = [], articles = [], login = Login.model }

init : ( Model, Cmd Msg )
init =
    ( model, Cmd.none )

-- UPDATE

type Msg
    = Video Video
    | Article Article
    | Submitter Submitter
    | Search String
    | Register
    | OnLogin Login.Msg

update : Msg -> Model -> Model
update msg model =
    case msg of
        Video v ->
            model

        Article v ->
            model

        Submitter v ->
            model

        Search v ->
            model

        Register ->
            model

        OnLogin subMsg ->
            case subMsg of
                Login.Attempt v ->
                    let
                        latest =
                            Login.update subMsg model.login
                    in
                        { model | login = runtime.tryLogin latest }

                Login.UserInput _ ->
                    { model | login = Login.update subMsg model.login }

                Login.PasswordInput _ ->
                    { model | login = Login.update subMsg model.login }

-- VIEW

view : Model -> Html Msg
view model =
    div []
        [ header []
            [ label [] [ text "Nikeza" ]
            , model |> sessionUI
            ]
        , footer [ class "copyright" ]
            [ label [] [ text "(c)2017" ]
            , a [ href "" ] [ text "GitHub" ]
            ]
        ]

sessionUI : Model -> Html Msg
sessionUI model =
    let
        loggedIn =
            model.login.loggedIn

        welcome =
            p [] [ text <| "Welcome " ++ model.login.username ++ "!" ]

        signout =
            a [ href "" ] [ label [] [ text "Signout" ] ]
    in
        if (not loggedIn) then
            Html.map OnLogin <| Login.view model.login
        else
            div [ class "signin" ] [ welcome, signout ]

Core.elm

module Domain.Core exposing (..)

import Controls.Login as Login exposing (Model, Loginfunction)

type Submitter
    = Submitter String

type Title
    = Title String

type Url
    = Url String

type Video
    = Video Post

type Article
    = Article Post

type alias Post =
    { submitter : Submitter, title : Title, url : Url }

type alias Loginfunction =
    Login.Model -> Login.Model

tryLogin : Login.Loginfunction -> String -> String -> Login.Model
tryLogin loginf username password =
    loginf <| Login.Model username password False

Login.elm

module Controls.Login exposing (..)

import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)

-- MODEL

type alias Model =
    { username : String
    , password : String
    , loggedIn : Bool
    }

model : Model
model =
    Model "" "" False

-- UPDATE

type Msg
    = UserInput String
    | PasswordInput String
    | Attempt ( String, String )

update : Msg -> Model -> Model
update msg model =
    case msg of
        UserInput v ->
            { model | username = v }

        PasswordInput v ->
            { model | password = v }

        Attempt ( username, password ) ->
            { model | username = username, password = password }

-- VIEW

view : Model -> Html Msg
view model =
    div []
        [ input [ class "signin", type_ "submit", value "Signin", onClick <| Attempt ( model.username, model.password ) ] []
        , input [ class "signin", type_ "password", placeholder "password", onInput PasswordInput, value model.password ] []
        , input [ class "signin", type_ "text", placeholder "username", onInput UserInput, value model.username ] []
        ]

TestAPI.elm

module Tests.TestAPI exposing (..)

import Controls.Login as Login exposing (Model)

tryLogin : Login.Model -> Login.Model
tryLogin credentials =
    let
        successful =
            String.toLower credentials.username == "test" && String.toLower credentials.password == "test"
    in
        if successful then
            { username = credentials.username, password = credentials.password, loggedIn = True }
        else
            { username = credentials.username, password = credentials.password, loggedIn = False }