phlex-reactive 0.11.7 → 0.12.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 +55 -0
- data/README.md +69 -3
- data/app/assets/stylesheets/phlex/reactive/effects.css +106 -0
- data/app/javascript/phlex/reactive/reactive_controller.js +308 -3
- data/app/javascript/phlex/reactive/reactive_controller.min.js +2 -2
- data/app/javascript/phlex/reactive/reactive_controller.min.js.map +3 -3
- data/lib/phlex/reactive/component/dsl.rb +66 -0
- data/lib/phlex/reactive/component/helpers.rb +24 -2
- data/lib/phlex/reactive/component/registry.rb +2 -0
- data/lib/phlex/reactive/effects.rb +169 -0
- data/lib/phlex/reactive/engine.rb +5 -0
- data/lib/phlex/reactive/reply.rb +20 -15
- data/lib/phlex/reactive/response.rb +28 -19
- data/lib/phlex/reactive/streamable.rb +69 -32
- data/lib/phlex/reactive/version.rb +1 -1
- data/lib/phlex/reactive.rb +30 -2
- metadata +3 -1
|
@@ -114,7 +114,12 @@ module Phlex
|
|
|
114
114
|
# thread-local path (so exclude:/visible_to: reach pgbus and Action Cable
|
|
115
115
|
# no-ops). Self-targeting verbs derive the target from the component's #id
|
|
116
116
|
# and REQUIRE a Streamable payload; container verbs need an explicit target.
|
|
117
|
-
def broadcast_component(owner, verb, payload, component, keys, morph:, target:, exclude:, visible_to:)
|
|
117
|
+
def broadcast_component(owner, verb, payload, component, keys, morph:, target:, exclude:, visible_to:, effect: nil)
|
|
118
|
+
if verb == :js && !effect.nil?
|
|
119
|
+
raise ArgumentError,
|
|
120
|
+
"broadcast_to js: takes no effect: — effects animate element streams (replace/update/" \
|
|
121
|
+
"append/prepend/remove), not client-op dispatches"
|
|
122
|
+
end
|
|
118
123
|
resolved_target = resolve_broadcast_target(verb, component, payload, target)
|
|
119
124
|
# broadcast_js_ops_json is a private class method on the owner Streamable
|
|
120
125
|
# class (reached via send); the instrumentation + pgbus thread-locals are
|
|
@@ -127,7 +132,7 @@ module Phlex
|
|
|
127
132
|
with_pgbus_broadcast_opts(exclude:, visible_to:) do
|
|
128
133
|
html = verb == :js ? nil : render_broadcast_html(component)
|
|
129
134
|
ops_json = verb == :js ? owner.send(:broadcast_js_ops_json, payload) : nil
|
|
130
|
-
keys.each { dispatch_broadcast(verb, it, resolved_target, html, ops_json, morph) }
|
|
135
|
+
keys.each { dispatch_broadcast(verb, it, resolved_target, html, ops_json, morph, effect) }
|
|
131
136
|
end
|
|
132
137
|
end
|
|
133
138
|
end
|
|
@@ -192,14 +197,18 @@ module Phlex
|
|
|
192
197
|
# Route ONE key to its Turbo::StreamsChannel call for the verb (issue #185).
|
|
193
198
|
# exclude:/visible_to: are NOT passed here — they ride the pgbus thread-locals
|
|
194
199
|
# set by with_pgbus_broadcast_opts (turbo-rails would swallow them as kwargs).
|
|
195
|
-
|
|
200
|
+
# `effect` (issue #215) rides the same attributes: seam morph does — every
|
|
201
|
+
# broadcast_*_to delegates to broadcast_action_to(attributes:), so the attr
|
|
202
|
+
# lands on the <turbo-stream> element on every verb.
|
|
203
|
+
def dispatch_broadcast(verb, key, target, html, ops_json, morph, effect = nil)
|
|
196
204
|
parts = Array(key)
|
|
205
|
+
wire = broadcast_wire(morph, effect)
|
|
197
206
|
case verb
|
|
198
|
-
when :replace then ::Turbo::StreamsChannel.broadcast_replace_to(*parts, target:, html:, **
|
|
199
|
-
when :update then ::Turbo::StreamsChannel.broadcast_update_to(*parts, target:, html:, **
|
|
200
|
-
when :append then ::Turbo::StreamsChannel.broadcast_append_to(*parts, target:, html
|
|
201
|
-
when :prepend then ::Turbo::StreamsChannel.broadcast_prepend_to(*parts, target:, html
|
|
202
|
-
when :remove then ::Turbo::StreamsChannel.broadcast_remove_to(*parts, target
|
|
207
|
+
when :replace then ::Turbo::StreamsChannel.broadcast_replace_to(*parts, target:, html:, **wire)
|
|
208
|
+
when :update then ::Turbo::StreamsChannel.broadcast_update_to(*parts, target:, html:, **wire)
|
|
209
|
+
when :append then ::Turbo::StreamsChannel.broadcast_append_to(*parts, target:, html:, **wire)
|
|
210
|
+
when :prepend then ::Turbo::StreamsChannel.broadcast_prepend_to(*parts, target:, html:, **wire)
|
|
211
|
+
when :remove then ::Turbo::StreamsChannel.broadcast_remove_to(*parts, target:, **wire)
|
|
203
212
|
when :js
|
|
204
213
|
::Turbo::StreamsChannel.broadcast_action_to(
|
|
205
214
|
*parts, action: "reactive:js", target:, attributes: { data: { reactive_ops: ops_json } }, render: false
|
|
@@ -207,11 +216,18 @@ module Phlex
|
|
|
207
216
|
end
|
|
208
217
|
end
|
|
209
218
|
|
|
210
|
-
# The ONE
|
|
211
|
-
#
|
|
212
|
-
#
|
|
213
|
-
|
|
214
|
-
|
|
219
|
+
# The ONE broadcast attributes compiler (issue #185, extended #215): the
|
|
220
|
+
# broadcast path emits extra <turbo-stream> attributes via `attributes:`
|
|
221
|
+
# (it has no `method:` kwarg) — method="morph" and/or the per-call
|
|
222
|
+
# data-reactive-effect. {} when neither, so the plain call's wire is
|
|
223
|
+
# byte-identical. (Replaces morph_wire.)
|
|
224
|
+
def broadcast_wire(morph, effect = nil)
|
|
225
|
+
return {} if !morph && effect.nil?
|
|
226
|
+
|
|
227
|
+
attrs = {}
|
|
228
|
+
attrs[:method] = "morph" if morph
|
|
229
|
+
attrs[:data] = { reactive_effect: Phlex::Reactive::Effects.wire_value(effect) } unless effect.nil?
|
|
230
|
+
{ attributes: attrs }
|
|
215
231
|
end
|
|
216
232
|
|
|
217
233
|
# Split the single verb kwarg out of the module-level broadcast_to's **verb
|
|
@@ -326,10 +342,17 @@ module Phlex
|
|
|
326
342
|
# action/target/token-ness structurally. renders_root: true — a replace
|
|
327
343
|
# re-renders the component's own root (carrying its fresh token); wire
|
|
328
344
|
# bytes are byte-identical.
|
|
329
|
-
|
|
345
|
+
# `effect:` (issue #215) — the PER-CALL effect override, stamped as
|
|
346
|
+
# data-reactive-effect on the <turbo-stream> element (false → "off").
|
|
347
|
+
# nil (the default) leaves the wire byte-identical. Same on every
|
|
348
|
+
# builder below.
|
|
349
|
+
def replace(model = nil, morph: false, effect: nil, **options)
|
|
330
350
|
component = build(model, options)
|
|
331
351
|
Phlex::Reactive::Stream.wrap(
|
|
332
|
-
|
|
352
|
+
Phlex::Reactive::Effects.annotate(
|
|
353
|
+
turbo_stream_builder.replace(component.id, html: render_component(component), **morph_method(morph)),
|
|
354
|
+
effect
|
|
355
|
+
),
|
|
333
356
|
action: "replace", target: component.id, renders_root: true
|
|
334
357
|
)
|
|
335
358
|
end
|
|
@@ -338,10 +361,13 @@ module Phlex
|
|
|
338
361
|
# Turbo 8 morphs the inner HTML in place instead of replacing it (issue
|
|
339
362
|
# #113) — a cross-tab update no longer clobbers a peer's focus/caret.
|
|
340
363
|
# Default (morph: false) is the unchanged plain update.
|
|
341
|
-
def update(model = nil, morph: false, **options)
|
|
364
|
+
def update(model = nil, morph: false, effect: nil, **options)
|
|
342
365
|
component = build(model, options)
|
|
343
366
|
Phlex::Reactive::Stream.wrap(
|
|
344
|
-
|
|
367
|
+
Phlex::Reactive::Effects.annotate(
|
|
368
|
+
turbo_stream_builder.update(component.id, html: render_component(component), **morph_method(morph)),
|
|
369
|
+
effect
|
|
370
|
+
),
|
|
345
371
|
action: "update", target: component.id, renders_root: true
|
|
346
372
|
)
|
|
347
373
|
end
|
|
@@ -349,26 +375,32 @@ module Phlex
|
|
|
349
375
|
# renders_root: false — append inserts a CHILD into `target`; even when
|
|
350
376
|
# the appended component carries its OWN token, that must never count as
|
|
351
377
|
# the container's own refresh (issue #44).
|
|
352
|
-
def append(target:, model: nil, **options)
|
|
378
|
+
def append(target:, model: nil, effect: nil, **options)
|
|
353
379
|
component = build(model, options)
|
|
354
380
|
Phlex::Reactive::Stream.wrap(
|
|
355
|
-
|
|
381
|
+
Phlex::Reactive::Effects.annotate(
|
|
382
|
+
turbo_stream_builder.append(target, html: render_component(component)),
|
|
383
|
+
effect
|
|
384
|
+
),
|
|
356
385
|
action: "append", target: target, renders_root: false
|
|
357
386
|
)
|
|
358
387
|
end
|
|
359
388
|
|
|
360
|
-
def prepend(target:, model: nil, **options)
|
|
389
|
+
def prepend(target:, model: nil, effect: nil, **options)
|
|
361
390
|
component = build(model, options)
|
|
362
391
|
Phlex::Reactive::Stream.wrap(
|
|
363
|
-
|
|
392
|
+
Phlex::Reactive::Effects.annotate(
|
|
393
|
+
turbo_stream_builder.prepend(target, html: render_component(component)),
|
|
394
|
+
effect
|
|
395
|
+
),
|
|
364
396
|
action: "prepend", target: target, renders_root: false
|
|
365
397
|
)
|
|
366
398
|
end
|
|
367
399
|
|
|
368
|
-
def remove(model = nil, **options)
|
|
400
|
+
def remove(model = nil, effect: nil, **options)
|
|
369
401
|
component = build(model, options)
|
|
370
402
|
Phlex::Reactive::Stream.wrap(
|
|
371
|
-
turbo_stream_builder.remove(component.id),
|
|
403
|
+
Phlex::Reactive::Effects.annotate(turbo_stream_builder.remove(component.id), effect),
|
|
372
404
|
action: "remove", target: component.id, renders_root: false
|
|
373
405
|
)
|
|
374
406
|
end
|
|
@@ -415,12 +447,13 @@ module Phlex
|
|
|
415
447
|
# thread to pgbus via the capability-gated instrument_broadcast path; on
|
|
416
448
|
# Action Cable they no-op. The class-level and module-level (Phlex::Reactive.
|
|
417
449
|
# broadcast_to) forms share broadcast_component (below).
|
|
418
|
-
def broadcast_to(*streamables, morph: false, target: nil, exclude: nil, visible_to: nil, each: nil,
|
|
450
|
+
def broadcast_to(*streamables, morph: false, target: nil, exclude: nil, visible_to: nil, each: nil,
|
|
451
|
+
effect: nil, **verb)
|
|
419
452
|
name, payload = extract_broadcast_verb(verb)
|
|
420
453
|
component = name == :js ? nil : coerce_broadcast_payload(payload)
|
|
421
454
|
keys = each ? each.map { Array(it) } : [streamables]
|
|
422
455
|
Phlex::Reactive::Streamable.broadcast_component(
|
|
423
|
-
self, name, payload, component, keys, morph:, target:, exclude:, visible_to:
|
|
456
|
+
self, name, payload, component, keys, morph:, target:, exclude:, visible_to:, effect:
|
|
424
457
|
)
|
|
425
458
|
end
|
|
426
459
|
|
|
@@ -509,9 +542,9 @@ module Phlex
|
|
|
509
542
|
morph ? { method: :morph } : {}
|
|
510
543
|
end
|
|
511
544
|
|
|
512
|
-
# (The broadcast path's
|
|
513
|
-
#
|
|
514
|
-
# broadcast_to share.)
|
|
545
|
+
# (The broadcast path's extra-attribute wire lives in
|
|
546
|
+
# Streamable.broadcast_wire — the single compiler both the class-level
|
|
547
|
+
# and module-level broadcast_to share; issue #185, extended #215.)
|
|
515
548
|
|
|
516
549
|
def renderer
|
|
517
550
|
Phlex::Reactive.renderer
|
|
@@ -586,10 +619,13 @@ module Phlex
|
|
|
586
619
|
# refreshes). Default (morph: false) is the plain outerHTML replace,
|
|
587
620
|
# byte-identical to before. Used by reply.replace / reply.morph. The morph:
|
|
588
621
|
# kwarg replaces the deleted to_stream_morph (issue #185).
|
|
589
|
-
|
|
622
|
+
# `effect:` (issue #215) — the per-call effect override on the actor's
|
|
623
|
+
# own reply stream, same wire as the class builders (nil = untouched).
|
|
624
|
+
def to_stream_replace(morph: false, effect: nil)
|
|
590
625
|
builder = self.class.turbo_stream_builder
|
|
591
626
|
html = self.class.render_component(self)
|
|
592
627
|
rendered = morph ? builder.replace(id, html:, method: :morph) : builder.replace(id, html:)
|
|
628
|
+
rendered = Phlex::Reactive::Effects.annotate(rendered, effect)
|
|
593
629
|
Phlex::Reactive::Stream.wrap(rendered, action: "replace", target: id, renders_root: true)
|
|
594
630
|
end
|
|
595
631
|
|
|
@@ -606,10 +642,11 @@ module Phlex
|
|
|
606
642
|
# false) is the unchanged plain update. Passing `method: :morph` inline
|
|
607
643
|
# (as #to_stream_morph does) keeps the plain call's wire byte-identical.
|
|
608
644
|
# Used by reply.update.
|
|
609
|
-
def to_stream_update(morph: false)
|
|
645
|
+
def to_stream_update(morph: false, effect: nil)
|
|
610
646
|
builder = self.class.turbo_stream_builder
|
|
611
647
|
html = self.class.render_component(self)
|
|
612
648
|
rendered = morph ? builder.update(id, html:, method: :morph) : builder.update(id, html:)
|
|
649
|
+
rendered = Phlex::Reactive::Effects.annotate(rendered, effect)
|
|
613
650
|
Phlex::Reactive::Stream.wrap(rendered, action: "update", target: id, renders_root: true)
|
|
614
651
|
end
|
|
615
652
|
|
|
@@ -647,9 +684,9 @@ module Phlex
|
|
|
647
684
|
# Render THIS instance as a remove stream. The component already knows its
|
|
648
685
|
# own #id, so no record/class reconstruction is needed (works for record-
|
|
649
686
|
# and state-backed components alike). Used by reply.remove.
|
|
650
|
-
def to_stream_remove
|
|
687
|
+
def to_stream_remove(effect: nil)
|
|
651
688
|
Phlex::Reactive::Stream.wrap(
|
|
652
|
-
self.class.turbo_stream_builder.remove(id),
|
|
689
|
+
Phlex::Reactive::Effects.annotate(self.class.turbo_stream_builder.remove(id), effect),
|
|
653
690
|
action: "remove", target: id, renders_root: false
|
|
654
691
|
)
|
|
655
692
|
end
|
data/lib/phlex/reactive.rb
CHANGED
|
@@ -704,6 +704,33 @@ module Phlex
|
|
|
704
704
|
|
|
705
705
|
attr_writer :flash_target
|
|
706
706
|
|
|
707
|
+
# Global effects opt-in (issue #215). nil (the default) = OFF: no root
|
|
708
|
+
# attrs are emitted anywhere, the wire is byte-identical to pre-effects,
|
|
709
|
+
# and the client interceptor sees nothing to do. Setting it is the
|
|
710
|
+
# opt-in AND the app-wide default set — refined per component with
|
|
711
|
+
# `reactive_effects` and per call with `effect:` (most specific wins):
|
|
712
|
+
#
|
|
713
|
+
# Phlex::Reactive.effects = true # { enter: :fade, exit: :fade, update: :highlight }
|
|
714
|
+
# Phlex::Reactive.effects = { enter: :slide, update: :highlight }
|
|
715
|
+
#
|
|
716
|
+
# Stored pre-normalized (wire strings), validated at WRITE time — a
|
|
717
|
+
# typo'd effect name raises here, not at render time.
|
|
718
|
+
def effects
|
|
719
|
+
defined?(@effects) ? @effects : nil
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
def effects=(value)
|
|
723
|
+
@effects = Phlex::Reactive::Effects.normalize_config(value)
|
|
724
|
+
@effects_generation = effects_generation + 1
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
# Monotonic write counter for effects= — the cache key half that lets
|
|
728
|
+
# each component class memoize its RESOLVED effect attrs (reactive_attrs
|
|
729
|
+
# is the token-signing hot path) without ever serving a stale config.
|
|
730
|
+
def effects_generation
|
|
731
|
+
@effects_generation ||= 0
|
|
732
|
+
end
|
|
733
|
+
|
|
707
734
|
# A user-visible flash rendered on every endpoint rescue path (issue #100).
|
|
708
735
|
# Default nil = today's behavior (a bare head, or the verbose_errors
|
|
709
736
|
# plain-text diagnostic). Set a lambda ->(kind) { "message" } (kind is
|
|
@@ -763,7 +790,8 @@ module Phlex
|
|
|
763
790
|
# verbs (replace:/remove:) require a Streamable payload (its #id is the target)
|
|
764
791
|
# — a plain component gets a guided error steering to update:. Shares the one
|
|
765
792
|
# broadcast implementation with the class-level form.
|
|
766
|
-
def broadcast_to(*streamables, morph: false, target: nil, exclude: nil, visible_to: nil, each: nil,
|
|
793
|
+
def broadcast_to(*streamables, morph: false, target: nil, exclude: nil, visible_to: nil, each: nil,
|
|
794
|
+
effect: nil, **verb)
|
|
767
795
|
name, payload = Phlex::Reactive::Streamable.extract_module_broadcast_verb(verb)
|
|
768
796
|
component = name == :js ? nil : payload
|
|
769
797
|
keys = each ? each.map { Array(it) } : [streamables]
|
|
@@ -773,7 +801,7 @@ module Phlex
|
|
|
773
801
|
# the module form is for BUILT component payloads.
|
|
774
802
|
owner = payload.is_a?(Phlex::Reactive::Streamable) ? payload.class : Phlex::Reactive::Streamable
|
|
775
803
|
Phlex::Reactive::Streamable.broadcast_component(
|
|
776
|
-
owner, name, payload, component, keys, morph:, target:, exclude:, visible_to:
|
|
804
|
+
owner, name, payload, component, keys, morph:, target:, exclude:, visible_to:, effect:
|
|
777
805
|
)
|
|
778
806
|
end
|
|
779
807
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: phlex-reactive
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -116,6 +116,7 @@ files:
|
|
|
116
116
|
- CHANGELOG.md
|
|
117
117
|
- LICENSE.txt
|
|
118
118
|
- README.md
|
|
119
|
+
- app/assets/stylesheets/phlex/reactive/effects.css
|
|
119
120
|
- app/controllers/phlex/reactive/actions_controller.rb
|
|
120
121
|
- app/javascript/phlex/reactive/compute.js
|
|
121
122
|
- app/javascript/phlex/reactive/compute.min.js
|
|
@@ -161,6 +162,7 @@ files:
|
|
|
161
162
|
- lib/phlex/reactive/defer.rb
|
|
162
163
|
- lib/phlex/reactive/deferred_render_job.rb
|
|
163
164
|
- lib/phlex/reactive/doctor.rb
|
|
165
|
+
- lib/phlex/reactive/effects.rb
|
|
164
166
|
- lib/phlex/reactive/engine.rb
|
|
165
167
|
- lib/phlex/reactive/inspector.rb
|
|
166
168
|
- lib/phlex/reactive/inspector/report.rb
|