Phoenix with Elm 1

Prerequisites

You’ll need to have the items below installed in order to follow along:

Versions used in this tutorial

If you can, please use the versions noted below as it will make following the tutorial easier. If you can’t then check the gotchas on the repo as others may have logged issues for the version that you are using.

Creating a Phoenix project

  1. The first thing that we want to do is to create a Phoenix project. Open a terminal and navigate to where you want to create the project. Then do the following:

    mix phoenix.new seat_saver
    cd seat_saver
    
  2. Now we’ll set up the database.

    Make sure that you have Postgres running and that you either have a postgres user set up in Postgres or that you have valid user credentials in both the config/dev.exs and config/test.exs files.

    Create the database for the project by running:

    mix ecto.create
    
  3. We can run the tests to check that everything went according to plan by running:

    mix test
    

    There should be 4 passing tests.

  4. If we fire up the Phoenix server

    iex -S mix phoenix.server
    

    and visit http://localhost:4000 in the browser, we should see something like this:

    Phoenix start page

Getting Elm and Phoenix to play together

Now that we have a basic Phoenix application in place, let’s add a basic Elm application into the mix. There are several ways that we can combine Phoenix with Elm:

  1. Keep the two applications completely separate. This is probably the easiest way to go.

    uncombined

    We compile Elm to JavaScript using the elm make command and just run it in a browser. We can then make use of HTTP or similar to talk between the two.

  2. Seeing as the Elm application compiles down to JavaScript we can just vendor it into our Phoenix application.

    vendored

    Compiling our Elm application into web/static/vendor will ensure that the Brunch pipeline picks it up when building our Phoenix application’s JavaScript. You can put your Elm application’s JavaScript anywhere within your Phoenix project as long as you tell Brunch where to find it. Another reason that I am placing it into web/static/vendor is that Elm builds to ES5 JavaScript and Brunch is setup by default not to transpile files in web/static/vendor.

  3. The third way, and the way that we will be using in this tutorial, is to embed Elm in your Phoenix application.

    embedded

    By embedding our Elm application into our Phoenix application we can take advantage of the existing Brunch pipeline to first compile our Elm application into a JavaScript file, and then to have the resulting JavaScript file added to the existing build pipeline so that it is available for our Phoenix application. This has the added bonus of enabling livereload every time you make a change in your Elm application files.

Adding Elm into Phoenix

Let’s start by adding an Elm application into our Phoenix application.

  1. Shutdown the Phoenix server (Ctrl+c twice) so that Brunch doesn’t build whilst we’re setting things up.

    If you forget to close the server you may find yourself with an elm-stuff folder and elm-package.json file in the root of your Phoenix project. Just delete these and carry on with the instructions below.
  2. In the terminal, at the root of the seat_saver project we just created, do the following:

    mkdir web/elm
    cd web/elm
    elm package install -y
    elm package install evancz/elm-html -y
    
  3. Create a file called SeatSaver.elm in the web/elm folder and add the following:

    module SeatSaver where
    
    import Html
    
    main = Html.text "Hello from Elm"
    

    This creates a new Elm module called SeatSaver and then imports the Html library so that we can use its functions. Every Elm application must have a main function that acts as its starting point. In our main function we call out to the text function in the Html library, passing it a string. This will result in that string being written out to the screen when the Elm application is run in a browser.

Building with Brunch

Now let’s set up Brunch to automatically build the Elm file for us whenever we save changes to it.

Brunch is an HTML5 build tool sort of like Grunt or Gulp. We’re going to use it to compile our Elm application into JavaScript and then package it up with the rest of our application’s JavaScript. We’re using Brunch because it is included by default with Phoenix. If you are not familiar with Brunch you should still be able to follow along with the instructions below. However, if you want to know more, the Brunch Guide is the best place to start.

  1. Add elm-brunch to your package.json directly after the "brunch": <version> line. Brunch runs plugins in the order in which they are found within the package.json file, so we put the elm-brunch plugin right at the top to ensure that the JavaScript resulting from the Elm compilation is available before any of the other JavaScript plugins start their tasks.

    // package.json
    {
      ...
      "dependencies": {
        "babel-brunch": "~6.0.0",
        "brunch": "~2.1.3",
        "elm-brunch": "~0.4.4",
        "clean-css-brunch": "~1.8.0",
        ...
      }
    }
    
  2. Return to the project root folder cd ../.. and run npm install.

  3. Edit your brunch-config.json file as follows, adding our Elm file into the watched list so that live reload will fire after any changes and making sure that elmBrunch is the first plugin:

    // brunch-config.json
    {
      ...
      paths: {
        watched: [
          ...
          "test/static",
          "web/elm/SeatSaver.elm"
        ],
        ...
      },
    
      plugins: {
        elmBrunch: {
          elmFolder: 'web/elm',
          mainModules: ['SeatSaver.elm'],
          outputFolder: '../static/vendor'
        },
        ...
      },
      ...
    }
    
  4. Your file package.json in the root of the project must contain:

    code>
      "dependencies": {
         "elm-brunch": "^0.5.0",
         "phoenix": "file:deps/phoenix",
         "phoenix_html": "file:deps/phoenix_html"
      },
    

Hooking up to the frontend

Now we need to adjust our Phoenix application to display the HTML output by the Elm application.

  1. Replace web/templates/page/index.html.eex with the following:

    <div id="elm-main"></div>
    
  2. By making this change we have broken one of our tests. To keep it passing for now, let’s make a small tweak to test/controllers/pagecontrollertest.exs.

    test "GET /", %{conn: conn} do
      conn = get conn, "/"
      assert html_response(conn, 200) =~ "<div id=\"elm-main\"></div>"
    end
    
  3. Now we can hook up our Elm application by adding the following to the bottom of our web/static/js/app.js file:

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

    This grabs the div we just set up by its ID and then calls Elm.embed passing in the name of our module Elm.SeatSaver and the div that we just captured.

    Elm.embed is not the only way to work with an Elm application. We could have avoided using an element to embed the application into by calling Elm.fullscreen(Elm.SeatSaver) instead.
  4. In order to keep things easier to see, let’s also change the web/templates/layout/app.html.eex

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="description" content="">
        <meta name="author" content="">
    
        <title>Hello SeatSaver!</title>
        <link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
      </head>
    
      <body>
        <div class="container">
    
          <main role="main">
            <%= render @view_module, @view_template, assigns %>
          </main>
    
        </div> <!-- /container -->
        <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
      </body>
    </html>
    
  5. Firing up the Phoenix server again should build the Elm file and output the JavaScript to web/static/vendor/seatsaver.js, which will in turn get compiled into priv/static/js/app.js (providing we made no further changes to brunch-config.json).

    iex -S mix phoenix.server
    
  6. If you point your browser to http://localhost:4000 now you should see something like this:

    Phoenix with Elm

Adding a simple View

OK so now we have a basic Phoenix application into which we’ve embedded a simple Elm application. Let’s start to flesh out our Elm application a little. Elm has a Model - Update - View architecture. Let’s take a look at what that means:

  1. Model Elm keeps its state in one single place, the Model. The Model is immutable.
  2. Update Elm has an update function that steps the model from one state to the next. It takes an Action to be performed and the current model and then swaps out the current model for a new one.
  3. View A representation of the current model that can be displayed to the user, be that HTML, SVG or some other visual representation.

We’ll start with the View. We want to create a simple representation of an airplane with seats that we can book. Something like this.

seat saver all available

It’s not the most accurate rendition, but it’s just enough for us to demonstrate everything we need to.

You can grab the assets from the SeatSaver repo. The necessary styles are in web/static/css/seatsaver.css and the required image is in web/static/assetsimages/seat.png.
  1. Open web/elm/SeatSaver.elm.
  2. Now we can create a View function. A View function is just an ordinary function that returns HTML. That is to say that its return value must be of the type Html. Elm is a statically typed language, so every value has a type. Luckily for us Elm also uses type inference so that we don’t typically have to declare what type a value has. We’ll explain more about types as we go through the tutorial, covering what we need to know when we need to know it. For now, change the main function as follows:

    main =
      view
    
    -- VIEW
    
    view =
      Html.text "Woo hoo, I'm in a View"
    

    A few other things to note here are that --VIEW is just a standard Elm comment and has no special value. We’re going to put all of our Elm code in one file in this tutorial to keep it easier to see what’s going on. These comments will help us to find things as we add more code.

    Also Elm idiomatically places two new lines between function definitions.

  3. If you check your browser now (start the server with iex -S mix phoenix.server if it’s not already running) you should see something like this.

    woo hoo I'm in a View

What we actually want to display here is a bunch of seats, but in order to do that we need to know what seat data is going to look like. For that we need to introduce the Model. Let’s do that next.

Adding a Model and enhancing the View

The Model for our application needs to keep track of a bunch of seats. Each seat will have a seat number and a flag to tell us whether it is occupied or not.

Adding a model

Let’s start with the concept of a Seat. A Seat needs to describe the state of a given seat on our airplane. In an object oriented language we’d probably reach for a class to describe a Seat. In Elm we’ll use a type. In fact Elm already has a type that we can use for this, called a record. A record allows us to store named key-value pairs like so:

{ key_1 = value_1, key_2 = value_2 }

We’ll use a record of a given structure for our seat. We can do this using a type alias. The type alias will enable us to say “when I talk about a Seat, I’m talking about a record with this expected structure” and we define it like so:

type alias Seat =
  { key_1 : value_1
  , key_2 : value_2
  }
This (I believe) is the idiomatic way to define multi-line collections in Elm. The first item is on the same row as the opening brace, commas start each subsequent line and we finish with the closing brace on a line of its own.
  1. At the top of our web/elm/SeatSaver.elm file under the main function, add the following:

    type alias Seat =
      { seatNo : Int
      , occupied : Bool
      }
    

    We’ve specified that our Seat type will be a record that contains two items: a seatNo key that will have a value of type Int, and an occupied key that will have a value of type Bool.

  2. The Model for our application is going to be a list of seats or, I should say, a List that has elements of type Seat. We can represent that as follows:

    type alias Model =
      List Seat
    

    As before we are using a type alias to say “when I refer to Model in my application, I am talking about a List that has elements of type Seat”.

  3. We’ll now create the initial model state to give us something to work with.

    init =
      [ { seatNo = 1, occupied = False }
      , { seatNo = 2, occupied = False }
      , { seatNo = 3, occupied = False }
      , { seatNo = 4, occupied = False }
      , { seatNo = 5, occupied = False }
      , { seatNo = 6, occupied = False }
      , { seatNo = 7, occupied = False }
      , { seatNo = 8, occupied = False }
      , { seatNo = 9, occupied = False }
      , { seatNo = 10, occupied = False }
      , { seatNo = 11, occupied = False }
      , { seatNo = 12, occupied = False }
      ]
    

    This gives us 12 seats, currently hard-wired, that we can use to get our View laid out.

  4. Adding a comment to demark the section, we should end up with something that looks like this.

    module SeatSaver where
    
    import Html
    
    main =
      view
    
    -- MODEL
    
    type alias Seat =
      { seatNo : Int
      , occupied : Bool
      }
    
    type alias Model =
      List Seat
    
    init =
      [ { seatNo = 1, occupied = False }
      , { seatNo = 2, occupied = False }
      , { seatNo = 3, occupied = False }
      , { seatNo = 4, occupied = False }
      , { seatNo = 5, occupied = False }
      , { seatNo = 6, occupied = False }
      , { seatNo = 7, occupied = False }
      , { seatNo = 8, occupied = False }
      , { seatNo = 9, occupied = False }
      , { seatNo = 10, occupied = False }
      , { seatNo = 11, occupied = False }
      , { seatNo = 12, occupied = False }
      ]
    
    -- VIEW
    
    view =
      Html.text "Woo hoo, I'm in a View"
    

Using the Model in the View

We now have a Model and some initial state. Let’s pass it into our View so that we can display something to our users.

  1. In our main function we can pass the initial model to the view function as a parameter.

    main =
      view init
    
  2. Now we can adjust our View to show the Model. We’ll represent our list of seats as an unordered list with each seat being represented by a list item. We can use Elm’s Html library to do this. Each element is represented by a function that takes two lists as arguments, e.g. ul [] []. The first list holds the element’s attributes, such as class, and the second its contents, which can in turn also be other elements.

    -- VIEW
    
    view model =
      ul [ class "seats" ] (List.map seatItem model)
    
    seatItem seat =
      li [ class "seat available" ] [ text (toString seat.seatNo) ]
    

    Our view function returns an HTML unordered list. That unordered list is formed by using List.map on the List of Seat that is our model, passing each seat to the seatItem function.

    Note that we wrap (List.map seatItem model) in parentheses. In Elm parentheses are used to denote precedence, much as they are in Maths. So here we are saying, run List.map seatItem model first and then pass the result of that as an argument to the ul function. Seeing as List.map returns a List, we don’t have to wrap that result in [].

    The seatItem function returns an HTML list item, which shows the seat’s seatNo as HTML text (after parsing into a string).

    Both the ul and the li functions take a class as an attribute. This is purely for styling purposes. We’ll come back to this in a bit more detail later.

  3. Before this code will work we need to change our imports.

    import Html exposing (ul, li, text)
    import Html.Attributes exposing (class)
    

    Firstly we add exposing (ul, li, text) to our original Html import. This allows us to call the ul, li and text functions from the Html library without prefixing them with Html.

    Next we add an import for Html.Attributes specifically exposing the class function.

    We can expose specific functions from a library by listing them in the exposing tuple or we can just make them all available by using the (..) syntax instead of individually listing them (i.e. import Html exposing (..)).
  4. Looking at http://localhost:4000 again we should now see our seats displayed.

    seat list

Type annotations

So far Elm has been happily inferring the types that we are using in our application, and it will continue to do so. However let’s take a moment to look at how we can make it more obvious to others who might read our code what types we are expecting. We can do this by using type annotations.

Type annotations are optional in Elm, but they help us, and others that read our code, to better see what is going on. They also allow us to specify the contract for our functions.

A type annotation goes on the line before a function definition and consists of the name of that function, followed by a colon, followed by a list of one or more types. The list of types is separated by ->. The very last type in this list is always the return type. Elm functions always return one value, so there is always one return type. The other types refer to the type of each parameter being passed into the function.

For example,

samesies : Int -> String -> Bool
samesies number word =
  (toString number) == word

The samesies function takes two arguments, one of type Int and one of type String and returns a value of type Bool. Its type annotation is samesies : Int -> String -> Bool.

Let’s add type annotations to our existing functions.

  1. Add the following above the main function:

    main : Html.Html
    

    Our main function takes no arguments and returns Html. We’re having to prefix the Html type with Html. because it is defined in the Html library. To save us from having to do that each time, let’s tweak the import so we use the (..) syntax instead rather than naming each function that we want to use.

    import Html exposing (..)
    

    Now we can change our main function’s type annotation to

    main : Html
    
  2. The next function is the init function (we don’t have to add type annotations to type definitions as they already state their expected types). The init function takes no arguments and returns a Model.

    init : Model
    
  3. The view function takes a Model as an argument and returns Html.

    view : Model -> Html
    
  4. Last but not least, the seatItem function takes a Seat as an argument and returns Html.

    seatItem : Seat -> Html
    
  5. The end result should look like this:

    module SeatSaver where
    
    import Html exposing (..)
    import Html.Attributes exposing (class)
    
    main : Html
    main =
      view init
    
    -- MODEL
    
    type alias Seat =
      { seatNo : Int
      , occupied : Bool
      }
    
    type alias Model =
      List Seat
    
    init : Model
    init =
      [ { seatNo = 1, occupied = False }
      , { seatNo = 2, occupied = False }
      , { seatNo = 3, occupied = False }
      , { seatNo = 4, occupied = False }
      , { seatNo = 5, occupied = False }
      , { seatNo = 6, occupied = False }
      , { seatNo = 7, occupied = False }
      , { seatNo = 8, occupied = False }
      , { seatNo = 9, occupied = False }
      , { seatNo = 10, occupied = False }
      , { seatNo = 11, occupied = False }
      , { seatNo = 12, occupied = False }
      ]
    
    -- VIEW
    
    view : Model -> Html
    view model =
      ul [ class "seats" ] (List.map seatItem model)
    
    seatItem : Seat -> Html
    seatItem seat =
      li [ class "seat available" ] [ text (toString seat.seatNo) ]
    
  6. Checking the browser again, nothing should have changed.

seat list

Type annotations, as mentioned above, are optional. Elm will infer our types for us. However it is good to get into the habit of using them. I find that they help me to figure out what a function should be doing. They also help to catch errors when defining functions in case we accidentally use a type that is not the one we were intending.

For example, let’s change our view function to the following:

view : Model -> Html
view model =
  List.map seatItem model

When you try to compile this, you will see the following error in your terminal window where the server is running:

type mismatch

Elm has fantastic error messages. Here it quite clearly tells us that “The type annotation for view does not match its definition.”. What this is telling us is that the view function is expected to return Html but has returned List Html instead.

Returning the view function to its original definition will fix this error.

view : Model -> Html
view model =
  ul [ class "seats" ] (List.map seatItem model)

Adding an Update

We mentioned that Elm has a Model - Update - View architecture. We’ve looked at the View and the Model, so let’s turn our attention now to the Update. The best way to get a handle on what the update function will need to do is by taking a look at its type annotation.

update : Action -> Model -> Model

The update function will take two arguments, one of type Action and one of type Model, and return a value of type Model. In actual fact it will take a type of Action to be performed and the current Model (or state) of the application, perform that Action and return a brand new Model.

It is important to note here that the update function does not change the current Model. It creates a whole new Model based on the current Model. The Model in Elm is immutable.

But what is an Action? Well it is a Union Type that lets us group a bunch of other types together. The end result is that we can then pattern match on those types in order to perform different “actions”.

Let’s look at an example,

-- UPDATE

type Action = Increment | Decrement

update : Action -> Model -> Model
update action model =
  case action of
    Increment -> model + 1
    Decrement -> model - 1

Let’s assume that our Model is an Int that initializes to 0. We have two Actions, Increment and Decrement. When the update function is called it is passed an Action and the current Model. It will then pattern match on the given Action to produce a new Model, either adding 1 or subtracting 1 from the current value of the Model accordingly.

From this we can see that the purpose of the update function, for now anyway, is to step the Model from one state to the next.

Adding an update function

Let’s update our Elm application so that we can toggle a Seat from available to occupied and vice versa.

  1. Add the following to your web/elm/SeatSaver.elm file. It doesn’t matter where you put it, but I typically stick the Update between the Model and View sections.

    -- UPDATE
    
    type Action = Toggle Seat
    
    update : Action -> Model -> Model
    update action model =
      case action of
        Toggle seatToToggle ->
          let
            updateSeat seatFromModel =
              if seatFromModel.seatNo == seatToToggle.seatNo then
                { seatFromModel | occupied = not seatFromModel.occupied }
              else seatFromModel
          in
            List.map updateSeat model
    

    OK, there’s a lot going on here, so let’s take it line by line. First of all we define an Action called Toggle. The Toggle Action will take an argument of type Seat. That is why we have Toggle Seat. We are not declaring two Actions here, otherwise there would have been a | between them.

    In our update function we have a case statement that just has one matcher currently for our Toggle Action. The Action will use List.map (in the in block at the bottom) to call the updateSeat function for each seat in the model (remember our model is a List of Seat).

    The updateSeat function is defined in the let block. The let block enables us to define functions that can be used within the local scope. The updateSeat function checks to see if the seat passed into it seatFromModel has a seatNo that matches the seatNo of the seatToToggle passed into the Action. If it matches, the function returns a new seat record with the occupied boolean flipped to the opposite value. If it doesn’t match it just returns a new seat record with the same values as the existing seatFromModel.

    Phew! The upshot of this is that, when the update function is called with the Toggle Action and a seat, it will return a new List with the given seat’s occupied boolean flipped.

Introducing StartApp

We now have our update function, but we’re not using it anywhere. We could at this point start looking at Elm Signals and Mailboxes, at folding and mapping and merging, but let’s not. Elm handily provides a wrapper around all of the necessary wiring required to have Actions routed around our application into the Update. This wrapper is called StartApp.

  1. Let’s add it to our application. Open a terminal window and do the following:

    # navigate to the web/elm folder where our Elm application lives
    cd web/elm
    
    # add the start-app library
    elm package install evancz/start-app -y
    
    # and then return to the project root, lest we forget
    cd ../..
    

    Now we can import it in our web/elm/SeatSaver.elm file.

    import StartApp.Simple
    
  2. We need to change our main function to use the start function from the StartApp.Simple library. This takes as an argument a record with our model, update and view functions, does all the necessary wiring under the covers and returns a Signal of Html values.

    A Signal in Elm is a value that changes over time. We’ll deal with them more thoroughly later. For now think of a Signal as a value that changes depending on the current state of our application. Our Signal of Html that the main function returns represents the HTML that shows the current state of our Model.
    main : Signal Html
    main =
      StartApp.Simple.start
        { model = init
        , update = update
        , view = view
        }
    
  3. We need to make one other change to join everything up. The View needs to have a way to pass events such as mouse clicks or key presses back to the Update. In order to do this, when using StartApp, we need to provide an address to send Actions to so that StartApp knows how to link everything together. We can do this as follows:

    -- VIEW
    
    view : Signal.Address Action -> Model -> Html
    view address model =
      ul [ class "seats" ] (List.map (seatItem address) model)
    
    seatItem : Signal.Address Action -> Seat -> Html
    seatItem address seat =
      li [ class "seat available" ] [ text (toString seat.seatNo) ]
    

    We need to pass the address in as the first argument, which has the type Signal.Address Action. Don’t worry too much about what is going on here just now. We will cover Signals in more detail later. All you need to know for now is that this gives us the “address” that we can send any Actions to from our View. StartApp uses this to route these through to our update function. We then add the argument address to the view function.

    Our seatItem will need to be set up in the same way so we pass the address to the seatItem when we call it (seatItem address). This may look a little odd at first, but what we are creating here is a partial function. In other words, a function where we have already provided one or more of the arguments, but not all of them. (seatItem address) returns the seatItem function with the first argument address pre-filled. The List.map function then provides each item in the model (aka the seat) as the second argument.

Clicking on a seat

We now have StartApp set up, but it doesn’t yet do anything.

  1. Let’s change our view so that we can click on a seat in the browser and have that update the model using our Toggle action.

    seatItem : Signal.Address Action -> Seat -> Html
    seatItem address seat =
      li
        [ class "seat available"
        , onClick address (Toggle seat)
        ]
        [ text (toString seat.seatNo) ]
    

    We’ve added an onClick function to our attributes, which takes the address to send the Action to as its first argument and the Action to be called (curried with the seat that was clicked to create a partial function) as its second.

    We need to import the onClick event for this to work.

    import Html.Events exposing (onClick)
    

    When we click on a seat we create a Toggle Action with the seat that was clicked as an argument and send it to the given address. StartApp will handle things from here, picking the Action up and routing it through the update function. This in turn will toggle the occupied flag of that seat.

  2. Changing the occupied flag is all well and good, but we can’t actually tell currently if that has happened or not. So that we get an indication that something has happened let’s change the style of the seat based on its occupied status.

    seatItem : Signal.Address Action -> Seat -> Html
    seatItem address seat =
      let
        occupiedClass =
          if seat.occupied then "occupied" else "available"
      in
        li
          [ class ("seat " ++ occupiedClass)
          , onClick address (Toggle seat)
          ]
          [ text (toString seat.seatNo) ]
    

    We’re using a let block again to define a local function occupiedClass that will return “occupied” if the seat is occupied or “available” if it is not. We then use the ++ function to concatenate the result of calling occupiedClass with the existing class string.

  3. Now, if you go to your browser, you should be able to click on the seats and see them turn from gray to green and back again!

    toggling a seat

Signals

Let’s take a moment to talk about what is happening behind the scenes in our Elm application.

What is a Signal?

In Elm a Signal is a way of routing messages around the application. They are initialized with a value and always have a value from that point onwards. The values on a Signal are immutable, but the Signals themselves can be thought of as being mutable because their values can be changed. I’ll not spend much time explaining what Signals actually are here. I found the Pragmatic Studio course on Elm Signals to be the best place to get to grips with Signals, and the Elm Reactivity tutorial is also a great resource.

Initializing our application

When we initialize our application we are actually putting an initial value onto a Signal that holds values of type Model. Under the covers StartApp then wires things together so that our View gets passed this initial value and then converts it into an initial value on a Signal of values of type Html. In other words we return the HTML that represents the current state of our application.

The Signal digram below demonstrates this.

signal - tutorial 1

Clicking on a seat

Let’s now take a look at what happens when we click on a seat in our browser.

  1. Elm captures the mouse click and converts it into a value on a Signal of type () (called unit).

    A unit type can be thought of as a type that has no value. We use it to represent a mouse click because we only care that a mouse click has happened, there is no value as such associated with that action.

    signal - tutorial 2

  2. StartApp needs to have an Action to pass to our update function in order to do anything. We use the onClick function supplied by the Html.Events package to capture any values added to the Signal () and convert them into Toggle Actions curried with the seat that was clicked. This Action is added by StartApp as a value onto a Signal of type Action.

    Please note that the Action is not executed at this point, it is just added to the Signal. StartApp will route any Actions to the update function for us, so it does look like this is what happens.

    signal - tutorial 3

  3. Once the update function is called with the Toggle Action, it changes the model to be a new List of Seat that has the occupied flag toggled for the seat that matches the seat we clicked (and subsequently curried our Toggle Action with).

    signal - tutorial 4

  4. Now that the Model has been updated, the View will update to reflect the new Model.

    signal - tutorial 5

And there we have it. We’ll use signal diagrams throughout this tutorial as they’re a good way to understand what is going on in the workings of our application. Signals are one of the concepts that I found quite hard to parse at first, but stick with them (and look at the resources mentioned above) and they’ll start to click (pun unavoided) after a while.

Introducing Effects

Currently our application only allows us to model a given state and perform actions that result in changes to that state. We create an initial state for our application with the init function and thereafter are only able to change that state via the update function. The update function always returns a new Model and so the only way to do anything other than generate a new Model is to have some kind of side effect happening before we return the new Model. This is bad form in purely functional languages like Elm.

So, what if we wanted to perform some action that didn’t directly affect the state of the application? Say, for example, we wanted to perform an HTTP request (an HTTP response may change the state of the application, but the initial HTTP request will not). Elm’s StartApp (as opposed to StartApp.Simple) provides Effects for this purpose. Effects enable us to perform tasks such as HTTP requests and channel the results back through the application in a form that Elm understands.

Let’s upgrade our application from StartApp.Simple to StartApp.

  1. We’ll start from the top. Change the main function to the following:

    app =
      StartApp.start
        { init = init
        , update = update
        , view = view
        , inputs = []
        }
    
    main : Signal Html
    main =
      app.html
    
    port tasks : Signal (Task Never ())
    port tasks =
      app.tasks
    

    We start by defining a function app (we’ll ignore its type annotation for now), and inside it we call the start function of StartApp, rather than of StartApp.Simple. This function takes a record with four keys: init which takes the initial model provided by our init function, the update and view functions as before, and a list of inputs. Inputs allow us to specify external Signals that provide Actions to our application. We’ll revisit these later. For now we initialize with an empty list.

    We then change our main function so that it calls the html function on the function returned by our app function. This gives us access to the HTML that results from the View function we passed to StartApp.

    Likewise we create a port so that we can use any tasks that pass through StartApp. We’ll discuss tasks and ports more in later posts.

  2. In order to be able to use the new StartApp and related packages we need to change our existing StartApp.Simple import to the following:

    import StartApp
    import Effects exposing (Effects, Never)
    import Task exposing (Task)
    

    And we’ll need to import the Effects package.

    cd web/elm
    elm package install evancz/elm-effects -y
    cd ../..
    
  3. Now that we’re using the new StartApp our initializer needs to return more than just the initial Model. It needs to return a tuple with the initial Model and an Effects Action. An Effects Action can be thought of as a way to send an Effect that will result in an Action. As we have no Action to send at this point we use a null Effect (supplied by Effects.none).

    Change the init function to the following:

    init : (Model, Effects Action)
    init =
      let
        seats =
          [ { seatNo = 1, occupied = False }
          , { seatNo = 2, occupied = False }
          , { seatNo = 3, occupied = False }
          , { seatNo = 4, occupied = False }
          , { seatNo = 5, occupied = False }
          , { seatNo = 6, occupied = False }
          , { seatNo = 7, occupied = False }
          , { seatNo = 8, occupied = False }
          , { seatNo = 9, occupied = False }
          , { seatNo = 10, occupied = False }
          , { seatNo = 11, occupied = False }
          , { seatNo = 12, occupied = False }
          ]
      in
        (seats, Effects.none)
    

    The in block now returns (seats, Effects.none).

  4. Because our update function steps the Model from one state to the next, it too needs to return this tuple of Model and Effects Action.

    update : Action -> Model -> (Model, Effects Action)
    update action model =
      case action of
        Toggle seatToToggle ->
          let
            updateSeat seatFromModel =
              if seatFromModel.seatNo == seatToToggle.seatNo then
                { seatFromModel | occupied = not seatFromModel.occupied }
              else seatFromModel
          in
            (List.map updateSeat model, Effects.none)
    

    Now we have the option of either changing the state of the Model, or performing an Effect like an HTTP request, or both (or neither in the case of a NoOp).

  5. If we visit http://localhost:4000 in our browser our application should look and behave the same as before.

    toggling a seat

Creating a simple data API in Phoenix

This part of the tutorial is actually going to be a bit of a detour. We’re going to fetch the initial data for our Elm application over HTTP from a data API that we’ll create in our Phoenix application. However we’re going to do this on a branch of the seat_saver repo (called http for reference), because we’re ultimately going to prefer to use Phoenix Channels for this instead. If you’re using version control you may wish to do this part as a branch as well to make it easier to revert at the start of the next part of the tutorial.

Rather than hard-wire the seats in the init function we want to fetch them from a database via our Phoenix application. We’ll start by creating a simple data API in the Phoenix application to serve that data.

  1. We can use the built-in Phoenix mix tasks to build a seats endpoint. Open a terminal and use the following command to generate an endpoint scaffold.

    mix phoenix.gen.json Seat seats seat_no:integer occupied:boolean
    
  2. Now follow the instructions mix gives you and make the following adjustment to the web/router.ex file

    defmodule SeatSaver.Router do
      use SeatSaver.Web, :router
    
      ...
    
      # Other scopes may use custom stacks.
      scope "/api", SeatSaver do
        pipe_through :api
    
        resources "/seats", SeatController, except: [:new, :edit]
      end
    end
    
  3. Back in the terminal, migrate the database and run the tests to make sure that nothing is broken (you should have 14 passing tests).

    mix ecto.migrate
    mix test
    
  4. Now we need to add some seat data. We can use the priv/repo/seeds.exs file for this. Add the following to the end of that file (note that the first two seats are occupied but the rest are not):

    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 1, occupied: true})
    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 2, occupied: true})
    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 3, occupied: false})
    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 4, occupied: false})
    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 5, occupied: false})
    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 6, occupied: false})
    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 7, occupied: false})
    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 8, occupied: false})
    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 9, occupied: false})
    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 10, occupied: false})
    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 11, occupied: false})
    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 12, occupied: false})
    
  5. Run mix run priv/repo/seeds.exs to apply the seeds.

  6. Elm is going to expect our field names to be in camel-case rather than snake-case. To keep things simple we’ll just change the seat_no key to seatNo on line 14 of web/views/seat_view.ex

    # web/views/seat_view.ex
    def render("seat.json", %{seat: seat}) do
      %{id: seat.id,
        seatNo: seat.seat_no,
        occupied: seat.occupied}
    end
    

    and adjust our test on line 21 of test/controllers/seatcontrollertest.exs to match.

    # test/controllers/seat_controller_text.exs
    test "shows chosen resource", %{conn: conn} do
      seat = Repo.insert! %Seat{}
      conn = get conn, seat_path(conn, :show, seat)
      assert json_response(conn, 200)["data"] == %{"id" => seat.id,
        "seatNo" => seat.seat_no,
        "occupied" => seat.occupied}
    end
    
  7. We can run the tests again using mix test to ensure that we haven’t broken anything.

  8. Restart your Phoenix server (CTRL-C twice to shutdown and then iex -S mix phoenix.server to start again) and you should see the following at http://localhost:4000/api/seats

    Data API

I’m using a Chrome extension called JSONView so your output might not look exactly the same.

Fetching data in Elm via HTTP

As mentioned earlier, in order to do HTTP requests in Elm we need to use Effects.

We’ll start by changing our init function so that it returns a tuple with an empty list for the Model and a function called fetchSeats, which we’ll create in a minute, as an Effect.

init : (Model, Effects Action)
init =
  ([], fetchSeats)

This initializes our Model to be an empty List (remember our Model is a List of Seat records) and then calls a function fetchSeats. The fetchSeats function will return an Effects Action that StartApp will subsequently call in order to make the HTTP request that will, hopefully, provide the seat data from our data API.

Building an Effect

We’ll now implement the fetchSeats function at the end of our web/elm/SeatSaver.elm file.

-- EFFECTS

fetchSeats: Effects Action
fetchSeats =
  Http.get decodeSeats "http://localhost:4000/api/seats"
    |> Task.toMaybe
    |> Task.map SetSeats
    |> Effects.task


decodeSeats: Json.Decoder Model
decodeSeats =
  let
    seat =
      Json.object2 (\seatNo occupied -> (Seat seatNo occupied))
        ("seatNo" := Json.int)
        ("occupied" := Json.bool)
  in
    Json.at ["data"] (Json.list seat)

There’s a lot going on here, so let’s walk through it. The purpose of our fetchSeats function is to create an Effects Action that StartApp can use to make an HTTP request to our data API. We also need to let StartApp know what we want it to do when we get a response. If it is successful, we want to parse the resulting JSON into a List of Seat records and then replace the existing Model, an empty List, with that List of Seat records. If it fails for any reason, including issues with parsing the JSON, we want to be able to handle that error. We can think of this as a job being prepared and put on a queue for StartApp to run.

We are using the Elm |> function to make it easier to see the process flow here. |> is just an alias for function application, and allows us to write our function inside out. The result of calling Http.get ... gets passed to Task.toMaybe, which in turn gets passed to Task.map ..., which then gets passed to Effects.task.

Note that Elm’s |> function is not quite the same as Elixir’s pipe operator, but they are close enough in purpose for it not to really matter. The only practical difference is that, in Elixir, the value being piped is given as the first argument to the subsequent function whereas, in Elm, it is the last argument. This is because in Elm the |> function is actually creating partial functions.

So let’s step through the fetchSeats function.

  1. Http.get decodeSeat "http://localhost:4000/api/seats" creates a function that can be called to perform the HTTP request. Note that it is not called here. It will be called by StartApp when the Effects Action generated by the fetchSeats function is run. As the first argument we supply a JSON decoder that will be used to parse the body of a successful response. We define that decoder in the decodeSeats function. You can read more about the JSON decoder on the Elm docs.

  2. If we look at the type annotation for Http.get we can see that what we are returned is Task Error value This means that we will get a Task that will either return a value of type Error or a value of some type other than Error. In Elm a Task is an asynchronous operation that might fail, a perfect example of which is an HTTP request. In our case, when the Task is performed, it will either fail with a type Error, or succeed with the type returned by our JSON decoder, i.e. the type Model.

    In this way Elm uses the type system to force us to handle the failure case by returning one type for a successful result and another type for a failure result. This means that any function being called with the result of this HTTP request cannot know in advance what the type of that result will be. The Task.toMaybe function lets us handle this uncertainty by wrapping the result in a Maybe. A Maybe is an option type that enables us to say “This might be a List of Seat records, or it might not.” More on this when we come to handle this in the update function.

  3. Speaking of the update function, we already know that this is the only place where we can make changes to the Model, and that is exactly what we need to do here. We want to replace the existing Model, which is currently initialized to an empty List, with the Model we receive from our JSON decoder. We do this by mapping our existing Task, using Task.map, to one that takes an Action to be performed, in our case the SetSeats Action, and any arguments for that Action, i.e. the Maybe-wrapped result from Task.toMaybe.

  4. Finally, in order for StartApp to run the task, we need to wrap it in an Effects Action. The Effects.task function does this. The result is an Effects Action that can be returned to our init function and used by StartApp. When StartApp runs this it will result in a call to the update function with a SetSeats Action that has our new Model derived from the call to the data API. In other words, we are telling Elm “once you’ve got the result of the HTTP request and parsed it into something you can use, call the SetSeats action passing in that result”.

    Pretty straightforward, right? ;)

Wiring it all together

  1. In order for that code to work we need to add the required imports:

    import Http
    import Json.Decode as Json exposing ((:=))
    
  2. then install them:

    cd web/elm
    elm package install evancz/elm-http -y
    cd ../..
    
  3. and then add the SetSeats Action to our update function.

    -- UPDATE
    
    type Action = Toggle Seat | SetSeats (Maybe Model)
    
    update : Action -> Model -> (Model, Effects Action)
    update action model =
      case action of
        ...
        SetSeats seats ->
          let
            newModel = Maybe.withDefault model seats
          in
            (newModel, Effects.none)
    

    Now we can see how we can use the Maybe we introduced in the fetchSeats function. If the Task completes successfully we will have a List of Seat records (aka a Model). If it fails then we won’t. As such the type annotation for this Action is SetSeats (Maybe Model). In other words the argument to SeatSeats may be a Model, or it may not.

    In our case statement we then use the Maybe.withDefault function to say “if the argument I’m given is anything other than a value of type Model return the current model, otherwise return the given argument”. As such, SetSeats will return a NoOp (i.e. (model, Effects.none)) if the Task failed or it will replace the existing Model with the List of Seat records if we successfully parsed one from the HTTP response (i.e. (seats, Effects.none)).

    In this way Elm forces us to handle both success and failure outcomes and protects us from runtime errors.

  4. Visiting http://localhost:4000/api/seats in our browser will still display the seats as before, but now the initial data is coming from our data API. We should always see the first two seats being displayed as occupied, even on a refresh (you may also see a slight delay before all the seats are displayed).

    Data API

So that’s how we fetch data via HTTP in Elm. Elm makes a lot of hard things easy for us. Unfortunately HTTP is one of the “easy” things it makes, at least initially, hard. There is good reason for this though. Elm is forcing us to work in a particular way so that we can protect ourselves from runtime exceptions in our applications.

Upgrading to Elm 0.16.0

A shiny new version of Elm was just released, and so we should upgrade for all of the goodness that it brings.

Before we start, let’s rewind our efforts. We don’t need most of the code that we added and it could serve to confuse things. We can re-add what we do need when we need it. If you created a branch in your own version of the project then you can just dump it, or do whatever you need to do to get back to the pre-HTTP state. If you can’t do that, then checking out the pre-http branch of the SeatSaver repo should get you to where you need to be.

You can check that you now have the correct version of elm by running elm repl. Type :exit to exit the repl.

Upgrading elm packages

  1. Navigate to the seat_saver project’s web/elm folder. If you try to run elm package install in the project root then it will install the packages there instead of in your Elm application.
  2. Update the elm-package.json file to look like the following:

    // web/elm/elm-package.json
    {
        "version": "1.0.0",
        "summary": "An example app for learning about using Elm with Phoenix",
        "repository": "https://github.com/cultivatehq/seatsaver.git",
        "license": "BSD3",
        "source-directories": [
            "."
        ],
        "exposed-modules": [],
        "dependencies": {
            "elm-lang/core": "3.0.0 <= v < 3.1.0",
            "evancz/elm-effects": "2.0.1 <= v < 3.0.0",
            "evancz/elm-html": "4.0.2 <= v < 5.0.0",
            "evancz/start-app": "2.0.2 <= v < 3.0.0"
        },
        "elm-version": "0.16.0 <= v < 0.17.0"
    }
    

    Note that we added a proper URL to the “repository” key. Elm requires that we do not have UPPER CASE letters in here now and will throw an error if we do. You can make this anything that makes sense for you, as long as there are no UPPER CASE letters.

    We then updated the versions on each of the “dependencies” that we are using to be the latest versions that are compatible with Elm 0.16.0.

    Finally, we update the “elm-version”.

  3. Run elm package install. This will update your packages to the right version.

  4. Return to the project root folder, cd ../...

  5. Run the tests to make sure that we haven’t broken anything mix test (you should have 4 passing tests).

  6. Start up the Phoenix server (iex -S mix phoenix.server) to compile the Elm application. If you completed first part of this tutorial before Elm 0.16.0 came out then you may get the following error:

    compile error

    This is because Elm 0.16.0 changed the way that record updates are done. The syntax has changed from <- to =. Change line 75 of web/elm/SeatSaver.elm to match the following:

    { seatFromModel | occupied = not seatFromModel.occupied }
    

    If you still have the server running, this should now recompile without error.

    I, and others, from time to time see a race condition occurring in the Brunch pipeline. You’ll see an Unexpected end of input error that is then almost immediately superseded by a successful compile.

    Brunch build error

    Most of the time this causes no issues, but sometimes it can prevent the assets from loading properly. A server restart will resolve this. If anyone finds a solution to this, please do let us know in the seat_saver repo issues. Thanks :)

  7. Go to http://localhost:4000 in your browser. The application should display as before and let you click seats to toggle them between occupied and available.

toggling a seat

Now we’re all up-to-date. We’ll start to use Phoenix’s Channels.

Introducing Phoenix channels

We took a look at how to fetch our initial seat data via an HTTP request. However, one of the most compelling reasons to use Phoenix is because of it’s first class support for Channels. Channels are a way to communicate with our Phoenix application in realtime across an open connection. They fit our Elm architecture well as they are all about the flow of data.

Before we start, let’s rewind our efforts. We don’t need most of the code that we added and it could serve to confuse things. We can re-add what we do need when we need it.

If you created a branch in your own version of the project then you can just dump it, or do whatever you need to do to get back to the pre-HTTP state.

If you can’t do that, then checking out the pre-http branch of the SeatSaver repo should get you to where you need to be.

Creating a channel

We’ll start by creating a channel and then look at how to join that channel. We’ll wrap things up for this part by fetching our initial seat data over that channel.

  1. Phoenix has a built-in mix generator for creating channels, so let’s use that.

    mix phoenix.gen.channel Seat seats
    
  2. This gives us a number of files, some of which we will need to now tweak to suit our use case. Start by updating web/channels/user_socket.ex to create a channel with a topic:subtopic of seats:planner that points to our newly generated SeatChannel module.

    defmodule SeatSaver.UserSocket do
      use Phoenix.Socket
    
      ## Channels
      # channel "rooms:*", SeatSaver.RoomChannel
      channel "seats:planner", SeatSaver.SeatChannel
    
      ...
    end
    
  3. Now open that SeatChannel module in file web/channels/seat_channel.ex and update the join function to have the same topic:subtopic pair.

    defmodule SeatSaver.SeatChannel do
      use SeatSaver.Web, :channel
    
      def join("seats:planner", payload, socket) do
        ...
      end
    
      ...
    end
    
  4. Finally update the associated test in test/channels/seatchanneltest.exs to also have that topic:subtopic pair.

    defmodule SeatSaver.SeatChannelTest do
      ...
    
      setup do
        {:ok, _, socket} =
          socket("user_id", %{some: :assign})
          |> subscribe_and_join(SeatChannel, "seats:planner")
    
       ...
      end
    
      ...
    
      test "shout broadcasts to seats:planner", %{socket: socket} do
        ...
      end
    
      ...
    end
    
  5. You can check to see if everything has worked as expected by running mix test (you should have 7 passing tests).

Joining the channel

Now that we have a channel, we need to set things up on the client side to connect to the channel.

  1. Open the web/static/js/socket.js file and change the topic:subtopic to seats:planner on line 57.

    ...
    
    socket.connect()
    
    // Now that you are connected, you can join channels with a topic:
    let channel = socket.channel("seats:planner", {})
    
    ...
    
  2. Now open web/static/js/app.js and uncomment line 21.

    import socket from "./socket"
    
  3. If you visit http://localhost:4000 and check the web console, you should see Joined successfully.

    Channel connected

Getting initial seat data

Let’s now fetch the initial seat data from the database and make it available to the client. Before we can get the initial data we need to store it in the database.

  1. Let’s start by creating a Seat model.

    mix phoenix.gen.model Seat seats seat_no:integer occupied:boolean
    
  2. Then migrate the database.

    mix ecto.migrate
    

    Please note: if you see the following error, it is because you will have created the seats table already. You can either skip this step or, if you want a clean slate, drop the table in psql (or your Postgres tool of choice).

    (Postgrex.Error) ERROR (duplicate_table): relation "seats" already exists

  3. Let’s run the generated tests with mix test to ensure that we haven’t broken anything so far. We should have 9 passing tests.

  4. We can use the priv/repo/seeds.exs file to populate some seat data for us, same as we did earlier. Add the following to the end of that file (note that the first two seats are occupied but the rest are not):

    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 1, occupied: true})
    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 2, occupied: true})
    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 3, occupied: false})
    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 4, occupied: false})
    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 5, occupied: false})
    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 6, occupied: false})
    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 7, occupied: false})
    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 8, occupied: false})
    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 9, occupied: false})
    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 10, occupied: false})
    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 11, occupied: false})
    SeatSaver.Repo.insert!(%SeatSaver.Seat{seat_no: 12, occupied: false})
    
  5. Run mix run priv/repo/seeds.exs to apply the seeds.

  6. We’re really only interested in the seat_no and occupied fields. Furthermore, we want to use camel case for the seat_no field when it is used in JSON data. We can do this by implementing the Poison Encoder protocol. Add the following to the bottom of your web/models/seat.ex file, after the end of the definition SeatSaver.Seat module.

    defimpl Poison.Encoder, for: SeatSaver.Seat do
      def encode(model, opts) do
        %{id: model.id,
          seatNo: model.seat_no,
          occupied: model.occupied} |> Poison.Encoder.encode(opts)
      end
    end
    
  7. Now change the join function in the web/channels/seat_channel.ex file to call send self(), :after_join on successful authorization, like this:

    def join("seats:planner", payload, socket) do
      if authorized?(payload) do
        send self(), :after_join
        {:ok, socket}
      else
        {:error, %{reason: "unauthorized"}}
      end
    end
    

    If you’ve watched the video for the talk that accompanies this tutorial, you’ll notice a difference in approach here. In the talk I supplied the seat data directly from the join function.

    Since then I was informed by Claudio Ortolina (@cloud8421) that this is not the preferred approach, but rather that a message is sent to self that instructs the data to be sent. This helps keep the client code clean. The code that deals with joining a channel is not also responsible for fetching the initial state, and the code that deals with fetching the initial state can be reused by the client-side application if required without having to worry about the join code.

    Sending self() a message inside a channel results in a call to function called handle_info/2.

  8. Let’s add the required handle_info function (also to the web/channels/seat_channel.ex file) with the following definition:

    def handle_info(:after_join, socket) do
      seats = (from s in SeatSaver.Seat, order_by: [asc: s.seat_no]) |> Repo.all
      push socket, "set_seats", %{seats: seats}
      {:noreply, socket}
    end
    
  9. Add the following to your web/static/js/socket.js file anywhere above the export default socket line:

    channel.on('set_seats', data => {
      console.log('got seats', data.seats)
    })
    
  10. If you go to http://localhost:4000 in your browser and open the console, you should see the following:

    Initial seat data in the console

Getting seat data into Elm

Now that we have data being sent to the client over our channel after we’ve joined it, we’ll want to pull that data into our Elm application so that we can use it to initialize our model.

Open the web/elm/SeatSaver.elm file and do the following:

  1. Change the init function to set the model to an empty List, like so:

    init : (Model, Effects Action)
    init =
      ([], Effects.none)
    
  2. In order to get data in and out of Elm we use a mechanism called ports. Add the following port to a signals section at the bottom of the file.

    -- SIGNALS
    
    port seatLists : Signal Model
    

    Sending a message to an incoming port will place the data on a Signal (we looked at Signals already. In our case we want to pass in a List of Seat records, in other words our Model.

  3. If you go to http://localhost:4000 in your browser you will see the following error:

    port error

  4. Ports need to be initialized with a starting value. This is because the port is a Signal and Signals need to have a starting value. We can do this by giving our port an initial value in web/static/js/app.js

    var elmDiv = document.getElementById('elm-main')
      , initialState = {seatLists: []}
      , elmApp = Elm.embed(Elm.SeatSaver, elmDiv, initialState);
    
  5. Now if you check the browser you should see

    initialized port

  6. OK, so we are now initializing our model to be an empty List and we have created a port through which we can send our seat data. Now we need to send that data through the port. To make it easier to work with, let’s move our channel code through from web/static/js/socket.js to web/static/js/app.js

    let channel = socket.channel("seats:planner", {})
    channel.join()
      .receive("ok", resp => { console.log("Joined successfully", resp) })
      .receive("error", resp => { console.log("Unable to join", resp) })
    
    channel.on('set_seats', data => {
      console.log('got seats', data.seats)
    })
    
  7. We can send our seat data to this port as follows:

    channel.on('set_seats', data => {
      console.log('got seats', data.seats)
      elmApp.ports.seatLists.send(data.seats)
    })
    

    Elm will automatically convert our JSON data into an Elm List for us if it can match the structure of the data passed into a type that it knows about. This is why we converted our seat_no Elixir field into the camel case version seatNo when outputting as JSON. Elm will recognise our JSON as a List of Seat records and convert it accordingly before placing it on the Signal.

  8. Looking at the browser again we still see no seat data. This is because we need to get the data from the port into StartApp so that it can be sent to our update function.

    still no seats

  9. So let’s get this data along to our update function. In order to pass the data into StartApp we need to put it on a Signal with values of type Action. We can do this using the Signal.map function. This converts every value on a given Signal to a different type on another Signal. In our Signals section add the following:

    incomingActions: Signal Action
    incomingActions =
      Signal.map SetSeats seatLists
    

    This is shorthand for the following:

    incomingActions: Signal Action
    incomingActions =
      Signal.map (\seatList -> SetSeats seatList) seatLists
    

    In other words, for every value on the seatLists Signal, convert it into a SetSeats Action with that value as its argument, and place it on the incomingActions Signal.

  10. Now we can add this Signal of Action as an input to our StartApp initializer in our app function.

    app =
      StartApp.start
        { init = init
        , update = update
        , view = view
        , inputs = [incomingActions]
        }
    
  11. And then add the SetSeats action to the update function

    type Action = Toggle Seat | SetSeats Model
    
    update : Action -> Model -> (Model, Effects Action)
    update action model =
      case action of
        Toggle seatToToggle ->
          ...
        SetSeats seats ->
          (seats, Effects.none)
    

    Because the seats that we pass in here are a List of Seats, aka a Model, we can just do a straight swap with the existing model. Thus we turn our current Model (an empty list) into a new Model (our given List of Seat records). We have no further action to take and so we have a no-op Effect.

  12. Checking the browser you should now see all of the seat data passed through.

    yay seat data

And there we have it! We are now fetching our seat data over a Phoenix channel.

Upgrade to Elixir 1.2

This is the simplest part of the upgrade because it doesn’t require any code changes to the application. The way you update Elixir will depend on your system. If in doubt see http://elixir-lang.org/install.

Upgrade to Phoenix 1.1.3

Getting to Phoenix 1.1.3 is a staged process. We’ll start by upgrading to 1.1.0, then go from 1.1.0 to 1.1.2 and finally upgrade from 1.1.2 to 1.1.3.

Upgrade to 1.1.0

This is, perhaps unsurprisingly given the minor version bump, the most involved from the Phoenix side. phoenix-ecto gets a major version bump (making changes to its API), gettext support for I18n is added and the way that layouts render the views that they wrap changes. See https://github.com/CultivateHQ/seat_saver/commit/00016ae4b0d7328984a1556c4585dd1a36c3edfd for full details.

Upgrade to 1.1.2

This update changes some things on the JavaScript side of things. Most notably there is a major version bump to Brunch, and there is a change to the way that the phoenix and phoenix.html modules are imported.

Whilst upgrading to Phoenix 1.1.2 the Brunch version is upgraded to ^2.1.1. In Brunch version 2.1.3 an update was made that stopped the elm-brunch plugin, that we use to build our Elm project, from working. As such we need to upgrade to elm-brunch ^0.4.4 at the same time.

See https://github.com/CultivateHQ/seat_saver/commit/621f87b03815359f81871d008c7f1037c06986cf for full details (the update to elm-brunch is actually done in the next commit because the issue didn’t appear when I first upgraded to Phoenix 1.1.2).

Upgrade to 1.1.3

The main change to note here isn’t actually shown in the commit. The way that the phoenix-new hex package is installed has changed. Rather than installing a particular version of the package, you no longer need the version number.

For example:

# old way
mix archive.install https://github.com/phoenixframework/phoenix/releases/download/v1.0.3/phoenix_new-1.0.3.ez

# new way
mix archive.install https://github.com/phoenixframework/archives/raw/master/phoenix_new.ez

See https://github.com/CultivateHQ/seat_saver/commit/2b8de911db7a723a58f59ee6a559f62f0a0e9ac0 for more details.

So far on Phoenix with Elm …

We looked at how to fetch our initial seat data via Phoenix channels. Our application, when it loads, opens a web socket to the server and then gets the initial seat data over this connection.

Now we want to take a look at how to send and receive data over that channel in response to user interaction with the site. For this next part of the tutorial we’re going to implement the mechanism that allows users to book a particular seat on the imaginary flight that we have.

Making a request

Let’s start with the request. We want the user to be able to click on a seat and for that click to result in a request over the web socket channel to our Phoenix application. We’ll approach this from an outside-in point of view because it will allow us to introduce new concepts when we are able to explain why we need them.

To send a request from our Elm application to our Phoenix application we need to use ports. Any data coming into or leaving our Elm application needs to go over ports. We already introduced incoming ports, but now we need an outgoing port. The type annotation for our port will look like this:

port seatRequests : Signal Seat

We’re going to be sending out data (Elm will use JSON under the covers) in the shape of a Seat. In order to be able to send things out over this port we need to hook it up to a Signal of type Seat. In our instance we also want to be able to send seat data to that Signal whenever we click on a seat in the UI. For this we can use a Mailbox. A Mailbox has an address that we can use to send values to and an associated Signal that contains the values sent to that Mailbox over time. In other words, any values we send to the Mailbox appear on its Signal. We can then attach our outgoing port to that Signal so that any values sent to the Mailbox are immediately sent out over the port.

Let’s start putting it together and explain more as we go along.

  1. We’ll start by creating our Mailbox. At the end of the web/elm/SeatSaver.elm file add the following:

    seatRequestsBox : Signal.Mailbox Seat
    seatRequestsBox =
      Signal.mailbox (Seat 0 False)
    

    This function returns a Signal.Mailbox that has an associated Signal of type Seat. Because Signals are values that change over time we need to give it an initial value. My solution here is a little hacky. We just create a new Seat record with a seatNo of 0 (so that it doesn’t match any of our database records) and set the occupied value to False.

  2. Now we can create our outgoing port that listens on the Mailbox’s Signal.

    port seatRequests : Signal Seat
    port seatRequests =
      seatRequestsBox.signal
    
  3. We now have a port that will send data out of our Elm application whenever we send a Seat record to the Mailbox that we set up. In order to consume this data we need to subscribe to that port in our web/static/app.js file.

    // listen for seat requests
    elmApp.ports.seatRequests.subscribe(seat => {
      console.log(seat)
    })
    

    We subscribe to the seatRequests port and give it a function to call whenever a new value is sent across the port. For now we are just logging what we receive to the console so that we can check that it works.

    Of course we’re not actually sending any values to that mailbox/port just now and so we won’t see anything happening yet. Let’s add that now.

  4. We want to be able to click on a seat in our UI and for that to result in a request being made. Whilst this sounds simple, in the Elm architecture it’s a little more complicated. When we make the request we don’t need to make any changes to the model (that will happen when we get a response) but we do want to have a side-effect that will make the request. We’ve seen this pattern before when we made an HTTP request.

    We start by changing the Action name on the onClick call of the seatItem view function from Toggle to RequestSeat.

    seatItem : Signal.Address Action -> Seat -> Html
    seatItem address seat =
      let
        ...
      in
        li
          ...
          , onClick address (RequestSeat seat)
          ...
    
  5. Then we create that Action in our update.

    type Action = ... | RequestSeat Seat
    
    update : Action -> Model -> (Model, Effects Action)
    update action model =
      case action of
        ...
        RequestSeat seat ->
          (model, sendSeatRequest seat)
    

    As you can see we just return the current model with no changes to it and, instead of an Effects.none, make a call to a function called sendSeatRequest passing it the given seat.

  6. Let’s now create that sendSeatRequest function. At the bottom of the web/elm/SeatSaver.elm file add the following function definition to it:

    -- EFFECTS
    
    sendSeatRequest : Seat -> Effects Action
    sendSeatRequest seat =
      Signal.send seatRequestsBox.address seat
        |> Effects.task
        |> Effects.map (always NoOp)
    

    Let’s look at each line in turn. The type annotation tells us that our sendSeatRequest function is going to take an argument of type Seat and then return an Effects Action (the function needs to return an Effects Action because that is the return type that our update function is expecting).

    The function definition looks similar to the fetchSeats function we built earlier. We use Signal.send to send the passed in seat to our mailbox, the address of which we get by calling seatRequestBox.address. However we don’t call this function straight away, we use an Effects.task to queue the request to happen as part of the Elm Effects process. The final line Effects.map (always NoOp) basically says, regardless of the result of running Signal.send seatRequestsBox.address seat always call a NoOp action in the update function.

    What NoOp function I hear you ask. The one we are just about to write. :)

    For the origin and discussion around the sendSeatRequest function see this gist and the associated conversation.

  7. A NoOp function is one that has no effect on the application whatsoever. In other words it does not change the current state of the model and it does not create an Effect. We add it to our update function as follows:

    type Action = ... | RequestSeat Seat | NoOp
    
    update : Action -> Model -> (Model, Effects Action)
    update action model =
      case action of
        ...
        RequestSeat seat ->
          (model, sendSeatRequest seat)
        NoOp ->
          (model, Effects.none)
    

    This allows us to make our request to the outside world without changing the current state of the application. We’ll concern ourselves with updating the application state when we get a response back to our request.

  8. If we visit localhost:4000 in the browser now and click on a seat, we should see the data for that seat appearing in the console.

    making the request

Dealing with the request

Now that we can send the data out of the Elm application, we need to use JavaScript to send that data over the Phoenix channel we set up earlier.

  1. We’ll start by changing our web/static/js/app.js file so that, instead of logging the received seat data to the console, we’ll push it over our channel.

    elmApp.ports.seatRequests.subscribe(seat => {
      channel.push("request_seat", seat)
    })
    
  2. Now we can open the web/channels/seat_channel.ex file and add the following function to handle the request_seat message.

    def handle_in("request_seat", payload, socket) do
      seat = Repo.get!(SeatSaver.Seat, payload["seatNo"])
      seat_params = %{occupied: !payload["occupied"]}
      changeset = SeatSaver.Seat.changeset(seat, seat_params)
    
      case Repo.update(changeset) do
        {:ok, seat} ->
          broadcast socket, "seat_updated", seat
          {:noreply, socket}
        {:error, _changeset} ->
          {:reply, {:error, %{message: "Something went wrong."}}, socket}
      end
    end
    

    The handle_in function pattern matches on the “request_seat” message and takes two further arguments: payload, which holds the seat data we send in, and socket to carry the current state of the socket.

    We start by retrieving the current seat record from the database by the seatNo given in the payload, and then create a changeset that sets the value of the occupied field to the opposite of the value given in the payload. Then we attempt to update the record in the database. If it is successful we broadcast an “updated” message to all channel connections and mark the socket as noreply. If it fails then we set a reply on the socket with an error message.

  3. Back in web/static/js/app.js we can now handle those expected responses by updating our port subscription.

    elmApp.ports.seatRequests.subscribe(seat => {
      channel.push("request_seat", seat)
             .receive("error", payload => console.log(payload.message))
    })
    
    channel.on("seat_updated", seat => console.log('updated seat: ', seat))
    

    First we handle the error by adding a call to the receive function after our push function call. We’ll just output any errors directly to the console for simplicity.

    Then we bind to the seat_updated channel broadcast passing it an anonymous function that, for now, also outputs to the console. This time with the seat data sent with the broadcast.

  4. Firing up a browser we can see now that the changes are being made and sent to the console, even though the UI is not currently updating visually.

    updating the seat

Handling the response

OK, so now we are making the request, updating the database and then broadcasting the result across the channel. We now need to send that response back to our Elm application and have that complete the circle.

  1. In our web/static/js/app.js file change the binding to the “seat_updated” event to the following:

    channel.on("seat_updated", seat => elmApp.ports.seatUpdates.send(seat))
    

    Instead of logging the seat data to the console we instead send it to a new Elm port, that we will define shortly, called seatUpdates.

  2. We’ll also need to set an initial value for the seatUpdates port. Back up where we define the initialState var, change it as follows:

    var ...
      , initialState = {
          seatLists: [],
          seatUpdates: {seatNo: 0, occupied: false}
        }
      , ...
    

    We use the same trick that we did on the Elm side of creating a dummy seat that doesn’t match one in our database.

  3. If we check our browser just now we’ll see a nice, explanatory error message from Elm telling us that we need to create the port that we just initialised. Let’s do that now.

    port error

  4. In our web/elm/SeatSaver.elm file we’ll start by defining the seatUpdates port.

    port seatUpdates: Signal Seat
    
  5. Now we need to convert this Signal of Seat into a Signal of Action so that we can route it to the update function. We already have an incomingActions Signal of Action that is hooked into the update function. Elm has a Signal.merge function that lets you combine two Signals of the same type into one Signal. Change your incomingActions function as follows:

    seatListsToSet: Signal Action
    seatListsToSet =
      Signal.map SetSeats seatLists
    
    seatsToUpdate: Signal Action
    seatsToUpdate =
      Signal.map Toggle seatUpdates
    
    incomingActions: Signal Action
    incomingActions =
      Signal.merge seatListsToSet seatsToUpdate
    

    We split out our existing Signal.map for seatLists into its own function. We then create an identical one for our new seatUpdates Signal. This function just maps any Seat values on that Signal into calls to the Toggle Action with that Seat.

    We then change our incomingActions function definition to merge our two new Signals into one combined Signal of Action.

  6. Now all we need to do is to change the definition of our Toggle Action in the update function.

    update : Action -> Model -> (Model, Effects Action)
    update action model =
      case action of
        Toggle seatToToggle ->
          let
            updateSeat seatFromModel =
              if seatFromModel.seatNo == seatToToggle.seatNo then
                { seatFromModel | occupied = seatToToggle.occupied }
              else seatFromModel
          in
            (List.map updateSeat model, Effects.none)
        ...
    

    We change the line { seatFromModel | occupied = not seatFromModel.occupied } to { seatFromModel | occupied = seatToToggle.occupied } so that the occupied state is taken from the passed in seat rather than just the opposite of what it originally was.

    The result will be a new model with the Seat that was provided in the response updated to have the correct occupied state. As we are now finished, we have no further Effects to make and so we use Effects.none as before.

  7. Visiting our application for one last time in the browser we are now able to click on seats, have them change their occupied state in the database and then update to show their new occupied state in the UI.

    And we're done