Referencing Stimulus Controllers as ViewComponent Sidecar Files with Propshaft Importmaps

  • Rails

AKA: How many Rails buzzwords can I fit in a single blog post title?

In a previous post, I described how I like to group related "Business Concept" objects together in app/aspects, rather than grouping them by type (app/jobs, app/commands, etc...)

In a sideproject I'm working on, I wanted to put my ViewComponent objects in that folder too, instead of app/components. Starting in ViewComponent 3, they support "sidecar" files, where you can put all the related files to that component in a subdirectory named the same as the component. Since I'm also using Stimulus for this project, and several of my Components have corresponding Stimulus controllers, I also wanted to put the controller.js file in that sidecar subdir, too.

For the final buzzword bingo, I'm also using Propshaft for this project, as I described in the last post. However, Propshaft only wants to look in the app/assets folder for Javascript controllers. I found a few different solutions, but none of them worked as-is, I had to cobble a few different solutions together.

To get started, here's what one of my "aspects" folder looks like:

$ tree app/aspects/scalers
app/aspects/scalers
├── form_component
│   ├── component_controller.js
│   └── form_component.html.haml
├── form_component.rb
├── target_form_component
│   ├── component_controller.js
│   └── target_form_component.html.haml
└── target_form_component.rb

I have two components, Scalers::FormComponent and Scalers::TargetFormComponent (one is a sub-form of the main, keep an eye out for an upcoming block post about that!).

First, I added the folders to the config/initializers/assets.rb:

diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb
index b649f68..5311092 100644
--- a/config/initializers/assets.rb
+++ b/config/initializers/assets.rb
@@ -8,3 +8,10 @@ Rails.application.config.assets.version = "1.0"
 # Add additional assets to the asset load path.
 # Rails.application.config.assets.paths << Emoji.images_path
 # Rails.application.config.assets.paths << Rails.root.join("app/assets/fonts")
+Rails.application.config.assets.paths << "app/components"
+Rails.application.config.assets.paths << "app/aspects"
+
+Rails.application.config.importmap.cache_sweepers << Rails.root.join("app/components")
+Rails.application.config.importmap.cache_sweepers << Rails.root.join("app/aspects")

(I don't know if the "cache_sweepers" part is needed, I haven't deleted a stimulus controller in the project yet. It doesn't seem to hurt either.)

Then in my config/importmap.rb, I added those folders as controllers:

+pin_all_from "app/components", under: "controllers", to: ""
+pin_all_from "app/aspects", under: "controllers", to: ""

Finally, to reference this controller, you have to use the awkward naming scheme of replacing the / with -- in the name. In my case, the <form> element I want to attach to the controller starts like this:

<form data-controller="scalers--form-component--component" ...>

I don't actually write that out by hand. My controllers include a module that handles all the Stimulus naming for me (another upcoming post). Here's one of the helper methods:

def stimulus_controller
  "#{self.class.name.underscore.dasherize.gsub('/', '--')}--component"
end

This wasn't hard to get working, it was just fiddly to figure out which parts of the different solutions I needed.