Rails5:如何在index.html.erb中同时展示待办清单及其子项?
Hey there! Let's break down the best practices for displaying your todo lists and their associated items on the index.html.erb page—this is a super common pattern, and we can make it clean, performant, and user-friendly with a few key steps:
Before touching the view, make sure you're not hitting the database multiple times for each list's items. This is called an N+1 query problem, and it'll slow down your page as your dataset grows.
In your TodoListsController's index action, use includes to preload all associated todo items in a single database call:
def index # Preloads all todo_items for every todo_list in one go @todo_lists = TodoList.includes(:todo_items).all end
This ensures you only make two database queries total (one for lists, one for all their items) instead of 1 + number_of_lists queries.
Use nested HTML elements to clearly separate each list from its items, and add classes for styling and interactivity. Here's a clean example:
<div class="todo-lists-container"> <% @todo_lists.each do |todo_list| %> <section class="todo-list"> <h2 class="todo-list__title"><%= todo_list.name %></h2> <ul class="todo-list__items"> <% todo_list.todo_items.each do |item| %> <li class="todo-item <%= 'todo-item--completed' if item.completed? %>"> <%= check_box_tag "todo_item[#{item.id}]", item.id, item.completed, remote: true, data: { item_id: item.id } %> <span class="todo-item__text"><%= item.description %></span> </li> <% end %> </ul> </section> <% end %> </div>
- Semantic tags like
<section>and<ul>make your markup accessible and easy to style. - The
todo-item--completedclass lets you visually distinguish finished items (e.g., strikethrough text). - We added
remote: trueto the checkbox for AJAX-powered updates (more on that next).
Instead of reloading the whole page when a user checks an item, use AJAX to update the state asynchronously. Here's how to set it up:
First, add a route for updating todo items
In config/routes.rb:
resources :todo_items, only: [:update]
Then, handle the update in your controller
class TodoItemsController < ApplicationController def update @todo_item = TodoItem.find(params[:id]) # Toggle the completed state based on whether the checkbox is checked @todo_item.update(completed: params[:todo_item][@todo_item.id].present?) respond_to do |format| format.js # This will render app/views/todo_items/update.js.erb end end end
Finally, add a JS response to update the DOM
Create app/views/todo_items/update.js.erb:
const todoItem = document.querySelector(`.todo-item[data-item-id="<%= @todo_item.id %>"]`); // Toggle the completed class based on the new state if (<%= @todo_item.completed? %>) { todoItem.classList.add('todo-item--completed'); } else { todoItem.classList.remove('todo-item--completed'); }
This will instantly update the item's appearance without a page refresh.
If you have long lists of items, let users collapse/expand lists to keep the page tidy. Add a simple click handler to the list title:
Update the view to include a toggle icon
<h2 class="todo-list__title" data-list-id="<%= todo_list.id %>"> <%= todo_list.name %> <span class="todo-list__toggle">▼</span> </h2>
Add CSS for hiding items
.todo-list__items--hidden { display: none; }
Add a small JS snippet for toggling
document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.todo-list__title').forEach(title => { title.addEventListener('click', () => { const listId = title.dataset.listId; const items = document.querySelector(`.todo-list[data-list-id="${listId}"] .todo-list__items`); const toggleIcon = title.querySelector('.todo-list__toggle'); items.classList.toggle('todo-list__items--hidden'); toggleIcon.textContent = items.classList.contains('todo-list__items--hidden') ? '▶' : '▼'; }); }); });
If you expect hundreds of lists/items, don't load everything at once. Use pagination for lists (via the will_paginate or kaminari gems) or lazy load items for each list (using Turbo Frames if you're on Rails 7+).
For example, show the first 5 items per list and let users load more:
<ul class="todo-list__items"> <% todo_list.todo_items.limit(5).each do |item| %> <!-- Render first 5 items --> <% end %> <% if todo_list.todo_items.count > 5 %> <%= turbo_frame_tag "more-items-#{todo_list.id}" do %> <%= link_to "View all #{todo_list.todo_items.count} items", more_todo_list_items_path(todo_list), remote: true %> <% end %> <% end %> </ul>
These steps cover the core best practices: optimizing data loading, building clean markup, adding smooth interactions, and scaling for larger datasets.
内容的提问来源于stack exchange,提问作者Fernando Aureliano

