Writing A Full Site in Phoenix and Elm

Last Updated

June 29th, 2016

Current Versions

Elixir: v1.2.6, Phoenix: v1.1.6, Elm: v0.17

Introduction

Elixir is a very nice functional language, and with Phoenix, we can use it to build very nice backends to our web applications. The trouble is, however, that as more and more sites become JavaScript-heavy, we lose our nice functional language to go work in those other languages. What are our options in the scenario that we want to stick with functional programming both in the backend but also in the front-end to our sites as well?

Creating Our Phoenix Site

We’ll start off by creating our initial Phoenix site just to power the Elm front-end and provide the backend API layer. Run the following command:

$ mix phoenix.new elm_articles

Adding Brunch-Elm

We’ll start off by adding the brunch-elm package via NPM which will allow us to work on Elm alongside our Phoenix application:

$ npm install --save-dev elm-brunch
// Configure your plugins
plugins: {
babel: {
// Do not use ES6 compiler in vendor code
ignore: [/web\/static\/vendor/]
},
elmBrunch: {
elmFolder: 'web/elm',
mainModules: ['Main.elm'],
outputFolder: '../static/js',
}
},
watched: [
'web/static',
'test/static',
'web/elm',
],
import Elm from './main';
const elmDiv = document.querySelector('#elm-target');
if (elmDiv) {
Elm.Main.embed(elmDiv);
}
$ mkdir web/elm
$ cd web/elm
$ elm package install elm-lang/html

Creating Our Elm Site

Now let’s start creating the actual code base for our initial foray into Elm. Run the following commands:

$ touch web/elm/Main.elm
module Main exposing (..)import Html exposing (text, Html)main : Html a
main =
text "Hello Foo"
<div id="elm-target"></div>
Our first Phoenix/Elm app!
$ mkdir web/elm/Components
$ touch web/elm/Components/ArticleList.elm
"source-directories": [
".",
"./Components"
],
module Components.ArticleList exposing (view)import Html exposing (Html, text, ul, li, div, h2)
import Html.Attributes exposing (class)
view : Html a
view =
div [ class "article-list" ] [
h2 [] [ text "Article List" ],
ul []
[ li [] [ text "Article 1" ]
, li [] [ text "Article 2" ]
, li [] [ text "Article 3" ] ] ]
module Main exposing (..)import Html exposing (Html, text, div)
import Html.Attributes exposing (class)
import Components.ArticleList as ArticleListmain : Html a
main =
div [ class "elm-app" ] [ ArticleList.view ]
module Components.ArticleList exposing (view)import Html exposing (Html, text, ul, li, div, h2)
import Html.Attributes exposing (class)
renderArticles : List (Html a)
renderArticles =
[ li [] [ text "Article 1" ]
, li [] [ text "Article 2" ]
, li [] [ text "Article 3" ] ]
view : Html a
view =
div [ class "article-list" ] [
h2 [] [ text "Article List" ]
, ul [] renderArticles ]
module Article exposing (view, Model)import Html exposing (Html, span, strong, em, a, text)
import Html.Attributes exposing (class, href)
type alias Model =
{ title : String, url : String, postedBy : String, postedOn: String }
view : Model -> Html a
view model =
span [ class "article" ]
[a [ href model.url ] [ strong [ ] [ text model.title ] ]
, span [ ] [ text (" Posted by: " ++ model.postedBy) ]
, em [ ] [ text (" (posted on: " ++ model.postedOn ++ ")") ]
]
type alias Model =
{ title : String, url : String, postedBy : String, postedOn: String }
module Components.ArticleList exposing (view)import Html exposing (Html, text, ul, li, div, h2)
import Html.Attributes exposing (class)
import List
import Article
articles : List Article.Model
articles =
[ { title = "Article 1", url = "http://google.com", postedBy = "Author", postedOn = "06/20/16" }
, { title = "Article 2", url = "http://google.com", postedBy = "Author 2", postedOn = "06/20/16" }
, { title = "Article 3", url = "http://google.com", postedBy = "Author 3", postedOn = "06/20/16" }
]
renderArticle : Article.Model -> Html a
renderArticle article =
li [ ] [ Article.view article ]
renderArticles : List (Html a)
renderArticles =
List.map renderArticle articles
view : Html a
view =
div [ class "article-list" ]
[ h2 [] [ text "Article List" ]
, ul [] renderArticles ]

Making ArticleList.elm More Idiomatic

ArticleList is working, and that’s great, but it’s completely divorced from any sort of meaningful user interaction. If we want it to be able to really react appropriately to any sort of interaction or events, we need to refactor it to be more idiomatic to the Elm architecture. We’ll need to define an initial model, an initializer, a view function, an update function, and a Msg union type. Our Model definition as it exists right now is fine, though, thankfully. Open up web/elm/Components/ArticleList.elm and let’s add to it:

module Components.ArticleList exposing (..)
import Html exposing (Html, text, ul, li, div, h2, button)
import Html.Attributes exposing (class)
import Html.Events exposing (onClick)
import List
import Article
type alias Model =
{ articles: List Article.Model }
articles : Model
articles =
{ articles =
[ { title = "Article 1", url = "http://google.com", postedBy = "Author", postedOn = "06/20/16" }
, { title = "Article 2", url = "http://google.com", postedBy = "Author 2", postedOn = "06/20/16" }
, { title = "Article 3", url = "http://google.com", postedBy = "Author 3", postedOn = "06/20/16" } ] }
type Msg
= NoOp
| Fetch
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
NoOp ->
(model, Cmd.none)
Fetch ->
(articles, Cmd.none)
initialModel : Model
initialModel =
{ articles = [] }
view : Model -> Html Msg
view model =
div [ class "article-list" ]
[ h2 [] [ text "Article List" ]
, button [ onClick Fetch, class "btn btn-primary" ] [ text "Fetch Articles" ]
, ul [] (renderArticles model) ]
renderArticles : Model -> List (Html a)
renderArticles model =
List.map renderArticle model.articles

Making Main.elm More Idiomatic

Some of you probably noticed that Main.elm was lacking a lot of the standard code that you tend to find in larger Elm applications, and none of it was really engineered around Elm’s standard architecture, which makes embedding apps that have any sense of user interaction significantly harder! Plus, our previous component that is now more idiomatic doesn’t work due to our substantial refactoring!

type alias Model =
{ articleListModel: ArticleList.Model }
initialModel : Model
initialModel =
{ articleListModel = ArticleList.initialModel }
init : (Model, Cmd Msg)
init =
( initialModel, Cmd.none )
type Msg
= ArticleListMsg ArticleList.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 )
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
import Html.App
view : Model -> Html Msg
view model =
div [ class "elm-app" ]
[ Html.App.map ArticleListMsg (ArticleList.view model.articleListModel) ]
main : Program Never
main =
Html.App.program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
Our final result (for now)

The Code So Far

web/elm/Components/ArticleList.elm:

module Components.ArticleList exposing (..)import Html exposing (Html, text, ul, li, div, h2, button)
import Html.Attributes exposing (class)
import Html.Events exposing (onClick)
import List
import Article
type alias Model =
{ articles: List Article.Model }
type Msg
= NoOp
| Fetch
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
NoOp ->
(model, Cmd.none)
Fetch ->
(articles, Cmd.none)
articles : Model
articles =
{ articles =
[ { title = "Article 1", url = "http://google.com", postedBy = "Author", postedOn = "06/20/16" }
, { title = "Article 2", url = "http://google.com", postedBy = "Author 2", postedOn = "06/20/16" }
, { title = "Article 3", url = "http://google.com", postedBy = "Author 3", postedOn = "06/20/16" } ] }
renderArticle : Article.Model -> Html a
renderArticle article =
li [ ] [ Article.view article ]
renderArticles : Model -> List (Html a)
renderArticles articles =
List.map renderArticle articles.articles
initialModel : Model
initialModel =
{ articles = [] }
view : Model -> Html Msg
view model =
div [ class "article-list" ]
[ h2 [] [ text "Article List" ]
, button [ onClick Fetch, class "btn btn-primary" ] [ text "Fetch Articles" ]
, ul [] (renderArticles model) ]
module Article exposing (view, Model)import Html exposing (Html, span, strong, em, a, text)
import Html.Attributes exposing (class, href)
type alias Model =
{ title : String, url : String, postedBy : String, postedOn: String }
view : Model -> Html a
view model =
span [ class "article" ] [
a [ href model.url ] [ strong [ ] [ text model.title ] ]
, span [ ] [ text (" Posted by: " ++ model.postedBy) ]
, em [ ] [ text (" (posted on: " ++ model.postedOn ++ ")")
]
]
module Main exposing (..)import Html exposing (Html, text, div)
import Html.App
import Html.Attributes exposing (class)
import Components.ArticleList as ArticleList-- MODELtype alias Model =
{ articleListModel : ArticleList.Model }
initialModel : Model
initialModel =
{ articleListModel = ArticleList.initialModel }
init : (Model, Cmd Msg)
init =
( initialModel, Cmd.none )
-- UPDATEtype Msg
= ArticleListMsg ArticleList.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 )
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
-- VIEWview : Model -> Html Msg
view model =
div [ class "elm-app" ]
[ Html.App.map ArticleListMsg (ArticleList.view model.articleListModel) ]
-- MAINmain : Program Never
main =
Html.App.program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}

Next Steps

That was quite a lot to get started, so we’ll pause here and continue on in the next article in this series, where we’ll fetch the data from our API, set up a couple of mock endpoints, and slowly start to stitch together a working site and SPA!

Next Article In This Series

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 am a software engineer, and now, published author! Check out my new book at https://www.packtpub.com/web-development/phoenix-web-development

I am a software engineer, and now, published author! Check out my new book at https://www.packtpub.com/web-development/phoenix-web-development