Abe Voelker's blog Programming stuff, mainly

Adding Elm to a Rails application

7 minute read

Elm logo

Recently I added Elm to a Rails application using webpack, and it took me a while to figure out as I’m new to both so I figured I’d share what I did.

A complete example app is available on GitHub (abevoelker/rails-elm-example); this post will walk through each step used to create it.

Prerequisites

Before starting you should install these:

When finished, make sure these binaries are available and working: node, npm, elm, elm-package

A simple Rails app

Let’s create a simple Rails app with a static text greeting. We will later replace the static greeting using Elm.

Create a new Rails app, then add/modify the following files in the application:

  • app/controllers/welcomes_controller.rb

    class WelcomesController < ApplicationController
      def index
      end
    end
    
  • app/views/welcomes/index.html.erb

    <p>Hello from Rails!</p>
    
  • config/routes.rb

    Rails.application.routes.draw do
      root to: "welcomes#index"
    end
    

Now when you start the Rails server with rails s and point your browser to http://localhost:3000, you should see the following greeting:

Rails hello world

Add webpack

Now we’re going to add webpack, which will make it easy for our Elm code to be dynamically compiled into JavaScript and included by our Rails app.

First add the webpack-rails gem to your Gemfile:

gem "webpack-rails"

Do a bundle install and then run bundle exec rails generate webpack_rails:install:

This will modify some existing files, and add some new ones:

  • Gemfile (modified)

    Foreman will be added to the Gemfile, which will be used to run the webpack dev server alongside the rails server in development mode.

  • Procfile (new)

    # Run Rails & Webpack concurrently
    # Example file from webpack-rails gem
    rails: bundle exec rails server
    webpack: ./node_modules/.bin/webpack-dev-server --config config/webpack.config.js
    

    Procfile is used by foreman to define all processes it should run, in this case two processes: the Rails server and the webpack dev server. You start foreman using bundle exec foreman start, and use it in place of bundle exec rails s in development.

  • package.json (new)

    {
      "name": "webpack-rails-example",
      "version": "0.0.1",
      "license": "MIT",
      "dependencies": {
        "stats-webpack-plugin": "^0.2.1",
        "webpack": "^1.9.11",
        "webpack-dev-server": "^1.9.0"
      }
    }
    

    package.json is npm’s project file, similar to a .gemspec file. Notably the dependencies section keeps track of npm packages required by the project.

  • config/webpack.config.js (new)

    This is webpack’s configuration file. It’s a large file so I won’t reproduce it here.

  • .gitignore (modified)

    Two directories are added to .gitignore. node_modules is managed by npm and can be ignored. public/webpack is where compiled webpack assets will be stored.

  • webpack/application.js (new)

    console.log("Hello world!");
    

    This is webpack’s default application entrypoint, although it can be changed by modifying webpack.config.js. The default application just logs “Hello world!” to the browser console.

Add webpack entry point to Rails

Next, let’s modify the application’s layout to use webpacked JavaScript rather than sprockets’s application.js. Remove this line from app/views/layouts/application.html.erb:

<%= javascript_include_tag 'application' %>

And now add the following line right above the closing </body> tag:

<%= javascript_include_tag *webpack_asset_paths("application") %>

We moved the JS include line so that when our JS loads, the HTML body is fully loaded and ready for JS to access/modify.1

Now if you start the Rails app with bundle exec foreman s and point your browser to http://localhost:5000 (note foreman starts Rails on port 5000 rather than 3000), you should see the default webpack application “Hello world!” in the developer console:

Rails hello world with webpack

If you take a look at the HTML source being rendered, you’ll see that the JS is actually being served from a different webserver than Rails (this is the webpack-dev-server):

<!DOCTYPE html>
<html>
  <head>
    <title>RailsElmExample</title>
    <meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="cbNbYslowkMUrKD9BY2xR1upbV+ctSAfI9t0LDDpZHIcw0K303NjnPcFd5eD8Kz+YRu9l9QNnWrB39esvxyaZg==" />

    <link rel="stylesheet" media="all" href="/assets/application.self-af04b226fd7202dfc532ce7aedb95a0128277937e90d3b3a3d35e1cce9e16886.css?body=1" data-turbolinks-track="reload" />
  </head>

  <body>
    <p>Hello from Rails!</p>

    <script src="http://localhost:3808/webpack/application.js"></script>
  </body>
</html>

Install Elm npm packages

Now we’re ready to start adding some Elm dependencies. First, use this command to install needed Elm npm packages:

$ npm install --save elm elm-hot-loader elm-webpack-loader

This will modify your package.json dependencies.

Install Elm packages

Now we’ll need to install the default Elm packages:

$ elm-package install

This will ask you to install elm-lang/core, elm-lang/html, elm-lang/virtual-dom, and will create a new elm-package.json file and a elm-stuff/ directory in the root of the project. elm-stuff can be added to .gitignore as it is managed by Elm.

Tell webpack how to handle Elm

Next we need to modify the webpack config to tell it to recognize .elm files and how to compile them. Open config/webpack.config.js and add this section to the inside of the config object:

module: {
  loaders: [{
    test: /\.elm$/,
    exclude: [/elm-stuff/, /node_modules/],
    loader: 'elm-hot!elm-webpack?verbose=true&warn=true'
  }]
},

Add Elm to Rails app

Now it’s time to add some Elm to our Rails app. There are two main ways to render HTML using Elm; one is letting Elm control the entire page <body> (referred to as fullscreen), and the other is letting Elm control a specific HTML node (referred to as embed). We are going to use the embed method to give Elm control of only a part of our page, which will probably be the typical approach for most legacy Rails apps wanting to add Elm.

First, let’s add a <div> to our welcome page which Elm will take control of. Modify app/views/welcomes/index.html.erb as follows to add a div:

<p>Hello from Rails!</p>
<div id="main"></div>

Elm component

Now we’re finally ready to add actual Elm code to the Rails app. We are going to store our Elm components in a new subdirectory, webpack/elm/, so first create that directory.

We will now create a single Elm component, Main.elm, stored as webpack/elm/Main.elm. Create that file with the following content:

import Html exposing (text)

main =
  text "Hello from Elm!"

Finally we must modify webpack/application.js to load this component and embed it in our target HTML div node:

// inject bundled Elm app into div#main
var Elm = require('./elm/Main.elm');
Elm.Main.embed( document.getElementById( 'main' ) );

Now if you reload http://localhost:5000 you should see the following output:

Elm hello world

Wrap-up

That’s all there is to it. Note that I haven’t tested this in a production deploy, but I believe it should still work. webpack-rails should compile output to public/webpack and the webpack_asset_paths view helper should know to look there.

Also, there’s obviously a lot more advanced things you can do with Elm than “hello world”. You should check out the Elm Guide to get a tour of what’s possible!

Footnotes

1 If you later start using webpack to manage your CSS in addition to your JS, you’ll have to slightly modify this line.

References