TurboFrame 与 Stimulus 搜索表单无法执行搜索
结论
这段代码需要检查三个环节:
- Stimulus 控制器是否已被浏览器加载并连接。如果 TypeScript 没有编译、构建进程未运行,或
form-request-submit-polyfill无法解析,submit()就不会执行。 - Turbo Frame 响应中是否包含
id="search-results"。即使请求成功,响应中没有同名 frame,Turbo 也无法更新页面。 - Controller 是否执行了搜索。当前的
Concert.all会返回全部记录,只设置@query不会筛选结果。
input->search#submit 配合 requestSubmit() 的写法本身没有明显问题。可以先确认 Stimulus 是否连接,再看请求有没有发出,然后检查 Turbo 响应,暂时不用先查数据库查询。
确认 Stimulus 控制器是否连接
先在控制器中临时加入 connect() 和事件日志:
import { Controller } from "@hotwired/stimulus"
import "form-request-submit-polyfill"
export default class extends Controller {
static targets = ["form", "input"]
declare readonly formTarget: HTMLFormElement
declare readonly inputTarget: HTMLInputElement
connect(): void {
console.log("search controller connected")
}
submit(event: Event): void {
console.log("search submit", event)
this.formTarget.requestSubmit()
}
}
刷新页面后,Console 中应立即出现:
search controller connected
输入文字时还应出现:
search submit
如果第一条日志都没有出现,问题就在 JavaScript 加载阶段,与 Rails controller 或 Turbo Frame 无关。
此时检查以下内容:
- 最终生成的 HTML 中有没有
data-controller="search"。 - 入口文件是否导入了
app/javascript/controllers/index.js。 - JavaScript 构建进程是否正在运行。
- 构建终端中有没有 TypeScript 或模块解析错误。
- 是否已经安装
form-request-submit-polyfill。 - 浏览器加载的是不是最新的构建产物。
如果项目使用 jsbundling-rails,开发时通常需要启动项目配置的构建监听进程,例如:
bin/dev
具体命令取决于项目使用 esbuild、Webpack、Rollup 还是其他构建方案。如果只运行 bin/rails server,.ts 文件不一定会持续编译。
导入控制器时,可以让构建工具自行解析扩展名:
import { application } from "./application"
import SearchController from "./search_controller"
application.register("search", SearchController)
如果项目没有配置 TypeScript 编译,可以把文件改为 search_controller.js,同时去掉类型声明。浏览器无法直接执行下面这种 TypeScript 语法:
formTarget: HTMLFormElement
如果目标浏览器较新,也可以先移除 polyfill,看看控制器能否正常连接:
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["form"]
declare readonly formTarget: HTMLFormElement
submit(): void {
this.formTarget.requestSubmit()
}
}
如果移除 import 后控制器开始连接,问题出在 form-request-submit-polyfill 的安装或模块解析上。
检查最终生成的 HTML
除了 ERB 源码,还要在浏览器开发者工具的 Elements 面板中查看实际生成的 HTML。结构应接近下面这样:
<div data-controller="search">
<form
data-search-target="form"
data-action="input->search#submit"
data-turbo-frame="search-results"
>
<input
name="query"
type="search"
data-search-target="input"
>
</form>
<turbo-frame id="search-results"></turbo-frame>
</div>
ERB 中的 data 属性也可以改成较常见的 Rails 写法,减少属性名拼写错误:
<%= turbo_frame_tag "search-form" do %>
<div data-controller="search">
<%= form_with(
url: concerts_path,
method: :get,
data: {
turbo_frame: "search-results",
search_target: "form",
action: "input->search#submit"
}
) do %>
<div class="flex justify-center">
<div class="w-4/5">
<%= text_field_tag(
:query,
params[:query],
type: "search",
placeholder: "Search concerts",
id: "search_query",
data: { search_target: "input" },
class: "w-full px-3 py-2 border border-gray-400 rounded-lg"
) %>
</div>
</div>
<% end %>
<%= turbo_frame_tag "search-results" %>
</div>
<% end %>
input 事件会冒泡,因此可以把 data-action 放在 <form> 上。排查时也可以暂时把它放到输入框上,以便直接确认事件来源:
data: {
search_target: "input",
action: "input->search#submit"
}
用 Network 面板定位问题
打开浏览器开发者工具的 Network 面板,然后输入搜索词。正常情况下会出现类似请求:
GET /concerts?query=rock
可以根据请求情况判断问题所在:
- 没有请求:Stimulus 没有连接、事件没有触发,或 JavaScript 没有编译加载。
- 请求已经发出,但返回 4xx/5xx:检查 Rails 日志和响应正文。
- 请求返回 200,但页面没有更新:检查响应中有没有
<turbo-frame id="search-results">。 - 页面更新了,但记录没有筛选:为 controller 补上搜索条件。
- 结果能够显示,但高亮时报错:检查查询词是否被直接当作正则表达式使用。
也可以在 Console 中直接测试表单提交:
document.querySelector('[data-search-target="form"]').requestSubmit()
如果这条命令能够发出请求,说明表单和 Turbo 基本正常,问题多半在 Stimulus 的加载或事件绑定上。
Turbo 响应必须包含同名 Frame
表单指定了:
data-turbo-frame="search-results"
所以 /concerts 返回的 HTML 中必须包含:
<turbo-frame id="search-results">
...
</turbo-frame>
例如,可以在 app/views/concerts/index.html.erb 中这样写:
<%= turbo_frame_tag "search-results" do %>
<%= render(
partial: "search_result",
collection: @concerts,
as: :concert,
locals: { query: @query }
) %>
<% end %>
如果只返回 _search_result.html.erb 中的 <article>,Turbo 无法在响应中找到目标 frame。
最终页面中也不能同时出现两个 id="search-results"。初始页面可以放一个空 frame,搜索响应再返回一个同名 frame,Turbo 会用响应中的内容替换当前 frame。
Controller 要执行实际筛选
当前代码是:
def index
@query = params[:query]
@concerts = Concert.all
end
这段代码读取了参数,却没有用它筛选记录。简单的名称搜索可以写成:
def index
@query = params[:query].to_s.strip
@concerts = Concert.all
if @query.present?
pattern = "%#{ActiveRecord::Base.sanitize_sql_like(@query)}%"
@concerts = @concerts.where("name LIKE ?", pattern)
end
end
LIKE 是否区分大小写取决于数据库及其配置。PostgreSQL 项目如果需要忽略大小写,可以根据项目情况使用 ILIKE:
@concerts = @concerts.where("name ILIKE ?", pattern)
如果还要搜索乐队、场馆或标签,需要根据实际关联和表结构添加 joins、查询条件及必要的 distinct。仅凭现有代码无法确定完整 SQL。
高亮前转义搜索词
下面的写法会把用户输入直接插入正则表达式:
<%= highlight(concert.name, /#{query}/i) %>
用户输入 [、(、* 等字符时,生成的正则表达式可能无效,也可能改变原本的匹配含义。应先对搜索词进行转义:
<% if query.present? %>
<% pattern = Regexp.new(Regexp.escape(query), Regexp::IGNORECASE) %>
<%= highlight(concert.name, pattern) %>
<% else %>
<%= concert.name %>
<% end %>
乐队名称也要使用同一个经过转义的 pattern:
<% band_names = concert.bands.map(&:name).join(", ") %>
<% if query.present? %>
<%= highlight(band_names, pattern) %>
<% else %>
<%= band_names %>
<% end %>
排查顺序
可以按以下顺序逐项检查,每一步只验证一个环节:
- 在
connect()中输出日志,确认 Stimulus 控制器已经连接。 - 在
submit()中输出日志,确认input事件已经触发。 - 在 Network 面板中确认
/concerts?query=...请求已经发出。 - 查看请求参数中是否包含
query。 - 检查响应状态,并确认 Rails controller 是否执行。
- 查看响应 HTML 中是否有
<turbo-frame id="search-results">。 - 确认 controller 使用
@query筛选了数据。 - 最后再处理高亮、关联查询和输入防抖等细节。
如果页面完全没有反应,Rails 也没有收到请求,应先检查 JavaScript 构建进程和 Stimulus 控制器的连接状态。Rails 没有日志,通常是因为请求根本没有离开浏览器,但后端搜索代码本身也仍需按前面的方式检查。
备注:内容仅供参考。
