Automate the Registration of Javascript Stimulus Controllers in a Phoenix app

JAVASCRIPT PHOENIX STIMULUS

Stimulus Mix Task

Today, we will look at how you can automate the registration of your javascript Stimulus controllers in Phoenix.

The release of Phoenix 1.6 saw the departure from Node and Webpack in favor of esbuild for asset pipeline management.

With Cassava, we have adopted this recommendation and now vendor our external libraries, including Stimulus, which we rely on for vanilla javascript.

However, in doing so, we lost the ability for Webpack to register our JavaScript Stimulus controllers automatically, as seen below.

import { Application } from "@hotwired/stimulus"
import { definitionsFromContext } from "@hotwired/stimulus-webpack-helpers"

window.Stimulus = Application.start()
const context = require.context("./controllers", true, /\.js$/)
Stimulus.load(definitionsFromContext(context))

Rails provides their community with a stimulus:manifest:update Rake task to cushion the blow when using other build systems like esbuild.

Let us do the same for Phoenix in the form of a Mix task.

Custom Mix Task

Given that our Stimulus controllers are defined in the assets/js/controllers folder, the goal is to generate an index.js manifest in the same folder and have our app.js reference this file.

We specify an import directive for each controller and register it in the Stimulus application context.

Each controller is registered with an identifier derived from the controller filename, with particular attention given to replacing all underscores with dashes. You can refer to documentation for more details on naming conventions in Stimulus.

defmodule Mix.Tasks.RegisterStimulusControllers do
  use Mix.Task
  require Logger

  @path "assets/js/controllers"

  @shortdoc "Register all stimulus controllers into app.js"
  def run(_) do
    @path
    |> File.ls()
    |> case do
      {:ok, files} ->
        Logger.info("Processing #{length(files)} Stimulus controllers")

        content =
          files
          |> Enum.filter(&(&1 =~ ~r/\.*_controller.js/))
          |> Enum.map(fn file_name ->
            reg_name =
              file_name
              |> String.replace_suffix("_controller.js", "")
              |> String.replace("_", "-")

            module_path = String.replace_suffix(file_name, ".js", "")

            controller_class_name =
              module_path
              |> String.split("_")
              |> Enum.map(&String.capitalize(&1))
              |> Enum.join("")

            """
            import #{controller_class_name} from "./#{module_path}"
            application.register("#{reg_name}", #{controller_class_name})
            """
          end)
          |> List.insert_at(
            0,
            """
            // This file is auto-generated from the `mix register_stimulus_controller` task.
            import { Application, Controller } from "../../vendor/stimulus/stimulus.js"
            const application = Application.start()
            """
          )
          |> Enum.join("\n")

        (@path <> "/index.js")
        |> File.write(content)
        |> tap(&IO.inspect(&1))

      error ->
        Logger.error("Failed to read controllers: #{inspect(error)}")
    end
  end
end

From within your assets/js/app.js file, we add an import directive referencing the newly minted manifest.


import "./controllers/index.js"

An example of this manifest is shown below.

import { Application, Controller } from "../../vendor/stimulus/stimulus.js"
const application = Application.start()

import ModalController from "./modal_controller"
application.register("modal", ModalController)

...

We can run this handy task via mix register_stimulus_controllers whenever a Stimulus controller is added, renamed, or removed.

Pruning

The transition from Webpack meant we could no longer rely on the TerserWebpackPlugin to prune our console logs automatically. Luckily, esBuild added the drop flag starting in version 0.14.10 to support this feature.

We add this step to our assets.deploy build step to enable pruning during deployments.

  defp aliases do
    [
        ...
      "assets.deploy": [
        "esbuild default --minify --drop:console --drop:debugger",
        ...
      ]
    ]
  end

Summary

Well, there you have it.

A Mix task to register your Stimulus controllers when using esbuild with Phoenix and vendoring your external javascript libraries.

You can head to Elixir School to learn more about creating your own custom Mix tasks.

As always, I welcome your feedback.

Michael


You no doubt have an opinion bubbling to the surface.
Let's go one step farther and add your voice to the conversation.
Your email is used to display your Gravatar and is never disclosed. As always, do review our moderation guidelines to keep the converstion friendly and respectful.