Want to start using Elm 0.18 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.
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…
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 forbefore
andafter
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 thevendor
folder a hack/workaround for now (since code that we write in theelm
directory does not constitute an external library). If you have been able to usebefore
/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 thejs/
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:
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:
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);
Leave a comment