Want to start using Elm on the front end of a Phoenix app (in this case, Phoenix 1.3)? This blog post will go over the steps I use to get these two talking to each other.

Looking to connect Elm to a Phoenix 1.4 app? Go and check out the update to this blog post: Connecting Elm to Phoenix 1.4 with webpack

Assuming you have already installed Phoenix 1.3, let’s kick things off with a new application.

Generate Phoenix app

mix phx.new phoenix_with_elm
cd phoenix_with_elm
mix ecto.create
mix phx.server

Navigate to http://localhost:4000/ and you should see the familiar Phoenix welcome screen.

Welcome to Phoenix

No surprises here. Close down the server, and let’s move on.

Generate Elm app

First, install Elm if you haven’t already:

npm install elm --global

Next, in order to help us generate an Elm app with a default structure and sensible configuration, we’ll use Create Elm App (inspired by Create React App):

npm install create-elm-app --global

Generate the new Elm app inside the “front end” of the Phoenix application, which in this case means the assets/ directory:

cd assets
create-elm-app elm

You should now have an elm folder alongside your js and css folders. Let’s make sure it works as we expect:

cd elm
elm-app start

Starting the Elm app should then automatically open a browser window for you at http://localhost:3000/, and you should see a message saying that…

Your Elm app is working

Note that the Elm app is running independently here: it knows nothing about the Phoenix environment that it’s located in, and is happily using assets, like the image that you see, from its own assets/elm/public/ directory.

Now that we’ve confirmed that both the Phoenix app and the Elm app work of their own accord, it’s time to connect them together. Close down the Elm server and let’s write some config.

Connect Elm to Phoenix

Phoenix uses Brunch out of the box as its asset build tool, so that’s what we’ll use to compile the Elm code. In order to do that, we’ll need the elm-brunch plugin, so let’s install that and get it configured.

First, navigate back to the assets/ folder and install elm-brunch:

npm install --save-dev elm-brunch

Then, open up brunch-config.js in a text editor and make the following changes:

exports.config = {
  // ...

  // Step 1: Add "elm" to the list of paths being watched.
  paths: {
    watched: ["static", "css", "elm", "js", "vendor"],
  },

  // Step 2: Add the elm-brunch plugin configuration.
  plugins: {
    elmBrunch: {
      elmFolder: "elm",
      mainModules: ["src/Main.elm"],
      outputFolder: "../vendor",
      outputFile: "elm.js",
      makeParameters: ["--warn"]
    },
    // ...
  }
  // ...
}

Note here specifically the line outputFolder: "../vendor": this is to ensure that the generated elm.js file gets compiled and imported before the js/app.js file (it is Brunch convention that files in the assets/vendor directory get compiled before code in other folders; see Brunch’s file config documentation for more details).

The brunch-config.js configuration does apparently allow for before and after statements to specify that some files should be compiled before or after others, but I have not had any luck getting them to work, so please consider having the Elm files compiled out into the vendor folder a hack/workaround for now (since code that we write in the elm directory does not constitute an external library). If you have been able to use before/after compilation order statements in a Phoenix/Elm project, please leave a comment with a link to your config! [Update 2018-02-16] See the update below that puts your code back in the js/ directory, where it belongs:

Display Elm app in Phoenix template

So that we show both Phoenix and Elm working together, let’s keep the default generated Phoenix layout template as-is, and replace the content of the page index template with a <div> tag for the Elm app:

lib/phoenix_with_elm_web/templates/page/index.html.eex

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

Next, we’ll target that <div> tag in the app Javascript file and get it to replace it with the content of the Elm app, so add the following to the end of the file:

assets/js/app.js

const elmDiv = document.getElementById("elm-main");
Elm.Main.embed(elmDiv);

Now, run mix phx.server again and navigate to http://localhost:4000 to see if we’re in business:

Image broken

Not quite yet, it would seem: we can see that the Elm app is being rendered in the template, but we’ve got a broken image. This is because that image currently lives inside the Elm app at assets/elm/public/logo.svg, and Phoenix doesn’t know anything about compilation of static image assets within Elm applications: it’s looking for assets under its own assets/static/ directory.

The path of least resistance here is, I think, to move all assets to where Phoenix is expecting to find them, and change the Elm code to point to them.

So, first, move the logo image into Phoenix’s image assets directory:

mv assets/elm/public/logo.svg assets/static/images/logo.svg

Then, change the Elm code to look for the image in Phoenix (/images/logo.svg), rather than in Elm (/logo.svg):

assets/elm/src/Main.elm

module Main exposing (..)

-- ...

view : Model -> Html Msg
view model =
    div []
        [ img [ src "/images/logo.svg" ] []
        , h1 [] [ text "Your Elm App is working!" ]
        ]

Now, at http://localhost:4000/, you should see the following:

Phoenix and Elm working

Great! You’re now successfully bootstrapped to start building out your new Phoenix-and-Elm powered app!




Update 2018-02-16

To put the Elm-generated Javascript file into the js directory (rather than in vendor, which should only be for third-party code), and then have the app use it properly, edit the codebase in the following way:

assets/brunch-config.js

exports.config = {
  // ...
  plugins: {
    // Specify outputFolder to be in the js/ directory, along with app.js
    elmBrunch: {
      elmFolder: "elm",
      mainModules: ["src/Main.elm"],
      outputFolder: "../js",
      outputFile: "elm.js",
      makeParameters: ["--warn"]
    },

    // Do not use ES6 compiler in vendor or Elm-generated Javascript code.
    babel: {
      ignore: [
        /vendor/,
        "js/elm.js"
      ]
    },
    // ...
  }
  // ...
}

Then, specifically import the Elm variable in from elm.js, now located in the same directory as app.js, rather than just assuming it is available to use:

assets/js/app.js

import Elm from "./elm"

const elmDiv = document.getElementById("elm-main");
Elm.Main.embed(elmDiv);

Back to “Display Elm app in Phoenix template”

Comments