An Introduction to Stimulus.js

Rails’ modest JavaScript framework is a great addition to its ecosystem

Rui Freitas
6 min readMar 23, 2020

Stimulus, as indicated on the website https://stimulusjs.org/, is a modest JavaScript framework that is simple to understand and use, but it still brings powerful abstractions to enable advanced use.

In this tutorial, we’ll dive into some practical examples of how to use Stimulus. If you want to follow along, you can start from this tutorial’s repository base branch: https://github.com/rodloboz/stimulus-rails/tree/base

Installing Stimulus

We’re using Rails 6 which ships with Webpack by default. In the terminal, let’s import Stimulus into our project with yarn add stimulus (or npm). Then we’ll add the default Stimulus boilerplate in app/javascript/vendor/stimulus.js and require this file in index.js in the same folder.

import { Application } from 'stimulus'
import { definitionsFromContext } from 'stimulus/webpack-helpers'

const application = Application.start()
const context = require.context('../controllers', true, /\.js$/)
application.load(definitionsFromContext(context))

The above sets up Stimulus to load controllers from app/javascript/controllers and connect them to HTML elements on our page. To connect an HTML element to a Stimulus controller, we use the HTML data attributes in that element with the following data-controller=“name-of-our-stimulus-controller”.

Let’s create a file in the controller folder slider_controller.js and add the following code:

import { Controller } from 'stimulus';

export default class extends Controller {

connect() {
console.log('Hello from Stimulus', this.element)
}
}

Then in index.html, let’s connect it to card__slider div:

<div class=”card__slider” data-controller=”slider”>

Reloading the page, we should see “Hello from Stimulus” in the inspector console followed by the card__slider div element. Here, we see two Stimulus concepts. The connect() function is part of the Stimulus controller lifecycle and is called whenever an element is connected to the controller. Every Stimulus controller also has a reference to the connected element available as this.element.

Tiny Slider

We want to turn the Artist’s list into a caroussel. We’re going to use tiny-slider, a vanilla JavaScript package. Add it to the project with yarn add tiny-slider and import the css in application.scss:

@import 'tiny-slider/dist/tiny-slider.css';

Edit the Slider controller:

import { tns } from 'tiny-slider/src/tiny-slider';
import { Controller } from 'stimulus';

export default class extends Controller {
static targets = [ 'container' ]

connect() {
this.initSlider();
}

initSlider() {
this.slider = tns({
container: this.containerTarget,
items: 5,
slideBy: 1,
gutter: 10,
mouseDrag: true,
arrowKeys: true,
controls: false,
nav: false,
});
}

next() {
this.slider.goTo('next');
}

prev() {
this.slider.goTo('prev');
}
}

Now we need to set up the HTML structure to connect and work with our controller. A Stimulus controller can have access and knowledge of the descendants of the connecting element. This is an important concept, as it will influence the placement of the data attribute connecting our controller.

In the case of this controller, we need to know about the slider container and we need to add event listeners to the left and right arrows. This is done, respectively by adding a data-target attribute with data-target="slider.container" and two data-action attributes, data-action="click->slider#next" and data-action="click->slider#prev".

Targets are identified by passing the name of the controller, a dot, and the name of the target as the value for data-target. Inside the controller, this target can be referenced as containerTarget and it must be added to the static targets array: static targets = [ 'container' ].

Actions add event listeners to their respective elements. They are identified by passing the name of the event, an arrow ->, the name of the controller, hashtag, and the name of the method we want to run as a callback to the event.

Content Loader

Let’s load make our app load artist albums on click without having to refresh the whole page. In Rails, we can use the templating engine and ask our backend to generate HTML snippets or partials which we can then inject into the page instead of coding HTML directly in JavaScript.

<div class="card__row">
<div class="card">
<%= image_tag album.image_url, class: "avatar-lg" %>
</div>
</div>

We will send a request to an AlbumsController in our backend with Stimulus. The controller is just a regular Rails controller action which will render a list of album partials without the layout:

class AlbumsController < ApplicationController
def index
artist = Artist.find(params[:artist_id])
@albums = artist.albums
render layout: false
end
end

The index view simply calls the partial for each album in our list static <%= render @albums %>. The route for this controller action is resources :albums, only: :index.

We’re now going to create the Stimulus controller. Our Albums Loader controller should behave like this:

  • After the initial page load, fetch the albums of the first Artist on the list.
  • When the user clicks on one of the Artists, fetch the albums for that artist.
  • When fetching albums from the backend, get the artist_id from the HTML element’s data attributes.

We’ll need to add a click action to each artist and let the controller know about the albums container element. Because they don’t share a common ancestor, we create a new root div to connect the controller data-controller="albums-loader".

The root element of a Stimulus controller can receive data using the data-maps API. The syntax is data-name-of-the-controller-placeholderName (we’re using url in this example). We use this to pass the URL path for the albums Rails controller with the respective Rails path helper:

<div data-controller="albums-loader"
data-albums-loader-url="<%= albums_path %>"
>

Next, we set up the Artist imagetag with the Stimulus action, make _each artist a target, and pass the Artist id as a data attribute:

<%= image_tag artist.image_url,
class: 'avatar-circle avatar-4xl avatar-grow',
data: {
id: artist.id,
target: 'albums-loader.artist',
action: 'click->albums-loader#load'
}
%>

The Stimulus controller looks like this:

import { Controller } from 'stimulus';

export default class extends Controller {
static targets = [ 'artist', 'container' ]

connect() {
this.id = this.artists[0].dataset.id;
this.fetchAlbums();
}

load() {
this.id = event.target.dataset.id;
this.fetchAlbums();
}

fetchAlbums() {
fetch(this.url)
.then(response => response.text())
.then(html => {
this.containerTarget.innerHTML = html;
});
}

get artists() {
return this.artistTargets;
}

get url() {
return `${this.data.get('url')}?artist_id=${this.id}`
}
}

By having each artist as a target of our controller, we can access the list of artists with this.artistTargets (notice the plural Targets instead of Target). On connect, we get the first artist in the list and load the albums from the backend. We also load each respective artist on user click by calling the same method fetchAlbums.

We use a getter for the final URL that we need to call, which composes the url from the data map passed from the HTML this.data.get('url), followed by the query string and the respective artist id which is set on the connect (the first artist in the list) and on the load (the event target id).

A great thing about Stimulus is that it abstracts away more complicated aspects of interaction with the backend, such as plugging in event listeners when receiving HTML strings from the backend and it integrates perfectly with Turbolinks and the rest of the Rails ecosystem.

You can see the version of this implementation here: https://github.com/rodloboz/stimulus-rails/tree/stimulus-addons

Improvement

To also load the name of the Artist, we need to make some small adjustments to the code.

We refactor our AlbumsController to return a JSON with two keys: an array of album partials rendered to string and the name of the artist:

class AlbumsController < ApplicationController
def index
artist = Artist.find(params[:artist_id])
albums = artist.albums
render json: {
albums: albums.map { |album| render_album(album) },
artist: artist.name
}
end

private

def render_album(album)
render_to_string(
partial: 'albums/album',
locals: { album: album }
)
end
end

We add another data target in the view to pass to the Stimulus controller:

<h2 class="heading">
Albums: <span data-target="albums-loader.artistName"></span>
</h2>

And then change the fetchAlbums to this:

fetchAlbums() {
fetch(this.url)
.then(response => response.json())
.then(({ albums, artist }) => {
this.artistNameTarget.innerHTML = artist
this.containerTarget.innerHTML = albums.join('');
});
}

You can see the final code here: https://github.com/rodloboz/stimulus-rails.

--

--

Rui Freitas

Lead Teacher @ Le Wagon | Web Developer @ Light the Fuse and Run: http://lightthefuse.run/ | Photographer @ Rod Loboz: https://blog.rodloboz.com/