Hotwire and View Components: A basic “to-do list”
Published on: 2024-08-02
Building a To-Do List Application
Let’s build a simple to-do list application without using a single line of javascript. We’ll utilize Turbo for interactive list updates and View Components to encapsulate the task creation and display elements.
For this application, we’ll use the following stack:
- Ruby on Rails 7.x
- Hotwire (Turbo)
- Sqlite
- Tailwind CSS
- Setting up the Rails Project. Begin by creating a new Rails project:
rails new todolist -T -M
-
Install Tailwind CSS
It would be very beneficial to consider Tailwind CSS over plain CSS or SASS for its dramatic impact on CSS maintainability and UI design productivity.
First, install the tailwindcss-rails gem
bundle add tailwindcss-rails
run the Tailwind installer, which will set up Tailwind:
rails tailwindcss:install
Next, configure the template paths in the config/tailwind.config.js file. Using these files, Tailwind generates the final CSS.
const defaultTheme =
require('tailwindcss/defaultTheme')
module.exports = {
content: [
'./public/*.html',
'./app/helpers/**/*.rb',
'./app/javascript/**/*.js',
'./app/views/**/*.{erb,haml,html,slim}',
'./app/components/**/*.{erb,haml,html,slim}'
],
theme: {
extend: {
fontFamily: {
sans: ['Inter var',
...defaultTheme.fontFamily.sans],
},
},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
require('@tailwindcss/container-queries'),
]
}
- Creating the Todo Model
rails g model Todo task:string completed:boolean
run migrations:
rails db:migrate
- Designing the View Component
Add view component support:
bundle add view_component
Create a View Component:
rails generate component Todolist::Item
After running the command, you might end up with:
app/
└── components/
└── todolist/
├── item_component.html.erb
└── item_component.rb
- Implementing the View Component
Define the necessary logic in item_component.rb:
module Todolist
class ItemComponent < ViewComponent::Base
def initialize(todo:)
super
@todo = todo
end
end
end
Make the following changes to item_component.html.erb:
<div id="todo_<%= @todo.id %>">
<%= form_with model: @todo, remote: true,
html: { id: dom_id(@todo),
class: "flex gap-4 pt-6
items-center" }
do |form| %>
<%= form.label :completed, "Completed",
class: "sr-only",
for: "completed_#{dom_id(@todo)}" %>
<%= form.check_box :completed,
id: "completed_#{dom_id(@todo)}",
class: 'size-6' %>
<%= form.label :task, "Task",
class: "sr-only",
for: "task_#{dom_id(@todo)}" %>
<%= form.text_field :task,
id: "task_#{dom_id(@todo)}",
class: 'rounded-xl w-full md:w-auto' %>
<%= form.submit 'Update',
class: 'bg-blue-300 p-2 rounded-xl
cursor-pointer hover:bg-gray-400
w-fit text-center w-auto' %>
<%= link_to 'Delete', todo_path(@todo),
method: :delete, title: 'Delete',
data: { turbo_confirm: 'Are you sure?',
turbo_method: :delete },
class: 'bg-red-400 p-2 rounded-xl
cursor-pointer hover:bg-gray-400
w-fit text-center w-auto' %>
<% end %>
</div>
This code generates a form for each to-do item on your list. It allows you to edit the task, mark it as complete or delete it, all while providing a smooth user experience with AJAX updates.
In essence:
- You’ve enabled AJAX with remote: true.
- You’ve provided instructions for Turbo on the client-side.
- Your server-side code now needs to handle the AJAX request and send back the right content to enable the seamless update.
- Creating the todos view
First, update the config/routes.rb to:
Rails.application.routes.draw do
resources :todos, except: %i[edit show new]
root 'todos#index'
end
add the file app/views/todos/index.html.erb:
<div class="flex flex-col gap-4">
<h1 class="font-bold">Todo List</h1>
<%= render 'form' %>
<div id="messages" class="text-red-500"></div>
<div id="todos" class="pr-8 flex flex-col">
<% @todos.all.each do |todo| %>
<%= render
Todolist::ItemComponent.new(todo: todo)
%>
<% end %>
</div>
</div>
the _form.html.erb partial is defined as follows:
<%= form_with
model: Todo.new,
url: todos_path,
local: true,
id: 'form',
class: 'flex gap-4',
autocomplete: false,
html: { autocomplete: "off" } do |f| %>
<%= f.label :task, 'Task', class: 'sr-only' %>
<%= f.text_field :task,
placeholder: 'New task',
class: 'rounded-xl' %>
<%= f.submit 'Add',
class: 'bg-gray-300 p-2 rounded-xl
cursor-pointer hover:bg-gray-400'
%>
<% end %>
- Adding the Controller
Create a TodosController
:
rails generate controller TodosController
this generates todos_controller.rb:
class TodosController < ApplicationController
before_action :set_todo, only: %i[update destroy]
def index
@todos = Todo.all
end
def create
@todo = Todo.new(todo_params)
if @todo.save
render turbo_stream: [
turbo_stream.append('todos',
Todolist::ItemComponent.new(todo:
@todo)),
turbo_stream.replace('form',
partial: 'todos/form'),
turbo_stream.update('messages', '')]
else
render_error_response
end
end
def update
if @todo.update(todo_params)
render turbo_stream: [
turbo_stream.replace("todo_#{@todo.id}",
Todolist::ItemComponent.new(todo:
@todo)),
turbo_stream.update('messages',
'Task was successfully updated.')]
else
render_error_response
end
end
def destroy
@todo.destroy
render turbo_stream:
turbo_stream.remove("todo_#{@todo.id}")
end
private
def render_error_response
@errors = @todo.errors.full_messages.join(', ')
render turbo_stream:
turbo_stream.update('messages', @errors)
end
def set_todo
@todo = Todo.find(params[:id])
end
def todo_params
params.require(:todo).permit(:task, :completed)
end
end
Interaction Flow:
-
Initial Page Load: The index action renders the page with the list of tasks.
-
Creating a new task:
- User fills out the form and submits it.
- The create action handles the request, saves the task, and sends back Turbo Stream instructions to:
- Render only the new task (view component) to the list.
- Reset the form.
- Clear any messages.
- Updating a task:
- User interacts with the form within an item (e.g., checks the “Completed” checkbox or updates the task text).
- The form submits the changes using AJAX (due to remote: true).
- The update action processes the update and responds with Turbo Stream instructions to:
- Replace the existing item only with the updated version.
- Display a success message.
- Deleting a task:
- The user clicks “Delete”.
- The link’s data attributes trigger a confirmation dialog (using turbo_confirm) and specify the delete method.
- The destroy action handles the deletion and sends back a Turbo Stream instruction to remove the item from the DOM.
Benefits of this Approach:
-
Improved User Experience: Turbo Streams provide a seamless and responsive experience for users by updating specific parts of the page without full-page reloads.
-
Code Organization: ViewComponents encapsulate the presentation logic for individual todo items, making the code more modular and maintainable.
-
Reduced Data Transfer: Turbo Streams optimize performance by transmitting only the necessary HTML fragments, reducing bandwidth consumption and improving page load times. They also offer several actions for manipulating content on a web page, allowing for efficient updates without full page reloads. These actions include:
- Append: Adds new content to an existing element.
- Replace: Replaces the content of a target element with new content.
- Update: Modifies the attributes or properties of an existing element.
- Remove: Deletes a specific target element from the page.
- Prepend: Inserts new content before the target element.
- After: Inserts new content after the target element.
Conclusion
Hotwire and View Components in Rails 7 create a powerful combination for building modern web applications. This duo leverages Hotwire’s server-driven approach and View Components’ modularity to deliver dynamic and engaging user experiences, while maintaining a clean and efficient codebase. This approach not only improves the user experience but also streamlines development by:
-
Enhancing Code Maintainability: View Components create modular and reusable building blocks, making code more organized and easier to understand.
-
Boosting Development Speed: Hotwire minimizes the need for complex JavaScript, allowing for faster development cycles.
-
Optimizing Performance: Turbo Streams ensure efficient data transfer, leading to improved performance and a smoother user experience.