A complete walkthrough of how to use Stimulus in Rails

Reading time: 5 minutes

Dive into the must-know Stimulus factors and characteristics, and learn how to build a to-do lists app with this technology in this new article.

Stimulus in Rails

As the tech fans we are, we probably already know that Rails 7 comes with Hotwire by default. Hotwire helps us build applications enjoying the single-page JavaScript apps’ benefits—faster and more fluid user interfaces—without much JavaScript needed. Although the key is using less JavaScript code, the truth is we still need it. 

That’s why it’s essential to meet the two core components of Hotwire: Turbo and Stimulus. In this article, we’re gonna focus on the second one, but it’s good to know that the connection of both makes our app development faster and easier. So, with that being said, let’s delve into Stimulus.

It’s nice to meet you, Stimulus

We can think of Stimulus.js as a smoothed JavaScript framework that aims to pave the way for easier inside-app HTML manipulation, ensuring it stays interactive when we don’t feel like using React or Vue.

Stimulus lays on either importmap-rails to make Stimulus available through ESM or Node-capable Rails—such as jsbundling-railsto add Stimulus in the bundle. Therefore, when utilized for Rails, we can effortlessly use it with import-mapped and JavaScript-bundled apps—if we don’t forget to install one of them before we get started. Another point for Stimulus. 

Stimulus in Rails

Stimulus key concepts

It’s been a hard code night, and Stimulus has been working like a dog. Although The Beatles’ song doesn’t sound like that, it’s entirely possible that it would if they’d know all the stuff Stimulus does. This framework connects JavaScript objects—known as controllers—to elements on the page relying only on simple annotations. What’s the result? An improved static or server-rendered HTML.

But controllers are not the only actors in this play. Other key concepts are

  • Actions, which use data-action attributes to connect controller methods to DOM events.
  • Targets, which have the purpose of locating elements of significance within a controller.
  • Values, that, while focused on the controller’s element, read, write, or observe data attributes.

What about the code? Let’s dig into it: 

<div data-controller="clipboard">
  PIN: <input data-clipboard-target="source" type="text" value="1234" readonly>
  <button data-action="clipboard#copy">Copy to Clipboard</button>
</div>

That code gives us some clues about Stimulus. For instance, looking at the HTML itself, we can get to know what’s happening without needing to check out the clipboard controller code. Another fact is that Stimulus doesn’t work unless it’s needed: it doesn’t create itself the HTML code, because it’s rendered on the server either on page load or through an Ajax request that changes the DOM. 

Plus, Stimulus has the power to add a CSS class that can hide, animate, or highlight an element, manipulating the existing HTML document. And, last but not least: although the focus is always on manipulating and not on creating elements, our dearest framework can build new DOM elements. 

Before we get started with our step-by-step app example, you must know that if you’re using Rails 6, you’re gonna need to follow some steps to install Stimulus. The same happens if you’re on Rails 7+ but the –skip-hotwire is passed to the generator. Here’s a guide where you’ll find what you need to install Stimulus with an installer or manually. Plus, you can check here that you installed all the must-have dependencies for creating a Rails app. 

For lists’ fans: Let’s create a to-do app 

We’re gonna follow the steps proposed in this tutorial but leave behind Tailwind. Ready? Let’s go.

Step zero: Building a brand new Rails app

To create a Rails application, we must run this command in a new terminal window:

rails new beproactive
cd beproactive

We called the app beproactive, but you can call it whatever you feel like. Now open the app in Rubymine—you can use your preferred editor.

mine 

Want to check if everything works well so far? Use this command to start the server and visit https://localhost:3000

bin/rails server 

Step 1: Are you on the to-do list?

It doesn’t feel like VIP, but we can get started with our to-do list. We’re gonna let Rails scaffold rest and learn from the inside how to build our task-manager step-by-step. For that, we will need a ‘Task’ model for our task manager, that has a description and ‘completed’ attributes for sure. We have to create the Task resource by using the ‘rails generate resource command’, which builds an empty model, controller, and migration to give life to the’ tasks’ table.

bin/rails generate resource task description:string{200} completed:boolean

A ‘20220212031029_create_tasks.rb’ migration file under the ‘db/migrate’ directory will be created once we run this command. This file must include this content:

class CreateTasks < ActiveRecord::Migration[7.0]
  def change
    create_table :tasks do |t|
      t.string :description, limit: 200
      t.boolean :completed
      t.timestamps
    end
  end
end

Now it’s time to build the ‘tasks’ table by running the migration: 

bin/rails db:migrate
== 20220212031029 CreateTasks: migrating ======================================

-- create_table(:tasks)

   -> 0.0021s

== 20220212031029 CreateTasks: migrated (0.0022s) =============================

Here’s a lil tip: by opening our SQLite database utilizing database viewers like DB Browser, we must be able to see a tasks table in it.

Step 2: Making our to-do tasks feels like home

Now it’s time get set up our home page. We’re gonna change the ‘routes.rb’ file to modify the home page to the ‘tasks’ page, and leave behind the Rails welcome page. We should open the ‘routes.rb’ file and add this directive at the top of it:

Rails.application.routes.draw do
  root "tasks#index"
  resources :tasks
end

After that, we have to restart our Rails app and reload the browser and… Wait? An error? Yeah, don’t worry. We got it covered, it’s because we haven’t built our ‘index’ action, we’re gonna solve it right away.

Now we’ll build the home index page. The ‘generate resource’ command must have created an empty ‘TasksController’ apart from the migration: 

# We have to build our first action to display all the tasks so # we're gonna fetch all the tasks from the database.
class TasksController < ApplicationController
 def index
  @tasks = Task.all
 end
end

The action by itself won’t make it work, you can check it by reloading the page, and you’ll see a different error. To solve this, we have to create the template—view—that corresponds to this action. Let’s get in the ‘app/views/tasks’ directory and add a file named ‘index.html.erb’ to get rid of this new error:

<h1 class="font-bold text-2xl">Task Manager</h1>

We should now reload the browser to check if we see the words “Tasks Manager” there. It’s all good? Let’s go then. 

Step 3: There are more tasks we have to complete

Nowadays we’re all kinda multitasking people, so, how can we show up those tasks in our apps so users can add theirs too? We have to get into our ‘TasksControllers’ and build a ‘new’ task in its ‘index’ action. Our form is gonna use this as its default task:

class TasksController < ApplicationController
  def index
    @tasks = Task.all
    @task = Task.new
  end
end

It’s time to add ‘_form.html. erb’ file in the ‘app/views/tasks directory’ and build the form partial which has an input field and a button to help us submit the task.

<%= form_with(model: task, class: "mb-7") do |form| %>
  <div class="mb-5">
    <%= form.text_field :description, placeholder: "Add new task", class: "inline-block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-96" %>
    <%= form.submit "Save", class: "btn py-2 ml-2 bg-blue-600 text-white" %>
  </div>
<% end %>

We want to display this form partial on the home page, so, we gotta add this code to the ‘index.html. erb’ file: 

<h1 class="font-bold text-2xl">Task Manager</h1>
<div class="mt-4">
  <%= render "form", task: @task %>
</div>

Why are we rendering the form as a partial? To enable us to reuse it later on a separate page so users can add all the details they want to their tasks. With that being said, let’s reload the page. We should see now the chance to add our task and a “Save” button. 

Looks good until we try it. Because when we click on “Save” after writing down our task, the page reloads. We’re gonna solve this by getting into the ‘TasksController’ and adding a ‘create’ action: 

class TasksController < ApplicationController
  def create
    @task = Task.new(task_params)
    respond_to do |format|
      if @task.save
        format.html { redirect_to tasks_url, notice: "Task was successfully created" }
      else
        format.html { render :new, status: :unprocessable_entity }
      end
    end
  end
  private
  def task_params
    params.require(:task).permit(:description)
  end
end

Now let’s show the notice message at the top to make sure our user knows that his or her task was created successfully. For that, we just have to update the ‘index.html.erb’ template:

<% if notice.present? %>
  <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block" id="notice">
    <%= notice %>
  </p>
<% end %>
<h1 class="font-bold text-2xl">Task Manager</h1>
<div class="mt-4">
  <%= render "form", task: @task %>
</div>

After adding a new task and saving it with the “Save” button, we should see the message we’ve created above. But the task appears to be gone. It actually seems not to be there because we must go to the ‘index.html.erb’ file and add this codethe last ‘div ‘below:

<% if notice.present? %>
  <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block" id="notice">
    <%= notice %>
  </p>
<% end %>
<h1 class="font-bold text-2xl">Task Manager</h1>
<div class="mt-4">
  <%= render "form", task: @task %>
</div>
<div id="tasks">
  <h1 class="font-bold text-lg mb-7">Tasks</h1>
  <div class="px-5">
    <%= render @tasks %>
  </div>
</div>

We’re gonna keep going by creating the ‘_task partial’. For that, we have to build a ‘_task.html.erb file’ in the app/views/directory

<div class="block mb-2">
  <%= task.description %>
</div>

See the first task? Great! You can add new ones if you want. 

A disclaimer before we continue: although we didn’t name it, we’ve already been using Hotwire. Actually, until now, we’ve just used Turbo. Remember we talked about it? It’s what makes the app not reload every time we add a task, transforming it into a more responsive and faster app. Now it’s time for Stimulus to show what it has.

Step 4: The best part of getting the job done

Where are the checkboxes that make us feel great after completing a task? Stimulus is gonna help us meet them. We gotta start by wrapping our task in a form to add a checkbox so let’s get this code in the ‘_task.html.erb’ file’:

<div class="block">
  <%= form_with(model: task, class:"text-lg inline-block my-3 w-72") do |form| %>
    <%= form.check_box :completed,
                       class: "mr-2 align-middle bg-gray-50 border-gray-300 focus:ring-3 focus:ring-blue-300 h-5 w-5 rounded checked:bg-green-500" %>
    <%= task.description %>
  <% end %>
</div>

After reloading the page, we should see the checkboxes. Now, we must get into our’ index.html. erb’ template and add a ‘data-controller’ attribute. We’re gonna add the ‘data-controller=” tasks”‘ attribute on the ‘div’ element rendering the tasks.

...
<div class="px-5" data-controller="tasks">
  <%= render @tasks %>
</div>

It’s important to notice that in Stimulus to get to know our elements of interest, we have to annotate their data attributes, like ‘data-controller’ and ‘data-action’. Now if we want to make this ‘data-controller’ work, we must go to the ‘app/javascript’ directory and add a ‘tasks_controller.js’ file, with the help of this code:

import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
    connect() {
        console.log(this.element)
    }
}

We should reload the page to make sure it works. We can even open the dev tools to test if it’s actually working smoothly and that Stimulus has connected the element to our controller in a good way. After we’ve done that, we can make a back-end request whenever the user clicks on a checkbox. For this to be possible, we just need to add the data attribute to update the ‘task’ partial_task.html.erb

<div class="block">
  <%= form_with(model: task, class:"text-lg inline-block my-3 w-72") do |form| %>
    <%= form.check_box :completed,
                       data: {
                         id: task.id,
                         action: "tasks#toggle"
                       },
                       class: "mr-2 align-middle bg-gray-50 border-gray-300 focus:ring-3 focus:ring-blue-300 h-5 w-5 rounded checked:bg-green-500" %>
    <%= task.description %>
  <% end %>
</div>

Once Rails render this, it gives us this HTML:

<input data-id="1" data-action="tasks#toggle" class="mr-2 .." type="checkbox" value="1" name="task[completed]" id="task_completed">

If we add the ‘toggle’ method in our tasks_controller.js file, this will make a JavaScript fetch call to the server. And we want that, we want Stimulus to call this method when the user clicks on the checkbox. So, let’s do it:

import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
    connect() 
        console.log(this.element)
    }
    toggle(e) {
        const id = e.target.dataset.id
        const csrfToken = document.querySelector("[name='csrf-token']").content
        fetch(`/tasks/${id}/toggle`, {
            method: 'POST', // *GET, POST, PUT, DELETE, etc.
            mode: 'cors', // no-cors, *cors, same-origin
            cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
            credentials: 'same-origin', // include, *same-origin, omit
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-Token': csrfToken
            },
            body: JSON.stringify({ completed: e.target.checked }) // body data type must match "Content-Type" header
        })
          .then(response => response.json())
          .then(data => {
             alert(data.message)
           })
    }

As the front-end is all set, let’s now get behind the scenes. It’s time for us to add the back-end code, empowered enough to handle this POST request from the browser. For that, first, we have to create a route in the ‘routes.rb’ file, which asks the router to call the ‘toggle’ action on the TasksController when a POST request is made with a specific URL pattern. 

Rails.application.routes.draw do
  # ...
  post "tasks/:id/toggle", to: "tasks#toggle"
end

Secondly, in the TasksController, we gotta add the ‘toggle’ method.

def toggle
  @task = Task.find(params[:id])
  @task.update(completed: params[:completed])
  render json: { message: "Success" }
end

Now let’s check some tasks and reload the page to be sure that they’re still there.

Step 5: In case we change our minds, we gotta make sure we can edit and delete 

This gets us to our last step. We’re gonna empower users to edit and delete their tasks. For that, we gotta get in the ‘tasks_controller.rb’ file and add these actions:

class TasksController < ApplicationController
  def edit
    @task = Task.find(params[:id])
  end
  def update
    @task = Task.find(params[:id])
    respond_to do |format|
      if @task.update(task_params)
        format.html { redirect_to tasks_url, notice: "Task was successfully updated" }
      else
        format.html { render :edit, status: :unprocessable_entity }
      end
    end
  end
  def destroy
    @task = Task.find(params[:id])
    @task.destroy
    redirect_to tasks_url, notice: "Post was successfully deleted."
  end
end

Now that the ‘edit’ action can find the task the user wants to edit and its view displays the edit form, it’s time to keep going by adding the ‘edit.html. erb’ file in the ‘app/views/tasks’ directory with the help of this code:

<div>
  <h1 class="font-bold text-2xl mb-3">Editing Task</h1>
  <div id="<%= dom_id @task %>">
    <%= render "form", task: @task %>
    <%= link_to "Never Mind", tasks_path, class: "btn mb-3 bg-gray-100" %>
  </div>
</div>

To finish, we have to add the ‘edit’ and ‘delete’ buttons right next to the task in the ‘_task.html. erb’ file:

<div class="block">
  <%= form_with(model: task, class:"text-lg inline-block my-3 w-72") do |form| %>
    ...
  <% end %>
  <%= link_to "Edit", edit_task_path(task),
              class: "btn bg-gray-100"
  %>
  <div class="inline-block ml-2">
    <%= button_to "Delete", task_path(task),
                  method: :delete,
                  data: { action: "tasks#delete" },
                  class: "btn bg-red-100" %>
  </div>
</div>

Once we’ve reloaded the page, we should see everything working well. For example, the editing form after clicking on its button, and even no full-page reload when we finish completing itit must just redirect us to the home page. We can now check: “Learning what Stimulus is about” and “Building my first app with it”, right? 

If you want to explore more about the latest innovations in coding and get guides to embrace them, just jump into our blog and get help from Effectus experts.