Here is one solution: morphdom + custom turbo stream action:
# 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 %>
Handle custom morph action on the front end:
// 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
Update
For custom actions there is action
method, it works in templates and controllers:
https://github.com/hotwired/turbo-rails/blob/v1.4.0/app/models/turbo/streams/tag_builder.rb#L214
<%= 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"} %>
To make it a method, add it to turbo tag builder:
# config/initializers/turbo_stream_actions.rb
module TurboStreamActions
def morph(...)
action(:morph, ...)
end
Turbo::Streams::TagBuilder.prepend(self)
end
Now you can do this:
<%= turbo_stream.morph :target_id, partial: "two", locals: {name: "three"} %>
turbo_stream.turbo_stream_action_tag :morph, template: render('generating_status'), target: 'generating_status'
is there a better way for calling custom actions in erb files? – Distracted