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.