Turbo
Turbo Stream actions
turbo_stream.toast and turbo_stream.stream_markdown, and an upsert action that merges append and replace.
The engine adds two actions to turbo-rails' turbo_stream builder when turbo-rails loads, so an
app without Turbo never sees them. The JavaScript adds a third action to Turbo itself.
turbo_stream.toast
Pops a toast from a stream response, for something that happened outside the flash cycle, where there's
no redirect or render to carry a flash. It appends to the flash_toasts mount, so the layout
needs one.
render turbo_stream: turbo_stream.toast("Invitation sent.")
render turbo_stream: turbo_stream.toast("Couldn\'t reach Slack.", tone: :bad, duration: 0)
It takes tone: (:good by default, then :warn, :bad,
:info, :neutral, :accent, :inverted), title:,
icon:, duration: in milliseconds (0 waits to be dismissed),
position:, width: and layout:. A block yields a builder for actions,
a leading slot and a body:
<%= turbo_stream.toast "Your export is ready.", title: "Reports" do |toast| %>
<% toast.actions do %>
<%= link_to "Download", export_path(@export), class: button_classes(size: :small) %>
<% end %>
<% end %>turbo_stream.stream_markdown
Hands a reply rendered so far to the streaming_markdown_tag with that id, which paces its way
toward it. Send the whole render every time, not the new part: the element works out what's new.
turbo_stream.stream_markdown "message_1_content", Markdown.render(message.content)
# From a model, through Turbo::Broadcastable
broadcast_action_to chat, action: :stream_markdown, target: "#{dom_id(self)}_content",
html: Markdown.render(content)Needs import "unmagic/components/streaming_markdown".
upsert
A stream action that merges append and replace. Its template holds one element
with a stable id. If that id is already on the page (rendered on load, streamed earlier, or drawn
optimistically) the element is replaced in place, so a record that's both on the page and broadcast never
shows twice. Otherwise it's inserted into the target at the position its id sorts to, so messages that
arrive out of order still land in order.
render turbo_stream: turbo_stream.action(:upsert, "task_rows", partial: "tasks/row", locals: { task: @task })
# Newest first: the order flips
broadcast_action_to "tasks", action: :upsert, target: "task_rows",
attributes: { order: "desc" }, partial: "tasks/row", locals: { task: self }
The ordering assumes time-ordered ids, such as UUIDv7 or ULID. With random ids the position is stable but
arbitrary. Needs import "unmagic/components/upsert", which "unmagic/components"
includes.