Module: Phlex::Reactive::Streamable
- Extended by:
- ActiveSupport::Concern
- Defined in:
- lib/phlex/reactive/streamable.rb
Overview
Streamable gives a self-contained Phlex component the ability to render
ITSELF as a Turbo Stream and to broadcast itself over a stream. Every
streamable component must implement #id returning a stable DOM id —
that id is the Turbo Stream target, so you never hand-pick targets.
Class methods (use in controllers):
render turbo_stream: Counter.replace(counter)
render turbo_stream: [Row.append(target: "items", model: @item),
Totals.update(@order)]
Broadcast methods (use in models/jobs/actions):
Counter.broadcast_replace_to(counter, model: counter)
Row.broadcast_append_to(@list, target: "items", model: @item)
Convention: the id you set on the root element in view_template must
equal what #id returns, so replace/broadcast_replace target it.
NOTE: we intentionally do NOT include Turbo::Streams::ActionHelper — it
pulls in ActionView::Helpers::TagHelper, which overrides Phlex's internal
tag method and breaks rendering. We use Turbo::Streams::TagBuilder
directly instead.
Defined Under Namespace
Classes: ThreadViewContext
Class Method Summary collapse
- .register(klass) ⇒ Object
-
.reset_all_view_contexts! ⇒ Object
Reset every streamable class's cached view context + builder.
Instance Method Summary collapse
-
#dom_id(record, prefix = nil) ⇒ Object
Render-context-free dom_id, safe to use inside
#id. -
#id ⇒ Object
Required: the stable DOM id used as the Turbo Stream target.
-
#to_stream_morph ⇒ Object
Render THIS instance as a MORPHING replace (issue #28):
<turbo-stream action="replace" method="morph">. -
#to_stream_remove ⇒ Object
Render THIS instance as a remove stream.
-
#to_stream_replace ⇒ Object
Render THIS already-built instance as a replace stream (used by the reactive action endpoint after an action mutated state).
-
#to_stream_token ⇒ Object
Render a TOKEN-ONLY refresh stream (issue #30): a tiny
<turbo-stream action="reactive:token">carrying the component's fresh signed token, with NO rendered body. - #to_stream_update ⇒ Object
Class Method Details
.register(klass) ⇒ Object
55 56 57 |
# File 'lib/phlex/reactive/streamable.rb', line 55 def register(klass) @registry_mutex.synchronize { @registry[klass] = true } end |
.reset_all_view_contexts! ⇒ Object
Reset every streamable class's cached view context + builder. Called from the engine's reloader (config.to_prepare) so a reloaded renderer controller is never served from a stale memo. No-op outside Rails.
49 50 51 52 53 |
# File 'lib/phlex/reactive/streamable.rb', line 49 def reset_all_view_contexts! @registry_mutex.synchronize do @registry.each_key(&:reset_turbo_view_context!) end end |
Instance Method Details
#dom_id(record, prefix = nil) ⇒ Object
Render-context-free dom_id, safe to use inside #id. The streamable
machinery calls #id BEFORE rendering, so Phlex's render-time dom_id
helper would raise HelpersCalledBeforeRenderError. This delegates to
ActionView::RecordIdentifier, which works anywhere — so
def id = dom_id(@todo) is safe.
290 291 292 |
# File 'lib/phlex/reactive/streamable.rb', line 290 def dom_id(record, prefix = nil) ::ActionView::RecordIdentifier.dom_id(record, prefix) end |
#id ⇒ Object
Required: the stable DOM id used as the Turbo Stream target. It MUST
match the id set on the component's root element in view_template.
281 282 283 |
# File 'lib/phlex/reactive/streamable.rb', line 281 def id raise NotImplementedError, "#{self.class} must implement #id for Turbo Stream targeting" end |
#to_stream_morph ⇒ Object
Render THIS instance as a MORPHING replace (issue #28):
<turbo-stream action="replace" method="morph">. Turbo 8's bundled
Idiomorph morphs the subtree in place — preserving the focused +
caret across the re-render — while still carrying the root's fresh
data-reactive-token-value (so the signed token refreshes). Used by
Response.morph / Response.replace(self, morph: true).
306 307 308 |
# File 'lib/phlex/reactive/streamable.rb', line 306 def to_stream_morph self.class.turbo_stream_builder.replace(id, html: self.class.render_component(self), method: :morph) end |
#to_stream_remove ⇒ Object
Render THIS instance as a remove stream. The component already knows its own #id, so no record/class reconstruction is needed (works for record- and state-backed components alike). Used by Response.remove.
343 344 345 |
# File 'lib/phlex/reactive/streamable.rb', line 343 def to_stream_remove self.class.turbo_stream_builder.remove(id) end |
#to_stream_replace ⇒ Object
Render THIS already-built instance as a replace stream (used by the reactive action endpoint after an action mutated state).
296 297 298 |
# File 'lib/phlex/reactive/streamable.rb', line 296 def to_stream_replace self.class.turbo_stream_builder.replace(id, html: self.class.render_component(self)) end |
#to_stream_token ⇒ Object
Render a TOKEN-ONLY refresh stream (issue #30): a tiny
<turbo-stream action="reactive:token"> carrying the component's fresh
signed token, with NO rendered body. It lets an action update only PART
of a component (its own hand-built streams) while still rolling the
signed identity token forward — the client reads the next token from this
attribute (#extractToken) and an inert client action writes it onto the
root (a pure attribute set, so a focused + caret survive). Unlike
to_stream_replace, it does NOT re-render the children, so a live input
the user is typing into is never torn down. Used by Response.streams.
The component carries its token via Component#reactive_token; a Streamable that isn't a Component (no token) simply has nothing to refresh — guarded by respond_to? so the primitive stays usable on a bare Streamable.
respond_to? MUST include private methods (the true arg): Component
defines reactive_token as PRIVATE, so a plain respond_to?(:reactive_token)
is false for every Component and the stream silently carries an EMPTY token —
which makes any non-self-rendering reply (reply.streams #30, reply.append /
reply.remove #35) add-once-only: the first action works, then the stale (here
empty) token is rejected on the next dispatch (cosmos#1939). A bare Streamable
has no reactive_token method at all, so it still returns false correctly.
335 336 337 338 |
# File 'lib/phlex/reactive/streamable.rb', line 335 def to_stream_token token = respond_to?(:reactive_token, true) ? reactive_token : nil %(<turbo-stream action="reactive:token" target="#{ERB::Util.html_escape(id)}" data-reactive-token-value="#{ERB::Util.html_escape(token)}"></turbo-stream>) end |
#to_stream_update ⇒ Object
310 311 312 |
# File 'lib/phlex/reactive/streamable.rb', line 310 def to_stream_update self.class.turbo_stream_builder.update(id, html: self.class.render_component(self)) end |