hotwire-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseHotwire Patterns for Rails 8
Rails 8 的 Hotwire 模式
Overview
概述
Hotwire = HTML Over The Wire - Build modern web apps without writing much JavaScript.
| Component | Purpose | Use Case |
|---|---|---|
| Turbo Drive | SPA-like navigation | Automatic, no code needed |
| Turbo Frames | Partial page updates | Inline editing, tabbed content |
| Turbo Streams | Real-time DOM updates | Live updates, flash messages |
| Stimulus | JavaScript sprinkles | Toggles, forms, interactions |
Hotwire = HTML Over The Wire - 无需编写大量JavaScript即可构建现代Web应用。
| 组件 | 用途 | 使用场景 |
|---|---|---|
| Turbo Drive | 类SPA导航 | 自动生效,无需编写代码 |
| Turbo Frames | 页面局部更新 | 在线编辑、标签页内容 |
| Turbo Streams | DOM实时更新 | 实时内容更新、提示消息 |
| Stimulus | 轻量JavaScript增强 | 切换控件、表单、交互功能 |
Quick Start
快速开始
Turbo Frames (Scoped Navigation)
Turbo Frames(作用域导航)
erb
<%# app/views/posts/index.html.erb %>
<%= turbo_frame_tag "posts" do %>
<%= render @posts %>
<%= link_to "Load More", posts_path(page: 2) %>
<% end %>
<%# Clicking "Load More" only updates content inside this frame %>erb
<%# app/views/posts/index.html.erb %>
<%= turbo_frame_tag "posts" do %>
<%= render @posts %>
<%= link_to "Load More", posts_path(page: 2) %>
<% end %>
<%# Clicking "Load More" only updates content inside this frame %>点击“加载更多”只会更新此frame内的内容
Turbo Streams (Real-time Updates)
Turbo Streams(实时更新)
erb
<%# app/views/posts/create.turbo_stream.erb %>
<%= turbo_stream.prepend "posts", @post %>
<%= turbo_stream.update "flash", partial: "shared/flash" %>erb
<%# app/views/posts/create.turbo_stream.erb %>
<%= turbo_stream.prepend "posts", @post %>
<%= turbo_stream.update "flash", partial: "shared/flash" %>Stimulus Controller
Stimulus 控制器
javascript
// app/javascript/controllers/toggle_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["content"]
toggle() {
this.contentTarget.classList.toggle("hidden")
}
}erb
<div data-controller="toggle">
<button data-action="toggle#toggle">Toggle</button>
<div data-toggle-target="content">Hidden content</div>
</div>javascript
// app/javascript/controllers/toggle_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["content"]
toggle() {
this.contentTarget.classList.toggle("hidden")
}
}erb
<div data-controller="toggle">
<button data-action="toggle#toggle">Toggle</button>
<div data-toggle-target="content">Hidden content</div>
</div>Workflow Checklist
工作流程检查清单
Hotwire Implementation:
- [ ] Identify update scope (full page vs partial)
- [ ] Choose pattern (Frame vs Stream vs Stimulus)
- [ ] Implement server response
- [ ] Add client-side markup
- [ ] Test with and without JavaScript
- [ ] Write system specHotwire Implementation:
- [ ] Identify update scope (full page vs partial)
- [ ] Choose pattern (Frame vs Stream vs Stimulus)
- [ ] Implement server response
- [ ] Add client-side markup
- [ ] Test with and without JavaScript
- [ ] Write system specWhen to Use Each Pattern
各模式的适用场景
| Scenario | Pattern | Why |
|---|---|---|
| Inline edit | Turbo Frame | Scoped replacement |
| Form submission | Turbo Stream | Multiple updates |
| Real-time feed | Turbo Stream + ActionCable | Push updates |
| Toggle visibility | Stimulus | No server needed |
| Form validation | Stimulus | Client-side feedback |
| Infinite scroll | Turbo Frame + lazy loading | Paginated content |
| Modal dialogs | Turbo Frame | Load on demand |
| Flash messages | Turbo Stream | Append/update |
| 场景 | 适用模式 | 原因 |
|---|---|---|
| 在线编辑 | Turbo Frame | 作用域内内容替换 |
| 表单提交 | Turbo Stream | 支持多内容更新 |
| 实时信息流 | Turbo Stream + ActionCable | 推送更新 |
| 可见性切换 | Stimulus | 无需服务器参与 |
| 表单验证 | Stimulus | 客户端即时反馈 |
| 无限滚动 | Turbo Frame + 懒加载 | 分页内容加载 |
| 模态对话框 | Turbo Frame | 按需加载 |
| 提示消息 | Turbo Stream | 追加/更新内容 |
References
参考资料
- See turbo-frames.md for frame patterns
- See turbo-streams.md for stream patterns
- See stimulus.md for controller patterns
- 查阅turbo-frames.md了解frame模式
- 查阅turbo-streams.md了解stream模式
- 查阅stimulus.md了解控制器模式
Testing Hotwire
测试Hotwire
System Specs
系统测试用例
ruby
undefinedruby
undefinedspec/system/posts_spec.rb
spec/system/posts_spec.rb
require 'rails_helper'
RSpec.describe "Posts", type: :system do
before { driven_by(:selenium_chrome_headless) }
it "updates post inline with Turbo Frame" do
post = create(:post, title: "Original")
visit posts_path
within("#post_#{post.id}") do
click_link "Edit"
fill_in "Title", with: "Updated"
click_button "Save"
end
expect(page).to have_content("Updated")
expect(page).not_to have_content("Original")end
it "adds comment with Turbo Stream" do
post = create(:post)
visit post_path(post)
fill_in "Comment", with: "Great post!"
click_button "Add Comment"
within("#comments") do
expect(page).to have_content("Great post!")
endend
end
undefinedrequire 'rails_helper'
RSpec.describe "Posts", type: :system do
before { driven_by(:selenium_chrome_headless) }
it "updates post inline with Turbo Frame" do
post = create(:post, title: "Original")
visit posts_path
within("#post_#{post.id}") do
click_link "Edit"
fill_in "Title", with: "Updated"
click_button "Save"
end
expect(page).to have_content("Updated")
expect(page).not_to have_content("Original")end
it "adds comment with Turbo Stream" do
post = create(:post)
visit post_path(post)
fill_in "Comment", with: "Great post!"
click_button "Add Comment"
within("#comments") do
expect(page).to have_content("Great post!")
endend
end
undefinedRequest Specs for Turbo Stream
Turbo Stream的请求测试用例
ruby
undefinedruby
undefinedspec/requests/posts_spec.rb
spec/requests/posts_spec.rb
RSpec.describe "Posts", type: :request do
describe "POST /posts" do
let(:valid_params) { { post: { title: "Test" } } }
it "returns turbo stream response" do
post posts_path, params: valid_params,
headers: { "Accept" => "text/vnd.turbo-stream.html" }
expect(response.media_type).to eq("text/vnd.turbo-stream.html")
expect(response.body).to include("turbo-stream")
endend
end
undefinedRSpec.describe "Posts", type: :request do
describe "POST /posts" do
let(:valid_params) { { post: { title: "Test" } } }
it "returns turbo stream response" do
post posts_path, params: valid_params,
headers: { "Accept" => "text/vnd.turbo-stream.html" }
expect(response.media_type).to eq("text/vnd.turbo-stream.html")
expect(response.body).to include("turbo-stream")
endend
end
undefinedCommon Patterns
常见模式
Inline Editing with Frame
使用Frame实现在线编辑
erb
<%# _post.html.erb %>
<%= turbo_frame_tag dom_id(post) do %>
<article>
<h2><%= post.title %></h2>
<%= link_to "Edit", edit_post_path(post) %>
</article>
<% end %>
<%# edit.html.erb %>
<%= turbo_frame_tag dom_id(@post) do %>
<%= form_with model: @post do |f| %>
<%= f.text_field :title %>
<%= f.submit "Save" %>
<%= link_to "Cancel", @post %>
<% end %>
<% end %>erb
<%# _post.html.erb %>
<%= turbo_frame_tag dom_id(post) do %>
<article>
<h2><%= post.title %></h2>
<%= link_to "Edit", edit_post_path(post) %>
</article>
<% end %>
<%# edit.html.erb %>
<%= turbo_frame_tag dom_id(@post) do %>
<%= form_with model: @post do |f| %>
<%= f.text_field :title %>
<%= f.submit "Save" %>
<%= link_to "Cancel", @post %>
<% end %>
<% end %>Flash Messages with Stream
使用Stream实现提示消息
ruby
undefinedruby
undefinedapp/controllers/application_controller.rb
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
after_action :flash_to_turbo_stream, if: -> { request.format.turbo_stream? }
private
def flash_to_turbo_stream
flash.each do |type, message|
flash.now[type] = message
end
end
end
undefinedclass ApplicationController < ActionController::Base
after_action :flash_to_turbo_stream, if: -> { request.format.turbo_stream? }
private
def flash_to_turbo_stream
flash.each do |type, message|
flash.now[type] = message
end
end
end
undefinedLazy Loading Frame
懒加载Frame
erb
<%= turbo_frame_tag "comments", src: post_comments_path(@post), loading: :lazy do %>
<p>Loading comments...</p>
<% end %>erb
<%= turbo_frame_tag "comments", src: post_comments_path(@post), loading: :lazy do %>
<p>Loading comments...</p>
<% end %>Debugging Tips
调试技巧
- Frame not updating? Check frame IDs match exactly
- Stream not working? Verify header includes turbo-stream
Accept - Stimulus not firing? Check controller name matches file name
- Events not working? Use
data-action="event->controller#method"
- Frame未更新? 检查frame的ID是否完全匹配
- Stream不生效? 确认请求头包含turbo-stream
Accept - Stimulus未触发? 检查控制器名称与文件名是否一致
- 事件不工作? 使用格式
data-action="event->controller#method"