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
endCSS 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 😊.