November 25, 2023
To-do list using only turbo-stream (Rails Hotwire)
ANOTHER TO-DO LIST IMPLEMENTATION 😂.
There are many examples of to-do lists on the Internet, but this one is special because my idea is to use only the turbo-stream to update the list!
Should I use turbo-stream to create a to-do list? Probably not, you can get almost the same result with just one turbo-frame (maybe just turbo-drive), which means less complexity. But I think this is a good example to understand the power of turbo-streams.
Â
There are many examples of to-do lists on the Internet, but this one is special because my idea is to use only the turbo-stream to update the list!
Should I use turbo-stream to create a to-do list? Probably not, you can get almost the same result with just one turbo-frame (maybe just turbo-drive), which means less complexity. But I think this is a good example to understand the power of turbo-streams.
Â
The application
Setup
Let's start creating the new project:
rails new todo cd todo rails g resource task description:string completed:boolean rails db:migrate
Â
Routes
# config/routes.rb resources :tasks root "tasks#index"
Controller
# app/controllers/tasks_controller.rb class TasksController < ApplicationController before_action :set_task, only: [:update, :destroy] def index @tasks = Task.all end def create @task = Task.create(task_params) end def update @task.update!(task_params) end def destroy @task.destroy! end private def set_task @task = Task.find(params[:id]) end def task_params params.require(:task).permit(:description, :completed) end end
CSS File
I'm not a designer, which means that this is an engineering design, i.e. a bad one 🙃.Â
body { font-family: 'Roboto', sans-serif; background-color: #FFFBF5; } hr { margin-bottom: 40px; } .tasks { width: 50rem; margin: 0 auto; } .tasks__item { display: flex; justify-content: space-between; align-items: center; padding: 10px; border-bottom: 1px solid #ccc; } .tasks__item:hover { background-color: #F7EFE5; } .tasks__item__completed { text-decoration: line-through; } .tasks__item__button { padding: 10px; background-color: #0766AD; color: white; border: none; border-radius: 5px; cursor: pointer; } .tasks__item__delete { display: inline; } .tasks__item__delete button { font-size: 0.8rem; text-decoration: none; color: #DED0B6; margin-left: 10px; background: none; border: none; cursor: pointer; } .tasks__item input[type="text"] { padding: 7px 5px; border: 1px solid #DED0B6; border-radius: 3px; width: 100%; color: #BBAB8C; margin-right: 20px; } .tasks__item input[type="text"]:focus { outline: none; border: 1px solid #`BBAB8C; }
Index HTML
This view is to render the tasks list:
# app/views/tasks/index.html.erb <h1>🚧 TODO List</h1> <hr /> <ul id="tasks-list" class="tasks"> <%= render partial: "task", collection: @tasks %> </ul> <ul class="tasks" id="new-task"> <%= render partial: "form", locals: { task: Task.new } %> </ul>
Task partial
This partial is for rendering a task:
# app/views/tasks/_task.html.erb <li id="<%= dom_id(task) %>" class="<%= class_names("tasks__item", "tasks__item__completed": task.completed) %>" > <span class="tasks__item__description"> <%= task.description %> <%= button_to "Delete", task_path(task), method: :delete, form: { class: "tasks__item__delete", data: { turbo_confirm: "Are you sure?" } } %> </span> <% if task.completed %> <%= button_to "Uncheck", task_path(task), class: "tasks__item__button", method: :patch, params: { task: { completed: false } } %> <% else %> <%= button_to "Check", task_path(task), class: "tasks__item__button", method: :patch, params: { task: { completed: true } } %> <% end %> </li>
Form partial
This partial is for rendering the form:
# app/views/tasks/_form.html.erb <%= form_with(model: task, class: "tasks__item") do |form| %> <%= form.text_field :description, required: true, placeholder: "Enter description", autofocus: true %> <%= form.submit "Create", class: "tasks__item__button" %> <% end %>
Create turbo-stream
This is a turbo-stream action. After creation, we would like to add a new item to the list and update the form to keep the input blank:
# app/views/tasks/create.turbo_stream.erb <%= turbo_stream.append "tasks-list", partial: 'tasks/task', locals: { task: @task } %> <%= turbo_stream.update "new-task", partial: 'tasks/form', locals: { task: Task.new } %>
Destroy turbo-stream
This is another turbo-stream action. After deletion, we would like to remove the element with the task ID:
# app/views/tasks/destroy.turbo_stream.erb <%= turbo_stream.remove dom_id(@task) %>
Update turbo-stream
After updating, we would like to update the task ID element:
# app/views/tasks/update.turbo_stream.erb <%= turbo_stream.replace dom_id(@task), partial: 'tasks/task', locals: { task: @task } %>
JavaScript
No need to write JavaScript 😊.