Playing With Fire

Exploring the web one Elixir at a time

Adding Elm to Phoenix

Unless you’ve been hiding under rock for the past year or so, you should be aware of Elm. If so far you have managed to miss this language/paradigm in your learning then there is no time like the present to pick it up and get familiar.

This article will present how to include Elm in your Phoenix projects so that your Elm code is intergrated into the asset pipeline and built automatically as you create a Phoenix application.

  

Elm

For the uniniatated, Elm is a functional programming langauge that uses Haskell-like syntax and a javascript based runtime that enables you to build web components and SPA’s. It is interoperable with javascript (it transpiles), but allows you to write pure functional code will all the messy interactions abstracted out. A great place to start is Elm Examples, and you won’t go far wrong if you get hold of Elm in Action (Manning) or Programming Elm (PragProg).

A detailed discussion about Elm is far too long for this article, so we’re assuming at least passing familiarity. We’re also assuming that you’re comfortable with Phoenix and Elixir - at least enough to set up JSON API’s using Phoenix.

 

What you will need…

  • Erlang >= 20
  • Elixir >= 1.5
  • Phoenix >= 1.3
  • Elm >= 0.18

 

Start the Phoenix build

For this application, we’re going to be scaffolding out the API for expediency. We’re not advocating that you build a production application this way, but for a toy app like this one its fine.

Let’s go ahead and create the app, which I’ll call ElmOnFire…

$ mix phx.new elm_on_fire

When asked, go ahead and install the dependencies.

Once this is done, update the database configuration to your local dev database (config/dev.exs):

config :elm_on_fire, ElmOnFire.Repo,
  adapter: Ecto.Adapters.Postgres,
  username: "postgres",                           <----- Update here
  password: "postgres",                           <----- and here
  database: "elm_on_fire_dev",
  hostname: "localhost",
  pool_size: 10

You can then go ahead and create the database:

$ mix ecto.create

All being well, you can now fire up Phoenix and see the standard initial landing page (http://localhost:4000):

$ mix phx.server

So far so good. Nothing unusual there.

 

Create the API

Let’s create a quick JSON API that returns some data so that we have something to display in our Elm app to show that everything works nicely.

$ mix phx.gen.json Students Student students name:string age:integer subject:string classification:string

Then do what the cmd line tells you and add the resources line given to your router.

lib/elm_on_fire_web/router.ex

defmodule ElmOnFireWeb.Router do
  use ElmOnFireWeb, :router

  ...

  # Other scopes may use custom stacks.
  scope "/api", ElmOnFireWeb do
    pipe_through :api

    resources "/students", StudentController, except: [:new, :edit]
  end
end

Once these changes have been done, save the router file and run the migration.

$ mix ecto.migrate

Let’s populate the database with some data - here’s a seed file that I prepared earlier.

seeds.exs

Put this file at priv/repo/seeds.exs and then run:

$ mix run priv/repo/seeds.exs

With this data now in, we can test the API is working.

If you haven’t already, start the server and hit the endpoint to see if its all working:

$ curl -H 'Content-Type: application/json' http://localhost:4000/api/students

{"data":[{"subject":"Humility","name":"James T Kirk","id":1,"classification":"Masters","age":30},{"subject":"Hair Dressing","name":"Jean-Luc Picard","id":2,"classification":"BTEC","age":40},{"subject":"Map Reading","name":"Kathryn Janeway","id":3,"classification":"Bsc","age":35}]}%

With this working we can now add Elm into the mix.

 

Add Elm

So lets add Elm to the pipeline, in your assets folder run:

$ npm i elm
$ npm i elm-brunch --save-dev

and add a new elm/ folder to the assets/ folder, so we have:

elm_on_fire/
    assets/
        elm/
        js/

With that now added, we need to update the brunch-config.js file to enable the build process.

Change the list of watched directories (~ line 40) from:

watched: ["static", "css", "js", "vendor"],

To

watched: ["static", "css", "js", "vendor", "elm"],

and add the following to the plugins object (~ line 51)

elmBrunch: {
  elmFolder: "elm/",
  mainModules: ["Main.elm"],
  outputFolder: "../vendor/"
}

In the newly created assets/elm, start creating the Elm app. For this we’re going to need the elm core (which all Elm projects need), but also we’ll need the HTTP module so that we can fetch the feed data and NoRedInks decode pipeline to facilitate easier decoding of the JSON package to our model in Elm.

$ cd /assets/elm
$ elm package install -y elm-lang/core
$ elm package install -y elm-lang/http
$ elm package install -y NoRedInk/elm-decode-pipeline

 

Build the Elm App

This is going to be a fairly simple app, so we’re going to keep things in one file, Main.elm, and we already know that we’re going to be using a remote API, so we’ll be using the Elm Html.program function right off the bat.

Create a new file assets/elm/Main.elm and fill with the following code

module Main exposing (main)

import Html exposing (..)
import Html.Attributes exposing (class)
import Json.Decode exposing (string, int, list, Decoder, at)
import Json.Decode.Pipeline exposing (decode, required)
import Http
import Debug


type alias Student =
    { name : String
    , age : Int
    , subject : String
    , classification : String
    }


type alias Model =
    { students : List Student
    }


type Msg
    = StudentData (Result Http.Error (List Student))


initialModel : Model
initialModel =
    { students =
        [ { name = ""
          , age = 0
          , subject = ""
          , classification = ""
          }
        ]
    }


studentDecoder : Decoder Student
studentDecoder =
    decode Student
        |> required "name" string
        |> required "age" int
        |> required "subject" string
        |> required "classification" string


decodeList : Decoder (List Student)
decodeList =
    list studentDecoder


decoder : Decoder (List Student)
decoder =
    at [ "data" ] decodeList


initialCmd : Cmd Msg
initialCmd =
    decoder
        |> Http.get "http://localhost:4000/api/students"
        |> Http.send StudentData


init : ( Model, Cmd Msg )
init =
    ( initialModel, initialCmd )


viewStudent : Student -> Html Msg
viewStudent student =
    tr []
        [ td [] [ text (student.name ++ " (" ++ toString student.age ++ ")") ]
        , td [] [ text student.subject ]
        , td [] [ text student.classification ]
        ]


view : Model -> Html Msg
view model =
    div []
        [ h1 [] [ text "Enrolled Students" ]
        , table [ class "table" ]
            [ thead []
                [ tr []
                    [ th [] [ text "Name (Age)" ]
                    , th [] [ text "Course" ]
                    , th [] [ text "Type" ]
                    ]
                ]
            , tbody [] (List.map viewStudent model.students)
            ]
        ]


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        StudentData (Ok students) ->
            ( { model | students = students }, Cmd.none )

        StudentData (Err _) ->
            ( model, Cmd.none )


main : Program Never Model Msg
main =
    program
        { init = init
        , view = view
        , update = update
        , subscriptions = \_ -> Sub.none
        }

If you can’t be bothered to type all that in, you can download from here Main.elm

Then in your lib/elm_on_fire_web/templates/page/index.html.eex, replace all the content with:

<div id="elm-main"></div>

and add

const elmDiv = document.getElementById('elm-main')
    , elmApp = Elm.Main.embed(elmDiv);

to your assets/js/app.js file.

Start your Phoenix server if it’s not running, and go to http://localhost:4000 and you should see: Screenshot image of data table

That’s its for now. Enjoy