Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

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.

Elm not running

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!

Elm running

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment