How To Add Stimulus to a Ruby on Rails Application

Introduction

If you are working with a Ruby on Rails project, your requirements may include some interactivity with the HTML generated by your view templates. If so, you have a few choices for how to implement this interactivity.

For example, you could implement a JavaScript framework like React or Ember. If your requirements include handling state on the client side, or if you are concerned about performance issues associated with frequent queries to the server, then choosing one of these frameworks may make sense. Many Single Page Applications (SPAs) take this approach.

However, there are several considerations to keep in mind when implementing a framework that manages state and frequent updates on the client side:

  1. It’s possible for loading and conversion requirements — things like parsing JavaScript, and fetching and converting JSON to HTML — to limit performance.
  2. Commitment to a framework may involve writing more code than your particular use case requires, particularly if you are looking for small-scale JavaScript enhancements.
  3. State managed on both the client and server side can lead to a duplication of efforts, and increases the surface area for errors.

As an alternative, the team at Basecamp (the same team that wrote Rails) has created Stimulus.js, which they describe as “a modest JavaScript framework for the HTML you already have.” Stimulus is meant to enhance a modern Rails application by working with server-side generated HTML. State lives in the Document Object Model (DOM), and the framework offers standard ways of interacting with elements and events in the DOM. It works side by side with Turbolinks (included in Rails 5+ by default) to improve performance and load times with code that is limited and scoped to a clearly defined purpose.

In this tutorial, you will install and use Stimulus to build on an existing Rails application that offers readers information about sharks. The application already has a model for handling shark data, but you will add a nested resource for posts about individual sharks, allowing users to build out a body of thoughts and opinions about sharks. This piece runs roughly parallel to How To Create Nested Resources for a Ruby on Rails Application, except that we will be using JavaScript to manipulate the position and appearance of posts on the page. We will also take a slightly different approach to building out the post model itself.

Prerequisites

To follow this tutorial, you will need:

  • A local machine or development server running Ubuntu 18.04. Your development machine should have a non-root user with administrative privileges and a firewall configured with ufw. For instructions on how to set this up, see our Initial Server Setup with Ubuntu 18.04 tutorial.
  • Node.js and npm installed on your local machine or development server. This tutorial uses Node.js version 10.16.3 and npm version 6.9.0. For guidance on installing Node.js and npm on Ubuntu 18.04, follow the instructions in the “Installing Using a PPA” section of How To Install Node.js on Ubuntu 18.04.
  • Ruby, rbenv, and Rails installed on your local machine or development server, following Steps 1-4 in How To Install Ruby on Rails with rbenv on Ubuntu 18.04. This tutorial uses Ruby 2.5.1, rbenv 1.1.2, and Rails 5.2.3.
  • SQLite installed, and a basic shark information application created, following the directions in How To Build a Ruby on Rails Application.

Step 1 — Creating a Nested Model

Our first step will be to create a nested Post model, which we will associate with our existing Shark model. We will do this by creating Active Record associations between our models: posts will belong to particular sharks, and each shark can have multiple posts.

To get started, navigate to the sharkapp directory that you created for your Rails project in the prerequisites:

  • cd sharkapp

To create our Post model, we’ll use the rails generate command with the model generator. Type the following command to create the model:

  • rails generate model Post body:text shark:references

With body:text, we’re telling Rails to include a body field in the posts database table — the table that maps to the Post model. We’re also including the :references keyword, which sets up an association between the Shark and Post models. Specifically, this will ensure that a foreign key representing each shark entry in the sharks database is added to the posts database.

Once you have run the command, you will see output confirming the resources that Rails has generated for the application. Before moving on, you can check your database migration file to look at the relationship that now exists between your models and database tables. Use the following command to look at the contents of the file, making sure to substitute the timestamp on your own migration file for what’s shown here:

  • cat db/migrate/20190805132506_create_posts.rb

You will see the following output:

Output
class CreatePosts < ActiveRecord::Migration[5.2] def change create_table :posts do |t| t.text :body t.references :shark, foreign_key: true t.timestamps end end end

As you can see, the table includes a column for a shark foreign key. This key will take the form of model_name_id — in our case, shark_id.

Rails has established the relationship between the models elsewhere as well. Take a look at the newly generated Post model with the following command:

  • cat app/models/post.rb
Output
class Post < ApplicationRecord belongs_to :shark end

The belongs_to association sets up a relationship between models in which a single instance of the declaring model belongs to a single instance of the named model. In the case of our application, this means that a single post belongs to a single shark.

Though Rails has already set the belongs_to association in our Post model, we will need to specify a has_many association in our Shark model as well in order for that relationship to function properly.

To add the has_many association to the Shark model, open app/models/shark.rb using nano or your favorite editor:

  • nano app/models/shark.rb

Add the following line to the file to establish the relationship between sharks and posts:

~/sharkapp/app/models/shark.rb
class Shark < ApplicationRecord
  has_many :posts
  validates :name, presence: true, uniqueness: true
  validates :facts, presence: true
end

One thing that is worth thinking about here is what happens to posts once a particular shark is deleted. We likely do not want the posts associated with a deleted shark persisting in the database. To ensure that any posts associated with a given shark are eliminated when that shark is deleted, we can include the dependent option with the association.

Add the following code to the file to ensure that the destroy action on a given shark deletes any associated posts:

~/sharkapp/app/models/shark.rb
class Shark < ApplicationRecord
  has_many :posts , dependent: :destroy
  validates :name, presence: true, uniqueness: true
  validates :facts, presence: true
end

Once you have finished making these changes, save and close the file. If you are working with nano, do this by pressing CTRL+X, Y, then ENTER.

You now have a model generated for your posts, but you will also need a controller to coordinate between the data in your database and the HTML that’s generated and presented to users.

Step 2 — Creating a Controller for a Nested Resource

Creating a posts controller will involve setting a nested resource route in the application’s main routing file and creating the controller file itself to specify the methods we want associated with particular actions.

To begin, open your config/routes.rb file to establish the relationship between your resourceful routes:

  • nano config/routes.rb

Currently, the file looks like this:

~/sharkapp/config/routes.rb
Rails.application.routes.draw do
  resources :sharks

  root 'sharks#index'
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end

We want to create a dependent relationship relationship between shark and post resources. To do this, update your route declaration to make :sharks the parent of :posts. Update the code in the file to look like the following:

~/sharkapp/config/routes.rb
Rails.application.routes.draw do
  resources :sharks do
    resources :posts
  end
  root 'sharks#index'
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end

Save and close the file when you are finished editing.

Next, create a new file called app/controllers/posts_controller.rb for the controller:

  • nano app/controllers/posts_controller.rb

In this file, we’ll define the methods that we will use to create and destroy individual posts. However, because this is a nested model, we’ll also want to create a local instance variable, @shark, that we can use to associate particular posts with specific sharks.

First, we can create the PostsController class itself, along with two private methods: get_shark, which will allow us to reference a particular shark, and post_params, which gives us access to user-submitted information by way of the params method.

Add the following code to the file:

~/sharkapp/app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :get_shark 

  private

  def get_shark
    @shark = Shark.find(params[:shark_id])
  end

  def post_params
    params.require(:post).permit(:body, :shark_id)
  end
end

You now have methods to get the particular shark instances with which your posts will be associated, using the :shark_id key, and the data that users are inputting to create posts. Both of these objects will now be available for the methods you will define to handle creating and destroying posts.

Next, above the private methods, add the following code to the file to define your create and destroy methods:

~/sharkapp/app/controllers/posts_controller.rb
. . .
  def create
    @post = @shark.posts.create(post_params)
  end

  def destroy
    @post = @shark.posts.find(params[:id])
    @post.destroy   
  end
. . .

These methods associate @post instances with particular @shark instances, and use the collection methods that became available to us when we created the has_many association between sharks and posts. Methods such as find and create allow us to target the collection of posts associated with a particular shark.

The finished file will look like this:

~/sharkapp/app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :get_shark 

  def create
    @post = @shark.posts.create(post_params)
  end

  def destroy
    @post = @shark.posts.find(params[:id])
    @post.destroy   
  end

  private

  def get_shark
    @shark = Shark.find(params[:shark_id])
  end

  def post_params
    params.require(:post).permit(:body, :shark_id)
  end
end

Save and close the file when you are finished editing.

With your controller and model in place, you can begin thinking about your view templates and how you will organize your application’s generated HTML.

Step 3 — Reorganizing Views with Partials

You have created a Post model and controller, so the last thing to think about from a Rails perspective will be the views that present and allow users to input information about sharks. Views are also the place where you will have a chance to build out interactivity with Stimulus.

In this step, you will map out your views and partials, which will be the starting point for your work with Stimulus.

The view that will act as the base for posts and all partials associated with posts is the sharks/show view.

Open the file:

  • nano app/views/sharks/show.html.erb

Currently, the file looks like this:

~/sharkapp/app/views/sharks/show.html.erb

<%= notice %>

Name: <%= @shark.name %>

Facts: <%= @shark.facts %>

<%= link_to 'Edit', edit_shark_path(@shark) %> | <%= link_to 'Back', sharks_path %>

When we created our Post model, we opted not to generate views for our posts, since we will handle them through our sharks/show view. So in this view, the first thing we will address is how we will accept user input for new posts, and how we will present posts back to the user.

Note: For an alternative to this approach, please see How To Create Nested Resources for a Ruby on Rails Application, which sets up post views using the full range of Create, Read, Update, Delete (CRUD) methods defined in the posts controller. For a discussion of these methods and how they work, please see Step 3 of How To Build a Ruby on Rails Application.

Instead of building all of our functionality into this view, we will use partials — reusable templates that serve a particular function. We will create one partial for new posts, and another to control how posts are displayed back to the user. Throughout, we’ll be thinking about how and where we can use Stimulus to manipulate the appearance of posts on the page, since our goal is to control the presentation of posts with JavaScript.

First, below shark facts, add an

header for posts and a line to render a partial called sharks/posts:

~/sharkapp/app/views/sharks/show.html.erb
. . . 

Facts: <%= @shark.facts %>

Posts

<%= render 'sharks/posts' %> . . .

This will render the partial with the form builder for new post objects.

Next, below the Edit and Back links, we will add a section to control the presentation of older posts on the page. Add the following lines to the file to render a partial called sharks/all:

~/sharkapp/app/views/sharks/show.html.erb
<%= link_to 'Edit', edit_shark_path(@shark) %> |
<%= link_to 'Back', sharks_path %>

<%= render 'sharks/all' %>

The

element will be useful when we start integrating Stimulus into this file.

Once you are finished making these edits, save and close the file. With the changes you’ve made on the Rails side, you can now move on to installing and integrating Stimulus into your application.

Step 4 — Installing Stimulus

The first step in using Stimulus will be to install and configure our application to work with it. This will include making sure we have the correct dependencies, including the Yarn package manager and Webpacker, the gem that will allow us to work with the JavaScript pre-processor and bundler webpack. With these dependencies in place, we will be able to install Stimulus and use JavaScript to manipulate events and elements in the DOM.

Let’s begin by installing Yarn. First, update your package list:

  • sudo apt update

Next, add the GPG key for the Debian Yarn repository:

  • curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -

Add the repository to your APT sources:

  • echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list

Update the package database with the newly added Yarn packages:

  • sudo apt update

And finally, install Yarn:

  • sudo apt install yarn

With yarn installed, you can move on to adding the webpacker gem to your project.

Open your project’s Gemfile, which lists the gem dependencies for your project:

  • nano Gemfile

Inside the file, you will see Turbolinks enabled by default:

~/sharkapp/Gemfile
. . . 
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '~> 5'
. . . 

Turbolinks is designed to improve performance by optimizing page loads: instead of having link clicks navigate to a new page, Turbolinks intercepts these click events and makes the page request using Asynchronous JavaScript and HTML (AJAX). It then replaces the body of the current page and merges the contents of the sections, while the JavaScript window and document objects and the element persist between renders. This addresses one of the main causes of slow page load times: the reloading of CSS and JavaScript resources.

We get Turbolinks by default in our Gemfile, but we will need to add the webpacker gem so that we can install and use Stimulus. Below the turbolinks gem, add webpacker:

~/sharkapp/Gemfile
. . . 
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '~> 5'
gem 'webpacker', '~> 4.x'
. . . 

Save and close the file when you are finished.

Next, add the gem to your project’s bundle with the bundle command:

  • bundle

This will generate a new Gemfile.lock file — the definitive record of gems and versions for your project.

Next, install the gem in the context of your bundle with the following bundle exec command:

  • bundle exec rails webpacker:install

Once the installation is complete, we will need to make one small adjustment to our application’s content security file. This is due to the fact that we are working with Rails 5.2+, which is a Content Security Policy (CSP) restricted environment, meaning that the only scripts allowed in the application must be from trusted sources.

Open config/initializers/content_security_policy.rb, which is the default file Rails gives us for defining application-wide security policies:

  • nano config/initializers/content_security_policy.rb

Add the following lines to the bottom of the file to allow webpack-dev-server — the server that serves our application’s webpack bundle — as an allowed origin:

~/sharkapp/config/initializers/content_security_policy.rb
. . . 
Rails.application.config.content_security_policy do |policy|
  policy.connect_src :self, :https, 'http://localhost:3035', 'ws://localhost:3035' if Rails.env.development?
end

This will ensure that the webpacker-dev-server is recognized as a trusted asset source.

Save and close the file when you are finished making this change.

By installing webpacker, you created two new directories in your project’s app directory, the directory where your main application code is located. The new parent directory, app/javascript, will be where your project’s JavaScript code will live, and it will have the following structure:

Output
├── javascript │ ├── controllers │ │ ├── hello_controller.js │ │ └── index.js │ └── packs │ └── application.js

The app/javascript directory will contain two child directories: app/javascript/packs, which will have your webpack entry points, and app/javascript/controllers, where you will define your Stimulus controllers. The bundle exec command that we just used will create the app/javascript/packs directory, but we will need to install Stimulus for the app/javascript/controllers directory to be autogenerated.

With webpacker installed, we can now install Stimulus with the following command:

  • bundle exec rails webpacker:install:stimulus

You will see output like the following, indicating that the installation was successful:

Output
. . . success Saved lockfile. success Saved 5 new dependencies. info Direct dependencies └─ [email protected] info All dependencies ├─ @stimulus/[email protected] ├─ @stimulus/[email protected] ├─ @stimulus/[email protected] ├─ @stimulus/[email protected] └─ [email protected] Done in 8.30s. Webpacker now supports Stimulus.js 🎉

We now have Stimulus installed, and the main directories we need to work with it in place. Before moving on to writing any code, we’ll need to make a few application-level adjustments to complete the installation process.

First, we’ll need to make an adjustment to app/views/layouts/application.html.erb to ensure that our JavaScript code is available and that the code defined in our main webpacker entry point, app/javascript/packs/application.js, runs each time a page is loaded.

Open that file:

  • nano app/views/layouts/application.html.erb

Change the following javascript_include_tag tag to javascript_pack_tag to load app/javascript/packs/application.js:

~/sharkapp/app/views/layouts/application.html.erb
. . .
    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
. . . 

Save and close the file when you have made this change.

Next, open app/javascript/packs/application.js:

  • nano app/javascript/packs/application.js

Initially, the file will look like this:

~/sharkapp/app/javascript/packs/application.js
. . . 
console.log('Hello World from Webpacker')

import "controllers"

Delete the boilerplate code that’s there, and add the following code to load your Stimulus controller files and boot the application instance:

~/sharkapp/app/javascript/packs/application.js
. . . 
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))

This code uses webpack helper methods to require the controllers in the app/javascript/controllers directory and load this context for use in your application.

Save and close the file when you are finished editing.

You now have Stimulus installed and ready to use in your application. Next, we’ll build out the partials that we referenced in our sharks show view — sharks/posts and sharks/all — using Stimulus controllers, targets, and actions.

Step 5 — Using Stimulus in Rails Partials

Our sharks/posts partial will use the form_with form helper to create a new post object. It will also make use of Stimulus’s three core concepts: controllers, targets, and actions. These concepts work as follows:

  • Controllers are JavaScript classes that are defined in JavaScript modules and exported as the module’s default object. Through controllers, you have access to particular HTML elements and the Stimulus Application instance defined in app/javascript/packs/application.js.
  • Targets allow you to reference particular HTML elements by name, and are associated with particular controllers.
  • Actions control how DOM events are handled by controllers, and are also associated with particular controllers. They create a connection between the HTML element associated with the controller, the methods defined in the controller, and a DOM event listener.

In our partial, we’re first going to build a form as we normally would using Rails. We will then add a Stimulus controller, action, and targets to the form in order to use JavaScript to control how new posts get added to the page.

First, create a new file for the partial:

  • nano app/views/sharks/_posts.html.erb

Inside the file, add the following code to create a new post object using the form_with helper:

~/sharkapp/app/views/sharks/_posts.html.erb
        <%= form_with model: [@shark, @shark.posts.build] do |form| %>
                <%= form.text_area :body, placeholder: "Your post here" %>
                
<%= form.submit %> <% end %>

So far, this form behaves like a typical Rails form, using the form_with helper to build a post object with the fields defined for the Post model. Thus, the form has a field for the post :body, to which we’ve added a placeholder with a prompt for filling in a post.

Additionally, the form is scoped to take advantage of the collection methods that come with the associations between the Shark and Post models. In this case, the new post object that’s created from user-submitted data will belong to the collection of posts associated with the shark we’re currently viewing.

Our goal now is to add some Stimulus controllers, events, and actions to control how the post data gets displayed on the page. The user will ultimately submit post data and see it posted to the page thanks to a Stimulus action.

First, we’ll add a controller to the form called posts in a

element:

~/sharkapp/app/views/sharks/_posts.html.erb
<%= form_with model: [@shark, @shark.posts.build] do |form| %> <%= form.text_area :body, placeholder: "Your post here" %>
<%= form.submit %> <% end %>

Make sure you add the closing

tag to scope the controller properly.

Next, we’ll attach an action to the form that will be triggered by the form submit event. This action will control how user input is displayed on the page. It will reference an addPost method that we will define in the posts Stimulus controller:

~/sharkapp/app/views/sharks/_posts.html.erb
<%= form_with model: [@shark, @shark.posts.build], data: { action: "posts#addBody" } do |form| %> . . . <%= form.submit %> <% end %>

We use the :data option with form_with to submit the Stimulus action as an additional HTML data attribute. The action itself has a value called an action descriptor made up of the following:

  • The DOM event to listen for. Here, we are using the default event associated with form elements, submit, so we do not need to specify the event in the descriptor itself. For more information about common element/event pairs, see the Stimulus documentation.
  • The controller identifier, in our case posts.
  • The method that the event should invoke. In our case, this is the addBody method that we will define in the controller.

Next, we’ll attach a data target to the user input defined in the :body