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.

Instance Method Summary collapse

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.



199
200
201
# File 'lib/phlex/reactive/streamable.rb', line 199

def dom_id(record, prefix = nil)
  ::ActionView::RecordIdentifier.dom_id(record, prefix)
end

#idObject

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`.

Raises:

  • (NotImplementedError)


190
191
192
# File 'lib/phlex/reactive/streamable.rb', line 190

def id
  raise NotImplementedError, "#{self.class} must implement #id for Turbo Stream targeting"
end

#to_stream_morphObject

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 <input> + 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).



215
216
217
# File 'lib/phlex/reactive/streamable.rb', line 215

def to_stream_morph
  self.class.turbo_stream_builder.replace(id, html: self.class.render_component(self), method: :morph)
end

#to_stream_removeObject

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.



244
245
246
# File 'lib/phlex/reactive/streamable.rb', line 244

def to_stream_remove
  self.class.turbo_stream_builder.remove(id)
end

#to_stream_replaceObject

Render THIS already-built instance as a replace stream (used by the reactive action endpoint after an action mutated state).



205
206
207
# File 'lib/phlex/reactive/streamable.rb', line 205

def to_stream_replace
  self.class.turbo_stream_builder.replace(id, html: self.class.render_component(self))
end

#to_stream_tokenObject

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 <input> + 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.



236
237
238
239
# File 'lib/phlex/reactive/streamable.rb', line 236

def to_stream_token
  token = respond_to?(:reactive_token) ? 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_updateObject



219
220
221
# File 'lib/phlex/reactive/streamable.rb', line 219

def to_stream_update
  self.class.turbo_stream_builder.update(id, html: self.class.render_component(self))
end