Building a To-Do List Using Hotwire and Stimulus
Tags: rails, hotwire, stimulus, ruby
Published: 2022-02-23 00:06:31

_Note 1: A new and updated version of this article is posted on my blog, [Write Software, Well](https://www.writesoftwarewell.com/building-to-do-list-using-hotwire-and-stimulus/)._

_Note 2: This is a really long article. If you prefer video and want to build this along with me, you can watch a recording on [YouTube](https://youtu.be/ayKxD1LDoe4). I also uploaded the code to [Github](https://github.com/akshayKhot/taskify)._

There's nothing like building a simple, run-of-the-mill to-do list when learning a new framework. It teaches you the basics of creating, reading, updating, and deleting data from various levels of the technology stack, including the front-end, back-end, and database. 

This post explains how to build such a list using the new [Hotwire](https://hotwired.dev/) and [Stimulus](https://stimulus.hotwired.dev/) frameworks from Basecamp. We will start with a new Rails app from scratch and learn about Hotwire and Stimulus as we build our list.

I am also going to use [Tailwind](https://tailwindcss.com/) to style our app, but it's not a requirement and the code examples should still work without it. But, if you haven't worked with Tailwind, I highly recommend that you give it a try.

Before we start coding, this is a quick preview of what we are going to build. It's a simple CRUD application. You can add new tasks, edit existing ones, delete a task, and also complete them.

![finished app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mivp7hgkhqm21oxc5mla.png)

I'll assume that you have installed the required dependencies for building a Rails app. If not, just follow the [getting started](https://guides.rubyonrails.org/getting_started.html#creating-a-new-rails-project) guide and you should be all set. For this application, I will use the following stack:

- Ruby on Rails
- Hotwire (Turbo Drive + Stimulus)
- Sqlite 
- Tailwind CSS

With that out of the way, let's start by creating a new Rails app. 

## Step 1: Create a new Rails application

Run the following command to create a new Rails application in a new terminal window. I will call my app `taskify`. 

```shell
➜ rails new taskify
➜ cd taskify
```

Open the app in your favorite editor. I will use Rubymine. 

```shell
➜ mine .
```

Let's run the app to make sure everything is set up correctly. Use the following command to start the server and visit the [https://localhost:3000](https://localhost:3000/) URL to see your app.

```shell
➜ bin/rails server 
```

If everything works, Rails should greet you with the following screen.

![rails welcome page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o4zxyrk1m8ws4lo0ykxj.png) 

## Step 2: Install Tailwind

Now you could build this task manager without Tailwind, just using plain CSS, but since I started learning Tailwind, I've gotten fond of it and like to use it in all my projects. If you don't want to use it (I highly suggest that you give it a try), the rest of the tutorial should still work for you. 

First, install the `tailwindcss-rails` gem that makes the setup painless. 

```shell
➜  bin/bundle add tailwindcss-rails
```

Then run the Tailwind installer, which will set up Tailwind for you, additionally setting up the `foreman`, which makes running the Tailwind very easy. For more details, check out my post on Foreman. 

```shell
➜  bin/rails tailwindcss:install
```

The next step is to configure the template paths in the `config/tailwind.config.js` file. Tailwind watches these files to generate the final CSS. However, the `tailwindcss-rails` gem does it automatically for you, also setting the tailwind directives in the `application.tailwind.css` file. 

```javascript
module.exports = {
  content: [
    './app/helpers/**/*.rb',
    './app/javascript/**/*.js',
    './app/views/**/*',
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
```

Finally, launch the Foreman script that will launch the Rails server and the Tailwind CLI to watch for changes in your HTML or ERB files and generate the CSS. 

```shell
➜ bin/dev
19:25:54 web.1  | started with pid 29201
19:25:54 css.1  | started with pid 29202
```

Note: Make sure you've already stopped the Rails app using `ctrl + c` that you launched earlier. Otherwise, the following command will throw an error saying "a server is already running".

If everything worked, you should still be greeted by the Rail logo if you reload your browser. We are now ready to start building our task manager.

## Step 3: Create the Task resource

Now, we could take the easy way out and generate a rails scaffold instead of a resource, which will set up everything for us, including the routes, controller actions, views, style, etc. But we want to really learn how to build this task manager step-by-step, so we will take the long route. However, I promise that you will learn a lot in the process. 

We need a `Task` model for our task manager, which has `description` and `completed` attributes. So let's generate the Task resource using the `rails generate resource` command. This command creates an empty model, controller, and migration to create the `tasks` table. 

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

> A resource is any object that you want users to be able to access via URI and perform [CRUD](http://en.wikipedia.org/wiki/Create,_read,_update_and_delete) (or some subset thereof) operations on can be thought of as a resource. [source](https://stackoverflow.com/questions/4686945/what-is-a-resource-in-rails)

Running this command will create a `20220212031029_create_tasks.rb` migration file under the `db/migrate` directory. The exact name will be different as Rails uses the timestamp to generate the name of the migration file. 

This file should have the following content. 

```ruby
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
```

Let's run the migration to create the `tasks` table. 

```shell
➜  bin/rails db:migrate
== 20220212031029 CreateTasks: migrating ======================================
-- create_table(:tasks)
   -> 0.0021s
== 20220212031029 CreateTasks: migrated (0.0022s) =============================
```

If you open your SQLite database using a database viewer such as [DB Browser](https://sqlitebrowser.org/), you should see a `tasks` table in the database.

## Step 4: Set up the home route

Next, we will change the `routes.rb` file to change the home page to the `tasks` page instead of the Rails welcome page. For this, open the `routes.rb` file and add the following directive at the top. 

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

> Rails recommends to put the root route at the top of config/routes.rb, as it will be matched first, as this is the most popular route of most Rails applications.


Now restart your Rails app, and reload the browser. You should see an error.

![unknown action error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a0hl8brz663gwxqlb4vu.png)
  

Rails throws this error because we haven't created our `index` action yet. We will do that in the next step. 

## Step 5: Build the Home (index) Page

In addition to the migration, the `generate resource` command should have also created an empty `TasksController` for you. 

```ruby
class TasksController < ApplicationController
end
```

Let's create our first action that will display all the tasks. In this action, we will fetch all the tasks from the database. 

```ruby
class TasksController < ApplicationController
  def index
    @tasks = Task.all
  end
end
```

Creating action is not enough. If you reload the page, you should see a different error because we didn't create the template (view) corresponding to this action.

![missing template error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p3gowzeolq94p1j0j1gl.png)

Let's fix that. In the `app/views/tasks` directory, add a file named `index.html.erb`. 

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

If you reload the browser now, the words "Task Manager" should greet you. If you see something like this, that means Tailwind is working correctly, too.

![home page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/klbp5xtuzkklqbvkbwmr.png) 

## Step 6: Ability to create new tasks

A task manager without any tasks is boring. So let's add a form using which the users can add new tasks. 

Start by creating a `new` task in the `index` action on our `TasksController`. Our form will use this as the default task. 

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

Next, we will create the form partial by adding a `_form.html.erb` file in the `app/views/tasks` directory. This form has an input field and a button to submit the task.

```erb
<%= 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 %>
```

Add the following code to the `index.html.erb` file to display this form partial on the home page. 

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

<div class="mt-4">
  <%= render "form", task: @task %>
</div>
```

Notice that we are rendering the form as a [partial](https://guides.rubyonrails.org/layouts_and_rendering.html#using-partials), so we can reuse it later, possibly in a separate page that lets users add more details to a task. 

> Partials allow you to break the rendering process into more manageable chunks. With a partial, you can move the code for rendering a particular piece of a response to its own file.

Finally, add some style in the `application.tailwind.css` file, so our buttons look good. I've also removed the `flex` class on my `main` element in the `application.html.erb` file to keep things simple. 

```scss
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
    .btn {
        @apply mt-3 rounded-lg py-1 px-5 inline-block font-medium cursor-pointer;
    }
}
```


Now reload the page. You should see something like this:

![new task form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fq721x8l5hhg3o24mcm2.png)
 
If you add some text and click "Save", the page reloads. However, opening the Devtools shows an error in the networks tab because we are not handling form submissions on the back-end. 

![dev tools](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oofd3m5ryhj2n74cc6ac.png)
 
Let's fix it by adding a `create` action on the `TasksController`. 

```ruby
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
```

> You might wonder why are we using `task_params` method instead of fetching them directly from `params` hash. This technique allows you to choose which attributes should be permitted for mass updating and thus prevent accidentally exposing that which shouldn't be exposed.
>
> `require` is used to mark parameters as required, and `permit` is used to set the parameter as permitted and limit which attributes should be allowed for mass updating. [source](https://api.rubyonrails.org/classes/ActionController/Parameters.html)


Finally, update the `index.html.erb` template to show the notice message at the top. 

```erb
<% 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>
```

Let's add a new task and hit the Save button. You should see the success message, saying that the task was successfully saved in the database.

![success](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8vbhkx36mogdrowylmek.png)
 
**But, wait? Where is my task?** It's not showing up because we haven't added any code on our view template to display the tasks. Let's fix that by adding the following code (the last `div` below) in the `index.html.erb` file. 

```erb
<% 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 are again using the `task` partial in the `<%= render @tasks %>` statement, which is a shortcut for looping over each task and rendering the `_task` partial for that task. Also, remember that we had set the @tasks variable in our `index` action on the TasksController. 

So let's create the `_task` partial by creating a `_task.html.erb` file in the `app/views/directory`. 

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

If you reload the page, you will see our first task. Hey, we did it!

![displaying tasks](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1nhh593sbxm5b5kkrd33.png)   

Go ahead and add a few more tasks.

![more tasks](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q1122gne9tqvrw3njwun.png)
  
At this point, you might be wondering, "**Hey, you said we were going to use Hotwire in this tutorial. When are we going to get to that part?**" If so, I have a surprise for you. We are already using **Hotwire**. At least, a sub-framework of it, called **Turbo Drive**, which is activated by default in a Rails 7 app.

Notice that the browser is not doing a full page reload when we add a new task. It also feels very responsive. The server redirects you to the index page whenever you click the button. However, you can see that the browser is not reloading the page, and your task shows up automatically. What gives?

The answer is the Turbo Drive framework that's part of the Hotwire stack. It's working behind the scenes to make your application faster. 

**How does Turbo Drive work?**

Turbo Drive intercepts all clicks on anchor links to the same domain. When you click a link or submit a form, Turbo Drive does the following:

1. Prevent the browser from following the link,
2. Change the browser URL using the History API,
3. Request the new page using a fetch request
4. Render the response HTML by
   - replacing the current <body> element with the response
   - merging the <head> element’s content.

The JavaScript window and document objects and the <html> element persist from one rendering to the next. The same goes for an HTML form. Turbo Drive converts Form submissions turned into fetch requests. Then it follows the redirect and renders the HTML response. As a result, your browser doesn't have to reload, and the app feels much faster.

To see how the app will behave without Hotwire, disable the Turbo Drive framework by adding following line in the `application.js` file. 

```javascript
Turbo.session.drive = false
```

Now if you try to add a task, you can verify that the browser did a full reload like traditional web applications. 

That said, some of you (project managers, mostly) must have noticed that there's no way to complete our tasks. What fun is there to keep piling more and more tasks, without having a way to complete them? We need a checkbox that will mark a task as **complete** or **incomplete**.

We will use another Hotwire framework called [Stimulus](https://stimulus.hotwired.dev/) to achieve this. As advertised, it is a modest JavaScript framework for the HTML you already have. I love the way it describes itself on the home page. 

> Stimulus is a JavaScript framework with modest ambitions. It doesn’t seek to take over your entire front-end—in fact, it’s not concerned with rendering HTML at all. Instead, it’s designed to augment your HTML with just enough behavior to make it shine.

## Step 7: Use Stimulus to complete the tasks

First, let's wrap our task in a form to add a checkbox. Add the following code in the `_task.html.erb` file. 

```erb
<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>
```

Reloading the page shows the beautiful checkbox next to our task.

![checkbox](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/imjx86ifoswakk5yuzpq.png)

Next, we will add a `data-controller` attribute in our `index.html.erb` template. I have skipped the rest of the code for brevity. You are only adding the `data-controller="tasks"` attribute on the `div` element rendering the tasks. 

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

In Stimulus, we mark our elements of interest by annotating their data attributes, such as `data-controller` and `data-action`. 

> Stimulus continuously monitors the page waiting for HTML data-controller attributes to appear. For each attribute, Stimulus looks at the attribute’s value to find a corresponding controller class, creates a new instance of that class, and connects it to the element.

For this `data-controller` to work, we need to add a `tasks_controller.js` file in the `app/javascript` directory. Add the following code to this file. 

```javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
    connect() {
        console.log(this.element)
    }
}
```

Stimulus calls the `connect` method each time it connects a controller to the document. Simply reload the page and open the dev tools window to test it's working. You should see the following output in the console. That means Stimulus has connected the element to our controller. 

![connect stimulus](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qa46x9pi6onxv6efneyp.png)
 
Next, we will make a back-end request whenever the user clicks the checkbox. For this, update the `task` partial (_task.html.erb) by adding the data attribute. 

```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>
```

When Rails renders this template, it spits out the following HTML. 

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

- The `data-id` attribute is not specific to Stimulus, but we are using it to pass the task id to the controller so that we can pass it to the server. 
- The `data-action` attribute tells Stimulus that whenever a user clicks on this checkbox, call the `toggle` method defined on the controller in the `tasks_controller.js` file.

> Note: The data-action attribute must be nested inside an element that's getting connected to the Stimulus controller. In our case, the task partial is rendered inside our div element in the index template. That's how Stimulus knows which method to call. 

Now add the `toggle` method in our tasks_controller file, which makes a JavaScript fetch call to the server. Again, Stimulus will call this method whenever the user clicks the checkbox. 

```javascript
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)
           })
    }
}
```

There are a few points to note in the above method. 

1. The `toggle` method takes the event as a parameter. We can access the checkbox element from the `target` property of the event. 
2. We retrieve the task `id` from the data attribute on the element, which we set earlier. Then we pass the `id` in the URL. 
3. We are passing the `csrfToken` in the header. Read more Cross-Site Request Forgery [here](https://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf). 
4. The body contains whether the checkbox was selected or not, letting the server know if the task is completed or marked incomplete. 

With the front-end ready, let's add the back-end code to handle this POST request from the browser. There are two steps to it. 

- Add a route in the `routes.rb` file that tells the router to call the `toggle` action on the TasksController, whenever a POST request is made with a specific URL pattern. 

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

- Add the `toggle` method on the TasksController finds the task using the `id` and updates the `completed` attribute.

```ruby
def toggle
  @task = Task.find(params[:id])
  @task.update(completed: params[:completed])

  render json: { message: "Success" }
end
```

You're all set! Go ahead and mark a few tasks complete (of course, after you've finished them in the real world). To make sure the changes were persisted in the database, reload the page, and you should see the completed tasks checked off. 

![success alert](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zflpadrcxlnhqr4vy21c.png)   

## Step 8: Add the edit and delete features

As the last step, we will allow the users of our task manager to edit and delete their tasks. Let's start by adding the corresponding actions in the `tasks_controller.rb` file. 

```ruby
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
```

The `edit` action finds the task that we want to edit, and the corresponding view displays the edit form. Add the `edit.html.erb` file in the `app/views/tasks` directory with the following code. 

```erb
<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>
```

Finally, add the `edit` and `delete` buttons next to the task, in the `_task.html.erb` file. 

```erb
<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>
```

That's it. Reload the page, and you should see our buttons to edit and delete the tasks. Clicking the `Delete` button should delete that task, without a full page reload.

![delete](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aglgms7oo5cghfzsf8og.png)

Clicking the `Edit` button should show you the form with the task in it. After making changes, it should redirect you to the home page. Again, without a full page reload. Turbo Drive makes the fetch request, gets the response, and replaces the DOM content.

![edit](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/usc7mhrf6h7styapr8lb.png)   

That's it. Congratulations, you have just implemented a full-featured CRUD task manager using Ruby on Rails and Hotwire frameworks. 

---

Wow, that was a really long article. I hope it was helpful. If you find any mistakes (both in the code or my writing) or know better approaches than what I have, please let me know. I am learning this along with you, too.