Class: Phlex::Reactive::Response
- Inherits:
-
Object
- Object
- Phlex::Reactive::Response
- Defined in:
- lib/phlex/reactive/response.rb
Overview
An explicit, immutable description of the ACTOR's HTTP response to a reactive action. An action MAY return one; if it returns anything else (the legacy contract — return value ignored), the endpoint falls back to the implicit single component.to_stream_replace.
A Response governs ONLY the actor's HTTP reply. Cross-tab updates still go through Streamable's broadcast_*_to(..., exclude: reactive_connection_id).
Response.replace(self) # re-render in place (the default, explicit)
Response.replace(self).flash(:error, msg) # surface a validation error
Response.replace(self).also_update("heading", html: @record.name) # + a companion element
Response.remove(self) # drop the element (e.g. moderation queue)
Response.redirect(article_url(@article)) # slug changed -> Turbo.visit the new URL
Response.replace(self).stream(Totals.update(@order)) # multi-stream
Instance Attribute Summary collapse
-
#redirect_url ⇒ Object
readonly
Returns the value of attribute redirect_url.
-
#streams ⇒ Object
readonly
Returns the value of attribute streams.
-
#token_component ⇒ Object
readonly
Returns the value of attribute token_component.
Class Method Summary collapse
-
.collection_append(component, name, model) ⇒ Object
--- Reactive collections (issue #35) --- Add/remove a row in a declared reactive_collection, emitting the row stream + the count companion update + the empty-state toggle as ONE Response.
- .collection_prepend(component, name, model) ⇒ Object
- .collection_remove(component, name, model) ⇒ Object
-
.flash_stream(_level, content, target:) ⇒ Object
Build a flash turbo-stream that appends
contentinto a host-app container. -
.morph(component) ⇒ Object
Re-render the component in place via Idiomorph (issue #28).
-
.redirect(url) ⇒ Object
Client-side full navigation (Turbo.visit).
-
.remove(component) ⇒ Object
Remove the component's element from the DOM.
-
.render_html(content) ⇒ Object
Resolve
contentto the HTML for a turbo-stream'shtml:. -
.replace(component, morph: false) ⇒ Object
Re-render the component in place (explicit form of today's default).
-
.streams(component, *strings) ⇒ Object
Partial / per-field update with a TOKEN-ONLY refresh (issue #30).
-
.update(component) ⇒ Object
Morph only inner HTML (preserves the root element + its token attr).
-
.update_stream(target, content) ⇒ Object
Build a turbo-stream that updates an arbitrary target id with
content(a Phlex component instance or an HTML string). -
.with(*strings) ⇒ Object
Escape hatch / multi-stream root: zero or more raw turbo-stream strings.
Instance Method Summary collapse
-
#also_replace(component, morph: false) ⇒ Object
Like #also_update, but renders ANOTHER Streamable component and replaces it by its own #id — for a companion that is itself a component.
-
#also_update(target, html:) ⇒ Object
Also re-render a COMPANION element alongside self — a page heading, a summary card, a badge that recomputes from the saved value (issue #25).
-
#flash(level, content, target: Phlex::Reactive.flash_target) ⇒ Object
Append a flash turbo-stream into a host-app container (default
, configurable via Phlex::Reactive.flash_target).- #initialize(streams: [], redirect_url: nil, render_self: true, token_component: nil) ⇒ Response constructor
render_self: when true (default for replace/update/with), the endpoint GUARANTEES the component's own replace is present so its data-reactive-token-value refreshes (the client extracts the next token from the response HTML).
- #redirect? ⇒ Boolean
- #refresh_token? ⇒ Boolean
True when a partial update (.streams) opted out of the full-self replace but still needs the token rolled forward — the endpoint appends the bound component's tiny token-only stream (issue #30).
- #render_self? ⇒ Boolean
- #stream(*more) ⇒ Object
Append extra turbo-stream strings (a sibling component, a flash).
Constructor Details
#initialize(streams: [], redirect_url: nil, render_self: true, token_component: nil) ⇒ Response
render_self: when true (default for replace/update/with), the endpoint GUARANTEES the component's own replace is present so its data-reactive-token-value refreshes (the client extracts the next token from the response HTML). remove/redirect set it false (nothing stays).
token_component: set by .streams (issue #30) — a partial update that opts OUT of the full-self replace but still needs the token refreshed. The endpoint appends this component's tiny to_stream_token instead, so the token rolls forward without re-rendering (and clobbering) the children.
207 208 209 210 211 212 213
# File 'lib/phlex/reactive/response.rb', line 207 def initialize(streams: [], redirect_url: nil, render_self: true, token_component: nil) @streams = streams.freeze @redirect_url = redirect_url @render_self = render_self @token_component = token_component freeze end
Instance Attribute Details
#redirect_url ⇒ Object (readonly)
Returns the value of attribute redirect_url.
20 21 22
# File 'lib/phlex/reactive/response.rb', line 20 def redirect_url @redirect_url end
#streams ⇒ Object (readonly)
Returns the value of attribute streams.
20 21 22
# File 'lib/phlex/reactive/response.rb', line 20 def streams @streams end
#token_component ⇒ Object (readonly)
Returns the value of attribute token_component.
20 21 22
# File 'lib/phlex/reactive/response.rb', line 20 def token_component @token_component end
Class Method Details
.collection_append(component, name, model) ⇒ Object
--- Reactive collections (issue #35) --- Add/remove a row in a declared reactive_collection, emitting the row stream + the count companion update + the empty-state toggle as ONE Response.
componentis the bound CONTAINER (it carries the declaration and the size resolver);modelis the row's record (or, for remove, a model OR a dom-id string). count/empty/size are optional — a stream is emitted only for the pieces declared.render_self is false: the row append/prepend/remove IS the update, so we must NOT also replace the whole container (that would re-render every row and clobber the just-streamed delta).
token_component is the CONTAINER (cosmos#1939): a reply that does NOT re-render self must STILL refresh self's signed token, or the list is add-once-only — correct on the first click, then every subsequent dispatch from the list root is rejected (its token went stale) with no error. The container owns the add/remove trigger, so the endpoint appends its inert
reactive:tokenstream (the same #30 machinery reply.streams uses) to roll the token forward without re-rendering the rows.73 74 75 76 77
# File 'lib/phlex/reactive/response.rb', line 73 def collection_append(component, name, model) definition = collection_def!(component, name) new(streams: collection_add_streams(definition, component, model, :append), render_self: false, token_component: component) end
.collection_prepend(component, name, model) ⇒ Object
79 80 81 82 83
# File 'lib/phlex/reactive/response.rb', line 79 def collection_prepend(component, name, model) definition = collection_def!(component, name) new(streams: collection_add_streams(definition, component, model, :prepend), render_self: false, token_component: component) end
.collection_remove(component, name, model) ⇒ Object
85 86 87 88 89
# File 'lib/phlex/reactive/response.rb', line 85 def collection_remove(component, name, model) definition = collection_def!(component, name) new(streams: collection_remove_streams(definition, component, model), render_self: false, token_component: component) end
.flash_stream(_level, content, target:) ⇒ Object
Build a flash turbo-stream that appends
contentinto a host-app container.contentis a Phlex component instance (rendered through the configured renderer so t()/url_for work) or a ready HTML string — supplied by the caller because the render context is off-request (there is no Railsflash).173 174 175
# File 'lib/phlex/reactive/response.rb', line 173 def flash_stream(_level, content, target:) Phlex::Reactive.flash_builder.append(target, html: render_html(content)) end
.morph(component) ⇒ Object
Re-render the component in place via Idiomorph (issue #28). Emits
<turbo-stream action="replace" method="morph">, so Turbo 8 morphs the subtree — the focused + caret survive the save. Use this for per-field reactive editing (a "spreadsheet" grid where a debounced save fires while the user is still typing/tabbing). The morphed root still carries the fresh signed token, so the next action verifies.36
# File 'lib/phlex/reactive/response.rb', line 36 def morph(component) = new(streams: [component.to_stream_morph])
.redirect(url) ⇒ Object
Client-side full navigation (Turbo.visit). Use when the current URL is dead (slug rename) or the outcome belongs on another page. Pass a *_url (the off-request render context has no request host for *_path).
49
# File 'lib/phlex/reactive/response.rb', line 49 def redirect(url) = new(redirect_url: url, render_self: false)
.remove(component) ⇒ Object
Remove the component's element from the DOM. Uses the instance to_stream_remove (the component already knows its own #id — no class-builder reconstruction; works for record- and state-backed).
44
# File 'lib/phlex/reactive/response.rb', line 44 def remove(component) = new(streams: [component.to_stream_remove], render_self: false)
.render_html(content) ⇒ Object
Resolve
contentto the HTML for a turbo-stream'shtml:. Two forms, both SAFE against injection by default:* a Phlex component instance — rendered through the configured renderer, which auto-escapes interpolated values. * any other value — coerced with to_s and handed to Turbo's TagBuilder, which HTML-ESCAPES a plain String. So a model value (`html: @record.name`) cannot inject markup. To emit intentional raw HTML, pass an `html_safe` String (Turbo leaves those verbatim) or a Phlex component. Same contract as the pre-existing flash_stream.193 194 195
# File 'lib/phlex/reactive/response.rb', line 193 def render_html(content) content.is_a?(::Phlex::SGML) ? Phlex::Reactive.render(content) : content.to_s end
.replace(component, morph: false) ⇒ Object
Re-render the component in place (explicit form of today's default).
morph: truemorphs the subtree (preserves the focused input + caret) instead of an outerHTML swap — see .morph (issue #28).26 27 28
# File 'lib/phlex/reactive/response.rb', line 26 def replace(component, morph: false) new(streams: [morph ? component.to_stream_morph : component.to_stream_replace]) end
.streams(component, *strings) ⇒ Object
Partial / per-field update with a TOKEN-ONLY refresh (issue #30). Emits EXACTLY the given streams — no forced full-self replace — but binds
componentso the endpoint appends its tinyto_stream_tokenstream. So the signed token rolls forward (the next action verifies) while the component's own live inputs are never torn down: ideal for a spreadsheet-like grid where a debounced save re-streams only a total cell and the user is still typing in a sibling field.Response.streams(self, Totals.update(@invoice)) # update only the totalsrender_self is false (we do NOT inject the full replace); the token is refreshed by the bound component's token stream instead.
164 165 166
# File 'lib/phlex/reactive/response.rb', line 164 def streams(component, *strings) new(streams: strings.flatten, render_self: false, token_component: component) end
.update(component) ⇒ Object
Morph only inner HTML (preserves the root element + its token attr).
39
# File 'lib/phlex/reactive/response.rb', line 39 def update(component) = new(streams: [component.to_stream_update])
.update_stream(target, content) ⇒ Object
Build a turbo-stream that updates an arbitrary target id with
content(a Phlex component instance or an HTML string). Used by #also_update to re-render a companion element that isn't itself a Streamable component.180 181 182
# File 'lib/phlex/reactive/response.rb', line 180 def update_stream(target, content) Phlex::Reactive.flash_builder.update(target, html: render_html(content)) end
.with(*strings) ⇒ Object
Escape hatch / multi-stream root: zero or more raw turbo-stream strings.
52
# File 'lib/phlex/reactive/response.rb', line 52 def with(*strings) = new(streams: strings.flatten)
Instance Method Details
#also_replace(component, morph: false) ⇒ Object
Like #also_update, but renders ANOTHER Streamable component and replaces it by its own #id — for a companion that is itself a component. Response.replace(self).also_replace(SummaryCard.new(order: @order))
morph: truemorphs the companion in place (issue #28) — use it when the companion also holds focusable inputs that must survive the re-render.251 252 253
# File 'lib/phlex/reactive/response.rb', line 251 def also_replace(component, morph: false) stream(morph ? component.to_stream_morph : component.to_stream_replace) end
#also_update(target, html:) ⇒ Object
Also re-render a COMPANION element alongside self — a page heading, a summary card, a badge that recomputes from the saved value (issue #25).
targetis the sibling element's DOM id.htmlis either:* a plain String — HTML-ESCAPED by Turbo, so a model value is safe: Response.replace(self).also_update("page_heading", html: @record.name) * a Phlex component — rendered + auto-escaped through the renderer (use this when the companion has its own markup), or an `html_safe` String for intentional raw HTML.Returns a NEW Response (immutable). The common "re-render self + N siblings" case no longer needs raw turbo_stream_builder.
242 243 244
# File 'lib/phlex/reactive/response.rb', line 242 def also_update(target, html:) stream(self.class.update_stream(target, html)) end
#flash(level, content, target: Phlex::Reactive.flash_target) ⇒ Object
Append a flash turbo-stream into a host-app container (default
, configurable via Phlex::Reactive.flash_target).228 229 230
# File 'lib/phlex/reactive/response.rb', line 228 def flash(level, content, target: Phlex::Reactive.flash_target) stream(self.class.flash_stream(level, content, target:)) end
#redirect? ⇒ Boolean
255
# File 'lib/phlex/reactive/response.rb', line 255 def redirect? = !@redirect_url.nil?
#refresh_token? ⇒ Boolean
True when a partial update (.streams) opted out of the full-self replace but still needs the token rolled forward — the endpoint appends the bound component's tiny token-only stream (issue #30).
261
# File 'lib/phlex/reactive/response.rb', line 261 def refresh_token? = !@token_component.nil?
#render_self? ⇒ Boolean
256
# File 'lib/phlex/reactive/response.rb', line 256 def render_self? = @render_self
#stream(*more) ⇒ Object
Append extra turbo-stream strings (a sibling component, a flash). Returns a NEW Response (immutable).
217 218 219 220 221 222 223 224
# File 'lib/phlex/reactive/response.rb', line 217 def stream(*more) self.class.new( streams: @streams + more.flatten, redirect_url: @redirect_url, render_self: @render_self, token_component: @token_component ) end
- #initialize(streams: [], redirect_url: nil, render_self: true, token_component: nil) ⇒ Response constructor