phlex-reactive 0.9.5 → 0.11.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 +315 -0
- data/README.md +176 -122
- data/app/controllers/phlex/reactive/actions_controller.rb +41 -8
- data/app/javascript/phlex/reactive/confirm_predicate.js +52 -0
- data/app/javascript/phlex/reactive/confirm_predicate.min.js +4 -0
- data/app/javascript/phlex/reactive/confirm_predicate.min.js.map +10 -0
- data/app/javascript/phlex/reactive/reactive_controller.js +475 -218
- 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/client_bindings.rb +57 -0
- data/lib/phlex/reactive/component/dsl.rb +158 -33
- data/lib/phlex/reactive/component/helpers.rb +478 -381
- data/lib/phlex/reactive/component/registry.rb +8 -1
- data/lib/phlex/reactive/component.rb +26 -12
- data/lib/phlex/reactive/defer.rb +1 -1
- data/lib/phlex/reactive/deferred_render_job.rb +2 -2
- data/lib/phlex/reactive/engine.rb +14 -0
- data/lib/phlex/reactive/js.rb +29 -15
- data/lib/phlex/reactive/reply.rb +105 -44
- data/lib/phlex/reactive/response.rb +139 -48
- data/lib/phlex/reactive/show_conditions.rb +249 -0
- data/lib/phlex/reactive/streamable.rb +244 -202
- data/lib/phlex/reactive/test_helpers.rb +10 -2
- data/lib/phlex/reactive/version.rb +1 -1
- data/lib/phlex/reactive.rb +137 -12
- metadata +6 -1
|
@@ -85,7 +85,7 @@ module Phlex
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
event[:outcome] = :ok
|
|
88
|
-
stream = payload["m"] == "morph"
|
|
88
|
+
stream = component.to_stream_replace(morph: payload["m"] == "morph")
|
|
89
89
|
render turbo_stream: stream
|
|
90
90
|
rescue Phlex::Reactive::InvalidToken => e
|
|
91
91
|
event[:outcome] = :invalid_token
|
|
@@ -119,7 +119,7 @@ module Phlex
|
|
|
119
119
|
payload = verified_payload
|
|
120
120
|
component_class = resolve_component(payload["c"])
|
|
121
121
|
event[:component] = component_class.name
|
|
122
|
-
action_def = component_class.
|
|
122
|
+
action_def = component_class.reactive_actions[reactive_action_name.to_sym]
|
|
123
123
|
|
|
124
124
|
# default-deny
|
|
125
125
|
unless action_def
|
|
@@ -189,7 +189,9 @@ module Phlex
|
|
|
189
189
|
return unless callable
|
|
190
190
|
|
|
191
191
|
message = callable.call(kind)
|
|
192
|
-
|
|
192
|
+
# flash_stream is a private Response builder (issue #182) — the endpoint's
|
|
193
|
+
# error path is an internal caller, so reach it via send.
|
|
194
|
+
Phlex::Reactive::Response.send(:flash_stream, :error, message, target: Phlex::Reactive.flash_target)
|
|
193
195
|
rescue => e # rubocop:disable Style/RescueStandardError
|
|
194
196
|
::Rails.logger&.warn("[phlex-reactive] error_flash raised: #{e.message}") if defined?(::Rails) &&
|
|
195
197
|
::Rails.respond_to?(:logger)
|
|
@@ -298,7 +300,7 @@ module Phlex
|
|
|
298
300
|
|
|
299
301
|
streams = result.streams
|
|
300
302
|
|
|
301
|
-
# Partial update (
|
|
303
|
+
# Partial update (reply.streams, issue #30): the action
|
|
302
304
|
# re-rendered only PART of the component and opted out of the full-self
|
|
303
305
|
# replace. Append a tiny token-only stream so the signed token still rolls
|
|
304
306
|
# forward WITHOUT re-rendering (and clobbering) the live inputs. Skip it
|
|
@@ -320,7 +322,7 @@ module Phlex
|
|
|
320
322
|
# for replace AND update of self (both re-render the root via
|
|
321
323
|
# render_component, carrying the token), and still adds the fallback
|
|
322
324
|
# replace when a hand-built `with(...)` stream omits it. Idempotent: a
|
|
323
|
-
#
|
|
325
|
+
# reply.replace/update already carries the token, so we
|
|
324
326
|
# don't double the self-render.
|
|
325
327
|
#
|
|
326
328
|
# GUARD 2 (issue #114): GLOBAL, un-scoped — "does ANY stream carry a
|
|
@@ -328,8 +330,21 @@ module Phlex
|
|
|
328
330
|
# ground-truth flag (rx_carries_token?), a raw string from a substring
|
|
329
331
|
# scan. Deliberately NOT rx_refreshes_token_for? (target-scoped): scoping
|
|
330
332
|
# this guard would regress update/morph of self on an aliased id.
|
|
333
|
+
#
|
|
334
|
+
# A self-render reply (reply.replace/update/morph — subject_component
|
|
335
|
+
# set) whose streams somehow carry no token gets the full self-replace
|
|
336
|
+
# (defensive, unchanged). A companion-only reply.with (NO subject, NO
|
|
337
|
+
# token_component) that doesn't re-render the root gets a token-ONLY
|
|
338
|
+
# refresh instead — issue #180 automatic token refresh: reply.with and
|
|
339
|
+
# reply.streams converge, the author never picks a verb to keep the token
|
|
340
|
+
# fresh, and a live input is never clobbered by a forced replace.
|
|
331
341
|
elsif result.render_self? && streams.none? { stream_carries_token?(it) }
|
|
332
|
-
streams =
|
|
342
|
+
streams =
|
|
343
|
+
if result.subject_component
|
|
344
|
+
[component.to_stream_replace, *streams]
|
|
345
|
+
else
|
|
346
|
+
[*streams, component.to_stream_token]
|
|
347
|
+
end
|
|
333
348
|
end
|
|
334
349
|
|
|
335
350
|
append_deferred_streams(streams, result)
|
|
@@ -470,13 +485,31 @@ module Phlex
|
|
|
470
485
|
# skipped — zero extra work on the production path.
|
|
471
486
|
def coerce_params(action_def, component_class: nil, action_name: nil)
|
|
472
487
|
dropped = Phlex::Reactive.verbose_errors ? [] : nil
|
|
473
|
-
raw = params.fetch(:params, {})
|
|
488
|
+
raw = unwrap_scope(params.fetch(:params, {}), component_class)
|
|
474
489
|
|
|
475
490
|
coerced = action_def.schema.coerce(raw, dropped)
|
|
476
491
|
log_dropped_params(dropped, action_def.params, component_class, action_name)
|
|
477
492
|
coerced
|
|
478
493
|
end
|
|
479
494
|
|
|
495
|
+
# Issue #184: a scoped component's fields POST bracketed (todo[title]), which
|
|
496
|
+
# Rails expands to { "todo" => { "title" => … } } before the action runs. Peel
|
|
497
|
+
# exactly ONE scope level so the FLAT schema { title: :string } matches (the
|
|
498
|
+
# #67 bracket-drop footgun). Only when the component declares reactive_scope
|
|
499
|
+
# AND the raw params carry that single key mapping to a Hash — otherwise the
|
|
500
|
+
# raw params pass through untouched (unscoped components + nested_attributes
|
|
501
|
+
# shapes are unaffected).
|
|
502
|
+
def unwrap_scope(raw, component_class)
|
|
503
|
+
scope = component_class.reactive_scope if component_class.respond_to?(:reactive_scope)
|
|
504
|
+
return raw unless scope
|
|
505
|
+
|
|
506
|
+
# At the endpoint `raw` is ActionController::Parameters, so `raw[scope]` is
|
|
507
|
+
# too — NOT a Hash. Accept either shape (both answer to the schema's
|
|
508
|
+
# coerce): unwrap only when the scope key maps to a nested params/hash.
|
|
509
|
+
nested = raw[scope.to_s]
|
|
510
|
+
nested.is_a?(Hash) || nested.is_a?(ActionController::Parameters) ? nested : raw
|
|
511
|
+
end
|
|
512
|
+
|
|
480
513
|
# ---- verbose_errors dropped-param logging --------------------------
|
|
481
514
|
# ParamSchema collects the dropped entries; the controller formats the ONE
|
|
482
515
|
# warn line (with the #16/#21 shape hints). Everything below runs ONLY when
|
|
@@ -564,7 +597,7 @@ module Phlex
|
|
|
564
597
|
diagnostic: :unknown_class
|
|
565
598
|
)
|
|
566
599
|
end
|
|
567
|
-
unless klass.respond_to?(:
|
|
600
|
+
unless klass.respond_to?(:reactive_actions) && klass.include?(Phlex::Reactive::Component)
|
|
568
601
|
raise Phlex::Reactive::InvalidToken.new(
|
|
569
602
|
"#{name} resolved but does not include Phlex::Reactive::Component",
|
|
570
603
|
diagnostic: :not_reactive_class
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// The client-side confirm-predicate registry — the multi-field escape hatch for
|
|
2
|
+
// conditional confirmation (issue #179).
|
|
3
|
+
//
|
|
4
|
+
// confirm: { when: { total: 0 }, message: } handles the single-field 80% case
|
|
5
|
+
// declaratively (the reactive_show conditions language), with NO JS. But some
|
|
6
|
+
// soft-validation is multi-field — "warn if the end date precedes the start",
|
|
7
|
+
// "warn if two related totals disagree" — which a single field=value condition
|
|
8
|
+
// can't express. This registry is the seam for that logic: name a pure function
|
|
9
|
+
// with `confirm: { predicate: "name", message: }` (Ruby) and register it here.
|
|
10
|
+
//
|
|
11
|
+
// The seam mirrors compute.js (setComputeReducer) and confirm.js: a settable
|
|
12
|
+
// registry with a lookup the controller calls. Register once at boot:
|
|
13
|
+
//
|
|
14
|
+
// import { setConfirmPredicate } from "phlex/reactive/confirm_predicate"
|
|
15
|
+
// setConfirmPredicate("end_before_start", ({ starts_at, ends_at }) =>
|
|
16
|
+
// ends_at !== "" && ends_at < starts_at)
|
|
17
|
+
//
|
|
18
|
+
// The predicate's signature is (fields) => boolean:
|
|
19
|
+
//
|
|
20
|
+
// fields — a plain object of { name: value } over the trigger root's collected
|
|
21
|
+
// controls (the SAME snapshot reactive_compute reads — #collectFields).
|
|
22
|
+
// Values are the raw control values (strings; a checkbox is a boolean).
|
|
23
|
+
//
|
|
24
|
+
// It returns truthy to WARN (the confirm dialog fires with the declared message)
|
|
25
|
+
// or falsy to PROCEED with no dialog. The predicate is soft-validation UX, NOT
|
|
26
|
+
// authorization: a user can bypass it (devtools, an unregistered name) and the
|
|
27
|
+
// action still hits the endpoint's real authorize/default-deny — never let a
|
|
28
|
+
// predicate stand in for a server-side check.
|
|
29
|
+
//
|
|
30
|
+
// A missing predicate (name never registered) makes the gate a NO-OP: the
|
|
31
|
+
// controller proceeds WITHOUT a dialog and warns, exactly like compute.js's
|
|
32
|
+
// unknown-reducer posture — a stale/typo'd name must not break the page or
|
|
33
|
+
// (worse) block a legitimate action behind a dialog that can never resolve.
|
|
34
|
+
|
|
35
|
+
const predicates = new Map()
|
|
36
|
+
|
|
37
|
+
// Register (or replace) the predicate for `key`. `fn` is
|
|
38
|
+
// (fields: Record<string, unknown>) => boolean — truthy warns, falsy proceeds.
|
|
39
|
+
export function setConfirmPredicate(key, fn) {
|
|
40
|
+
predicates.set(key, fn)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Look up a registered predicate; undefined when none — the controller then
|
|
44
|
+
// proceeds without a dialog (and warns) rather than throwing or blocking.
|
|
45
|
+
export function confirmPredicate(key) {
|
|
46
|
+
return predicates.get(key)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Test seam: clear the registry so a predicate registered in one test can't leak.
|
|
50
|
+
export function __resetConfirmPredicateRegistryForTest() {
|
|
51
|
+
predicates.clear()
|
|
52
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var j=new Map;function u(b,q){j.set(b,q)}function v(b){return j.get(b)}function w(){j.clear()}export{u as setConfirmPredicate,v as confirmPredicate,w as __resetConfirmPredicateRegistryForTest};
|
|
2
|
+
|
|
3
|
+
//# debugId=2F392605F11792F264756E2164756E21
|
|
4
|
+
//# sourceMappingURL=confirm_predicate.min.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["confirm_predicate.js"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"// The client-side confirm-predicate registry — the multi-field escape hatch for\n// conditional confirmation (issue #179).\n//\n// confirm: { when: { total: 0 }, message: } handles the single-field 80% case\n// declaratively (the reactive_show conditions language), with NO JS. But some\n// soft-validation is multi-field — \"warn if the end date precedes the start\",\n// \"warn if two related totals disagree\" — which a single field=value condition\n// can't express. This registry is the seam for that logic: name a pure function\n// with `confirm: { predicate: \"name\", message: }` (Ruby) and register it here.\n//\n// The seam mirrors compute.js (setComputeReducer) and confirm.js: a settable\n// registry with a lookup the controller calls. Register once at boot:\n//\n// import { setConfirmPredicate } from \"phlex/reactive/confirm_predicate\"\n// setConfirmPredicate(\"end_before_start\", ({ starts_at, ends_at }) =>\n// ends_at !== \"\" && ends_at < starts_at)\n//\n// The predicate's signature is (fields) => boolean:\n//\n// fields — a plain object of { name: value } over the trigger root's collected\n// controls (the SAME snapshot reactive_compute reads — #collectFields).\n// Values are the raw control values (strings; a checkbox is a boolean).\n//\n// It returns truthy to WARN (the confirm dialog fires with the declared message)\n// or falsy to PROCEED with no dialog. The predicate is soft-validation UX, NOT\n// authorization: a user can bypass it (devtools, an unregistered name) and the\n// action still hits the endpoint's real authorize/default-deny — never let a\n// predicate stand in for a server-side check.\n//\n// A missing predicate (name never registered) makes the gate a NO-OP: the\n// controller proceeds WITHOUT a dialog and warns, exactly like compute.js's\n// unknown-reducer posture — a stale/typo'd name must not break the page or\n// (worse) block a legitimate action behind a dialog that can never resolve.\n\nconst predicates = new Map()\n\n// Register (or replace) the predicate for `key`. `fn` is\n// (fields: Record<string, unknown>) => boolean — truthy warns, falsy proceeds.\nexport function setConfirmPredicate(key, fn) {\n predicates.set(key, fn)\n}\n\n// Look up a registered predicate; undefined when none — the controller then\n// proceeds without a dialog (and warns) rather than throwing or blocking.\nexport function confirmPredicate(key) {\n return predicates.get(key)\n}\n\n// Test seam: clear the registry so a predicate registered in one test can't leak.\nexport function __resetConfirmPredicateRegistryForTest() {\n predicates.clear()\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "AAkCA,IAAM,EAAa,IAAI,IAIhB,SAAS,CAAmB,CAAC,EAAK,EAAI,CAC3C,EAAW,IAAI,EAAK,CAAE,EAKjB,SAAS,CAAgB,CAAC,EAAK,CACpC,OAAO,EAAW,IAAI,CAAG,EAIpB,SAAS,CAAsC,EAAG,CACvD,EAAW,MAAM",
|
|
8
|
+
"debugId": "2F392605F11792F264756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|