Set up Elm in IHP
Archived content from the IHP with Elm series
This is just archived content from part 1 because IHP integrated this part of the tutorial into a boilerplate in the framework. It could be helpful to reference in case you want to set it up manually in an exisiting project.
Update .gitignore
Let's update .gitignore
as soon as possible to avoid pushing unwanted stuff into git.
.cache
elm-stuff
static/elm
Initialize node and elm
In your default.nix
file in the root folder, add Node.js
and elm
to otherDeps
:
otherDeps = p: with p; [
# Native dependencies, e.g. imagemagick
nodejs elmPackages.elm
];
To update your local environment, close the server (ctrl+c) and run
nix-shell --run 'make -B .envrc'
Then initialize Node.js and elm at the project root.
npm init -y
elm init
For this tutorial, we will rename the src
folder that elm generated into elm
.
mv src elm
Set the source directories folder to "elm" in elm.json
.
{
"type": "application",
"source-directories": ["elm"],
...
Getting the Haskell template ready
Let's start writing the Elm entrypoint into the Haskel template.
Go to Web/View/Static/Welcome.hs
and replace all the html inside the HSX in VelcomeView
:
instance View WelcomeView where
html WelcomeView = [hsx|
<h1>The Book App</h1>
<div class="elm">Elm app not loaded 💩</div>
<script src="elm/index.js"></script>
|]
If your IHP app is not already running, run it with ./start
and see the output on localhost:8000
.
As you see, Elm has not been loaded, because we naturally haven't written any Elm code yet. Let's close the server (ctrl+c) and do that now.
Setting up Elm
Install node-elm-compiler
for compiling and elm-hot
for hot reloading in development. parcel-bundler
is a "zero config" JavaScript bundler.
npm install node-elm-compiler parcel-bundler
npm install elm-hot concurrently --save-dev
You could do it all without a bundler like Parcel. IHP discourages bundlers, and I agree that it's not always necessary.
Still, Parcel provides valuable niceties like tight production minification and good hot reloading in development, so I prefer to use Parcel when things get a bit more advanced.
Create index.js
and Main.elm
in the elm folder:
touch elm/index.js elm/Main.elm
The elm/index.js
should look like this to initialize the Elm file. This snippet takes in account inserting flags from IHP and it lets you instantiate several Elm applications at the same time.
"use strict";
import { Elm } from "./Main.elm";
// Run Elm on all elm Nodes
function initializeWidgets() {
const elmNodes = document.querySelectorAll(".elm");
elmNodes.forEach((node) => {
const app = Elm.Main.init({
node
});
// Initialize ports below this line
});
}
// Initialize Elm on page load
window.addEventListener("load", (event) => {
initializeWidgets();
});
// Initialize Elm on Turbolinks transition
document.addEventListener("turbolinks:load", (e) => {
initializeWidgets();
});
Finally, lets' insert the code for elm/Main.elm
!
module Main exposing (main)
import Browser
import Html exposing (Html, p, text)
type alias Model =
{}
type Msg
= NoOp
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
NoOp ->
( model, Cmd.none )
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
view : Model -> Html msg
view model =
p [] [ text "Hello, Elm 🌳🚀" ]
main : Program () Model Msg
main =
Browser.element
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}
init : () -> ( Model, Cmd Msg )
init _ =
( {}
, Cmd.none
)
Add the scripts for building the app both in production and development into package.json
:
"scripts": {
"run-dev-elm": "parcel watch elm/index.js --out-dir static/elm",
"build": "parcel build elm/index.js --out-dir static/elm"
},
In the start
script at the root folder, replace the RunDevServer
line with the following line:
npx concurrently --raw "RunDevServer" "npm run run-dev-elm"
With that you can now run both the IHP app and the JavaScript simultaneously with ./start
There you should have it! Elm in Haskell with hot reloading and the Elm debugger is ready for you in the bottom right corner. Beautiful!
Build for production
When pushing your IHP app to production, you need to make sure that it builds the Elm applications.
Go to the Makefile
in the project root. If you wish, you can remove jQuery and other packages you won't need. I usually keep the following JS_FILES
, and in any case remember to add the static/elm/index.js
at the bottom.
JS_FILES += ${IHP}/static/vendor/flatpickr.js
JS_FILES += ${IHP}/static/helpers.js
JS_FILES += ${IHP}/static/vendor/morphdom-umd.min.js
JS_FILES += ${IHP}/static/vendor/turbolinks.js
JS_FILES += ${IHP}/static/vendor/turbolinksInstantClick.js
JS_FILES += ${IHP}/static/vendor/turbolinksMorphdom.js
JS_FILES += static/elm/index.js
We're keeping flatpickr
because we are using the datepicker, but other than that we probably won't need jQuery for example, but your mileage may vary.
We're keeping bootstrap css just so we don't need to do any styling for this tutorial.
Put this code at the bottom of the Makefile
to build Elm in production.
static/elm/index.js:
NODE_ENV=production npm ci
NODE_ENV=production npm run build
It should now be ready to ship to production for example to IHP Cloud.
For a complete overview of what has been done, see the diff from a fresh IHP install.
Next up
I want to take this application further in future posts showing you how to interact between IHP and Elm through flags, http requests and setting up a good widget architecture.