phlex-reactive 0.4.8 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +868 -3
- data/README.md +986 -32
- data/app/controllers/phlex/reactive/actions_controller.rb +282 -190
- data/app/javascript/phlex/reactive/compute.js +52 -7
- data/app/javascript/phlex/reactive/compute.min.js +4 -0
- data/app/javascript/phlex/reactive/compute.min.js.map +10 -0
- data/app/javascript/phlex/reactive/confirm.min.js +4 -0
- data/app/javascript/phlex/reactive/confirm.min.js.map +10 -0
- data/app/javascript/phlex/reactive/reactive_controller.js +1487 -80
- data/app/javascript/phlex/reactive/reactive_controller.min.js +4 -0
- data/app/javascript/phlex/reactive/reactive_controller.min.js.map +10 -0
- data/lib/generators/phlex/reactive/component/USAGE +2 -1
- data/lib/generators/phlex/reactive/component/templates/component.rb.erb +1 -2
- data/lib/generators/phlex/reactive/component/templates/component_spec.rb.erb +33 -2
- data/lib/generators/phlex/reactive/install/install_generator.rb +9 -3
- data/lib/generators/phlex/reactive/install/templates/phlex_reactive.rb.erb +37 -0
- data/lib/phlex/reactive/component/dsl.rb +249 -0
- data/lib/phlex/reactive/component/helpers.rb +595 -0
- data/lib/phlex/reactive/component/identity.rb +92 -0
- data/lib/phlex/reactive/component/registry.rb +200 -0
- data/lib/phlex/reactive/component.rb +30 -442
- data/lib/phlex/reactive/doctor.rb +333 -0
- data/lib/phlex/reactive/engine.rb +27 -9
- data/lib/phlex/reactive/js.rb +222 -0
- data/lib/phlex/reactive/log_subscriber.rb +64 -0
- data/lib/phlex/reactive/param_schema.rb +390 -0
- data/lib/phlex/reactive/reply.rb +5 -3
- data/lib/phlex/reactive/response.rb +156 -16
- data/lib/phlex/reactive/stream.rb +98 -0
- data/lib/phlex/reactive/streamable.rb +307 -43
- data/lib/phlex/reactive/test_helpers/matchers.rb +112 -0
- data/lib/phlex/reactive/test_helpers.rb +308 -0
- data/lib/phlex/reactive/version.rb +1 -1
- data/lib/phlex/reactive.rb +329 -14
- data/lib/tasks/phlex_reactive.rake +14 -0
- metadata +19 -1
|
@@ -17,14 +17,15 @@ module Phlex
|
|
|
17
17
|
# Response.redirect(article_url(@article)) # slug changed -> Turbo.visit the new URL
|
|
18
18
|
# Response.replace(self).stream(Totals.update(@order)) # multi-stream
|
|
19
19
|
class Response
|
|
20
|
-
attr_reader :streams, :redirect_url, :token_component
|
|
20
|
+
attr_reader :streams, :redirect_url, :token_component, :subject_component
|
|
21
21
|
|
|
22
22
|
class << self
|
|
23
23
|
# Re-render the component in place (explicit form of today's default).
|
|
24
24
|
# `morph: true` morphs the subtree (preserves the focused input + caret)
|
|
25
25
|
# instead of an outerHTML swap — see .morph (issue #28).
|
|
26
26
|
def replace(component, morph: false)
|
|
27
|
-
new(streams: [morph ? component.to_stream_morph : component.to_stream_replace]
|
|
27
|
+
new(streams: [morph ? component.to_stream_morph : component.to_stream_replace],
|
|
28
|
+
subject_component: component)
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
# Re-render the component in place via Idiomorph (issue #28). Emits
|
|
@@ -33,10 +34,14 @@ module Phlex
|
|
|
33
34
|
# per-field reactive editing (a "spreadsheet" grid where a debounced save
|
|
34
35
|
# fires while the user is still typing/tabbing). The morphed root still
|
|
35
36
|
# carries the fresh signed token, so the next action verifies.
|
|
36
|
-
def morph(component) = new(streams: [component.to_stream_morph])
|
|
37
|
+
def morph(component) = new(streams: [component.to_stream_morph], subject_component: component)
|
|
37
38
|
|
|
38
|
-
#
|
|
39
|
-
|
|
39
|
+
# Update only inner HTML (preserves the root element + its token attr).
|
|
40
|
+
# `morph: true` morphs the inner HTML in place (issue #113) instead of
|
|
41
|
+
# replacing it, so a cross-tab update keeps a peer's focus/caret.
|
|
42
|
+
def update(component, morph: false)
|
|
43
|
+
new(streams: [component.to_stream_update(morph:)], subject_component: component)
|
|
44
|
+
end
|
|
40
45
|
|
|
41
46
|
# Remove the component's element from the DOM. Uses the instance
|
|
42
47
|
# to_stream_remove (the component already knows its own #id — no
|
|
@@ -130,7 +135,7 @@ module Phlex
|
|
|
130
135
|
# or an already-built dom-id string (e.g. the value the row used as #id).
|
|
131
136
|
def collection_row_remove(definition, model)
|
|
132
137
|
if model.is_a?(String)
|
|
133
|
-
Phlex::Reactive.
|
|
138
|
+
Phlex::Reactive.stream_builder.remove(model)
|
|
134
139
|
else
|
|
135
140
|
definition.item.remove(model)
|
|
136
141
|
end
|
|
@@ -167,18 +172,111 @@ module Phlex
|
|
|
167
172
|
|
|
168
173
|
# Build a flash turbo-stream that appends `content` into a host-app
|
|
169
174
|
# container. `content` is a Phlex component instance (rendered through
|
|
170
|
-
# the configured renderer so t()/url_for work) or a
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
|
|
174
|
-
|
|
175
|
+
# the configured renderer so t()/url_for work) or a String — supplied
|
|
176
|
+
# by the caller because the render context is off-request (there is no
|
|
177
|
+
# Rails `flash`). The LEVEL reaches the wire (issue #77): string
|
|
178
|
+
# content gets a level-carrying wrapper (or the configured
|
|
179
|
+
# Phlex::Reactive.flash_component); component content renders VERBATIM
|
|
180
|
+
# — the caller owns the markup, no wrapper, no double-wrapping.
|
|
181
|
+
def flash_stream(level, content, target:, dismiss_after: nil)
|
|
182
|
+
Phlex::Reactive.stream_builder.append(target, html: flash_html(level, content, dismiss_after))
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Resolve flash `content` to HTML, carrying the level (issue #77):
|
|
186
|
+
# * a Phlex component instance — rendered verbatim, exactly as before
|
|
187
|
+
# (byte-identical; it also bypasses flash_component).
|
|
188
|
+
# * a String with Phlex::Reactive.flash_component configured — the
|
|
189
|
+
# app's component is instantiated new(level:, content:) and
|
|
190
|
+
# rendered through the existing render_html path.
|
|
191
|
+
# * a String otherwise — the default level-carrying wrapper below.
|
|
192
|
+
def flash_html(level, content, dismiss_after = nil)
|
|
193
|
+
# Verbatim Phlex component content owns its own markup (and lifecycle),
|
|
194
|
+
# so dismiss_after has nowhere to hang — it wraps only string content.
|
|
195
|
+
return render_html(content) if content.is_a?(::Phlex::SGML)
|
|
196
|
+
|
|
197
|
+
component_class = Phlex::Reactive.flash_component
|
|
198
|
+
if component_class
|
|
199
|
+
html = render_html(component_class.new(level:, content:))
|
|
200
|
+
return dismiss_after ? wrap_dismiss(html, dismiss_after) : html
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
default_flash_html(level, content, dismiss_after)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# The default string-flash wrapper:
|
|
207
|
+
# <div class="reactive-flash reactive-flash--{level}"
|
|
208
|
+
# data-reactive-flash-level="{level}">{content}</div>
|
|
209
|
+
# The level is unconditionally HTML-escaped (it lands in a class name
|
|
210
|
+
# and a data attribute). The content keeps the exact pre-#77 injection
|
|
211
|
+
# contract, now applied INSIDE the wrapper: ERB::Util.html_escape
|
|
212
|
+
# escapes a plain String but passes an html_safe String verbatim —
|
|
213
|
+
# the same behavior Turbo's TagBuilder gave the unwrapped string, so
|
|
214
|
+
# a model value still can't inject markup while intentional raw HTML
|
|
215
|
+
# (html_safe) still renders. The wrapper is html_safe by construction
|
|
216
|
+
# (both interpolations are escaped above), so the TagBuilder emits it
|
|
217
|
+
# as-is.
|
|
218
|
+
def default_flash_html(level, content, dismiss_after = nil)
|
|
219
|
+
level_attr = CGI.escapeHTML(level.to_s)
|
|
220
|
+
body = ERB::Util.html_escape(content.to_s)
|
|
221
|
+
|
|
222
|
+
# html_safe is safe by construction: every interpolated piece is escaped
|
|
223
|
+
# (dismiss_attr coerces to an integer, so it can't inject an attribute).
|
|
224
|
+
%(<div class="reactive-flash reactive-flash--#{level_attr}" \
|
|
225
|
+
data-reactive-flash-level="#{level_attr}"#{dismiss_attr(dismiss_after)}>#{body}</div>).html_safe
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# The self-dismiss attribute, or "" when off. The value is forced through
|
|
229
|
+
# to_i so a String/float (or an injection attempt) becomes a plain integer
|
|
230
|
+
# — the client's document-level handler reads it as a timeout in ms.
|
|
231
|
+
def dismiss_attr(dismiss_after)
|
|
232
|
+
return "" if dismiss_after.nil?
|
|
233
|
+
|
|
234
|
+
%( data-reactive-dismiss-after="#{dismiss_after.to_i}")
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Wrap already-rendered flash HTML (the flash_component path) in a
|
|
238
|
+
# dismiss-bearing div. SafeBuffer#to_s returns SELF, so concatenate the
|
|
239
|
+
# html_safe pieces (never .to_s + raw) to keep the buffer safe.
|
|
240
|
+
def wrap_dismiss(html, dismiss_after)
|
|
241
|
+
(%(<div#{dismiss_attr(dismiss_after)}>).html_safe + html.html_safe + "</div>".html_safe)
|
|
175
242
|
end
|
|
176
243
|
|
|
177
244
|
# Build a turbo-stream that updates an arbitrary target id with `content`
|
|
178
245
|
# (a Phlex component instance or an HTML string). Used by #also_update to
|
|
179
246
|
# re-render a companion element that isn't itself a Streamable component.
|
|
180
247
|
def update_stream(target, content)
|
|
181
|
-
Phlex::Reactive.
|
|
248
|
+
Phlex::Reactive.stream_builder.update(target, html: render_html(content))
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Build a `reactive:js` turbo-stream carrying server-pushed client DOM
|
|
252
|
+
# ops (issue #97). `ops` is a Phlex::Reactive::JS chain or a raw
|
|
253
|
+
# [[op, args], ...] array; `target` (an element id or nil) scopes op
|
|
254
|
+
# resolution on the client (nil → document-scoped). The ops JSON is
|
|
255
|
+
# HTML-escaped into the attribute exactly like to_stream_token — a raw
|
|
256
|
+
# interpolation would be an injection vector. The client's registered
|
|
257
|
+
# reactive:js handler runs the ops through the SAME whitelist interpreter
|
|
258
|
+
# as on_client (default-deny), so an unknown op is warn-and-skipped there.
|
|
259
|
+
def js_stream(ops, target: nil)
|
|
260
|
+
json = js_ops_json(ops)
|
|
261
|
+
target_attr = target ? %( target="#{ERB::Util.html_escape(target)}") : ""
|
|
262
|
+
%(<turbo-stream action="reactive:js"#{target_attr} \
|
|
263
|
+
data-reactive-ops="#{ERB::Util.html_escape(json)}"></turbo-stream>).html_safe
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Normalize `ops` to its JSON wire form, rejecting an empty chain — a
|
|
267
|
+
# reactive:js stream with no ops is a dead stream (a mistake at the call
|
|
268
|
+
# site), so fail loudly rather than emit an inert tag.
|
|
269
|
+
def js_ops_json(ops)
|
|
270
|
+
if ops.is_a?(Phlex::Reactive::JS)
|
|
271
|
+
raise ArgumentError, "js(...) got no ops — a dead reactive:js stream" if ops.empty?
|
|
272
|
+
|
|
273
|
+
ops.to_json
|
|
274
|
+
else
|
|
275
|
+
list = Array(ops)
|
|
276
|
+
raise ArgumentError, "js(...) got no ops — a dead reactive:js stream" if list.empty?
|
|
277
|
+
|
|
278
|
+
list.to_json
|
|
279
|
+
end
|
|
182
280
|
end
|
|
183
281
|
|
|
184
282
|
# Resolve `content` to the HTML for a turbo-stream's `html:`. Two forms,
|
|
@@ -204,11 +302,19 @@ module Phlex
|
|
|
204
302
|
# OUT of the full-self replace but still needs the token refreshed. The
|
|
205
303
|
# endpoint appends this component's tiny to_stream_token instead, so the
|
|
206
304
|
# token rolls forward without re-rendering (and clobbering) the children.
|
|
207
|
-
|
|
305
|
+
#
|
|
306
|
+
# subject_component: the component a self-targeting builder (replace/morph/
|
|
307
|
+
# update) re-renders (issue #97). Distinct from token_component so it does
|
|
308
|
+
# NOT trip refresh_token? — it exists only to default #js's target to the
|
|
309
|
+
# bound component's id, so reply.morph.js(js.focus("@root")) scopes to the
|
|
310
|
+
# morphed root without the caller repeating the id.
|
|
311
|
+
def initialize(streams: [], redirect_url: nil, render_self: true, token_component: nil,
|
|
312
|
+
subject_component: nil)
|
|
208
313
|
@streams = streams.freeze
|
|
209
314
|
@redirect_url = redirect_url
|
|
210
315
|
@render_self = render_self
|
|
211
316
|
@token_component = token_component
|
|
317
|
+
@subject_component = subject_component
|
|
212
318
|
freeze
|
|
213
319
|
end
|
|
214
320
|
|
|
@@ -219,14 +325,38 @@ module Phlex
|
|
|
219
325
|
streams: @streams + more.flatten,
|
|
220
326
|
redirect_url: @redirect_url,
|
|
221
327
|
render_self: @render_self,
|
|
222
|
-
token_component: @token_component
|
|
328
|
+
token_component: @token_component,
|
|
329
|
+
subject_component: @subject_component
|
|
223
330
|
)
|
|
224
331
|
end
|
|
225
332
|
|
|
333
|
+
# Chain a `reactive:js` op stream — server-pushed client DOM ops (issue
|
|
334
|
+
# #97) — onto this reply, so a focus/dispatch/class toggle runs on the
|
|
335
|
+
# client after the render applies:
|
|
336
|
+
#
|
|
337
|
+
# reply.morph.js(js.focus("[name=next]").dispatch("app:saved"))
|
|
338
|
+
#
|
|
339
|
+
# `ops` is a Phlex::Reactive::JS chain (or a raw [[op, args], ...] array).
|
|
340
|
+
# `target` (an element id) scopes op resolution on the client; it defaults
|
|
341
|
+
# to the bound component's id (subject_component/token_component) so
|
|
342
|
+
# self-scoped ops (@root, a component-relative selector) just work, and is
|
|
343
|
+
# nil (document-scoped) for a subject-free reply.with. Returns a NEW
|
|
344
|
+
# Response (immutable) — the op stream rides the same stream() plumbing, so
|
|
345
|
+
# the endpoint emits it LAST (after all render streams) and a focus op sees
|
|
346
|
+
# the post-render DOM.
|
|
347
|
+
def js(ops, target: :__default)
|
|
348
|
+
resolved = target == :__default ? default_js_target : target
|
|
349
|
+
stream(self.class.js_stream(ops, target: resolved))
|
|
350
|
+
end
|
|
351
|
+
|
|
226
352
|
# Append a flash turbo-stream into a host-app container (default
|
|
227
353
|
# <div id="flash">, configurable via Phlex::Reactive.flash_target).
|
|
228
|
-
|
|
229
|
-
|
|
354
|
+
# `dismiss_after:` (ms) makes the flash self-remove after the timeout — a
|
|
355
|
+
# document-level client handler removes any [data-reactive-dismiss-after]
|
|
356
|
+
# container, so it works for reply AND broadcast flashes (issue #100). It
|
|
357
|
+
# wraps only STRING content; a verbatim Phlex component owns its lifecycle.
|
|
358
|
+
def flash(level, content, target: Phlex::Reactive.flash_target, dismiss_after: nil)
|
|
359
|
+
stream(self.class.flash_stream(level, content, target:, dismiss_after:))
|
|
230
360
|
end
|
|
231
361
|
|
|
232
362
|
# Also re-render a COMPANION element alongside self — a page heading, a
|
|
@@ -259,6 +389,16 @@ module Phlex
|
|
|
259
389
|
# but still needs the token rolled forward — the endpoint appends the bound
|
|
260
390
|
# component's tiny token-only stream (issue #30).
|
|
261
391
|
def refresh_token? = !@token_component.nil?
|
|
392
|
+
|
|
393
|
+
private
|
|
394
|
+
|
|
395
|
+
# The default `target` for #js: the bound component's id when this reply is
|
|
396
|
+
# component-scoped (replace/morph/update set subject_component; .streams and
|
|
397
|
+
# collections set token_component), else nil — a subject-free reply.with is
|
|
398
|
+
# document-scoped unless the caller passes target: explicitly.
|
|
399
|
+
def default_js_target
|
|
400
|
+
(@subject_component || @token_component)&.id
|
|
401
|
+
end
|
|
262
402
|
end
|
|
263
403
|
end
|
|
264
404
|
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phlex
|
|
4
|
+
module Reactive
|
|
5
|
+
# A Turbo Stream that IS an html_safe String — it drops into
|
|
6
|
+
# `render turbo_stream:`, ERB/Phlex interpolation, and Turbo test-helper
|
|
7
|
+
# substring asserts unchanged — but ALSO carries the structural facts the
|
|
8
|
+
# action endpoint needs: `rx_action`, `rx_target`, `rx_renders_root?`, and a
|
|
9
|
+
# token flag computed ONCE from ground truth (the html bytes).
|
|
10
|
+
#
|
|
11
|
+
# WHY (issue #114): the endpoint used to reverse-engineer turbo-stream
|
|
12
|
+
# SEMANTICS out of MARKUP — substring-scanning every stream for
|
|
13
|
+
# `data-reactive-token-value` and regexing the opening tag's `action="..."`
|
|
14
|
+
# against a self-render allowlist. The gem was parsing strings it just built,
|
|
15
|
+
# and every new stream shape needed a new patch to the heuristics. A Stream
|
|
16
|
+
# knows its action, target, and token-ness at CONSTRUCTION, so the endpoint
|
|
17
|
+
# reads fields instead of scanning markup.
|
|
18
|
+
#
|
|
19
|
+
# Subclasses ActiveSupport::SafeBuffer deliberately. The sole thing
|
|
20
|
+
# `render turbo_stream: [s1, s2]` does to each element is `plain_string << s`
|
|
21
|
+
# (verified against actionpack 8.1.3 / turbo-rails 2.0.23: the renderer
|
|
22
|
+
# returns the array unchanged, Response#body= stores it, and Buffer#body does
|
|
23
|
+
# `buf = +""; each { |chunk| buf << chunk }`). `String#<<` needs `#to_str`,
|
|
24
|
+
# which a SafeBuffer subclass inherits — so the ivars ride to the wire and
|
|
25
|
+
# the bytes are byte-identical to today's TagBuilder output. Being IS-A-String
|
|
26
|
+
# also satisfies every public-API interop requirement (clause 2) for free.
|
|
27
|
+
#
|
|
28
|
+
# Metadata-loss is a FEATURE, not a bug. `dup`/`+` keep the class + ivars, so
|
|
29
|
+
# they stay on the structural path with accurate fields (the bytes are
|
|
30
|
+
# unchanged). `gsub`/interpolation/`*` reshape the bytes and return a plain
|
|
31
|
+
# String/SafeBuffer — precisely when the object is no longer a
|
|
32
|
+
# structurally-known stream and SHOULD be treated as opaque. The endpoint's
|
|
33
|
+
# `is_a?(Stream) && rx_action` guard turns every such loss into the SAFE
|
|
34
|
+
# legacy regex path. INVARIANT: never `+`/`gsub`/`<<` a built Stream and keep
|
|
35
|
+
# using it as a Stream — re-`wrap` the result to recompute the flags.
|
|
36
|
+
class Stream < ActiveSupport::SafeBuffer
|
|
37
|
+
# The attribute the client's #extractToken reads. Its PRESENCE (by name) is
|
|
38
|
+
# what "carries a token" means — the same contract the old substring scan
|
|
39
|
+
# used, so an accidentally-empty token still flags true (not a regression;
|
|
40
|
+
# the real cosmos#1939 guard lives upstream in #to_stream_token).
|
|
41
|
+
TOKEN_ATTR = "data-reactive-token-value"
|
|
42
|
+
|
|
43
|
+
# Actions whose OPENING tag re-renders the component's OWN root, so the
|
|
44
|
+
# root's fresh token rolls the signed identity forward. `append`/`prepend`
|
|
45
|
+
# insert CHILDREN and are deliberately excluded (issue #44): a child row's
|
|
46
|
+
# token embedded in an append `<template>` must NEVER count as the
|
|
47
|
+
# container's refresh. `reactive:token` is our inert token-only refresh.
|
|
48
|
+
SELF_RENDER_ACTIONS = %w[replace update reactive:token].freeze
|
|
49
|
+
|
|
50
|
+
class << self
|
|
51
|
+
# The single construction seam every builder routes its already-html_safe
|
|
52
|
+
# output through. `html` MUST already be html_safe (a TagBuilder result or
|
|
53
|
+
# an explicitly `.html_safe` string) — we do NOT escape, so the wire stays
|
|
54
|
+
# byte-identical. `renders_root` is set STRUCTURALLY by the builder that
|
|
55
|
+
# knows its own semantics; `carries_token` is GROUND TRUTH.
|
|
56
|
+
def wrap(html, action:, target:, renders_root:)
|
|
57
|
+
new(html.to_s, action: action, target: target, renders_root: renders_root)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def initialize(html = "", action: nil, target: nil, renders_root: false)
|
|
62
|
+
super(html) # SafeBuffer copies the bytes verbatim — byte-identical wire.
|
|
63
|
+
@rx_action = action&.to_s
|
|
64
|
+
@rx_target = target&.to_s
|
|
65
|
+
@rx_renders_root = renders_root ? true : false
|
|
66
|
+
# ONE O(bytes) scan at BUILD time — the ground-truth token flag. A
|
|
67
|
+
# `with(...)` raw string that omits the token and a replace whose render
|
|
68
|
+
# happened to include one both answer honestly; the flag is NEVER
|
|
69
|
+
# inferred from `action` (that would reintroduce cosmos#1939).
|
|
70
|
+
@rx_carries_token = include?(TOKEN_ATTR)
|
|
71
|
+
freeze # an immutable value object
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
attr_reader :rx_action, :rx_target
|
|
75
|
+
|
|
76
|
+
def rx_renders_root? = @rx_renders_root
|
|
77
|
+
def rx_carries_token? = @rx_carries_token
|
|
78
|
+
|
|
79
|
+
# Does THIS stream ALREADY refresh `component_target`'s signed token by
|
|
80
|
+
# re-rendering its own root? Purely structural — replaces the two-method
|
|
81
|
+
# opening-tag regex (carries_token_for? + self_render_stream_for?). All must
|
|
82
|
+
# hold: it carries a token AND re-renders the root AND targets THIS
|
|
83
|
+
# component AND the action is a self-render action.
|
|
84
|
+
# * A sibling replace (different target) → false, so ours still refreshes
|
|
85
|
+
# (issue #30).
|
|
86
|
+
# * An appended child row (renders_root false) → false, so the container's
|
|
87
|
+
# token still refreshes even though the row embeds its own (issue #44).
|
|
88
|
+
# `component_target` is the RAW dom id (never the escaped `target="..."`
|
|
89
|
+
# attribute); escaping happens only in the emitted HTML.
|
|
90
|
+
def rx_refreshes_token_for?(component_target)
|
|
91
|
+
@rx_carries_token &&
|
|
92
|
+
@rx_renders_root &&
|
|
93
|
+
@rx_target == component_target.to_s &&
|
|
94
|
+
SELF_RENDER_ACTIONS.include?(@rx_action)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|