phlex-reactive 0.15.4 → 0.15.5
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 +16 -0
- data/README.md +11 -0
- data/app/javascript/phlex/reactive/core.min.js +6 -6
- data/app/javascript/phlex/reactive/core.min.js.map +3 -3
- data/app/javascript/phlex/reactive/reactive_controller.min.js +4 -4
- data/app/javascript/phlex/reactive/reactive_controller.min.js.map +3 -3
- data/app/javascript/phlex/reactive/runtime.js +83 -1
- data/lib/phlex/reactive/response.rb +3 -4
- data/lib/phlex/reactive/streamable.rb +0 -1
- data/lib/phlex/reactive/version.rb +1 -1
- metadata +1 -1
|
@@ -2021,6 +2021,30 @@ function deferNeeded(root) {
|
|
|
2021
2021
|
)
|
|
2022
2022
|
}
|
|
2023
2023
|
|
|
2024
|
+
// Issue #410: a tokenless CLIENT-ONLY root (a ClientBindings component: filter,
|
|
2025
|
+
// show, on_client) has no action of its own. It is transparent to server round
|
|
2026
|
+
// trips: a server trigger inside it dispatches on the nearest token-bearing root
|
|
2027
|
+
// above, and that root's #collectFields looks through it. A lazy or deferred
|
|
2028
|
+
// shell is tokenless by design and is NOT client-only.
|
|
2029
|
+
const AWAKE_ROOT = '[data-controller~="reactive"]'
|
|
2030
|
+
const ANY_ROOT = '[data-controller~="reactive"],[data-reactive-dormant~="reactive"]'
|
|
2031
|
+
|
|
2032
|
+
const clientOnlyRoot = (root) => root.getAttribute?.("data-reactive-token-value") == null && !deferNeeded(root)
|
|
2033
|
+
|
|
2034
|
+
// The first `selector` root at or above `from` that is not client-only, or
|
|
2035
|
+
// `stop` when the walk reaches it first, or null.
|
|
2036
|
+
function serverRoot(from, selector, stop) {
|
|
2037
|
+
let root = from?.closest?.(selector)
|
|
2038
|
+
while (root && root !== stop && clientOnlyRoot(root)) root = root.parentElement?.closest?.(selector)
|
|
2039
|
+
return root ?? null
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
// Connected controllers by root element, so a client-only root can hand a
|
|
2043
|
+
// dispatch to the server root above it. A dispatch for a root that has not
|
|
2044
|
+
// connected yet (a dormant root being woken) waits here until it does.
|
|
2045
|
+
const rootControllers = new WeakMap()
|
|
2046
|
+
const heldForwards = new WeakMap()
|
|
2047
|
+
|
|
2024
2048
|
// Dirty tracking with its navigate-away guard, and the paste-trigger gate:
|
|
2025
2049
|
// the root or anything inside it carries one of the two markers. (A nested
|
|
2026
2050
|
// root's marker loads the module for its parent too; the module then checks
|
|
@@ -2574,6 +2598,7 @@ export default class extends Controller {
|
|
|
2574
2598
|
// queueing them NOW — a connect-time seed below (a compute output write)
|
|
2575
2599
|
// dispatches real input events. The queue itself drains at the very end.
|
|
2576
2600
|
earlyState().connected.add(this.element)
|
|
2601
|
+
rootControllers.set(this.element, this)
|
|
2577
2602
|
|
|
2578
2603
|
// Root-id guard (issue #48). The token round trip assumes the reactive root
|
|
2579
2604
|
// element's id == component.id: the server targets component.id and the client
|
|
@@ -2999,6 +3024,14 @@ export default class extends Controller {
|
|
|
2999
3024
|
)
|
|
3000
3025
|
}
|
|
3001
3026
|
this.#drainEarly()
|
|
3027
|
+
// Issue #410: dispatches a client-only root inside handed over before this
|
|
3028
|
+
// root connected (a dormant root woken by that very trigger).
|
|
3029
|
+
// currentTarget is null once propagation ended: pin the trigger back on.
|
|
3030
|
+
for (const [event, target] of heldForwards.get(this.element) ?? []) {
|
|
3031
|
+
if (event.currentTarget !== target) Object.defineProperty(event, "currentTarget", { value: target, configurable: true })
|
|
3032
|
+
this.dispatch(event)
|
|
3033
|
+
}
|
|
3034
|
+
heldForwards.delete(this.element)
|
|
3002
3035
|
}
|
|
3003
3036
|
|
|
3004
3037
|
// The trigger elements whose :once descriptor THIS connection replayed —
|
|
@@ -3099,6 +3132,7 @@ export default class extends Controller {
|
|
|
3099
3132
|
}
|
|
3100
3133
|
// Early triggers (issue #273): a disconnected root records again.
|
|
3101
3134
|
earlyState().connected.delete(this.element)
|
|
3135
|
+
if (rootControllers.get(this.element) === this) rootControllers.delete(this.element)
|
|
3102
3136
|
this.element.removeAttribute?.("data-reactive-connected")
|
|
3103
3137
|
// A spent :once replay is remembered per element so a morph REPLY (root
|
|
3104
3138
|
// still connected) cannot re-arm it. A disconnect drops Stimulus's own
|
|
@@ -3116,6 +3150,12 @@ export default class extends Controller {
|
|
|
3116
3150
|
// a per-controller promise makes each dispatch wait for the previous one, so
|
|
3117
3151
|
// it always uses the freshest token.
|
|
3118
3152
|
dispatch(event) {
|
|
3153
|
+
// Issue #410: a client-only root has no action of its own — a server
|
|
3154
|
+
// trigger inside it belongs to the token-bearing root above. FIRST, so that
|
|
3155
|
+
// root runs every check below with its own element.
|
|
3156
|
+
if (event.params?.action && event.params.action !== LAZY_MATERIALIZE_ACTION && this.#clientOnly()) {
|
|
3157
|
+
return this.#forwardToServerRoot(event)
|
|
3158
|
+
}
|
|
3119
3159
|
// A shortcut (issue #357) that is the other platform's twin, or pressed
|
|
3120
3160
|
// in a field the user is typing in, is a complete no-op: the native
|
|
3121
3161
|
// default (text undo) proceeds and nothing is announced. Checked FIRST: a
|
|
@@ -3280,6 +3320,37 @@ export default class extends Controller {
|
|
|
3280
3320
|
}
|
|
3281
3321
|
|
|
3282
3322
|
|
|
3323
|
+
// A tokenless client-only root (issue #410). tokenValue is the Stimulus view
|
|
3324
|
+
// of data-reactive-token-value; the attribute check also rules out a shell.
|
|
3325
|
+
#clientOnly() {
|
|
3326
|
+
return !this.tokenValue && clientOnlyRoot(this.element)
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
// Hand a server trigger to the nearest token-bearing root above (issue #410).
|
|
3330
|
+
// A dormant one is woken (as early.js wakes a root on its own trigger) and
|
|
3331
|
+
// runs the dispatch when it connects. With no such root nothing is sent.
|
|
3332
|
+
#forwardToServerRoot(event) {
|
|
3333
|
+
const root = serverRoot(this.element.parentElement, ANY_ROOT)
|
|
3334
|
+
if (!root) {
|
|
3335
|
+
if (this.element.closest?.('[data-reactive-verbose="true"], [data-reactive-debug="true"]'))
|
|
3336
|
+
console.warn(`[phlex-reactive] "${event.params.action}": no token on this root or above, nothing sent`)
|
|
3337
|
+
return
|
|
3338
|
+
}
|
|
3339
|
+
const controller = rootControllers.get(root)
|
|
3340
|
+
if (controller) return controller.dispatch(event)
|
|
3341
|
+
// Held past this event's dispatch: cancel its native default NOW, by
|
|
3342
|
+
// dispatch()'s own rule, or a submit/link would act before the request.
|
|
3343
|
+
const { shortcut, window: windowBound, optimistic } = event.params
|
|
3344
|
+
const target = event.currentTarget ?? event.target
|
|
3345
|
+
if ((shortcut != null || !windowBound) && !this.#keepsNativeToggle(optimistic, target)) event.preventDefault()
|
|
3346
|
+
heldForwards.set(root, [...(heldForwards.get(root) ?? []), [event, target]])
|
|
3347
|
+
const data = root.dataset
|
|
3348
|
+
if (data.reactiveDormant) {
|
|
3349
|
+
data.controller = `${data.controller ?? ""} ${data.reactiveDormant}`.trim()
|
|
3350
|
+
delete data.reactiveDormant
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3283
3354
|
// CLIENT-ONLY trigger entry point (issue #95) — the zero-round-trip sibling
|
|
3284
3355
|
// of dispatch(). Wired by on_client: applies the declared op chain
|
|
3285
3356
|
// (data-reactive-ops-param, built by Phlex::Reactive::JS) locally. NO token,
|
|
@@ -4218,6 +4289,17 @@ export default class extends Controller {
|
|
|
4218
4289
|
return (el) => this.#ownsField(el)
|
|
4219
4290
|
}
|
|
4220
4291
|
|
|
4292
|
+
// #ownershipFilter for SERVER params (issue #410): nested client-only roots
|
|
4293
|
+
// are transparent, so a control under one is this root's when the first
|
|
4294
|
+
// token-bearing root above it is this.element. A nested token-bearing root
|
|
4295
|
+
// keeps its own fields (issue #15). Bindings, computes and listnav keep the
|
|
4296
|
+
// strict nearest-root #ownershipFilter.
|
|
4297
|
+
#serverFieldFilter() {
|
|
4298
|
+
const nested = this.element.querySelectorAll(AWAKE_ROOT)
|
|
4299
|
+
if (nested.length === 0 || [...nested].every(clientOnlyRoot)) return () => true
|
|
4300
|
+
return (el) => serverRoot(el, AWAKE_ROOT, this.element) === this.element
|
|
4301
|
+
}
|
|
4302
|
+
|
|
4221
4303
|
// One walk over THIS root's named controls (not a nested reactive root's),
|
|
4222
4304
|
// returning both the scalar `fields` and any chosen `files`. The ownership
|
|
4223
4305
|
// predicate is hoisted ONCE (issue #117) via #ownershipFilter — in the common
|
|
@@ -4229,7 +4311,7 @@ export default class extends Controller {
|
|
|
4229
4311
|
#collectFields() {
|
|
4230
4312
|
const fields = {}
|
|
4231
4313
|
const files = []
|
|
4232
|
-
const owns = this.#
|
|
4314
|
+
const owns = this.#serverFieldFilter() // compute ONCE per dispatch (issue #117)
|
|
4233
4315
|
const controls = []
|
|
4234
4316
|
this.element.querySelectorAll("input[name], select[name], textarea[name]").forEach((field) => {
|
|
4235
4317
|
if (!owns(field)) return
|
|
@@ -42,9 +42,9 @@ module Phlex
|
|
|
42
42
|
collection_remove: "reply.remove(model, from: :name)"
|
|
43
43
|
}.freeze
|
|
44
44
|
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
45
|
+
# `it` is illegal inside the define_singleton_method block (it takes
|
|
46
|
+
# ordinary |*, **| params), so the outer each_key must name its param
|
|
47
|
+
# explicitly.
|
|
48
48
|
REMOVED_CLASS_VERBS.each_key do |verb|
|
|
49
49
|
define_singleton_method(verb) do |*, **|
|
|
50
50
|
raise ArgumentError,
|
|
@@ -52,7 +52,6 @@ module Phlex
|
|
|
52
52
|
"return #{REMOVED_CLASS_VERBS[verb]} from the action (reply is the only door)"
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
|
-
# rubocop:enable Style/ItBlockParameter
|
|
56
55
|
|
|
57
56
|
class << self
|
|
58
57
|
# Re-render the component in place (explicit form of today's default).
|
|
@@ -625,7 +625,6 @@ module Phlex
|
|
|
625
625
|
# Define the guided-error stub for each removed broadcast method (issue
|
|
626
626
|
# #185). `verb` is referenced in define_method AND the message, so the outer
|
|
627
627
|
# block param must be named — `it` is illegal here.
|
|
628
|
-
# rubocop:disable-next Style/ItBlockParameter
|
|
629
628
|
Phlex::Reactive::Streamable::REMOVED_BROADCASTS.each_key do |verb|
|
|
630
629
|
define_method(verb) do |*, **|
|
|
631
630
|
raise NoMethodError,
|