Turbo Rails 101: Building a todo app with Turbo
Tags: rails, ruby, turbo, tutorial
Published: 2022-02-11 17:11:42

When Rails 7 released in December, Turbo became a default component of all new Ruby on Rails applications. This was an exciting moment for Rails developers that want to use Rails full-stack — especially folks on small teams with limited resources to pour into building and maintaining a React or Vue frontend.

Along with that excitement has come a constant stream of developers trying to learn how Turbo works, and how to integrate it into their Rails applications successfully.

For many, that learning journey has included a lot of roadblocks — Turbo requires some changes to how you structure applications and the official documentation is not yet as detailed as it could be. The good news is, most folks hit the same roadblocks early on their journey, which means we can help folks faster by addressing the common points of confusion.

In particular, there is confusion about how to use Turbo Frames and Turbo Streams together, and confusion about how Turbo Streams work.

Today, we are going to build a simple todo list application, powered entirely by Turbo. While building, we will take a few detours to look more deeply at a few common Turbo behaviors, and we will directly address two of the most common misconceptions that I see from folks who are brand new to using Turbo in their Rails applications.

When we are finished, our application will work like this:

![A screen recording of a user of a web application adding and removing todo items from a list of todos](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/asak5gmieeuqcwh8qinw.gif)

This tutorial is written for Rails developers who are brand new to Turbo. The content is very much Turbo 101 and may not be useful if you are already comfortable working with Turbo Frames and Turbo Streams. This article also assumes comfort with writing standard CRUD applications in Rails — if you are have never used Rails before, this is not the right place to start!

As usual, you can find the complete code for our demo application on [Github](https://github.com/DavidColby/turbo-todo).

Let’s start building!

## Application setup
We will start with a brand new Rails 7 application which comes with Turbo out of the box.

Generate a new Rails application with Tailwind CSS for styling. From your terminal:

```bash
rails new turbo-todo --css=tailwind
cd turbo-todo
```

And then scaffold up a `Todo` resource. From your terminal again:

```bash
rails g scaffold Todo name:string status:integer
```

Update migration to set default value for status:

```ruby
class CreateTodos < ActiveRecord::Migration[7.0]
  def change
    create_table :todos do |t|
      t.string :name
      t.integer :status, default: 0

      t.timestamps
    end
  end
end
```

Finally, create and migrate the database:

## Create new todos with Turbo Streams
The Rails scaffold generator provides a fully functional implementation of todos out of the box. If you start up the Rails app and head to `/todos` you can create, edit, and delete todos to your heart’s content, but every request will initiate a full page turn. Not very exciting.

Our goal in this section is to update the existing todo scaffold to use Turbo Streams to insert newly created todos into the DOM without a full page turn or any custom JavaScript.

Start by replacing the content of the index view, `app/views/todos/index.html.erb`, with the following:

```erb
<div class="mx-auto w-1/2">
  <h2 class="text-2xl text-gray-900">
    Your todos
  </h2>
  <div class="w-full max-w-2xl bg-gray-100 py-8 px-4 border border-gray-200 rounded shadow-sm">
    <div class="py-2 px-4">
      <%= render "form", todo: Todo.new %>
    </div>
    <ul id="todos">
      <%= render @todos %>
    </ul>
  </div>
</div>
```

Here, we updated the page layout so it looks a little nicer and inserted the new todo form directly on to the page. Users will use this form to add new todos, and existing todos will be rendered in the `<ul>` below the form.

Note that the `<ul>` has an id of `todos`. Turbo Streams target elements in the DOM by id, and the `todos` id will be used to insert newly created todos into the DOM.

Update `app/views/todos/_todo.html.erb` to render each todo properly inside of the `<ul>`:

```erb
<li class="py-2 px-4 border-b border-gray-300">
  <%= todo.name %>
</li>
```

The `form` partial we render in the index view needs a few adjustments too. In `app/views/todos/_form.html.erb`:

```erb
<%= form_with(model: todo, id: "#{dom_id(todo)}_form") do |form| %>
  <% if todo.errors.any? %>
    <div id="error_explanation" class="bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3">
      <h2><%= pluralize(todo.errors.count, "error") %> prohibited this todo from being saved:</h2>

      <ul>
        <% todo.errors.each do |error| %>
          <li><%= error.full_message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>
  <div class="flex items-stretch flex-grow">
    <%= form.label :name, class: "sr-only" %>
    <%= form.text_field :name, class: "block w-full rounded-none rounded-l-md sm:text-sm border-gray-300", placeholder: "Add a new todo..." %>
    <%= form.submit class: "-ml-px relative px-4 py-2 border border-blue-600 text-sm font-medium rounded-r-md text-white bg-blue-600 hover:bg-blue-700" %>
  </div>
<% end %>
```

Note the addition of an `id` to the `<form>`, using the `dom_id` of the `todo` passed to the partial, which will be used to target Turbo Stream updates.

To use these new ids to update the DOM, we need to tell our controller to render a Turbo Stream when the form is submitted.

To do this, head to the `TodosController` and update the `create` action:

```ruby
def create
  @todo = Todo.new(todo_params)

  respond_to do |format|
    if @todo.save
      format.turbo_stream
      format.html { redirect_to todo_url(@todo), notice: "Todo was successfully created." }
    else
      format.html { render :new, status: :unprocessable_entity }
    end
  end
end
```

The change here is the addition of `format.turbo_stream` to the happy path in the action. `format.turbo_stream` tells Rails that when a Turbo Stream request is sent to the `create` action, respond with a matching `create.turbo_stream.erb` file.

If you are a long-time Rails developer, this will feel very similar to responding with `js.erb` files in response to ajax requests.

In order for this to work, we need to create the `create.turbo_stream.erb` file, otherwise you will get an error about a missing template.

From your terminal:

```bash
touch app/views/todos/create.turbo_stream.erb
```

And then fill that new file in:

```erb
<%= turbo_stream.prepend "todos" do %>
  <%= render "todo", todo: @todo %>
<% end %>
<%= turbo_stream.replace "#{dom_id(Todo.new)}_form" do %>
  <%= render "form", todo: Todo.new %>
<% end %>
```

Our first look at a Turbo Stream! In `create.turbo_stream.erb` we render two `<turbo-stream>` elements.

The first [prepends](https://turbo.hotwired.dev/reference/streams#prepend) the newly created todo to the list of `todos`, targeting the `<ul>` with the id of `todos`. The second [replaces](https://turbo.hotwired.dev/reference/streams#replace) the todo form with a fresh copy of the form, allowing us to clear the todo form after each successful submission.

At this point, you can start up your Rails application with `bin/dev`. Head to [localhost:3000/todos](http://localhost:3000/todos), create a couple of todos and see that they automatically append to the list of todos. Magic.

![A screen recording of a user of a web application adding a todo item to the list of todos](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nn5i7uy1tbkrslbpj5eu.gif)

Let’s pause here and review what is happening in a little more detail. Each time the user submits the new todo form, the request sent to the server includes an [Accept header](https://turbo.hotwired.dev/handbook/streams#streaming-from-http-responses) that identifies the request as a Turbo Stream request:

```
text/vnd.turbo-stream.html, text/html, application/xhtml+xml
```

Turbo sets this header automatically on all `POST`, `PUT`, `PATCH`, `DELETE` form submissions, with no intervention required from the developer.

[turbo-rails](https://github.com/hotwired/turbo-rails/blob/2a2a5ec12e4233cf33e5d3f0a6855834f4e5e4ec/lib/turbo/engine.rb#L51) registers a `turbo_stream` Mime type to enable responding to inbound Turbo Stream form submissions with `turbo_stream` content. We see this in action in the `TodosController`:

```ruby
def create
  respond_to do |format|
    format.turbo_stream
  end
end
```

When calling `format.turbo_stream` without passing a block, Rails conventions expect that a file that matches the action and Mime type exists — in our case, `create.turbo_stream.erb`.

In `create.turbo_stream.erb`, we render `<turbo-stream>` elements using the `turbo_stream` helper. Rails renders the `create.turbo_stream` view to HTML and sends that HTML back to the frontend:

```html
<turbo-stream action="prepend" target="todos">
  <template>
    <li class="py-2 px-4 border-b border-gray-300">
      A new todo
    </li>
  </template>
</turbo-stream>

<turbo-stream action="replace" target="new_todo_form">
  <template>
    <form id="new_todo_form" action="/todos" accept-charset="UTF-8" method="post"><input type="hidden" name="authenticity_token" value="TmpPYfxQOln1t3jmigbzZ49ciBWjivGEjp_nJUWJMeUJZSoeA8dvbkLeD6kLkmHZx-zc8kOzcqu69tWGYASlpQ" autocomplete="off" />
      <div class="flex items-stretch flex-grow">
        <label class="sr-only" for="todo_name">Name</label>
        <input class="block w-full rounded-none rounded-l-md sm:text-sm border-gray-300" placeholder="Add a new todo..." type="text" name="todo[name]" id="todo_name" />
        <input type="submit" name="commit" value="Create Todo" class="-ml-px relative px-4 py-2 border border-blue-600 text-sm font-medium rounded-r-md text-white bg-blue-600 hover:bg-blue-700" data-disable-with="Create Todo" />
      </div>
    </form>
  </template>
</turbo-stream>
```

Turbo extracts the Turbo Stream elements from the HTML and uses each element’s action and content to update the DOM.

Now we understand a bit about what is happening when our Turbo-powered from is submitted, which also gives us the knowledge to knock out a few common misconceptions about Turbo.

### Turbo Streams can target any element, not just Turbo Frames
First, many new Turbo developers think that Turbo Streams can only target Turbo Frames. This misconception causes them to run into issues nesting their forms within an unnecessary Turbo Frame or to end up with invalid HTML by wrapping `<tr>` or `<li>` elements in `<turbo-frame>` elements.

Issues caused by this misconception come up almost daily on the Rails Internet and Turbo Streams can be very difficult to work with while laboring under this misconception. Although the documentation never links Turbo Frames and Turbo Streams in this way, the issue persists.

So, let’s get very clear here: Turbo Streams target elements in the DOM by id (or [class](https://github.com/hotwired/turbo/pull/113), less commonly). Any element with an id can be targeted by a Turbo Stream, not just Turbo Frame elements.

### Turbo Streams do not require WebSockets
Turbo Streams have gotten a lot of attention because they can be used with WebSockets to proactively send updates to many users at once outside of the standard request/response cycle.

With `turbo-rails`, developers can easily `broadcast` updates from models and background jobs to send `<turbo-stream>` snippets over WebSockets with  `ActionCable`.

These types of WebSockets-powered Turbo Stream broadcasts are great — but as you saw in the `TodosController`, you can also just render Turbo Stream tags in response to a request from a browser. You can make great use of Turbo Streams without WebSockets.

Now that we have gotten way down into the weeds of Turbo Streams, let’s zoom back out a bit and take our first look at Turbo Frames.

## Editing existing todos
Users will edit their todos by clicking on the name of the todo. When they click on the name, the edit form for that todo will render in place of the todo in the list, like this:

![A screen recording of a user of a web application clicking a todo in a list. An edit form replaces the todo item in the list.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9rpso65k4ohqchsf1hdn.gif)

To build this functionality, we will use a Turbo Frame to scope navigation to the piece of the page we want to update. Start by updating the existing todo partial like this:

```erb
<li id="<%= "#{dom_id(todo)}_container" %>" class="py-2 px-4 border-b border-gray-300">
  <%= turbo_frame_tag dom_id(todo) do %>
    <%= link_to todo.name, edit_todo_path(todo) %>
  <% end %>
</li>
```

Here, we added a unique id to each `<li>`. Nested within the `<li>`, we added a `<turbo-frame>` using the `turbo-rails` provided `turbo_frame_tag` helper method.

Within the Turbo Frame is a `link_to` pointing to the `edit` action in the `TodosController`. Because the link is within the Turbo Frame, Turbo will expect the server to return a Turbo Frame with a matching id. Turbo will extract the matching Turbo Frame from the response HTML and use it to replace the original content of the frame.

In our case, that means when the user clicks on the todo’s name, `edit.html.erb` will render an edit form and that form will replace the link to the edit page.

Let’s see this in action. Update `app/views/edit.html.erb`:

```erb
<%= turbo_frame_tag dom_id(@todo) do %>
  <%= render "form", todo: @todo %>
<% end %>
```

The `turbo_frame_tag` has an id that matches the `turbo_frame_tag` in the `todo` partial.

With that change in place, refresh the `todos` index page and click on the name of a todo. If all has gone well, you will see that the edit form replaces that todo in the list. If you submit the form, you will see that you get redirected to the show page of the todo you edited — not quite there yet!

We will fix this issue with another Turbo Stream rendered from the server, this time for `TodosController#update`.

From your terminal:

```bash
touch app/views/todos/update.turbo_stream.erb
```

Update the new `update.turbo_stream.erb` view like this:

```erb
<%= turbo_stream.replace "#{dom_id(@todo)}_container" do %>
  <%= render "todo", todo: @todo %>
<% end %>
```

We are using a Turbo Stream `replace` action again. This time the Turbo Stream action replaces the content of the `<li>` wrapping the todo with the content of the `todo` partial. Because the edit form is replaced by the updated Todo, we do not need to reset the edit form like we did the new form in `create.turbo_stream.erb`.

Update the `TodosController` to respond to Turbo Stream requests:

```ruby
def update
  respond_to do |format|
    if @todo.update(todo_params)
      format.turbo_stream
      format.html { redirect_to todo_url(@todo), notice: "Todo was successfully updated." }
      format.json { render :show, status: :ok, location: @todo }
    else
      format.html { render :edit, status: :unprocessable_entity }
      format.json { render json: @todo.errors, status: :unprocessable_entity }
    end
  end
end
```

Now when the edit form is submitted successfully the edit form is replaced with an updated version of the edited todo’s partial.

At this point, we can create and edit todos without a full page turn but you may have noticed that we are not using Turbo Streams to handle invalid form submissions. The `else` path in the `create` and `update` actions is missing a `turbo_stream` response. We will fix that in the next section.

## Handling form errors
To demonstrate handling form errors, we need to first add a validation to the `Todo` model so that we can send invalid form submissions to the server. In `app/models/todo.rb`:

```ruby
validates_presence_of :name
```

Form submissions with a blank `name` will fail to save, letting us test out error handling with Turbo Streams.

Head back to the `TodosController` and update the `create` and `update` actions:

```ruby
def create
  @todo = Todo.new(todo_params)

  respond_to do |format|
    if @todo.save
      format.turbo_stream
      format.html { redirect_to todo_url(@todo), notice: "Todo was successfully created." }
    else
      format.turbo_stream { render turbo_stream: turbo_stream.replace("#{helpers.dom_id(@todo)}_form", partial: "form", locals: { todo: @todo }) }
      format.html { render :new, status: :unprocessable_entity }
    end
  end
end

def update
  respond_to do |format|
    if @todo.update(todo_params)
      format.turbo_stream
      format.html { redirect_to todo_url(@todo), notice: "Todo was successfully updated." }
      format.json { render :show, status: :ok, location: @todo }
    else
      format.turbo_stream { render turbo_stream: turbo_stream.replace("#{helpers.dom_id(@todo)}_form", partial: "form", locals: { todo: @todo }) }
      format.html { render :edit, status: :unprocessable_entity }
      format.json { render json: @todo.errors, status: :unprocessable_entity }
    end
  end
end
```

When the todo fails to save, we render a `turbo_stream` directly from the controller, replacing the content of the form with an updated version of the form so that errors are displayed to the user.

This method of rendering Turbo Streams inline in the controller is an alternative to creating views like `create.turbo_stream.erb`  — either approach will work. In practice, it tends to be easier to manage complex Turbo Stream responses with dedicated views while rendering simple responses inline works fine for single stream responses.

## Deleting todos
Next up, we will add the ability to delete todos without a page turn by using another Turbo Stream rendered from the controller.

Start by updating the todo partial to add a delete button:

```erb
<li id="<%= "#{dom_id(todo)}_container" %>" class="py-2 px-4 border-b border-gray-300">
  <%= turbo_frame_tag dom_id(todo) do %>
    <div class="flex justify-between items-center space-x-2">
      <%= link_to todo.name, edit_todo_path(todo) %>

      <%= button_to todo_path(todo), class: "bg-red-600 px-4 py-2 rounded hover:bg-red-700", method: :delete do %>
        <span class="sr-only">Delete</span>
        <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor" title="Delete todo">
          <path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
        </svg>
      <% end %>
    </div>
  <% end %>
</li>
```

Note the `method: :delete` on the button, which ensures the button hits the `destroy` action on the controller. The svg icon here is from [Heroicons](https://heroicons.com/)  — feel free to just make the button say “Delete” if you like, that will work fine too.

In the `TodosController`, update the `destroy` action:

```ruby
def destroy
  @todo.destroy

  respond_to do |format|
    format.turbo_stream { render turbo_stream: turbo_stream.remove("#{helpers.dom_id(@todo)}_container") }
    format.html { redirect_to todos_url, notice: "Todo was successfully destroyed." }
    format.json { head :no_content }
  end
end
```

This inline `turbo_stream` uses the Turbo Stream [remove](https://turbo.hotwired.dev/reference/streams#remove) action to remove the target element from the DOM entirely.

Refresh the index page, click the delete button on a todo and see that the todo is removed from the DOM without a full page turn.

## Mark todos as complete
A todo list is not very helpful if todos cannot be marked as complete. In this section, we will add a button to toggle todos complete and incomplete, relying as usual on Turbo Streams to update the DOM for us.

To begin, let’s define a simple `status` enum in the `Todo` model:

```ruby
enum status: {
  incomplete: 0,
  complete: 1
}
```

Back to `app/views/todos/_todo.html.erb`:

```erb
<li id="<%= "#{dom_id(todo)}_container" %>" class="py-2 px-4 border-b border-gray-300">
  <%= turbo_frame_tag dom_id(todo) do %>
    <div class="flex justify-between items-center space-x-2">
      <%= link_to todo.name, edit_todo_path(todo), class: todo.complete? ? 'line-through' : '' %>

      <div class="flex justify-end space-x-3">
        <% if todo.complete? %>
          <%= button_to todo_path(todo, todo: { status: 'incomplete' }), class: "bg-green-600 px-4 py-2 rounded hover:bg-green-700", method: :patch do %>
            <span class="sr-only">Mark as incomplete</span>
            <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor">
              <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
            </svg>
          <% end %>
        <% else %>
          <%= button_to todo_path(todo, todo: { status: 'complete' }), class: "bg-gray-400 px-4 py-2 rounded hover:bg-green-700", method: :patch do %>
            <span class="sr-only">Mark as incomplete</span>
            <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor">
              <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
            </svg>
          <% end %>
        <% end %>

        <%= button_to todo_path(todo), class: "bg-red-600 px-4 py-2 rounded hover:bg-red-700", method: :delete do %>
          <span class="sr-only">Delete</span>
          <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor" title="Delete todo">
            <path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
          </svg>
        <% end %>
      </div>
    </div>
  <% end %>
</li>
```

There’s a lot here, let’s cut through the noise to highlight the important functional pieces.

The todo edit link gets struck through when the todo is complete:

```erb
<%= link_to todo.name, edit_todo_path(todo), class: todo.complete? ? 'line-through' : '' %>
```

If the todo is complete, we render `button_to` to mark the todo as incomplete. Incomplete todos get a `button_to` to mark the todo as complete.

```erb
<% if todo.complete? %>
  <%= button_to todo_path(todo, todo: { status: 'incomplete' }), class: "bg-green-600 px-4 py-2 rounded hover:bg-green-700", method: :patch do %>
<% else %>
  <%= button_to todo_path(todo, todo: { status: 'complete' }), class: "bg-gray-400 px-4 py-2 rounded hover:bg-green-700", method: :patch do %>
<% end %>
```

In either case the `patch` request goes to `TodosController#update` as a Turbo Stream request, and the existing `update.turbo_stream.erb` view is rendered.

If this were a real application, we would pull these buttons out into helper methods or into a view component, but for our purposes, we can live with a messy partial.

## Complete/incomplete todos in separate tabs
Now that users can mark todos as complete, it would be nice to not have to see completed todos all of the time. We will finish up our Turbo-powered todo application by adding a tabbed interface to the todos index page, allowing users to toggle between incomplete and complete todos.

Get started by adding simple filtering logic to the `index` action in the `TodosController`:

```ruby
def index
  @todos = Todo.where(status: params[:status].presence || 'incomplete')
end
```

Then update `app/views/todos/index.html.erb`:

```erb
<div class="mx-auto w-1/2">
  <h2 class="text-2xl text-gray-900">
    Your todos
  </h2>
  <%= turbo_frame_tag "todos-container", class: "block max-w-2xl w-full bg-gray-100 py-8 px-4 border border-gray-200 rounded shadow-sm" do %>
    <div class="border-b border-gray-200 w-full">
      <ul class="flex space-x-2 justify-center">
        <li>
          <%= link_to "Incomplete",
            todos_path(status: "incomplete"),
            class: "inline-block py-4 px-4 text-sm font-medium text-center text-gray-500 border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300" 
          %>
        <li>
          <%= link_to "Complete",
            todos_path(status: "complete"),
            class: "inline-block py-4 px-4 text-sm font-medium text-center text-gray-500 border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300"
          %>
        </li>
    </ul>
    </div>
    <% unless params[:status] == "complete" %>
      <div class="py-2 px-4">
        <%= render "form", todo: Todo.new %>
      </div>
    <% end %>
    <ul id="todos">
      <%= render @todos %>
    </ul>
  <% end %>
</div>
```

The index view now wraps the todo content in a `todos-container` `<turbo-frame>`. As with the edit links for each individual todo, this Turbo Frame will scope navigation within the frame, allowing the list of Todos to be updated without changing the content on the rest of the page.

Inside of the new `todos-container` frame, we added links to view incomplete and complete todos. Logic to hide the new todo form when viewing complete todos was also added, since newly created todos are always incomplete.

Because the links to view incomplete and complete todos are within the `todos-container` Turbo Frame, each time those links are clicked, Turbo will replace the content of the `todos-container` with updated content from the server.

Conveniently, we do not need to change anything about the `index` action to render Turbo Frame content. Even though the entire page re-renders when the `index` action is called, Turbo will extract the `todos-container` frame from the response and discard the rest. If that small bit of inefficiency bothers you, it is possible to be [more efficient](https://www.colby.so/posts/turbo-frames-on-rails#conditional-template-rendering-using-variants).

With this change in place, we have a slight problem with the status toggle behavior. Right now, when the user marks a todo as complete, the todo is updated but it stays on the list. Instead, when a todo’s status is updated, we would like to remove it from the list.

Implementing this functionality will require adding a new, non-RESTful action to the `TodosController`. We will call this new action `change_status`. Start in the `TodosController`:

```ruby
before_action :set_todo, only: %i[ show edit update destroy change_status ]

def change_status
  @todo.update(status: todo_params[:status])
  respond_to do |format|
    format.turbo_stream { render turbo_stream: turbo_stream.remove("#{helpers.dom_id(@todo)}_container") }
    format.html { redirect_to todos_path, notice: "Updated todo status." }
  end
end
```

Here, we updated the `set_todo` `before_action` to set the `@todo` instance variable when `change_status` is called and we defined `change_status`.

`change_status` updates the status of the given todo and then removes that that todo from the DOM. This will work for marking todos as complete and for marking them as incomplete — either way, we just target the id of the `<li>` and use a Turbo Stream `remove` action.

We added this new action because the `update` action we used in the first implementation of this feature use a `replace` Turbo Stream action, instead of removing the todo from the DOM. We could have hacked the `update` action to handle status changes differently or created a whole new `TodoStatusChangesController` for this, but there’s no reason to do that in our learning application.

Update `config/routes.rb` to add the new route to the application:

```ruby
resources :todos do
  patch :change_status, on: :member
end
```

And finally, update the todo partial one last time to use the `change_status_todo_path` on the status toggle buttons:

```erb
<% if todo.complete? %>
  <%= button_to change_status_todo_path(todo, todo: { status: 'incomplete' }), class: "bg-green-600 px-4 py-2 rounded hover:bg-green-700", method: :patch do %>
    <span class="sr-only">Mark as incomplete</span>
    <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor">
      <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
    </svg>
  <% end %>
<% else %>
  <%= button_to change_status_todo_path(todo, todo: { status: 'complete' }), class: "bg-gray-400 px-4 py-2 rounded hover:bg-green-700", method: :patch do %>
    <span class="sr-only">Mark as complete</span>
    <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor">
      <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
    </svg>
  <% end %>
<% end %>
```

With that last change in place, you can refresh the page and see that todos are now grouped into tabs. Toggle the status on a few todos and see that they are removed from the list of todos. Change tabs and see that the todo list updates:

![A screen recording of a user of a web application adding and removing todo items from a list.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tk5jmp5s8am9fqkrhciz.gif)

Our application is so small that there’s no real way to tell that changing tabs is only updating the content of the `todos-container` Turbo Frame, right? It could just be updating the whole page and we would never be able to tell. A quick way to test the Turbo Frame out (and to see why partial page updates can be so useful) is to add a dummy input to the page, outside of the `todos-container` frame:

![A screen recording of a user of a web application entering text into an input and then navigating a list of todos while the input text stays in place.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jb17j4ii7iejlxia5s2e.gif)

Neat!

## Degrading gracefully
Throughout this application, we use `respond_to` blocks to render responses to Turbo Stream requests. The nice thing about this approach is that we always have a `format.html` back up ready to go if a user does not have JavaScript enabled in their browser.

Because we constructed our application in this way, the application remains fully functional even when JavaScript is disabled. The partial page updates powered by Turbo give way to regular full page turns. Turbo applications, constructed thoughtfully, tend to rely less on JavaScript to function, making it easier to build applications that can gracefully fall back to normal, server rendered HTML and full page turns.

![A screen recording of a user of a web application disabling JavaScript in their browser and then using the todo application without any issues](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/557bs66iljakcmbq721q.gif)

Depending on your application’s audience, this may not be a top priority, but Turbo-powered applications give you a solid base to build from if your application needs to serve JavaScript-free users.

## Wrapping up
Today we built a Turbo-powered todo application, using Turbo Streams and Turbo Frames to make fast, efficient page updates in response to user actions.

This simple application served as a base to explore the basics of Turbo Streams and Turbo Frames and gave us a chance to debunk a few common misconceptions about Turbo Streams in the process.

As you move forward in your Turbo journey, remember that Turbo Streams are for responding to form submissions. Streams give you the tools to update one or many elements after a form submission. You can render Turbo Streams inline in a controller, or from views.

Turbo Frames are for scoping GET requests to a single piece of the page. Use Turbo Frames to add tabbed content to a page, to power search and filter interfaces, or for inline editing like we saw today. Turbo Frames always replace the entire content of the target frame, and only one frame can be updated per GET request.

If you need more sophisticated update behavior (like appending items) or you need to update multiple elements at once, you cannot (easily) use a Turbo Frame. 

In this tutorial, we looked at basic use cases for Streams and Frames; however, we only just scratched the surface of what you can do with Turbo.

To continue learning, a thorough review of the Turbo [reference documentation](https://turbo.hotwired.dev/reference/drive) is a good starting point. In particular, familiarizing yourself with the [events](https://turbo.hotwired.dev/reference/events) Turbo emits is important for more advanced use cases. Understanding Turbo Frame options is also important — functionality like [eager](https://turbo.hotwired.dev/reference/frames#eager-loaded-frame) and [lazy loaded](https://turbo.hotwired.dev/reference/frames#lazy-loaded-frame) frames, [breaking out](https://turbo.hotwired.dev/reference/frames#frame-with-overwritten-navigation-targets) of frames, and [targeting frames](https://turbo.hotwired.dev/reference/frames#frame-with-overwritten-navigation-targets) from the outside all help unlock powerful Turbo Frame-powered experiences.

In addition to the Turbo documentation, you might find my more in-depth explorations of using [Turbo Streams](https://www.colby.so/posts/turbo-streams-on-rails) and [Turbo Frames](https://www.colby.so/posts/turbo-frames-on-rails) in Rails useful.

For a much deeper dive into building a real application with Turbo, [Alexandre Ruban’s](https://twitter.com/alexandre_ruban) (in-progress) [Hotrails course](https://www.hotrails.dev/) is a great resource.

That’s all for today. As always, thanks for reading!