Class: Phlex::Reactive::Response

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

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.



109
110
111
112
113
114
115
# File 'lib/phlex/reactive/response.rb', line 109

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_urlObject (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

#streamsObject (readonly)

Returns the value of attribute streams.



20
21
22
# File 'lib/phlex/reactive/response.rb', line 20

def streams
  @streams
end

#token_componentObject (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

.flash_stream(_level, content, target:) ⇒ Object

Build a flash turbo-stream that appends ‘content` into a host-app container. `content` is 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 Rails `flash`).



75
76
77
# File 'lib/phlex/reactive/response.rb', line 75

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 <input> + 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 ‘content` to the HTML for a turbo-stream’s ‘html:`. 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.


95
96
97
# File 'lib/phlex/reactive/response.rb', line 95

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: true` morphs 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 ‘component` so the endpoint appends its tiny `to_stream_token` stream. 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 totals

render_self is false (we do NOT inject the full replace); the token is refreshed by the bound component’s token stream instead.



66
67
68
# File 'lib/phlex/reactive/response.rb', line 66

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.



82
83
84
# File 'lib/phlex/reactive/response.rb', line 82

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: true` morphs the companion in place (issue #28) — use it when the companion also holds focusable inputs that must survive the re-render.



153
154
155
# File 'lib/phlex/reactive/response.rb', line 153

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). ‘target` is the sibling element’s DOM id. ‘html` is 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.



144
145
146
# File 'lib/phlex/reactive/response.rb', line 144

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 <div id=“flash”>, configurable via Phlex::Reactive.flash_target).



130
131
132
# File 'lib/phlex/reactive/response.rb', line 130

def flash(level, content, target: Phlex::Reactive.flash_target)
  stream(self.class.flash_stream(level, content, target:))
end

#redirect?Boolean

Returns:

  • (Boolean)


157
# File 'lib/phlex/reactive/response.rb', line 157

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

Returns:

  • (Boolean)


163
# File 'lib/phlex/reactive/response.rb', line 163

def refresh_token? = !@token_component.nil?

#render_self?Boolean

Returns:

  • (Boolean)


158
# File 'lib/phlex/reactive/response.rb', line 158

def render_self? = @render_self

#stream(*more) ⇒ Object

Append extra turbo-stream strings (a sibling component, a flash). Returns a NEW Response (immutable).



119
120
121
122
123
124
125
126
# File 'lib/phlex/reactive/response.rb', line 119

def stream(*more)
  self.class.new(
    streams: @streams + more.flatten,
    redirect_url: @redirect_url,
    render_self: @render_self,
    token_component: @token_component
  )
end