Writing A Full Site in Phoenix and Elm

Part 3: Navigation From The Ground Up

Brandon Richey
11 min readJul 8, 2016

Last Updated

July 8th, 2016

Current Versions

Elixir: v1.3.1, Phoenix: v1.2.0, Elm: v0.17.1

Previous Post in this Series

Introduction

Our Elm application is neat but barely functional right now. One thing we’ll need to really let our application shine a bit more is add a cursory sense of navigation. We can jump in by starting off refactoring our view function to be a little more extensible.

Refactoring the View Function

So right now, view just looks like this:

view : Model -> Html Msg
view model =
div [ class "elm-app" ]
[ Html.App.map ArticleListMsg (ArticleList.view model.articleListModel) ]

That means our view can ONLY ever display the Article List, but that’s clearly not what we’re going to want long-term. Let’s refactor this out by moving the Html.App.map call into its own function:

articleListView : Model -> Html Msg
articleListView model =
Html.App.map ArticleListMsg (ArticleList.view model.articleListModel)

And now our view function should look like this:

view : Model -> Html Msg
view model =
div [ class "elm-app" ]
[ articleListView model ]

Also, it may not make sense for us to be so tightly coupling the articleView to our main application view. What if we want to navigate around and display different components? Right now we really do not have a good way to handle this, so let’s write one more function, pageView. pageView will accept a model as an argument and we’ll tweak the model later to also include a “currentView” member. Let’s write our pageView function first and see if that gives us a good path forward in writing the model changes and union types necessary for base navigation.

pageView : Model -> Html Msg
pageView model =
articleListView model

And again, we’ll return back to our view function and continue refactoring.

view : Model -> Html Msg
view model =
div [ class "elm-app" ]
[ pageView model ]

Modifying our Model for Navigation

Our navigation is sort of useless without a way to actually change it. Let’s design out how we think we’ll want this to work:

  1. Our model should store what pageView is currently displaying
  2. There should be an update call to change what pageView displays
  3. We know what each value is that pageView will look for

Based on these, let’s create a new union type, Page. This will start off with two values: Root and ArticleList. For Root, we’ll need to create a welcomeView that will function as the welcome page the user sees when they first visit our site.

type Page
= RootView
| ArticleListView

Next, we’ll modify our model to track the current view a user is seeing:

type alias Model =
{ articleListModel : ArticleList.Model
, currentView : Page
}

Then we’ll need to update our Msg union type to allow for a new type of Msg that changes the model to display something new.

type Msg
= ArticleListMsg ArticleList.Msg
| UpdateView Page

Finally, we need to change the update function to be able to react to this new Msg:

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
ArticleListMsg articleMsg ->
let (updatedModel, cmd) = ArticleList.update articleMsg model.articleListModel
in ( { model | articleListModel = updatedModel }, Cmd.map ArticleListMsg cmd )
UpdateView page ->
({ model | currentView = page }, Cmd.none)

Changing Our Views

Now we have everything we need to be able to actually do navigation, but we’ll need to do a little legwork to actually allow for the navigation to happen.

First, we’ll modify our pageView function to actually look at model.currentView and display something different depending on that model’s value:

pageView : Model -> Html Msg
pageView model =
case model.currentView of
RootView ->
welcomeView
ArticleListView ->
articleListView model

We don’t have our welcomeView function written yet, so this will bomb out. Let’s write that very quickly:

welcomeView : Html Msg
welcomeView =
h2 [] [ text "Welcome to Elm Articles!" ]

Note: Make sure you add h2 to the list of functions exposed by the Html import, or this will fail. Alternatively, change the Html import statement to be:

import Html exposing (..)

Upon doing this and refreshing the page, you should see the following:

Adding a Header for Navigation

We need to provide some sort of navigation header as well to let the user get around. You’ll start to see some logic that is looking eerily like routing logic, which is intentional. Instead of jumping right into elm-navigation or hop or one of the other routing packages out there, I felt it’d make a lot more sense for us to tackle things from a very low level and work our way up to more complicated route state management!

Time for us to write a navigation header:

header : Html Msg
header =
div []
[ h1 [] [ text "Elm Articles" ]
, ul []
[ li [] [ a [ href "#", onClick (UpdateView RootView) ] [ text "Home" ] ]
, li [] [ a [ href "#articles", onClick (UpdateView ArticleListView) ] [ text "Articles" ] ]
]
]

You’ll need a few more imports for this function above to work:

import Html.Attributes exposing (class, href)
import Html.Events exposing (onClick)

There’s not much to explain in our header function. We display a list (just a standard unordered list), and each of those contain an anchor tag. The href value is set to ‘#’ for the root route, and we have an onClick handler set up to call our update function with the appropriate Msg. Recall that our UpdateView Msg is a constructor that takes in the Page union type as its only argument, so that’s why our onClick looks like this:

onClick (UpdateView RootView)

If you didn’t wrap the UpdateView constructor call in parantheses, you’d get an error like the following:

Function `onClick` is expecting 1 argument, but was given 2.77|                                     onClick UpdateView ArticleListView
^^^^^^^^^^^^^^^
Maybe you forgot some parentheses? Or a comma?

Next, return to our view function and add the header to our div:

view : Model -> Html Msg
view model =
div [ class "elm-app" ]
[ header, pageView model ]

Now return to our page, and we should see a handy little navigation bar up at the top and the links are clickable, and even change our displayed view!

Firing Off Events When Switching Routes

One thing that we’ll probably want as part of our application is a way to load up data or fire off particular events per each time certain routes are loaded up. The good news is that with how simple our routing mechanism is right now, that becomes a very trivially easy thing to do! Let’s take a look at our update function (specifically, the UpdateView branch) and modify it to listen for particular page loads:

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
ArticleListMsg articleMsg ->
let (updatedModel, cmd) = ArticleList.update articleMsg model.articleListModel
in ( { model | articleListModel = updatedModel }, Cmd.map ArticleListMsg cmd )
UpdateView page ->
case page of
ArticleListView ->
({ model | currentView = page }, Cmd.map ArticleListMsg ArticleList.fetchArticles)
_ ->
({ model | currentView = page }, Cmd.none)

In UpdateView page, we pattern match off of the supplied Page we’re transitioning to. If we’re switching to the ArticleListView, we’ll want to fire off the fetchArticles task defined inside of ArticleList.elm. Since doing this would mean our function would return an ArticleList.Msg instead of our Main Msg type, we need to use Cmd.map to transform the return Msg type of ArticleList.fetchArticles from ArticleList.Msg into ArticleListMsg (defined inside of our Main application’s Msg union type). That way we know that any resulting Msgs from ArticleList.fetchArticle need to be handled by our parent update statement (and converted into ArticleListMsg). We use Cmd.map because we’re saying that every resulting Cmd Msg needs to get passed to the constructor for ArticleListMsg.

Now, when you navigate to the Article List, it will fetch and display the Articles from our endpoint automatically! I’m going to remove the “Fetch Articles” button from the ArticleList view, since we don’t really need it anymore!

Adding a Show Link to the Article List

One thing that may not be immediately clear is what if one of our child components, like ArticleList, want to trigger navigation to a different component, such as an ArticleShow component. You can’t do this directly on the ArticleList, so you’ll instead want to bubble up the event to the parent and let that handle routing the way we were already handling routing.

Remember that we’re essentially adding a means of navigation to the ArticleList component that we’re expecting the parent to be able to handle, so we’ll create a new union type called SubPage in ArticleList.elm:

type SubPage
= ListView
| ShowView Article.Model

And we’ll also need to update our ArticleList Msg to handle the new Msg of changing our navigation, so we’ll update the Msg union type:

type Msg
= NoOp
| Fetch
| FetchSucceed (List Article.Model)
| FetchFail Http.Error
| RouteToNewPage SubPage

And since we added a new Msg type, we need to add a handler for our update function that takes in this RouteToNewPage Msg. However, we don’t actually care what it does; it’s the parent’s job to handle those details, so we’ll add a dummy case to our update function:

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
NoOp ->
(model, Cmd.none)
Fetch ->
(model, fetchArticles)
FetchSucceed articleList ->
(Model articleList, Cmd.none)
FetchFail error ->
case error of
Http.UnexpectedPayload errorMessage ->
Debug.log errorMessage
(model, Cmd.none)
_ ->
(model, Cmd.none)
_ ->
(model, Cmd.none)

Now that we have a way of handling our RouteToNewPage Msg, we need a way to dispatch it. To do this we’ll need to add a new link to each Article that we display in the list. We should get into a habit of recognizing reusable code and breaking it apart into separate functions wherever we can. Let’s take a look at our articleLink function that we’ll write:

articleLink : Article.Model -> Html Msg
articleLink article =
a
[ href ("#article/" ++ article.title ++ "/show")
, onClick (RouteToNewPage (ShowView article))
]
[ text " (Show)" ]

We arbitrarily set the href attribute on our link, but our onClick is where the action is. On our onClick event, we fire off the RouteToNewPage Msg and we pass the ShowView SubPage, and pass the article that’s getting passed in to this function call into the ShowView constructor. Next, we’ll need to modify our renderArticle function to include our new articleLinkcall.

renderArticle : Article.Model -> Html Msg
renderArticle article =
li [ ] [
div [] [ Article.view article, articleLink article ]
]

Adding an Article Show Component

Now that we have a way to dispatch events that will move us to displaying an article, we should build a handy little ArticleShow component. Create a new file, ./Components/ArticleShow.elm, and we’ll populate it with the following:

module Components.ArticleShow exposing (..)import Components.Article as Article
import Html exposing (..)
import Html.Attributes exposing (href)
type Msg = NoOpview : Article.Model -> Html Msg
view model =
div []
[ h3 [] [ text model.title ]
, a [ href model.url ] [ text ("URL: " ++ model.url) ]
, br [] []
, span [] [ text ("Posted by: " ++ model.postedBy ++ " On: " ++ model.postedOn) ]
]

This isn’t a very complicated component, overall. The biggest gotcha here is that we have to create a dummy Msg that just has a NoOp value, since this show component doesn’t actually do anything!

Modifying Our Main Component to Display the Show Component

Open up Main.elm and we need to start making modifications. First, we need to import our new ArticleShow component:

import Components.ArticleShow as ArticleShow

And we’ll need to update our Page union type to include a view for ArticleShow:

type Page
= RootView
| ArticleListView
| ArticleShowView Article.Model

Note that the constructor for our ArticleShowView Page requires a passed-in Article.

We also need to update the Msg type to include messages from ArticleShow:

type Msg
= ArticleListMsg ArticleList.Msg
| UpdateView Page
| ArticleShowMsg ArticleShow.Msg

Next, we need to tackle the changes to the update function, and it’s a doozy.

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
ArticleListMsg articleMsg ->
case articleMsg of
ArticleList.RouteToNewPage page ->
case page of
ArticleList.ShowView article ->
({ model | currentView = (ArticleShowView article) }, Cmd.none)
_ ->
(model, Cmd.none)
_ ->
let (updatedModel, cmd) = ArticleList.update articleMsg model.articleListModel
in ( { model | articleListModel = updatedModel }, Cmd.map ArticleListMsg cmd )
UpdateView page ->
case page of
ArticleListView ->
({ model | currentView = page }, Cmd.map ArticleListMsg ArticleList.fetchArticles)
_ ->
({ model | currentView = page }, Cmd.none)
ArticleShowMsg articleMsg ->
(model, Cmd.none)

So, the biggest change here is that we need to catch any ArticleListMsgs and we pattern match off of the articleMsg argument itself. In the case of it being a RouteToNewPage from the ArticleList component, we change our currentView to dispatch to our ArticleShowView with the expected article. Otherwise, we return out the default dummy response. This block:

case articleMsg of
ArticleList.RouteToNewPage page ->
case page of
ArticleList.ShowView article ->
({ model | currentView = (ArticleShowView article) }, Cmd.none)
_ ->
(model, Cmd.none)

If we do not get a RouteToNewPage Msg, then we’ll just handle it the same way we used to handle these events.

_ ->
let (updatedModel, cmd) = ArticleList.update articleMsg model.articleListModel
in ( { model | articleListModel = updatedModel }, Cmd.map ArticleListMsg cmd )

Since we updated our Msg to include ArticleShowMsg, we need to handle that, so we’ll also handle the ArticleShowMsg case with a dummy handler.

ArticleShowMsg articleMsg ->
(model, Cmd.none)

Next, we need to change our pageView function to deal with the new ArticleShowView.

pageView : Model -> Html Msg
pageView model =
case model.currentView of
RootView ->
welcomeView
ArticleListView ->
articleListView model
ArticleShowView article ->
articleShowView article

Finally, we add an articleShowView function that takes in an article (Article.Model). It needs to map any Msgs dispatched from the ArticleShow component into the ArticleShowMsg Msg so that our main component knows how to deal with that:

articleShowView : Article.Model -> Html Msg
articleShowView article =
Html.App.map ArticleShowMsg (ArticleShow.view article)

Conclusion

Woo! That was a lot, but now we have a pretty base semblance of navigation inside of our Elm application! We can show our article list, we can view each article individually, and we even can trigger onLoad events for each transition (something which drove me crazy in the course of writing this article until I took a step back and implemented it from the ground up!). This wasn’t quite the full CRUD that I had intended to do for this post, but given how difficult this stuff was originally for me, I felt it would be nice to spend this post talking about navigation and how to handle it, how to route messages from a child to the parent, and how to trigger messages when we load a new route!

Check out my new book!

Hey everyone! If you liked what you read here and want to learn more with me, check out my new book on Elixir and Phoenix web development:

I’m really excited to finally be bringing this project to the world! It’s written in the same style as my other tutorials where we will be building the scaffold of a full project from start to finish, even covering some of the trickier topics like file uploads, Twitter/Google OAuth logins, and APIs!

--

--