Loading Multiple Elm Apps to a Single Source File using Webpacker

Assumptions: let’s assuming you are using Rails, with Webpacker, and you want to display to completely different Elm apps on the same page, and you ALSO want to initialise them based on a particular condition. How would you go about doing that?

I wanted a situation where I could load up two entirely different elm apps in the same Javascript file. In other words, one “entry point”, and two elm apps in there. You need to combine two elm apps into one output file. How can you do this using Webpacker?

I only needed to do this for two files. So i just hard coded the paths in there.

// loaders/elm.js

const { resolve } = require('path')

const isProduction = process.env.NODE_ENV === 'production'
const isDevelopment = process.env.NODE_ENV === 'development'
const elmSource = resolve(process.cwd())
const elmBinary = `${elmSource}/node_modules/.bin/elm`


const options = { 
  cwd: elmSource, 
  pathToElm: elmBinary, 
  optimize: isProduction, 
  verbose: isDevelopment, 
  debug: isDevelopment,
  files: [
     resolve(elmSource, "app/javascript/Main.elm"),
     resolve(elmSource, "app/javascript/Decider.elm")
     ]   //// This is the critical point - I had to manually specify the paths of the Elm modules I want to combine.
}

// config.output_path

const elmWebpackLoader = {
  loader: 'elm-webpack-loader',
  options: options
}

module.exports = {
  test: /\.elm(\.erb)?$/,
  exclude: [/elm-stuff/, /node_modules/],
  use: isProduction ? [elmWebpackLoader] : [{ loader: 'elm-hot-webpack-loader' }, elmWebpackLoader]
}

And having combined them, I can simply initialise them using plain old javascript. I prefer running the initailisation code via Stimulus JS - this means everytime the app appears in the DOM, it will be instantiated, and I don’t have to worry about running instantiation code again and again, if for example, I was fetching and rendering HTML via AJAX which would contain embedded elm apps.

Here is my initialisation code:

// plugin_initialisation_controller.js  //// We are using Stimulus JS here.
import { Controller } from "stimulus"

import { Elm } from '../Main'

export default class extends Controller { 
  static targets = ["elmApp"]

  connect(){
    this.setUpElmApp()    
  }

  setUpElmApp(){      
     let someCondition = true 

      if (someCondition) {        
        var elmApp = Elm.Main.init({node: node})          
      }             
      else
      {
        // Sweet - we are setting up a completely different Elm App here     
        // if a particular condition is not met   
        var elmApp = Elm.Decider.init({node: node})          
      }
  }
}

And you can simply set up the elm app wherever you like, like so:

        <div data-controller="plugin-initializer">          
            <%= content_tag :div,  "data-plugin-initializer-target": "elmApp" do %>
            <% end %>          
        </div>

Special shout out to the folks you wrote the elm-webpack-loader plugin, which does all the hard work!

Notable references:

Google Groups Discussion Elm-Webpack-loader

Written on September 24, 2020