Let's start building our content catalog. We'll start off just by adding a Main module, a Topics component, an About component, and Navigation. Let's get started.

Project

Setup (off-video)

We'll kick off a new project:

mkdir content-catalog
cd content-catalog
elm package install -y elm-lang/html
mkdir src
vim elm-package.json
{
    "version": "1.0.0",
    "summary": "A content catalog in elm",
    "repository": "https://github.com/knewter/content-catalog.git",
    "license": "BSD3",
    "source-directories": [
        "src"
    ],
    "exposed-modules": [],
    "dependencies": {
        "elm-lang/core": "4.0.1 <= v < 5.0.0",
        "elm-lang/html": "1.0.0 <= v < 2.0.0"
    },
    "elm-version": "0.17.0 <= v < 0.18.0"
}
vim src/Main.elm
module Main exposing (..)

import Html.App as App
import Html exposing (..)
import Html.Attributes exposing (..)


type alias Model =
    {}


type Msg
    = NoOp


init : ( Model, Cmd Msg )
init =
    {} ! []


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        NoOp ->
            model ! []


subscriptions : Model -> Sub Msg
subscriptions _ =
    Sub.none


view : Model -> Html Msg
view model =
    div []
        [ navigationView model
        , currentPageView model
        ]


navigationView : Model -> Html Msg
navigationView model =
    nav []
        [ ul []
            [ li [] [ a [ href "#" ] [ text "Home" ] ]
            , li [] [ a [ href "#" ] [ text "Topics" ] ]
            ]
        ]


currentPageView : Model -> Html Msg
currentPageView model =
    text "home page"


main : Program Never
main =
    App.program
        { init = init
        , update = update
        , subscriptions = subscriptions
        , view = view
        }

Implementation

We've got a basic app structure in place. Let's have a look at it:

elm reactor

Visit .

Here you can see have a navigation section and a main content area in use. Let's add an About module that just contains a view to render as the default body:

vim src/About.elm
module About exposing (..)

import Html exposing (Html, text)


view : Html msg
view =
    text "This is the about page"

And we'll switch the Main module to use this for the default view:

view : Model -> Html Msg
view model =
    div []
        [ navigationView model
        , About.view
        ]

We can reload the page to see this change.

Let's teach our data layer about routing. We'll define a Route module that is going to be deferred to for a lot of this to avoid cluttering our Main module.

vim src/Route.elm
module Route exposing (..)

type Location
    = Home
    | Topics


type alias Model =
    Maybe Location


init : Maybe Location -> Model
init location =
    location

Here we define Route module that has a Location type that defines the sorts of locations our app knows about. The model is a Maybe Location because it's possible someone tries to visit an invalid URL. Let's wire this into our Main:

import Route

type alias Model =
    { route : Route.Model
    }


init : ( Model, Cmd Msg )
init =
    { route = Route.init (Just Route.Home)
    }
        ! []

view : Model -> Html Msg
view model =
    let
        body =
            case model.route of
                Just (Route.Home) ->
                    About.view

                Just (Route.Topics) ->
                    text "topics view goes here"

                Nothing ->
                    text "Not found!"
    in
        div []
            [ navigationView model
            , body
            ]

So here we're switching the body based on the route in our model. Let's modify the route in our init and see the Topics view get shown:

init : ( Model, Cmd Msg )
init =
    { route = Route.init (Just Route.Topics)
    }
        ! []

Refresh, and we're looking at the Topics view.

Next, we'll introduce elm-lang/navigation. This is a library explicitly for managing the location of the browser - it's not a 'router' in the sense that other frameworks tend to have, and if you read the docs for the package you can see a bit of reasoning around that.

We'll install the package.

elm package install -y elm-lang/navigation

Now we're going to begin implementing routes as they are implemented in the mantl-ui-frontend, which I've linked to in the resources section.

First things first, we'll switch to using Navigation.program rather than Html.App.program. This adds two important things we need to pass to program: A Parser, which turns the raw URL string into useful data, and an urlUpdate function, which takes that data and the current model and produces a new (model, Cmd msg). We also need to take in that data in our init function as the first argument.

main : Program Never
main =
    Navigation.program (Navigation.makeParser Route.locFor)
        { init = init
        , update = update
        , urlUpdate = updateRoute
        , view = view
        , subscriptions = subscriptions
        }

-- We'll also add our `updateRoute` which sets our model's route based on
-- location changes.
updateRoute : Maybe Route.Location -> Model -> ( Model, Cmd Msg )
updateRoute route model =
    { model | route = route } ! []

All that's left to have this wired up is to define the Route.locFor function, which will tell our app what location it should be on based on parsing the URL. Let's add it:

import String exposing (split)
import Navigation

locFor : Navigation.Location -> Maybe Location
locFor path =
    let
        -- We'll look at the path's hash and split on slash, ignoring empty
        -- segments and the hash symbol.
        segments =
            path.hash
                |> split "/"
                |> List.filter (\seg -> seg /= "" && seg /= "#")
    in
        case segments of
            -- No segments means we're on the home page
            [] ->
                Just Home

            -- "/topics" means we're on the topics page
            [ "topics" ] ->
                Just Topics

            -- Otherwise, return `Nothing` and let our "not found" view take over
            _ ->
                Nothing

We can try it out...and it's not quite right! That's because our init function needs to accept a location to be a Navigation.program, so let's tweak that:

init : Maybe Route.Location -> ( Model, Cmd Msg )
init location =
    let
        route =
            Route.init location
    in
        { route = route
        }
            ! []

Now, if we visit http://localhost:8000/src/Main.elm/#/topics we're on the topics view, but if we visit ,http://localhost:8000/src/Main.elm/#/> we're on the main view. You can also visit http://localhost:8000/src/Main.elm/#/asdf to see our notfound view.

That's enough for today.

Summary

In today's episode, we saw how to implement what I'm still going to call routing using elm-lang/navigation. We also started to build out a few more subcomponents for our application, so we can avoid the trap of only using examples that are a single Elm file. I hope you've enjoyed it - there's lots more to do. See you soon!

Resources