hotwire-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Hotwire Patterns for Rails 8

Rails 8 的 Hotwire 模式

Overview

概述

Hotwire = HTML Over The Wire - Build modern web apps without writing much JavaScript.
ComponentPurposeUse Case
Turbo DriveSPA-like navigationAutomatic, no code needed
Turbo FramesPartial page updatesInline editing, tabbed content
Turbo StreamsReal-time DOM updatesLive updates, flash messages
StimulusJavaScript sprinklesToggles, forms, interactions
Hotwire = HTML Over The Wire - 无需编写大量JavaScript即可构建现代Web应用。
组件用途使用场景
Turbo Drive类SPA导航自动生效,无需编写代码
Turbo Frames页面局部更新在线编辑、标签页内容
Turbo StreamsDOM实时更新实时内容更新、提示消息
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 spec
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 spec

When to Use Each Pattern

各模式的适用场景

ScenarioPatternWhy
Inline editTurbo FrameScoped replacement
Form submissionTurbo StreamMultiple updates
Real-time feedTurbo Stream + ActionCablePush updates
Toggle visibilityStimulusNo server needed
Form validationStimulusClient-side feedback
Infinite scrollTurbo Frame + lazy loadingPaginated content
Modal dialogsTurbo FrameLoad on demand
Flash messagesTurbo StreamAppend/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
undefined
ruby
undefined

spec/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!")
end
end end
undefined
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!")
end
end end
undefined

Request Specs for Turbo Stream

Turbo Stream的请求测试用例

ruby
undefined
ruby
undefined

spec/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")
end
end end
undefined
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")
end
end end
undefined

Common 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
undefined
ruby
undefined

app/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
undefined
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
undefined

Lazy 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

调试技巧

  1. Frame not updating? Check frame IDs match exactly
  2. Stream not working? Verify
    Accept
    header includes turbo-stream
  3. Stimulus not firing? Check controller name matches file name
  4. Events not working? Use
    data-action="event->controller#method"
  1. Frame未更新? 检查frame的ID是否完全匹配
  2. Stream不生效? 确认
    Accept
    请求头包含turbo-stream
  3. Stimulus未触发? 检查控制器名称与文件名是否一致
  4. 事件不工作? 使用
    data-action="event->controller#method"
    格式