Turbo Rails 入门:使用 Turbo 构建待办事项应用
Rails 7 于 12 月发布时,Turbo 成为了所有新建 Ruby on Rails 应用的默认组件。对于希望使用 Rails 全栈开发的 Rails 开发者来说,这无疑是一个激动人心的时刻——尤其是那些资源有限、难以投入到 React 或 Vue 前端构建和维护中的小型团队开发者。
伴随着这份热情,不断有开发者试图了解 Turbo 的工作原理,以及如何成功地将其集成到他们的 Rails 应用程序中。
对许多人来说,Turbo 的学习之旅充满了坎坷——它需要改变应用程序的架构方式,而且官方文档还不够详尽。好消息是,大多数人在学习初期都会遇到同样的问题,这意味着我们可以通过解决常见的困惑点来更快地帮助他们。
尤其令人困惑的是,如何将 Turbo Frames 和 Turbo Streams 结合使用,以及 Turbo Streams 的工作原理。
今天,我们将构建一个完全由 Turbo 驱动的简单待办事项应用程序。在构建过程中,我们会稍作停留,深入了解一些常见的 Turbo 行为,并直接解决我在 Rails 应用程序中使用 Turbo 的新手身上看到的两个最常见的误解。
完成后,我们的应用程序将按如下方式运行:
本教程面向完全不熟悉 Turbo 的 Rails 开发者。内容主要讲解 Turbo 的基础知识,如果您已经熟悉 Turbo Frames 和 Turbo Streams,可能就用不上了。此外,本文假设您已经能够编写标准的 Rails CRUD 应用——如果您之前从未接触过 Rails,那么本文可能并不适合您!
和往常一样,您可以在Github上找到我们演示应用程序的完整代码。
让我们开始建造吧!
应用程序设置
我们将从一个全新的 Rails 7 应用程序开始,该应用程序自带 Turbo 插件。
使用 Tailwind CSS 创建一个新的 Rails 应用,用于样式设置。在终端中:
rails new turbo-todo --css=tailwind
cd turbo-todo
然后搭建一个Todo资源框架。再次从终端打开:
rails g scaffold Todo name:string status:integer
更新迁移文件,设置状态的默认值:
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
最后,创建并迁移数据库:
使用 Turbo Streams 创建新的待办事项
Rails 脚手架生成器提供了一个开箱即用的完整待办事项实现。启动 Rails 应用后,/todos您可以随意创建、编辑和删除待办事项,但每次请求都会触发页面翻页。这可不太令人兴奋。
本节的目标是更新现有的待办事项框架,使其使用 Turbo Streams 将新创建的待办事项插入到 DOM 中,而无需完全翻页或任何自定义 JavaScript。
首先将索引视图的内容替换app/views/todos/index.html.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>
在这里,我们更新了页面布局,使其看起来更美观,并将新的待办事项表单直接插入到页面上。用户将使用此表单添加新的待办事项,现有待办事项将显示在<ul>表单下方。
请注意,该元素<ul>具有 id 值todos。Turbo Streams 通过 id 定位 DOM 中的元素,并将todos使用该 id 将新创建的待办事项插入到 DOM 中。
更新代码app/views/todos/_todo.html.erb,使每个待办事项都能在以下位置正确渲染<ul>:
<li class="py-2 px-4 border-b border-gray-300">
<%= todo.name %>
</li>
我们在索引视图中渲染的部分form也需要一些调整。在app/views/todos/_form.html.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 %>
id请注意,这里添加了一个<form>,使用了dom_id传递todo给 partial 的,这将用于定位 Turbo Stream 更新。
要使用这些新 ID 更新 DOM,我们需要告诉控制器在表单提交时渲染 Turbo Stream。
为此,请前往TodosController并更新create操作:
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
这里的改动是format.turbo_stream在 action 的 happy path 中添加了 `--up` 语句。format.turbo_stream这告诉 Rails,当向createaction 发送 Turbo Stream 请求时,返回一个匹配的create.turbo_stream.erb文件。
如果你是一名资深的 Rails 开发人员,这感觉与响应js.erbajax 请求并返回文件非常相似。
为了使此功能生效,我们需要创建该create.turbo_stream.erb文件,否则您将收到有关缺少模板的错误。
从您的终端:
touch app/views/todos/create.turbo_stream.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 %>
我们首次看到了Turbo Stream!在这里,create.turbo_stream.erb我们渲染两个<turbo-stream>元素。
第一个操作将新创建的待办事项添加到列表中todos,目标<ul>是 id 为 的待办事项todos。第二个操作将待办事项表单替换为表单的新副本,从而允许我们在每次成功提交后清除待办事项表单。
此时,你可以启动你的 Rails 应用程序bin/dev。访问localhost:3000/todos,创建几个待办事项,你会看到它们自动添加到待办事项列表中。神奇吧!
我们先暂停一下,更详细地回顾一下发生了什么。每次用户提交新的待办事项表单时,发送到服务器的请求都会包含一个Accept 标头,该标头会将该请求标识为 Turbo Stream 请求:
text/vnd.turbo-stream.html, text/html, application/xhtml+xml
Turbo 会自动在所有表单提交中设置此标头POST,PUT无需PATCH开发DELETE人员干预。
turbo-rails注册了一个turbo_streamMIME 类型,以便能够响应传入的 Turbo Stream 表单提交并返回turbo_stream内容。我们可以在以下示例中看到它的实际应用TodosController:
def create
respond_to do |format|
format.turbo_stream
end
end
当调用时format.turbo_stream不传递代码块,Rails 约定要求存在一个与操作和 Mime 类型匹配的文件——在我们的例子中是create.turbo_stream.erb。
在 Rails 中create.turbo_stream.erb,我们<turbo-stream>使用辅助函数来渲染元素turbo_stream。Rails 将create.turbo_stream视图渲染成 HTML,并将该 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 从 HTML 中提取 Turbo Stream 元素,并使用每个元素的操作和内容来更新 DOM。
现在我们对提交涡轮增压表单时发生的事情有了一些了解,这也让我们能够消除一些关于涡轮增压的常见误解。
涡轮流可以针对任何元素,而不仅仅是涡轮帧。
首先,许多新手 Turbo 开发者认为 Turbo Streams 只能针对 Turbo Frames。这种误解导致他们遇到各种问题,例如将表单嵌套在不必要的 Turbo Frame 中,或者将 `<form>`<tr>或 ` <div>`<li>元素包裹在<turbo-frame>`<div>` 元素中,最终导致 HTML 代码无效。
这种误解导致的问题几乎每天都会在 Rails 社区出现,而如果抱持这种误解,使用 Turbo Streams 将会非常困难。尽管官方文档从未将 Turbo Frames 和 Turbo Streams 以这种方式联系起来,但这个问题依然存在。
所以,这里要明确一点:Turbo Streams 通过 id(或较少情况下通过class)来定位 DOM 中的元素。任何具有 id 的元素都可以被 Turbo Streams 定位,而不仅仅是 Turbo Frame 元素。
Turbo Streams 不需要 WebSocket。
Turbo Streams 之所以备受关注,是因为它可以与 WebSocket 一起使用,在标准的请求/响应周期之外,主动地一次性向多个用户发送更新。
借助turbo-rails,开发人员可以轻松地broadcast从模型和后台作业更新,并<turbo-stream>通过 WebSocket 发送代码片段ActionCable。
这种基于 WebSocket 的 Turbo Stream 广播方式非常棒——但正如你之前看到的TodosController,你也可以直接渲染 Turbo Stream 标签来响应浏览器的请求。即使不使用 WebSocket,你也能很好地利用 Turbo Stream。
既然我们已经深入了解了 Turbo Streams 的细节,让我们稍微拉远视角,来看一下 Turbo Frames。
编辑现有待办事项
用户可以通过点击待办事项名称来编辑待办事项。点击名称后,该待办事项的编辑表单将替换列表中的待办事项,如下所示:
为了实现此功能,我们将使用 Turbo Frame 将导航范围限定到要更新的页面部分。首先,按如下方式更新现有的待办事项局部视图:
<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>
在这里,我们为每个元素添加了一个唯一的 ID <li>。在嵌套的元素中<li>,我们<turbo-frame>使用turbo-rails提供的turbo_frame_tag辅助方法添加了一个元素。
Turbo Frame 中包含link_to指向edit操作的链接TodosController。由于该链接位于 Turbo Frame 内,Turbo 会期望服务器返回一个具有匹配 ID 的 Turbo Frame。Turbo 将从响应 HTML 中提取匹配的 Turbo Frame,并使用它来替换框架的原始内容。
在我们的例子中,这意味着当用户点击待办事项的名称时,edit.html.erb会渲染一个编辑表单,并且该表单会替换指向编辑页面的链接。
让我们看看实际效果。更新app/views/edit.html.erb:
<%= turbo_frame_tag dom_id(@todo) do %>
<%= render "form", todo: @todo %>
<% end %>
该元素具有与局部视图中turbo_frame_tag元素匹配的 id 。turbo_frame_tagtodo
更改生效后,刷新todos首页并点击待办事项名称。如果一切顺利,您会看到列表中的该待办事项被编辑表单替换。提交表单后,您将被重定向到已编辑待办事项的显示页面——还没完成!
我们将通过从服务器渲染的另一个 Turbo Stream 来解决这个问题,这次是针对TodosController#update。
从您的终端:
touch app/views/todos/update.turbo_stream.erb
更新新update.turbo_stream.erb视图,如下所示:
<%= turbo_stream.replace "#{dom_id(@todo)}_container" do %>
<%= render "todo", todo: @todo %>
<% end %>
我们再次使用了 Turbo Streamreplace操作。这次,Turbo Stream 操作会将<li>包裹待办事项的表单内容替换为局部视图的内容todo。由于编辑表单已被更新后的待办事项替换,因此我们不需要像之前创建新表单那样重置编辑表单create.turbo_stream.erb。
更新系统TodosController以响应 Turbo Stream 请求:
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
现在,当编辑表单成功提交后,编辑表单将被已编辑的待办事项部分的更新版本所替换。
目前,我们可以创建和编辑待办事项而无需翻页,但您可能已经注意到,我们没有使用 Turbo Streams 来处理无效的表单提交。`and`操作else中的路径缺少响应。我们将在下一节中修复此问题。createupdateturbo_stream
处理表单错误
为了演示如何处理表单错误,我们首先需要向模型添加验证,以便Todo可以将无效的表单提交发送到服务器。app/models/todo.rb
validates_presence_of :name
表单提交时如果填写的是空白,name则无法保存,这使我们能够测试 Turbo Streams 的错误处理功能。
返回TodosController并更新create操作update:
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
当待办事项保存失败时,我们会turbo_stream直接从控制器渲染一个表单,将表单内容替换为更新后的表单版本,以便向用户显示错误信息。
这种在控制器中内联渲染 Turbo Streams 的方法是创建视图的替代方案create.turbo_stream.erb——两种方法都可行。实际上,使用专用视图来管理复杂的 Turbo Streams 响应往往更容易,而对于单个流响应,内联渲染简单的响应也完全可以。
删除待办事项
接下来,我们将通过使用从控制器渲染的另一个 Turbo Stream 来添加无需翻页即可删除待办事项的功能。
首先更新待办事项模板,添加删除按钮:
<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>
注意按钮上的标记method: :delete,它确保按钮能够触发destroy控制器上的相应操作。这里的 SVG 图标来自Heroicons——如果你愿意,也可以直接把按钮上的文字改成“删除”,效果也一样好。
在 中TodosController,更新destroy操作:
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
此内联代码turbo_stream使用 Turbo Stream 的remove操作将目标元素从 DOM 中完全移除。
刷新首页,点击待办事项上的删除按钮,可以看到该待办事项已从 DOM 中删除,而无需翻页。
标记为已完成
如果待办事项无法标记为已完成,那么待办清单就没什么用处了。在本节中,我们将添加一个按钮来切换待办事项的完成状态,并像往常一样依赖 Turbo Streams 来更新 DOM。
首先,让我们status在模型中定义一个简单的枚举Todo:
enum status: {
incomplete: 0,
complete: 1
}
返回app/views/todos/_todo.html.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>
这里的信息量很大,让我们去掉无关信息,突出重要的功能部分。
待办事项完成后,编辑链接会被划掉:
<%= link_to todo.name, edit_todo_path(todo), class: todo.complete? ? 'line-through' : '' %>
如果待办事项已完成,我们会将button_to其标记为未完成。未完成的待办事项会收到一个button_to标记,表示该待办事项已完成。
<% 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 %>
无论哪种情况,patch请求都会TodosController#update作为 Turbo Stream 请求发送,并update.turbo_stream.erb渲染现有视图。
如果这是一个真正的应用程序,我们会将这些按钮提取到辅助方法或视图组件中,但就我们的目的而言,我们可以接受一个混乱的局部视图。
已完成/未完成的待办事项分别放在不同的标签页中
既然用户可以将待办事项标记为已完成,那么最好不要总是看到已完成的待办事项。我们将通过在待办事项索引页面添加标签式界面来完善我们基于 Turbo 的待办事项应用程序,使用户可以在未完成和已完成的待办事项之间切换。
首先,index在操作中添加简单的筛选逻辑TodosController:
def index
@todos = Todo.where(status: params[:status].presence || 'incomplete')
end
然后更新app/views/todos/index.html.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>
现在,索引视图将待办事项内容包裹在一个todos-container <turbo-frame>. 与每个待办事项的编辑链接一样,此 Turbo Frame 将限制框架内的导航范围,从而允许更新待办事项列表而不会更改页面其余部分的内容。
在新框架内todos-container,我们添加了查看未完成和已完成待办事项的链接。此外,由于新创建的待办事项始终未完成,因此我们还添加了在查看已完成待办事项时隐藏新建待办事项表单的逻辑。
因为查看未完成和已完成待办事项的链接位于todos-containerTurbo Frame 内,所以每次点击这些链接时,Turbo 都会将内容替换todos-container为来自服务器的更新内容。
方便的是,我们无需对index渲染 Turbo Frame 内容的操作做任何更改。即使index调用该操作时整个页面都会重新渲染,Turbo 也会todos-container从响应中提取框架并丢弃其余部分。如果您觉得这点效率损失有点困扰,可以进一步提高效率。
这项更改实施后,状态切换行为出现了一些问题。目前,当用户将待办事项标记为已完成时,待办事项的状态会更新,但它仍然保留在列表中。我们希望的是,当待办事项的状态更新时,它能从列表中移除。
实现此功能需要向组件添加一个新的非 RESTful 操作TodosController。我们将这个新操作称为“” change_status。从以下位置开始TodosController:
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
在这里,我们更新了在调用时set_todo before_action设置@todo实例变量,并定义了。change_statuschange_status
change_status更新指定待办事项的状态,然后将其从 DOM 中移除。这适用于将待办事项标记为已完成和未完成——无论哪种情况,我们只需定位到待办事项的 ID<li>并使用 Turbo Streamremove操作即可。
我们添加这个新操作是因为update该功能最初实现中使用的操作是replaceTurbo Stream 操作,而不是从 DOM 中移除待办事项。我们本可以修改该update操作以不同的方式处理状态更改,或者TodoStatusChangesController为此创建一个全新的操作,但在我们的学习应用程序中没有必要这样做。
更新config/routes.rb应用程序,添加新路由:
resources :todos do
patch :change_status, on: :member
end
最后,再次更新待办事项模板,使其使用change_status_todo_path状态切换按钮:
<% 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 %>
完成最后一项更改后,刷新页面即可看到待办事项已按标签分组。切换几个待办事项的状态,可以看到它们已从待办事项列表中移除。切换标签,可以看到待办事项列表已更新:
我们的应用程序非常小,所以很难判断切换标签页是否只更新了todos-containerTurbo Frame 的内容,对吧?它也可能更新了整个页面,而我们根本无法分辨。测试 Turbo Frame 的一个快速方法(以及了解为什么局部页面更新如此有用)是在页面中添加一个虚拟输入框,放在框架之外todos-container:
整洁的!
优雅地降级
在本应用中,我们使用respond_to代码块来渲染对 Turbo Stream 请求的响应。这种方法的优点在于,format.html即使用户的浏览器未启用 JavaScript,我们始终有备用方案可用。
由于我们采用了这种应用程序架构,即使禁用 JavaScript,应用程序也能保持完整功能。由 Turbo 驱动的部分页面更新会过渡到常规的完整页面翻页。精心设计的 Turbo 应用程序通常对 JavaScript 的依赖性较低,因此更容易构建能够优雅地回退到正常服务器渲染 HTML 和完整页面翻页的应用程序。
根据应用程序的目标受众,这可能不是首要任务,但如果您的应用程序需要服务于没有 JavaScript 的用户,那么 Turbo 驱动的应用程序可以为您提供一个坚实的基础。
总结
今天我们构建了一个基于 Turbo 的待办事项应用程序,使用 Turbo Streams 和 Turbo Frames 来响应用户操作,实现快速、高效的页面更新。
这个简单的应用程序作为探索 Turbo Streams 和 Turbo Frames 基础知识的基础,并让我们有机会在此过程中破除一些关于 Turbo Streams 的常见误解。
在使用 Turbo 的过程中,请记住 Turbo Streams 用于响应表单提交。Streams 为您提供了在表单提交后更新一个或多个元素的工具。您可以将 Turbo Streams 直接渲染到控制器中,也可以从视图中渲染。
Turbo Frames 用于将 GET 请求限定在页面的特定区域。您可以使用 Turbo Frames 为页面添加选项卡式内容、实现搜索和筛选界面,或者像今天这样进行内联编辑。Turbo Frames 会始终替换目标框架的全部内容,并且每次 GET 请求只能更新一个框架。
如果您需要更复杂的更新行为(例如追加项目)或需要一次更新多个元素,则无法(轻易地)使用 Turbo Frame。
在本教程中,我们了解了流和帧的基本用例;然而,我们仅仅触及了使用 Turbo 可以做的事情的皮毛。
要继续学习,全面阅读 Turbo参考文档是一个很好的起点。尤其重要的是,要熟悉Turbo 发出的事件,这对于更高级的用例至关重要。了解 Turbo Frame 的选项也很重要——诸如即时加载和延迟加载帧、跳出帧以及从外部定位帧等功能,都有助于解锁强大的 Turbo Frame 体验。
除了 Turbo 官方文档之外,您可能还会发现我对在 Rails 中使用Turbo Streams和Turbo Frames的更深入探讨也很有用。
想要更深入地了解如何使用 Turbo 构建真正的应用程序,Alexandre Ruban 的(正在开发中的)Hotrails 课程是一个很好的资源。
今天就到这里。一如既往,感谢阅读!
文章来源:https://dev.to/davidcolbyatx/turbo-rails-101-building-a-todo-app-with-turbo-5fn2





