我有一个简单的物品清单。每个项目都有经常变化的属性(例如每秒)。
我使用
turbo_stream_from "list_items"
以保持数据新鲜。
我想在每个 list_item 旁边添加一个复选框,以便能够例如从列表中删除特定项目。
当用户检查/选择特定项目时,以及之后通过 turbo 流接收到新更改时 -> 选择丢失,因为该项目的 HTML 已被完全替换。
有没有办法通过“Rails 标准”来处理这个问题?考虑 React/Vue(具有虚拟 DOM 的库)是否更好?
这是一个解决方案:morphdom + 自定义涡轮流操作:
# app/models/item.rb
class Item < ApplicationRecord
after_create_commit -> { broadcast_append_to(:list_items) }
after_destroy_commit -> { broadcast_remove_to(:list_items) }
# send a custom turbo stream action, instead of `update`
after_update_commit -> { broadcast_action_to(:list_items, action: :morph, target: self, **broadcast_rendering_with_defaults({})) }
# `Item.play` from a console to send some test streams
def self.play
Item.destroy_all
10.times { Item.create(name: "") }
item_ids = Item.ids
count = 0
loop do
item = Item.find(item_ids.sample)
item.update(name: item.name + ("a".."z").to_a.sample)
Item.where("length(name) > 15").update_all(name: "")
count += 1
Turbo::StreamsChannel.broadcast_prepend_to(:list_items,
target: :items,
html: %(<div id="counter">#{count}</div>)
)
end
end
end
# app/views/items/index.html.erb
<%= turbo_stream_from :list_items %>
<%= tag.div id: :items, class: "grid gap-2" do %>
<%= render @items %>
<% end %>
# app/views/items/_item.html.erb
<%= tag.div id: dom_id(item), class: "flex gap-4" do %>
<%= check_box field_name(:item, item.id), :_destroy %>
<%= link_to item.name, item %>
<% end %>
在前端处理自定义morph动作:
// config/importmap.rb
pin "morphdom", to: "https://cdn.jsdelivr.net/npm/[email protected]/dist/morphdom-esm.js"
// app/javascript/application.js
import morphdom from "morphdom";
// this function is called when `turbo_stream.action(:morph, ...)` renders
// on the front end.
Turbo.StreamActions.morph = function () {
this.targetElements.forEach((target) => {
morphdom(target, this.templateContent, {
// this is the magic bit
onBeforeElUpdated: function (fromElement, toElement) {
if (fromElement.isEqualNode(toElement)) return false;
return true;
},
});
});
};
https://github.com/patrick-steele-idem/morphdom
https://turbo.hotwired.dev/handbook/streams#custom-actions
更新
对于自定义操作有
action
方法,它适用于模板和控制器:<%= tag.div "one", id: :target_id %>
<%= turbo_stream.action :morph, :target_id, tag.div("two") %>
<%= turbo_stream.action :morph, :target_id, partial: "two", locals: {name: "two"} %>
要使其成为一种方法,请将其添加到 turbo 标签生成器中:
# config/initializers/turbo_stream_actions.rb
module TurboStreamActions
def morph(...)
action(:morph, ...)
end
Turbo::Streams::TagBuilder.prepend(self)
end
现在你可以这样做:
<%= turbo_stream.morph :target_id, partial: "two", locals: {name: "three"} %>