Stimulus.js is a JavaScript framework created by the founders of Basecamp. It's easy to integrate but powerful and allows us to manipulate the HTML we already have in our application. It's a perfect addition to your Rails application if you don't want to use React or Vue but still make your app to be interactive.

In this article, we are going to create a simple Rails application for adding pets to the database and add the framework along with some example code but you can easily repeat all steps mentioned here to add the framework to your existing application.

Rails installation

We are going to use Ruby 2.7.0 via RVM and install the newest Rails version available at the moment of writing this article. Before continuing, please make sure that you have the newest version of Node.js installed in your system. Please visit https://nodejs.org/en/download/ for detailed instructions on how to install Node.js in your OS.

Use the proper Ruby version and install Rails:

rvm use 2.7.0
gem install rails

We can now generate a new project. We don't have to care about the database so built-in SQLite will be enough and we don't have to explicitly specify the database driver.

rails new railsstimulus
cd railsstimulus

Pets application

As mentioned in the beginning, we are going to create a simple application where it will be possible to add a pet to the database. Each pet record will consist of the pet name and age. To speed up the development, we will use a built-in in Rails scaffold generator which will generate controllers along with the views for simple create, edit, delete and show actions.

rails g scaffold Animal name:string age:integer

Now create the database and migrate the animals table that we generate in the previous step:

bundle exec rake db:create db:migrate

The last step is to point the application main address to show the pets list. In order to do this, please open config/routes.rb file and put there the following contents:

Rails.application.routes.draw do
  resources :animals
  root to: 'animals#index'
end

Now you can run rails s command in the console, visit localhost:3000 and verify if everything is working fine so far. You can play with the application and add the pets you have.

Follow to get a free ebook about Rails!

Stimulus.js installation

We can now add Stimulus.js framework to our application by executing the following command in the command line:

yarn add stimulus

We will keep the logic for the framework in JavaScript controllers. The files will be stored under the directory app/javascript/controllers so let's create it now:

mkdir ./app/javascript/controllers

To make the controllers load automatically in our application, we have to create an index file that will load the controllers. Create app/javascript/controllers/index.js file with the following code:

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

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

We have a file that will load controllers but our application doesn't load the index.js file yet so open app/javascript/packs/application.js file and require the directory:

require("controllers")

We are now ready to create our first Stimulus controller.

Creating Stimulus controller

We will create a simple controller that will count the number of characters in the name input. The count will change as the user will type or delete the characters. Let's create a new file called animal-form_controller.js in the previously created directory for javascript controllers. For now, we will just create an empty class with a simple initializer to make sure that the code is called when the page with animal form is accessed:

import { Controller } from "stimulus"

export default class extends Controller {
  initialize() {
    console.log('Hello from stimulus!');
  }
}

Nothing is displayed in the console because we have to define in the view, which controller should be attached. Let's open the app/views/animals/_form.html.erb file and wrap the existing HTML code into div with data-controller attribute:

<div data-controller="animal-form">
  <%= form_with(model: animal, local: true) do |form| %>
    # ...
  <% end %>
</div>

Reload the page and you should see Hello from stimulus! text in the browser console. It's time to add our logic for counting the characters in the input.

Counting characters in input with Stimulus.js controller

Stimulus framework would love to help us to count characters from given input but we need to tell it first from which input we would like to pull the value. We can do this by setting the data-target attribute on the input with the following value: animal-form.name . The value consists of two parts: the name of the controller and the name of the field.

Let's update the app/views/animals/_form.html.erb view and line form.text_field :name :

<%= form.text_field :name, 'data-target' => "animal-form.name" %>

Now we can update our javascript controller with the following code:

import { Controller } from "stimulus"

export default class extends Controller {
  static get targets() {
    return ['name'];
  }
}

We deleted the initialize method as we don't need it anymore. Instead, the targets method was defined which holds the array of the targets that the framework should take into account. Now we have to listen to the changes in the name input in order to count the number of characters when the name is updated.

To assign given action to the given element in our view, we have to use the data-action attribute which will have the following value: keyup->animal-form#countCharacters. This time the value consists of three elements:

  • keyup-> - the name of the event
  • animal-form - the name of the controller
  • #countCharacters - the name of the method which will handle the event in our javascript controller

Let's update our name input once again:

<%= form.text_field :name, 'data-target' => "animal-form.name", 'data-action' => 'keyup->animal-form#countCharacters'  %>

and the countCharacters method:

import { Controller } from "stimulus"

export default class extends Controller {
  static get targets() {
    return ['name'];
  }

  countCharacters(event) {
    console.log(event);
  }
}

You can now reload the page, open the browser console and try to change the value of name input. You should see the data being logged in your console.

Updating the HTML code

Let's now add our counter to the HTML page:

<div class="field">
  <%= form.label :name%>
  <%= form.text_field :name, 'data-target' => "animal-form.name", 'data-action' => 'keyup->animal-form#countCharacters'  %>
  Characters: <span data-target='animal-form.counter'><%= animal.name.to_s.size %></span>
</div>

As you can see, we added a data-target attribute to the span element to have control over it in our javascript controller. When the form is rendered, the current number of characters is displayed so we don't have to take care of the initialization on the javascript level.

The last step is to update our controller, add a new target, and update the event method to change the text each time user deletes or adds characters in the input:

import { Controller } from "stimulus"

export default class extends Controller {
  static get targets() {
    return ['name', 'counter'];
  }

  countCharacters(event) {
    let characters = this.nameTarget.value.length;
    this.counterTarget.innerText = characters;
  }
}

Our small controller is now working very well:

stimulus animation