An Introduction to Stimulus.js

Rui Freitas
Dec 17, 2019 · 6 min read

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

Image for post
Image for post
Photo by Marius Masalar on Unsplash

You can see the finished code here: https://github.com/rodloboz/stimulus-rails or visit the Heroku app here: https://stimulus-rails-tutorial.herokuapp.com/

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

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

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

<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 liststatic <%= render @albums %>. The route for this controller action isresources :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 image_tag 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

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.

Light the Fuse and Run

Web development in Ruby on Rails, React, Vue.js and Elixir

Rui Freitas

Written by

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

Light the Fuse and Run

Web development in Ruby on Rails, React, Vue.js and Elixir

Rui Freitas

Written by

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

Light the Fuse and Run

Web development in Ruby on Rails, React, Vue.js and Elixir

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store