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 toPhoenix
see: 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 [email protected]
Once you have Elm
installed,
run the following command in your terminal
to confirm:
elm --version
you should see:
0.19.1
For 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-mailer
cd app
mix ecto.setup
mix phx.server
Open 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 init
See: 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]: y
Type 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 = "Alex" -- set name to your name!
main =
text ("Hello " ++ name ++ "!")
elm make elm/src/Main.elm --output=../priv/static/assets/elm.js
That 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.
Phoenix
can handle asset compilation during deployment.
In the /assets/elm
directory,
run the following command
to install
esbuild-plugin-elm
npm install -D esbuild-plugin-elm
Create 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 './index.js';
e.g: app.js#L28
With all 3 files saved,
run the Phoenix
server:
mix phx.server
You 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 finished
That 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
Elm
Test!
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