Elm and Rails Sitting in a Tree

Tom Wey and Josh Clayton

I’ve been enjoying developing with Elm for a while. In case you’re not familiar with Elm, it’s a strongly-, statically-typed language that compiles to JavaScript. The developer experience is fantastic, largely due to the awesome compiler.

Thanks to Webpacker, it’s now straightforward to integrate Elm into a Rails app. I recently worked on my first Rails + Webpacker + Elm app and I’ll talk about a few patterns that came out of the project.

Getting Started

From the docs:

“Webpacker makes it easy to use the JavaScript pre-processor and bundler Webpack 3.x.x+ to manage application-like JavaScript in Rails”

I won’t duplicate the docs here, but there are handy task commands for getting up and running with Elm.

Elm on Rails

One of the great things about Rails is that it provides clear patterns of how to organize your code in the project. Webpacker adds the idea of “packs” which are entry points for webpack. These live under app/javascript/packs by default. Our application isn’t an SPA; instead, we have a sprinkling of Elm components (or widgets) throughout the app, generally one per page. So we have a single pack per component. Typically these consist of a single line:

// app/javascript/packs/my_component.js

import "my_component";

where my_component is a directory under app/javascript containing the code for the component and an index.js file, which webpack will load by default.

The pack is included on a page with Webpacker’s javascript_pack_tag helper. Typically our components exist on a single page, so we call the pack tag helper in the relevant view. To keep control over where on the page the actual script tag is rendered, we make use of Rails’ content_for method.

In our view:

<!-- app/views/products/show.html.erb -->

<% content_for :tail_scripts do %>
  <%= javascript_pack_tag "my_component" %>
<% end %>

and in our layout, at the end of our body tag:

<!-- app/views/layouts/application.html.erb --!>

<html>
  <head>
    <!-- head content --!>
  </head>
  <body>
    <!-- content --!>
    <%= yield :tail_scripts %>
  </body>
</html>

In the simplest case the index.js for my_component looks something like this:

// app/javascript/my_component/index.js

import Elm from './elm/Main'

document.addEventListener('DOMContentLoaded', () => {
  const containerId = "my-elm-container";
  const target = document.getElementById(containerId);
  Elm.Main.embed(target);
})

This embeds our Elm component in a DOM element, rendered by Rails, on the page. Our Elm code lives under app/javascript/my_component/elm/, and our entry point within this directory is Main.elm.

Interacting with Elm from the outside

Mostly our Elm components don’t act in isolation and require some context, either from the current page, or higher level global config.

Flags

A scenario that frequently occurred was the requirement to pass some initial state (or config) from the server (Rails) to our Elm component. The pattern that we used to achieve this was to set data attributes on the container which we render in our Rails view, read these in our component entry JavaScript (i.e. the file at app/javascript/my_component/index.js we showed above), and pass the values to Elm as flags.

Let’s say we wanted to pass in a name of some kind, which we have in an instance variable @name in Rails. In our Rails view this would look like:

<!-- app/views/products/show.html.erb -->

<%= content_tag :div, nil, id: "my-elm-container", data: { name: @name } %>

We can now update our component entry JavaScript to read the data attribute and pass it into our Elm component as a flag:

// app/javascript/my_component/index.js

import Elm from './elm/Main';

document.addEventListener('DOMContentLoaded', () => {
  const containerId = "my-elm-container";
  const target = document.getElementById(containerId);
  const name = target.getAttribute("data-name");
  Elm.Main.embed(target, {
    name: name,
  });
})

For more general config (e.g. an API host shared across multiple Elm components), we could make this available in a meta tag within Rails’ application layout and pass values in as needed to our Elm component:

<!-- app/views/layouts/application.html.erb --!>
<html>
  <head>
    <!-- head content --!>
    <meta name="apiRoot" value="https://api.example.com">
  </head>
  <body>
    <!-- content --!>
  </body>
</html>

With an initial value available in a meta tag, we now write a function to retrieve the value:

// app/javascript/meta-tag-bootstrap.js

export default function loadMetaTagData(name) {
  const element = document.querySelectorAll(`meta[name=${name}]`)[0];

  if (typeof element !== "undefined") {
    return element.getAttribute("value");
  } else {
    return null;
  }
}

Finally, in our JavaScript file that runs the Elm application, we can import the function and retrieve the value from the meta tag based on its name attribute:

// app/javascript/my_component/index.js

import Elm from './elm/Main';
import loadMetaTagData from "../meta-tag-bootstrap";

document.addEventListener('DOMContentLoaded', () => {
  const globalConfig = {
    api_root: loadMetaTagData("apiRoot")
  };

  const containerId = "my-elm-container";
  const target = document.getElementById(containerId);
  const name = target.getAttribute("data-name");

  Elm.Main.embed(target, Object.assign(globalConfig, {
    name: name,
  }));
});

 Ports

Because we’re not working with a single page app, but rather a series of self- contained Elm components, it was sometimes necessary to communicate changes within Elm to the outside world, and vice versa. This might be be as simple as reflecting a change made in Elm elsewhere on the page, or perhaps triggering some vanilla JavaScript. The answer here is Elm ports.

Data out

In this example, we broadcast a change to a quantity value over a port and subscribe to it in our JavaScript code.

In our Elm code we have a function which takes our Model and an Int, and returns a tuple ( Model, Cmd Msg ). This becomes the return tuple from our update function.

-- app/javascript/my_component/elm/Main.elm

updateQuantity : Model -> Int -> ( Model, Cmd Msg )
updateQuantity model quantity =
    ( { model | quantity = quantity }, quantityChanged quantity )

Where quantityChanged is defined as a port which sends an integer out to JavaScript:

-- app/javascript/my_component/elm/Main.elm

port quantityChanged : Int -> Cmd msg

Note that the module where we define our port(s) must be specified as a port module.

Then in our JavaScript we subscribe to the port and receive the sent value:

// app/javascript/my_component/index.js

// ...
  const component = Elm.Main.embed(target, Object.assign(globalConfig, {
    name: name,
  }));

  component.ports.quantityChanged.subscribe((newQuantity) => {
    console.log(`Quantity changed to: ${newQuantity}`);
  });
// ...

Data in

For completeness lets say we also want to be able to set an absolute quantity from JavaScript. In this case let’s add a reset button outside of our Elm code, which when clicked sends in a quantity of 0.

Our inbound port receives an Int and is defined like this:

-- app/javascript/my_component/elm/Main.elm

port setQuantity : (Int -> msg) -> Sub msg

In order to recieve messages from this port we subscribe to it and wire it up with our app:

-- app/javascript/my_component/elm/Main.elm

subscriptions : Model -> Sub Msg
subscriptions model =
    setQuantity SetQuantity

main : Program Flags Model Msg
main =
    Html.programWithFlags
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }

The subscription maps our port to a Msg which we will handle in our update function, just like any other Msg. The Msg is defined as SetQuantity Int and the relevant branch in our update looks like this:

-- app/javascript/my_component/elm/Main.elm

SetQuantity quantity ->
    updateQuantity model <| max 0 quantity

This contains a guard to ensure that our quantity isn’t ever < 0. There are more elegant ways to handle this, for example a custom Quantity type, which are outside of the scope of this article. See our post on avoiding primitives in Elm for more on leveraging Elm’s type system.

In our JavaScript code we can send a value to Elm over the port like this:

component.ports.setQuantity.send(0);

Sending more complex data over ports

If you find yourself needing to send more complex data types over ports you’ll need to encode as JSON on the way in and decode on the way out. Elm has support for decoding and encoding JSON in the standard library. For a more in depth look see our posts on Decoding JSON Structures with Elm and Bridging Elm and JavaScript with Ports.

Wrapping Up

That just about wraps up the basics of how we integrated Elm components into our Rails app. It feels like a great choice by the Rails team to embrace Webpacker, and means that we’ve gained a bunch of flexibility around the choices we can make for front-end development with Rails.

If you want to play around with any of the examples, a full example Rails app is available on GitHub.