Subscribe now

Content Catalog, Part 3: Topics and Content [07.07.2016]

In the last episode, we introduced link helpers so you could navigate around our app. Now we'll introduce pages for each topic, and extract a Topic component. Giddyup.

Project

We're starting out with the content-catalog repo tagged with before_episode_014.4.

If we look in Main.elm we can see where we've stubbed out our Topic view. Let's replace that with a function in a new module:

import Topic

-- ...

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

Now we'll make the Topic module:

module Topic exposing (..)

import Html exposing (Html, text)


view : Html msg
view =
    text "Topics view goes here"

If we refresh the page, the topics view should still work. Now we want to introduce a list of topics. Let's do that:

type alias Topic =
    { id : Int
    , title : String
    , slug : String
    }


fakeTopics : List Topic
fakeTopics =
    [ { id = 1, title = "Elixir", slug = "elixir" }
    , { id = 2, title = "Elm", slug = "elm" }
    ]

So here's our stubbed out list of topics. We want to render them on the page:

import Html exposing (..)


view : List Topic -> Html msg
view topics =
    ul []
        (List.map topicListItemView topics)


topicListItemView : Topic -> Html msg
topicListItemView topic =
    li [] [ text topic.title ]

We'll need to pass the fakeTopics in as an argument when we call the view function from the Main module:

view : Model -> Html Msg
view model =
    let
        body =
            case model.route of
                -- ...
                Just (Route.Topics) ->
                    Topic.view Topic.fakeTopics
                -- ...

Now if we refresh the page, we have a list of topics. We'd like to be able to route to them by slug, so we'll update our urlFor and locFor functions, as well as add a Location for viewing a particular topic by slug:

type Location
    = Home
    | Topics
    | Topic String

urlFor : Location -> String
urlFor loc =
    let
        url =
            case loc of
                -- ...
                Topic slug ->
                    "/topics/" ++ slug
                -- ...


locFor : Navigation.Location -> Maybe Location
locFor path =
    let
        -- ...
    in
        case segments of
            -- ...
            [ "topics", slug ] ->
                Just (Topic slug)
            -- ...

Does this compile? Nope.

-- MISSING PATTERNS ----------------------------------------------- src/Main.elm

This `case` does not have branches for all possibilities.

48|>            case model.route of
49|>                Just (Route.Home) ->
50|>                    About.view
51|>
52|>                Just (Route.Topics) ->
53|>                    Topic.view Topic.fakeTopics
54|>
55|>                Nothing ->
56|>                    text "Not found!"

You need to account for the following values:

    Maybe.Just (Route.Topic _)

Add a branch to cover this pattern!

Elm just told us we ought to actually have a view for this route, eh? Let's add it:

view : Model -> Html Msg
view model =
    let
        body =
            case model.route of
                -- ...
                Just (Route.Topic slug) ->
                    Topic.viewTopic slug Topic.fakeTopics
                -- ...

Of course the compiler will gladly inform us that there is no such view, so we'll add it:

viewTopic : String -> List Topic -> Html msg
viewTopic slug topics =
    let
        currentTopic =
            List.filter (\t -> t.slug == slug) topics
                |> List.head
    in
        case currentTopic of
            Nothing ->
                text "Topic not found!"

            Just topic ->
                text ("This is the " ++ topic.slug ++ " topic")

Now, if we visit http://localhost:8000/src/Main.elm#/topics/elm, we can see the content for this view! Obviously we should be linking to it though...

topicListItemView : Topic -> Html msg
topicListItemView topic =
    li [] [ link ( Route.Topic topic.slug, topic.title ) ]

This would almost work, but the link function is in Main and we obviously can't import that. For now, I'll create a Helpers module that contains the link function, and use that in both Main and Topic:

vim src/Helpers.elm
module Helpers exposing (link)

import Route
import Html exposing (Html, a, text)
import Html.Attributes exposing (href)


link : ( Route.Location, String ) -> Html msg
link ( loc, label ) =
    a [ href <| Route.urlFor loc ] [ text label ]
vim src/Main.elm
import Helpers exposing (link)
vim src/Topic.elm
import Helpers exposing (link)

And with that, everything should Just Work.

Summary

In today's episode, we added some nested routes and learned that the design for this doesn't preclude us from doing more interesting things. We also extracted a Helpers module that screams that it is going to accrete too much unrelated stuff, but we can deal with that when we get to it.

The biggest problem in this code is that we've basically duplicated the concept of a 404 from the root. But is it a problem really? I think it's a really interesting problem to noodle on for a bit.

Also, huge props to Brian Hicks for talking through my first attempt at this, which was more convoluted than made sense. Blog post about that coming soon though...

I hope you enjoyed exploring nested routes. See you soon!

Resources