This short post is intended as a how-to get up and running with StimulusReflex and the new ~importmap-rails~ gem to go all transpiler/bundler-less in your asset handling. Since not all generators in StimulusReflex have been updated to reflect this, I will keep this post updated ✌.
For reference, here is the link to the Github repository with the source code created in this walkthrough.
If you have any questions meanwhile, don’t hesitate to jump on the Discord server 💚.
Prepare Rails
Since Rails 7.0, importmaps, along with Hotwire aka Turbo and Stimulus, is the default frontend stack. Before starting out, we’ll make sure that we’re running the correct Rails version.
$ rails -v
Rails 7.0.0
$ rails new stimulus_reflex_importmaps
$ cd stimulus_reflex_importmaps
This will install and configure turbo-rails
, stimulus-rails
, and importmap-rails
, which are the default starting with Rails 7.
Note: If you start from an existing app (e.g. Rails 6.1), you will need to add and install those yourself:
$ bundle add importmap-rails turbo-rails stimulus-rails
$ bin/rails importmap:install
$ bin/rails turbo:install
$ bin/rails stimulus:install
Set up StimulusReflex
Next, we’re going to add the StimulusReflex gem with using the most recent prerelease.
$ bundle add stimulus_reflex -v 3.5.0.pre8
Since the StimulusReflex installer hasn’t been updated to reflect the latest changes in Rails, we’ll have to do a couple of chores ourselves.
1. Enable Caching
StimulusReflex advises to use the cache store for session persistence, so we’ll configure that in development.rb
:
# config/environments/development.rb
Rails.application.configure do
# ...
config.session_store :cache_store
# ...
end
And enable it:
$ bin/rails dev:cache
2. Appease the StimulusReflex Sanity Checker
By default, StimulusReflex’s sanity checker will prevent your Rails process from starting if the Ruby and Javascript versions are not congruent. While the behavior of the checker is reworked at the moment, we will have to work around it for now.
To this end, set up a StimulusReflex initializer to bypass it:
$ bin/rails generate stimulus_reflex:initializer
# config/initializers/stimulus_reflex.rb
StimulusReflex.configure do |config|
config.on_failed_sanity_checks = :warn
end
3. Pin the Required Packages
For the importmap approach, we need to use the bin/importmap
tool to pin the StimulusReflex javascript package as follows:
$ bin/importmap pin stimulus_reflex@3.5.0-pre8
Pinning "stimulus_reflex" to https://ga.jspm.io/npm:stimulus_reflex@3.5.0-pre8/javascript/stimulus_reflex.js
Pinning "@hotwired/stimulus" to https://ga.jspm.io/npm:@hotwired/stimulus@3.0.1/dist/stimulus.js
Pinning "@rails/actioncable" to https://ga.jspm.io/npm:@rails/actioncable@7.0.0/app/assets/javascripts/actioncable.esm.js
Pinning "cable_ready" to https://ga.jspm.io/npm:cable_ready@5.0.0-pre8/javascript/index.js
Pinning "morphdom" to https://ga.jspm.io/npm:morphdom@2.6.1/dist/morphdom.js
Pinning "stimulus" to https://ga.jspm.io/npm:stimulus@3.0.1/dist/stimulus.js
Note that due to the namespace change that Hotwire has undergone, we have now pinned two separate Stimulus packages (@hotwired/stimulus
and stimulus
). StimulusReflex still references the legacy glue package stimulus
, the work to transfer everything cleanly to @hotwired/stimulus
is still in progress. Since we hand off the application
to StimulusReflex, it shouldn’t be a problem though (see below).
4. Initialize the StimulusReflex Client
We use the ActionCable consumer obtained from Turbo’s cable interface to intialize StimulusReflex with a top level await
:
// app/javascript/controllers/index.js
import StimulusReflex from "stimulus_reflex"; // <-- add this
import { application } from "./application";
import { cable } from "@hotwired/turbo-rails"; // <-- add this
// Eager load all controllers defined in the import map under controllers/**/*_controller
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading";
eagerLoadControllersFrom("controllers", application);
// Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!)
// import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
// lazyLoadControllersFrom("controllers", application)
// initialize StimulusReflex w/top-level await
const consumer = await cable.getConsumer()
StimulusReflex.initialize(application, { consumer, debug: true });
Test Everything with a CounterReflex
For the rest of this walkthrough, I follow the official Quickstart Guide closely (for any in-depth explanation of what is going on here, please look there). Let’s first add a PagesController
with only an index
route:
$ bin/rails g controller Pages index
# config/routes.rb
Rails.application.routes.draw do
resources :pages, only: :index
end
# app/controllers/pages_controller.rb
class PagesController < ApplicationController
def index
end
end
Then, we’ll generate a CounterReflex
with an increment
action and use it to increase a @count
index variable derived from a data-count
attribute on the invoking element:
$ bin/rails g stimulus_reflex Counter increment
# app/reflexes/counter_reflex.rb
class CounterReflex < ApplicationReflex
def increment
@count = element.dataset[:count].to_i + element.dataset[:step].to_i
end
end
In the index
view, we’ll furnish a simple <a>
tag with a data-reflex
attribute that invokes this increment
action on click:
<!-- app/views/pages/index.html.erb -->
<a href="#"
data-reflex="click->Counter#increment"
data-step="1"
data-count="<%= @count.to_i %>">
Increment <%= @count.to_i %>
</a>
Now it’s time to start Rails and kick the tires:
$ bin/rails s
Success! 🎉
Testing Client-Side Invocation
Now that we’ve established that the declarative way works, let’s double check that imperative client-side invocation functions, too. Again, nothing new here, refer to the relevant section in the Quick Start Guide.
Instead of a data-reflex
, we are going to reference the Stimulus controller directly, and hook up a data-action
:
<!-- app/views/pages/index.html.erb -->
<a href="#"
data-controller="counter"
data-action="click->counter#increment">
Increment <%= @count %>
</a>
Remember that this is now dependent on our Stimulus counter_controller
, so we have some work to do there:
// app/javascript/controllers/counter_controller.js
import ApplicationController from './application_controller'
export default class extends ApplicationController {
connect () {
super.connect()
}
increment() {
this.stimulate('Counter#increment', 1)
}
}
To stay in sync with the Quick Start guide, let’s switch to session storage and passing the step
via an argument:
# app/reflexes/counter_reflex.rb
class CounterReflex < ApplicationReflex
def increment(step = 1)
session[:count] = session[:count].to_i + step
end
end
We pick this up in the controller again, to render a new @count
:
# app/controllers/pages_controller.rb
class PagesController < ApplicationController
def index
@count = session[:count].to_i
end
end
Voilà 🥂
Conclusion
We’ve established a way to get StimulusReflex up and running with importmaps. The tooling isn’t there yet, but this post essentially lays out the steps that have to be taken to patch it. I will keep updating it as the installers and generators evolve to become importmap-rails
compliant.