To build a more advanced UI/UX,
there is nothing better than Elm.
A step-by-step guide to getting Elm
working in a Phoenix app
with live reloading.
By the end of this guide you will understand how all the pieces fit together.
Latest Phoenix,
latest Elm
and esbuild;
the fastest build system available.
Before you start, make sure you have the following installed:
Elixir: https://elixir-lang.org/install.html
New toElixir, see: https://github.com/dwyl/learn-elixirPhoenix: https://hexdocs.pm/phoenix/installation.html
New toPhoenixsee: https://github.com/dwyl/learn-phoenix-frameworkNode.js: https://nodejs.org/enElm: https://guide.elm-lang.org/install/elm.html e.g:npm install -g elm@elm0.19.1
Once you have Elm installed,
run the following command in your terminal
to confirm:
elm --versionyou should see:
0.19.1For this example, we are creating a basic Phoenix App
without the live dashboard or mailer (email)
but with Ecto (Postgres database)
so that we can simulate a real-world app.
mix phx.new app --no-dashboard --no-mailercd appmix ecto.setupmix phx.serverOpen your web browser to the URL: http://localhost:4000
You should see the default Phoenix home page:
So far so good. π
Let's add Elm!
Change directory in /assets,
and create a directory called elm.
Inside the /assets/elm directory,
run the following command:
elm initSee: https://elm-lang.org/0.19.1/init
You will see the following prompt:
Hello! Elm projects always start with an elm.json file. I can create them!
Now you may be wondering, what will be in this file? How do I add Elm files to
my project? How do I see it in the browser? How will my code grow? Do I need
more directories? What about tests? Etc.
Check out <https://elm-lang.org/0.19.1/init> for all the answers!
Knowing all that, would you like me to create an elm.json file now? [Y/n]: yType y and Enter to continue:
Okay, I created it. Now read that link!That will have created a new directory at /assets/elm/src
and an elm.json file.
Create a new file with the path:
/assets/src/Main.elm
and add the following contents to it:
module Main exposing (..)
import Html exposing (text)
name : String
name = "Alex" -- set name to your name!
main : Html.Html msg
main =
text ("Hello " ++ name ++ "!")elm make elm/src/Main.elm --output=../priv/static/assets/elm.jsThat results in an un-optimized elm.js file that is 488kb
For development/testing purposes this is fine;
we can optimize/minify it for production later. (see below)
Let's include this file in our Phoenix template just to show that it works.
Note: this will not work in production, it's just for basic illustration as a "quick win".
Open the
lib/app_web/templates/layout/root.html.heex
file
and add the following lines just before the </body> element:
<script type="text/javascript" src={Routes.static_path(@conn, "/assets/elm.js")}></script>
<script>
const $root = document.createElement('div');
document.body.appendChild($root);
Elm.Main.init({
node: $root
});
</script>With those lines added to the root.html.heex file.
Run mix phx.server again and refresh your browser:
http://localhost:4000/
You should see something similar to the following:
That Hello Alex! was rendered by Elm.
Now that we know it can work the hard way, let's do it properly!
undo those changes made in root.html.heex
and save the file.
Next we will include the elm app
into the esbuild pipeline
so that:
- We can have a watcher and hot reloader.
Phoenixcan handle asset compilation during deployment.
In the /assets/elm directory,
run the following command
to install
esbuild-plugin-elm
npm install -D esbuild-plugin-elmCreate a file with the path
assets/elm/src/index.js
and add the the following code:
import { Elm } from './Main.elm';
const $root = document.createElement('div');
document.body.appendChild($root);
Elm.Main.init({
node: $root
});Ref: phenax/esbuild-plugin-elm/example/src/index.js
Create a new file with the path:
assets/elm/build.js
and add the following code to it:
const esbuild = require('esbuild');
const ElmPlugin = require('esbuild-plugin-elm');
const isProduction = process.env.MIX_ENV === "prod"
async function watch() {
const ctx = await esbuild.context({
entryPoints: ['src/index.js'],
bundle: true,
outfile: '../js/elm.js',
plugins: [
ElmPlugin({
debug: true
}),
],
}).catch(_e => process.exit(1))
await ctx.watch()
}
async function build() {
await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
minify: true,
outfile: '../js/elm.js',
plugins: [
ElmPlugin(),
],
}).catch(_e => process.exit(1))
}
if (isProduction)
build()
else
watch()Ref: phenax/esbuild-plugin-elm/example/build.js
Open the config/dev.exs file
and locate the watchers: section.
Add the following line the list:
node: ["./build.js", "--watch", cd: Path.expand("../assets/elm", __DIR__)]
Open the assets/js/app.js
file and add the following lines
near the top of the file:
// import the compiled Elm app:
import './elm.js';e.g: app.js#L28
With all 3 files saved,
run the Phoenix server:
mix phx.serverYou should see output similar to this:
[info] Running AppWeb.Endpoint with cowboy 2.9.0 at 127.0.0.1:4000 (http)
[info] Access AppWeb.Endpoint at http://localhost:4000
[watch] build finished, watching for changes...
Success!
Main βββ> /var/folders/f2/3ptgvnsd4kg6v04dt7j1y5dc0000gp/T/2022327-49759-ua0u9f.iqdp.js
[watch] build started (change: "js/index.js")
[watch] build finishedThat confirms that the Elm build + watchers are working. π
With the Phoenix server running,
and a browser window open
pointing to the Phoenix App: http://localhost:4000
Open the assets/elm/src/Main.elm file and
change the line:
name = "Alex"to:
name = "World"When you save the file it will automatically reload
in your web browser and will update the name accordingly:
So the watcher and live reloading is working!
This is still very far from being a "real world" App. But the "starter" is here!
- "Productionize" the asset compilation: https://hexdocs.pm/phoenix/asset_management.html#esbuild-plugins
@SimonLab if you have time to help extend, please go for it! π
- Add
ElmTest!
Create a Phoenix Endpoint that returns json
that can invoked from Elm.
e.g: Return an
inspiring quote
Borrow from: https://github.com/dwyl/phoenix-content-negotiation-tutorial
esbuild-plugin-elm: https://github.com/phenax/esbuild-plugin-elm- Adding a custom watcher to Phoenix:
https://dev.to/contact-stack/adding-a-custom-watcher-to-phoenix-1e10
thanks to
@michaeljones



