phlex-reactive 0.12.2 → 0.12.3
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 +49 -0
- data/README.md +135 -1
- data/app/javascript/phlex/reactive/compute.js +152 -0
- data/app/javascript/phlex/reactive/compute.min.js +2 -2
- data/app/javascript/phlex/reactive/compute.min.js.map +3 -3
- data/app/javascript/phlex/reactive/reactive_controller.js +214 -2
- 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 +84 -0
- data/lib/phlex/reactive/component/helpers.rb +16 -0
- data/lib/phlex/reactive/component/registry.rb +2 -0
- data/lib/phlex/reactive/component.rb +9 -0
- data/lib/phlex/reactive/js.rb +15 -0
- data/lib/phlex/reactive/show_conditions.rb +116 -0
- data/lib/phlex/reactive/streamable.rb +13 -8
- data/lib/phlex/reactive/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8d6ffd3dcf058d459bb58de296bf1d5cb9dd0808164d35c71ffbf60f437e0301
|
|
4
|
+
data.tar.gz: 11fc37b314bb89a87fa45db4c34328e2976dc82e00106383328b0fb18dd16097
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1625e8fb285dc9ca91d73609896452ff5998a6fcb90eca3e3f7ef98a1397db764a966a161f81db9795b0489afd193035a62255bf7223c72271515573e10ecd32
|
|
7
|
+
data.tar.gz: 6c1448c90d1aad7e1814938b363c28108b924a20ef7bb7a8a4f67b1cfaa2706f62cc3724a43f72aa7aedeb28da2aaf177924268972dd965b261a305381244529
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,50 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
8
8
|
|
|
9
9
|
### Added
|
|
10
10
|
|
|
11
|
+
- **Reducer-emitted client ops (`$ops`), a `submit` op, and declarative
|
|
12
|
+
`reactive_on_complete` (#226).** The "normalize on input, commit when
|
|
13
|
+
complete" field (a one-time-code entry being the canonical case) is now
|
|
14
|
+
declarable end to end, single- or multi-input, with no bespoke controller:
|
|
15
|
+
- **`js.submit(to = :root)`** joins the op vocabulary: requestSubmit the
|
|
16
|
+
target's own form (the target itself when it *is* a form, `input.form`
|
|
17
|
+
for a control, else `closest("form")`). A real cancelable `submit` event
|
|
18
|
+
fires, so it composes with native/Turbo forms AND
|
|
19
|
+
`on(:save, event: "submit")` interception —
|
|
20
|
+
`select(**on_client(:change, js.submit("form")))` is the one-line general
|
|
21
|
+
autosubmit. **Actor-only like focus**: refused in `broadcast_to(js:)`
|
|
22
|
+
(allowed in `reply.js`); `on_client(:submit, js.submit)` raises (it would
|
|
23
|
+
re-fire itself).
|
|
24
|
+
- **The reserved `$ops` reducer output**: a `reactive_compute` reducer may
|
|
25
|
+
return `$ops` holding an op chain — built with the new immutable `ops`
|
|
26
|
+
builder exported from `phlex/reactive/compute` (verbs mirror the wire op
|
|
27
|
+
names) or a raw `[[op, args], …]` list — run through the same frozen
|
|
28
|
+
client-op whitelist as a final phase after the write set settles.
|
|
29
|
+
**Rising-edge, keyed on chain content, event-gated**: an identical chain
|
|
30
|
+
never re-fires (a capped 7th keystroke can't re-submit), a changed chain
|
|
31
|
+
fires again (per-digit focus advance across split boxes), and the
|
|
32
|
+
connect/morph seed pass arms without firing (a validation-error re-render
|
|
33
|
+
with a complete value never self-submits).
|
|
34
|
+
- **`{ length: … }` in the ONE conditions language**: exact
|
|
35
|
+
(`{ length: 6 }`) or Integer-Range length predicates for
|
|
36
|
+
`reactive_show` / `reactive_show_targets` / `reactive_on_complete`,
|
|
37
|
+
counted in **codepoints** on both sides (Ruby `String#length`, client
|
|
38
|
+
`[...value].length`) — the shared parity fixture gains a multibyte proof
|
|
39
|
+
vector.
|
|
40
|
+
- **`reactive_on_complete`** — the zero-JavaScript declarative twin: the
|
|
41
|
+
same `if:`/`if_any:`/`unless:` kwargs as `reactive_show` plus `run:` (a
|
|
42
|
+
`js` chain, now buildable at class level, or an allowlist-checked raw
|
|
43
|
+
list), emitted as one root attr and evaluated by the generic controller
|
|
44
|
+
with the same rising-edge/arming semantics as `$ops`.
|
|
45
|
+
`reactive_on_complete if: { code: { length: 6 } },
|
|
46
|
+
run: js.dispatch("code:complete")` + `on(:verify, event: "code:complete")`
|
|
47
|
+
is a complete auto-committing code field with no reducer at all.
|
|
48
|
+
- Dummy flagships: `VerificationCodeComponent` (single input + `$ops`
|
|
49
|
+
submit), `SplitCodeComponent` (six boxes: paste redistribution,
|
|
50
|
+
reducer-driven focus advance, hidden joined-code output),
|
|
51
|
+
`CodeCompleteComponent` (declarative, zero JS), and the
|
|
52
|
+
`AutosubmitFilterComponent` select-driven GET filter — each with a
|
|
53
|
+
real-browser system spec.
|
|
54
|
+
|
|
11
55
|
- **Instance-dynamic wire names — keyword escape hatches on the
|
|
12
56
|
field-compiling helpers (#224).** A form builder's wire name is computed per
|
|
13
57
|
instance (`user[tags]`), which the class-level `reactive_scope` compile can't
|
|
@@ -240,6 +284,11 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
240
284
|
|
|
241
285
|
### Fixed
|
|
242
286
|
|
|
287
|
+
- **`rake bench` ran the removed `broadcast_replace_to` API and exited 1.**
|
|
288
|
+
`benchmark/micro/broadcast.rb` migrated to the #185 spellings
|
|
289
|
+
(`broadcast_to(*key, replace:)` / `broadcast_to(each:, replace:)`); the
|
|
290
|
+
transport double is unchanged.
|
|
291
|
+
|
|
243
292
|
- **`reactive_nested_remove(confirm:)` now interpolates `%{field}` on client-added
|
|
244
293
|
rows (#222).** A row added in the browser via `reactive_nested_add` is a
|
|
245
294
|
`cloneNode` of the `<template>`, and the clone path (`#renumberNestedRow` /
|
data/README.md
CHANGED
|
@@ -853,6 +853,16 @@ button(**on_client(:click, js
|
|
|
853
853
|
another component or a plain Stimulus controller can react to a client-only
|
|
854
854
|
interaction — `to:` picks the element (default: the component root), `detail:`
|
|
855
855
|
is the payload.
|
|
856
|
+
- **`submit(to = :root)`** commits the **target's own form** via
|
|
857
|
+
`requestSubmit()`: the target itself when it *is* a form, its form owner for a
|
|
858
|
+
control (`input.form`, honoring a `form=` attribute), else the nearest ancestor
|
|
859
|
+
form. Constraint validation runs and a **real cancelable `submit` event**
|
|
860
|
+
fires, so it composes with both a native/Turbo form *and* an
|
|
861
|
+
`on(:save, event: "submit")` interception. **Actor-only like focus**: allowed
|
|
862
|
+
from `on_client` / `reply.js` / a reducer's `$ops`, refused in
|
|
863
|
+
`broadcast_to(js:)` (a broadcast would force-submit every subscriber's form).
|
|
864
|
+
Binding a submit op to the `submit` event itself raises at render — it would
|
|
865
|
+
re-fire itself forever.
|
|
856
866
|
- **`transition: { during:, from:, to: }`** on `show`/`hide`/`toggle` animates the
|
|
857
867
|
visibility flip: `during`+`from` are applied, then `from`→`to` swaps on the next
|
|
858
868
|
frame, and the helper classes are cleaned up on `animationend` (with a timeout
|
|
@@ -863,6 +873,27 @@ button(**on_client(:click, js
|
|
|
863
873
|
modifiers: the dropdown above closes on any click outside the component, and
|
|
864
874
|
window-bound triggers never `preventDefault`, so links elsewhere keep working.
|
|
865
875
|
|
|
876
|
+
**The general autosubmit story.** With `submit` in the vocabulary, the classic
|
|
877
|
+
`onchange="this.form.requestSubmit()"` filter form is one declared line — no
|
|
878
|
+
bespoke controller, no reactive action, and Turbo Drive turns the resulting
|
|
879
|
+
submit into a normal visit:
|
|
880
|
+
|
|
881
|
+
```ruby
|
|
882
|
+
form(action: "/products", method: "get") do
|
|
883
|
+
select(name: "sort", **mix(on_client(:change, js.submit("form")), data: { testid: "sort" })) do
|
|
884
|
+
option(value: "name") { "Name" }
|
|
885
|
+
option(value: "price") { "Price" }
|
|
886
|
+
end
|
|
887
|
+
end
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
Use `change`-bound autosubmit for discrete controls (selects, radios,
|
|
891
|
+
checkboxes). For a **text** field that should commit "when the value is
|
|
892
|
+
complete," an unconditional `on_client(:input, js.submit)` would fire on every
|
|
893
|
+
keystroke — that conditional case is exactly what a reducer's
|
|
894
|
+
[`$ops`](#client-side-computes-reactive_compute--reactive_text) and
|
|
895
|
+
[`reactive_on_complete`](#declarative-completion-reactive_on_complete) are for.
|
|
896
|
+
|
|
866
897
|
**Client ops are ephemeral UI — the one contract to internalize.** Any server
|
|
867
898
|
re-render of the component (an action reply, a broadcast, a morph) rebuilds
|
|
868
899
|
from server state and resets whatever the ops toggled: the menu closes, the tab
|
|
@@ -958,7 +989,11 @@ end
|
|
|
958
989
|
- **The value language**: a **Hash is an AND** (multiple keys ANDed), an
|
|
959
990
|
**Array is membership**, a **Range is a threshold** (`10..` ≥ 10, `..10` ≤ 10,
|
|
960
991
|
`...10` < 10, `10..20` between), `true`/`false` compare a checkbox's checked
|
|
961
|
-
state, `nil` matches blank
|
|
992
|
+
state, `nil` matches blank, and **`{ length: … }` compares the value's
|
|
993
|
+
length** — exact (`{ length: 6 }`) or an Integer Range (`{ length: 6.. }`,
|
|
994
|
+
`{ length: 4..8 }`). Length counts **codepoints** on both sides (Ruby
|
|
995
|
+
`String#length`, client `[...value].length`), so multibyte input agrees; a
|
|
996
|
+
blank field has length 0. `unless:` **negates** and composes with `if:`.
|
|
962
997
|
Never an expression — every term is a declared literal, so there is no eval
|
|
963
998
|
surface. A blank/non-numeric value fails a numeric term **closed** (hidden).
|
|
964
999
|
- **OR-of-AND** — `if_any:` takes an array of AND-hashes (`if_any: [{ director:
|
|
@@ -1097,6 +1132,105 @@ setComputeReducer("preview", ({ title }) => ({
|
|
|
1097
1132
|
a later morph repaints stale text — the same reconcile contract the whole
|
|
1098
1133
|
new-vs-persisted split relies on.
|
|
1099
1134
|
|
|
1135
|
+
**Reducer-emitted ops (`$ops`) — commit when complete.** A reducer's outputs
|
|
1136
|
+
write fields and text; the reserved **`$ops`** key lets it emit a **conditional
|
|
1137
|
+
side effect** — the missing piece that used to force a bespoke controller next
|
|
1138
|
+
to an otherwise-declarative compute. Return an op chain (the `ops` builder
|
|
1139
|
+
mirrors the Ruby `js` verbs, or use a raw `[[op, args], …]` array) and the
|
|
1140
|
+
controller runs it through the **same frozen op whitelist** `on_client` uses,
|
|
1141
|
+
as a final phase **after** the field writes, text sinks, and their dispatched
|
|
1142
|
+
`input` events settle. The canonical one-time-code field:
|
|
1143
|
+
|
|
1144
|
+
```js
|
|
1145
|
+
import { setComputeReducer, ops } from "phlex/reactive/compute"
|
|
1146
|
+
|
|
1147
|
+
setComputeReducer("otp", ({ code }) => {
|
|
1148
|
+
const digits = code.replace(/\D/g, "").slice(0, 6)
|
|
1149
|
+
return { code: digits, $ops: digits.length === 6 ? ops.submit() : null }
|
|
1150
|
+
})
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
```ruby
|
|
1154
|
+
form(action: "/verify", method: "post",
|
|
1155
|
+
**mix(reactive_root(compute: :otp), on(:verify, event: "submit"))) do
|
|
1156
|
+
input(name: "code", inputmode: "numeric", autocomplete: "one-time-code")
|
|
1157
|
+
end
|
|
1158
|
+
```
|
|
1159
|
+
|
|
1160
|
+
Typing, pasting `123-456`, or platform SMS autofill all arrive as `input`
|
|
1161
|
+
events → the reducer normalizes, and at six digits `submit` requestSubmits the
|
|
1162
|
+
form — which `on(:verify, event: "submit")` intercepts into **one signed action
|
|
1163
|
+
POST**. The contract that makes this safe:
|
|
1164
|
+
|
|
1165
|
+
- **Rising edge, keyed on content.** The chain runs only when it **differs**
|
|
1166
|
+
from the previous pass's chain (including from "absent"). Returning the same
|
|
1167
|
+
chain again is settled — a 7th keystroke capped back to the same six digits
|
|
1168
|
+
can't re-submit — while a **different** chain fires again (a multi-box
|
|
1169
|
+
reducer advancing focus emits a new `ops.focus` target per digit). A pass
|
|
1170
|
+
returning `null`/no `$ops` re-arms.
|
|
1171
|
+
- **Event-gated.** The connect/morph **seed pass arms without firing** — a form
|
|
1172
|
+
re-rendered with an already-complete value (a validation-error morph, a
|
|
1173
|
+
browser restore) never auto-fires, which is what breaks the
|
|
1174
|
+
submit → error re-render → re-seed → submit loop.
|
|
1175
|
+
- **Whitelisted.** `$ops` is consumed before the write phases (never painted as
|
|
1176
|
+
a field/text/mirror), and unknown ops warn-and-skip while siblings apply.
|
|
1177
|
+
- **Multi-input works with the same machinery**: declare all boxes as inputs,
|
|
1178
|
+
join + redistribute in the reducer (a paste into any box fans out one digit
|
|
1179
|
+
per box), mirror the joined value into a hidden field, advance focus with a
|
|
1180
|
+
per-digit `ops.focus`, and `ops.submit()` on completion. See
|
|
1181
|
+
`spec/dummy/app/components/split_code_component.rb` for the full six-box
|
|
1182
|
+
example.
|
|
1183
|
+
|
|
1184
|
+
When you *don't* want to auto-submit, the same slot dispatches a completion
|
|
1185
|
+
event (`ops.dispatch("code:complete")`) for a sibling to react to, or enables
|
|
1186
|
+
the submit button (`ops.remove_attr("[type=submit]", "disabled")`) and leaves
|
|
1187
|
+
the commit to the user.
|
|
1188
|
+
|
|
1189
|
+
### Declarative completion (`reactive_on_complete`)
|
|
1190
|
+
|
|
1191
|
+
The `$ops` escape hatch puts the condition in the reducer; when the condition
|
|
1192
|
+
is expressible in the [conditions language](#value-conditional-visibility-reactive_show),
|
|
1193
|
+
`reactive_on_complete` declares the whole binding in Ruby — **zero JavaScript,
|
|
1194
|
+
no reducer**:
|
|
1195
|
+
|
|
1196
|
+
```ruby
|
|
1197
|
+
class CodeCompleteComponent < ApplicationComponent
|
|
1198
|
+
include Phlex::Reactive::Streamable
|
|
1199
|
+
include Phlex::Reactive::Component
|
|
1200
|
+
|
|
1201
|
+
reactive_state :code
|
|
1202
|
+
action :verify, params: { code: :string }
|
|
1203
|
+
|
|
1204
|
+
reactive_on_complete if: { code: { length: 6 } }, run: js.dispatch("code:complete")
|
|
1205
|
+
|
|
1206
|
+
def view_template
|
|
1207
|
+
div(**mix(reactive_root, on(:verify, event: "code:complete"))) do
|
|
1208
|
+
input(name: "code")
|
|
1209
|
+
end
|
|
1210
|
+
end
|
|
1211
|
+
end
|
|
1212
|
+
```
|
|
1213
|
+
|
|
1214
|
+
The generic controller evaluates the conditions over the owned fields on every
|
|
1215
|
+
`input`/`change` (scope-aware, same resolver as `reactive_show`) and runs the
|
|
1216
|
+
declared ops on the **rising edge** — once, when the conditions first become
|
|
1217
|
+
true; going false re-arms; the connect/morph pass arms **without** firing, so a
|
|
1218
|
+
re-render with already-satisfied conditions never self-fires. The pieces:
|
|
1219
|
+
|
|
1220
|
+
- **Conditions** are the same `if:` / `if_any:` / `unless:` kwargs
|
|
1221
|
+
`reactive_show` takes — including the `length:` form above, which is what
|
|
1222
|
+
makes "exactly six characters" declarable.
|
|
1223
|
+
- **`run:`** is a `js` chain (available at class level) or a raw op list
|
|
1224
|
+
(re-checked through the attribute allowlist). `run: js.submit` auto-commits;
|
|
1225
|
+
`run: js.dispatch(...)` lets a sibling `on(:action, event: "...")` turn
|
|
1226
|
+
completion into a signed action, as above.
|
|
1227
|
+
- **Several bindings** coexist under names:
|
|
1228
|
+
`reactive_on_complete :commit, if: …, run: js.submit` — each latches
|
|
1229
|
+
independently; redeclaring a name overrides it (normal registry inheritance).
|
|
1230
|
+
- Prefer `$ops` when completion needs **normalization first** (strip
|
|
1231
|
+
separators, cap length) — the reducer already knows the cleaned value;
|
|
1232
|
+
prefer `reactive_on_complete` when the raw field value is the truth.
|
|
1233
|
+
|
|
1100
1234
|
### Cross-root mirrors (`mirror:`) — painting a recap outside the root
|
|
1101
1235
|
|
|
1102
1236
|
`reactive_text` is deliberately **root-isolated** (a nested component's nodes are
|
|
@@ -71,6 +71,39 @@
|
|
|
71
71
|
// the re-entrant `changed === "field_c"` pass derives field_a back to the
|
|
72
72
|
// value it already holds — no write, no event, settled in one bounce.
|
|
73
73
|
|
|
74
|
+
// THE RESERVED `$ops` OUTPUT (issue #226). Besides field/text outputs, a
|
|
75
|
+
// reducer may return the reserved key `$ops` holding a chain of client DOM
|
|
76
|
+
// ops — built with the `ops` builder below, or a raw [[name, args], ...]
|
|
77
|
+
// array. The controller consumes it as a PHASE 4 of the single-pass write set:
|
|
78
|
+
// the ops run AFTER the field writes, text sinks, and phase-3 input dispatches
|
|
79
|
+
// settle, through the SAME frozen CLIENT_OPS whitelist on_client uses (an
|
|
80
|
+
// unknown op warns + is skipped). `null`/`undefined`/absent = no effect.
|
|
81
|
+
//
|
|
82
|
+
// RISING-EDGE semantics, keyed on CONTENT: the chain runs only when it
|
|
83
|
+
// DIFFERS from the previous pass's chain (including from "absent"), and only
|
|
84
|
+
// on EVENT-DRIVEN passes. Returning the SAME chain again is settled — no
|
|
85
|
+
// re-fire (a 7th keystroke capped back to the same complete value can't
|
|
86
|
+
// re-submit), mirroring the change-guarded field writes. Returning a
|
|
87
|
+
// DIFFERENT chain fires again — a multi-box reducer advancing focus
|
|
88
|
+
// box-by-box emits a new focus target per digit, each a new intent. The
|
|
89
|
+
// connect/morph SEED pass (issue #199 — recompute with no event) ARMS the
|
|
90
|
+
// latch but never fires — a form re-rendered with an already-complete value
|
|
91
|
+
// (a validation-error morph, a browser restore) must not auto-fire; that is
|
|
92
|
+
// what breaks the submit → error re-render → re-seed → submit loop. A later
|
|
93
|
+
// pass returning no $ops re-arms. The canonical use — a one-time-code field
|
|
94
|
+
// that normalizes on input and commits when complete:
|
|
95
|
+
//
|
|
96
|
+
// import { setComputeReducer, ops } from "phlex/reactive/compute"
|
|
97
|
+
// setComputeReducer("otp", ({ code }) => {
|
|
98
|
+
// const digits = code.replace(/\D/g, "").slice(0, 6)
|
|
99
|
+
// return { code: digits, $ops: digits.length === 6 ? ops.submit() : null }
|
|
100
|
+
// })
|
|
101
|
+
//
|
|
102
|
+
// `submit` commits the target's own form via requestSubmit() — the real submit
|
|
103
|
+
// event fires, so an on(:verify, event: "submit") interception (or a native/
|
|
104
|
+
// Turbo form) handles it exactly like a user submit. submit/focus are
|
|
105
|
+
// ACTOR-ONLY: usable here and in on_client/reply.js, refused in broadcasts.
|
|
106
|
+
|
|
74
107
|
const reducers = new Map()
|
|
75
108
|
|
|
76
109
|
// Register (or replace) the reducer for `key`. `fn` is
|
|
@@ -91,3 +124,122 @@ export function computeReducer(key) {
|
|
|
91
124
|
export function __resetComputeRegistryForTest() {
|
|
92
125
|
reducers.clear()
|
|
93
126
|
}
|
|
127
|
+
|
|
128
|
+
// --- The reducer-side op-chain builder (issue #226) --------------------------
|
|
129
|
+
//
|
|
130
|
+
// A thin, IMMUTABLE mirror of the Ruby Phlex::Reactive::JS builder: verbs carry
|
|
131
|
+
// the WIRE op names (snake_case) and append [name, args] pairs; every verb
|
|
132
|
+
// returns a NEW instance, so a chain held in a constant can never be mutated by
|
|
133
|
+
// later use. `.ops` exposes the raw [[name, args], ...] list the controller
|
|
134
|
+
// interprets (and toJSON serializes it, so a chain can also feed a hand-built
|
|
135
|
+
// ops attr). Targets: a CSS selector string, or omit for "@root" (the
|
|
136
|
+
// component's own root). No build-time attr validation here — the interpreter's
|
|
137
|
+
// allowlist (guardAttr) is the enforcement point; this builder only shapes the
|
|
138
|
+
// wire.
|
|
139
|
+
const ROOT_SENTINEL = "@root"
|
|
140
|
+
|
|
141
|
+
function targetArgs(to, { global, transition } = {}) {
|
|
142
|
+
const args = { to: to ?? ROOT_SENTINEL }
|
|
143
|
+
if (global) args.global = true
|
|
144
|
+
if (transition) args.transition = normalizeTransition(transition)
|
|
145
|
+
return args
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Named legs { during, from, to } → the [during, from, to] wire array (the
|
|
149
|
+
// issue #186 vocabulary). Loud at authoring time, like the Ruby builder.
|
|
150
|
+
// Frozen, like every nested payload — the chain's immutability contract must
|
|
151
|
+
// hold all the way down (the Ruby twin freezes its legs array too).
|
|
152
|
+
function normalizeTransition(transition) {
|
|
153
|
+
const named =
|
|
154
|
+
transition && typeof transition === "object" && !Array.isArray(transition) &&
|
|
155
|
+
["during", "from", "to"].every((k) => k in transition)
|
|
156
|
+
if (!named) throw new Error("[phlex-reactive] ops transition takes named legs { during, from, to }")
|
|
157
|
+
return Object.freeze([String(transition.during), String(transition.from), String(transition.to)])
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
class OpsChain {
|
|
161
|
+
constructor(list = Object.freeze([])) {
|
|
162
|
+
this.ops = list
|
|
163
|
+
Object.freeze(this)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
show(to, opts) {
|
|
167
|
+
return this.#append("show", targetArgs(to, opts))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
hide(to, opts) {
|
|
171
|
+
return this.#append("hide", targetArgs(to, opts))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
toggle(to, opts) {
|
|
175
|
+
return this.#append("toggle", targetArgs(to, opts))
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
add_class(to, classes, opts) {
|
|
179
|
+
return this.#append("add_class", classArgs(to, classes, opts))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
remove_class(to, classes, opts) {
|
|
183
|
+
return this.#append("remove_class", classArgs(to, classes, opts))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
toggle_class(to, classes, opts) {
|
|
187
|
+
return this.#append("toggle_class", classArgs(to, classes, opts))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
set_attr(to, name, value, opts) {
|
|
191
|
+
return this.#append("set_attr", { ...targetArgs(to, opts), name: String(name), value: String(value) })
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
remove_attr(to, name, opts) {
|
|
195
|
+
return this.#append("remove_attr", { ...targetArgs(to, opts), name: String(name) })
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
toggle_attr(to, name, opts) {
|
|
199
|
+
return this.#append("toggle_attr", { ...targetArgs(to, opts), name: String(name) })
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
focus(to, opts) {
|
|
203
|
+
return this.#append("focus", targetArgs(to, opts))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
focus_first(to, opts) {
|
|
207
|
+
return this.#append("focus_first", targetArgs(to, opts))
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
text(to, value, opts) {
|
|
211
|
+
return this.#append("text", { ...targetArgs(to, opts), value: String(value ?? "") })
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
dispatch(name, { to, detail, global } = {}) {
|
|
215
|
+
const args = { name: String(name), to: to ?? ROOT_SENTINEL, detail: detail ?? {} }
|
|
216
|
+
if (global) args.global = true
|
|
217
|
+
return this.#append("dispatch", args)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
submit(to, opts) {
|
|
221
|
+
return this.#append("submit", targetArgs(to, opts))
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
toJSON() {
|
|
225
|
+
return this.ops
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
#append(name, args) {
|
|
229
|
+
return new OpsChain(Object.freeze([...this.ops, Object.freeze([name, Object.freeze(args)])]))
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// classes: one class string or an array of them (never whitespace-split — a
|
|
234
|
+
// classList token can't contain spaces, so splitting would only mask a bug).
|
|
235
|
+
// Loud on an empty/missing list, and frozen (the Ruby twin freezes its class
|
|
236
|
+
// list too) — a chain held in a constant must stay immutable all the way down.
|
|
237
|
+
function classArgs(to, classes, opts) {
|
|
238
|
+
const list = classes == null ? [] : (Array.isArray(classes) ? classes : [classes]).map(String)
|
|
239
|
+
if (list.length === 0) throw new Error("[phlex-reactive] a class op needs at least one class")
|
|
240
|
+
return { ...targetArgs(to, opts), classes: Object.freeze(list) }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// The shared empty chain — start every reducer effect from here:
|
|
244
|
+
// $ops: done ? ops.dispatch("code:complete").submit() : null
|
|
245
|
+
export const ops = new OpsChain()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var F=new Map;function M(j,q){F.set(j,q)}function P(j){return F.get(j)}function Q(){F.clear()}var J="@root";function x(j,{global:q,transition:w}={}){let B={to:j??J};if(q)B.global=!0;if(w)B.transition=K(w);return B}function K(j){if(!(j&&typeof j==="object"&&!Array.isArray(j)&&["during","from","to"].every((w)=>(w in j))))throw Error("[phlex-reactive] ops transition takes named legs { during, from, to }");return Object.freeze([String(j.during),String(j.from),String(j.to)])}class G{constructor(j=Object.freeze([])){this.ops=j,Object.freeze(this)}show(j,q){return this.#j("show",x(j,q))}hide(j,q){return this.#j("hide",x(j,q))}toggle(j,q){return this.#j("toggle",x(j,q))}add_class(j,q,w){return this.#j("add_class",D(j,q,w))}remove_class(j,q,w){return this.#j("remove_class",D(j,q,w))}toggle_class(j,q,w){return this.#j("toggle_class",D(j,q,w))}set_attr(j,q,w,B){return this.#j("set_attr",{...x(j,B),name:String(q),value:String(w)})}remove_attr(j,q,w){return this.#j("remove_attr",{...x(j,w),name:String(q)})}toggle_attr(j,q,w){return this.#j("toggle_attr",{...x(j,w),name:String(q)})}focus(j,q){return this.#j("focus",x(j,q))}focus_first(j,q){return this.#j("focus_first",x(j,q))}text(j,q,w){return this.#j("text",{...x(j,w),value:String(q??"")})}dispatch(j,{to:q,detail:w,global:B}={}){let H={name:String(j),to:q??J,detail:w??{}};if(B)H.global=!0;return this.#j("dispatch",H)}submit(j,q){return this.#j("submit",x(j,q))}toJSON(){return this.ops}#j(j,q){return new G(Object.freeze([...this.ops,Object.freeze([j,Object.freeze(q)])]))}}function D(j,q,w){let B=q==null?[]:(Array.isArray(q)?q:[q]).map(String);if(B.length===0)throw Error("[phlex-reactive] a class op needs at least one class");return{...x(j,w),classes:Object.freeze(B)}}var U=new G;export{M as setComputeReducer,U as ops,P as computeReducer,Q as __resetComputeRegistryForTest};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=3CB48262B60C70DD64756E2164756E21
|
|
4
4
|
//# sourceMappingURL=compute.min.js.map
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["compute.js"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"// The client-side compute (data-binding) registry — the \"instant\" half of the\n// new/unpersisted-record UX.\n//\n// A record-backed reactive component round-trips every change to the server\n// (the signed identity re-finds the record; the server re-renders). A NEW,\n// unpersisted record has no such server truth to re-render against on every\n// keystroke — the classic answer is a bespoke Stimulus controller doing the math\n// in the browser (carlqvist's new_order_controller.js). This registry lets that\n// math be a DECLARED part of the component instead: `reactive_compute :name,\n// inputs:, outputs:` (Ruby) names a reducer registered here, and the generic\n// reactive controller runs it on `input` — writing the outputs with NO round\n// trip. When the component ALSO carries on(...) (a persisted record, or a draft\n// you sync), the debounced POST reconciles from the authoritative server reply.\n//\n// The seam mirrors confirm.js: a settable registry with a lookup the controller\n// calls. Register once at boot:\n//\n// import { setComputeReducer } from \"phlex/reactive/compute\"\n// setComputeReducer(\"payment_split\", ({ allowance, cash, leasing, total }) => ({\n// allowance, leasing, cash: total - allowance - leasing,\n// }))\n//\n// The reducer's signature is (values, meta):\n//\n// values — a plain object of { inputName: value } over the declared inputs.\n// Untyped inputs (the array form) AND :number-typed inputs arrive as\n// Numbers (blank/NaN → 0); :string-typed inputs (the hash form,\n// issue #104) arrive as the RAW string. `reactive_compute :x, inputs:\n// { title: :string, qty: :number }` is what selects per-input types;\n// `inputs: %i[a b]` stays all-numeric (backward compatible).\n// meta — { changed }: the name (string) of the declared input the\n// triggering event edited, or null (a direct recompute() call, or a\n// target this root doesn't own / didn't declare as an input).\n//\n// OUTPUTS may be a form FIELD or a TEXT NODE (issue #104). An output whose name\n// matches an owned control writes its .value (+ the change-guarded input\n// dispatch below). An output with NO matching field writes textContent to every\n// owned [data-reactive-text=\"<name>\"] node (reactive_text(:name)) — XSS-safe by\n// construction, change-guarded, NO input dispatch (a text node has no listener\n// contract). A declared INPUT also mirrors into its own text node on every\n// input via an always-run pass — so reactive_text(:title) is a live field\n// preview with NO registered reducer at all.\n//\n// It returns a plain object of { outputName: value } — only the outputs it\n// names are written, so it can leave the edited field (and its caret)\n// untouched. A one-argument reducer keeps working unchanged (it just ignores\n// meta). `changed` is what makes a MULTI-WAY / MUTUAL rebalance expressible as\n// one reducer (issue #75) — branch on which field the user edited:\n//\n// setComputeReducer(\"three_way_split\", ({ field_a, field_b, field_c, total }, { changed }) => {\n// if (changed === \"field_c\") return { field_a: total - field_c - field_b }\n// return { field_c: total - field_a - field_b }\n// })\n//\n// Output writes are CHANGE-GUARDED: the controller writes a field and\n// dispatches a bubbling `input` event on it ONLY when the new value differs\n// from the field's current value (real browsers never fire `input` on a\n// programmatic .value write, so the controller dispatches explicitly — that's\n// what drives a chained summary repaint, matching the server's set_value +\n// dispatch(\"input\") contract). Returning the SAME value a field already holds\n// is skipped entirely — no write, no event — which is why a reducer with\n// overlapping inputs/outputs (like payment_split above) settles instead of\n// re-entering itself forever.\n//\n// CONVERGENCE REQUIREMENT: because an output write dispatches a REAL input\n// event (issue #76), recompute re-enters with changed = that OUTPUT field's\n// name (when it's also a declared input). A branching reducer must therefore\n// be convergent: the re-entrant pass must compute values EQUAL to what the\n// first pass already wrote to the DOM, so the change guard settles the chain.\n// The three_way_split above is: after `changed === \"field_a\"` writes field_c,\n// the re-entrant `changed === \"field_c\"` pass derives field_a back to the\n// value it already holds — no write, no event, settled in one bounce.\n\nconst reducers = new Map()\n\n// Register (or replace) the reducer for `key`. `fn` is\n// (values: Record<string, number>, meta: { changed: string | null })\n// => Record<string, unknown>.\nexport function setComputeReducer(key, fn) {\n reducers.set(key, fn)\n}\n\n// Look up a registered reducer; undefined when none — the controller then makes\n// #recompute a no-op rather than throwing (a missing reducer must not break the\n// page; it just means no client-side binding for that root).\nexport function computeReducer(key) {\n return reducers.get(key)\n}\n\n// Test seam: clear the registry so a reducer registered in one test can't leak.\nexport function __resetComputeRegistryForTest() {\n reducers.clear()\n}\n"
|
|
5
|
+
"// The client-side compute (data-binding) registry — the \"instant\" half of the\n// new/unpersisted-record UX.\n//\n// A record-backed reactive component round-trips every change to the server\n// (the signed identity re-finds the record; the server re-renders). A NEW,\n// unpersisted record has no such server truth to re-render against on every\n// keystroke — the classic answer is a bespoke Stimulus controller doing the math\n// in the browser (carlqvist's new_order_controller.js). This registry lets that\n// math be a DECLARED part of the component instead: `reactive_compute :name,\n// inputs:, outputs:` (Ruby) names a reducer registered here, and the generic\n// reactive controller runs it on `input` — writing the outputs with NO round\n// trip. When the component ALSO carries on(...) (a persisted record, or a draft\n// you sync), the debounced POST reconciles from the authoritative server reply.\n//\n// The seam mirrors confirm.js: a settable registry with a lookup the controller\n// calls. Register once at boot:\n//\n// import { setComputeReducer } from \"phlex/reactive/compute\"\n// setComputeReducer(\"payment_split\", ({ allowance, cash, leasing, total }) => ({\n// allowance, leasing, cash: total - allowance - leasing,\n// }))\n//\n// The reducer's signature is (values, meta):\n//\n// values — a plain object of { inputName: value } over the declared inputs.\n// Untyped inputs (the array form) AND :number-typed inputs arrive as\n// Numbers (blank/NaN → 0); :string-typed inputs (the hash form,\n// issue #104) arrive as the RAW string. `reactive_compute :x, inputs:\n// { title: :string, qty: :number }` is what selects per-input types;\n// `inputs: %i[a b]` stays all-numeric (backward compatible).\n// meta — { changed }: the name (string) of the declared input the\n// triggering event edited, or null (a direct recompute() call, or a\n// target this root doesn't own / didn't declare as an input).\n//\n// OUTPUTS may be a form FIELD or a TEXT NODE (issue #104). An output whose name\n// matches an owned control writes its .value (+ the change-guarded input\n// dispatch below). An output with NO matching field writes textContent to every\n// owned [data-reactive-text=\"<name>\"] node (reactive_text(:name)) — XSS-safe by\n// construction, change-guarded, NO input dispatch (a text node has no listener\n// contract). A declared INPUT also mirrors into its own text node on every\n// input via an always-run pass — so reactive_text(:title) is a live field\n// preview with NO registered reducer at all.\n//\n// It returns a plain object of { outputName: value } — only the outputs it\n// names are written, so it can leave the edited field (and its caret)\n// untouched. A one-argument reducer keeps working unchanged (it just ignores\n// meta). `changed` is what makes a MULTI-WAY / MUTUAL rebalance expressible as\n// one reducer (issue #75) — branch on which field the user edited:\n//\n// setComputeReducer(\"three_way_split\", ({ field_a, field_b, field_c, total }, { changed }) => {\n// if (changed === \"field_c\") return { field_a: total - field_c - field_b }\n// return { field_c: total - field_a - field_b }\n// })\n//\n// Output writes are CHANGE-GUARDED: the controller writes a field and\n// dispatches a bubbling `input` event on it ONLY when the new value differs\n// from the field's current value (real browsers never fire `input` on a\n// programmatic .value write, so the controller dispatches explicitly — that's\n// what drives a chained summary repaint, matching the server's set_value +\n// dispatch(\"input\") contract). Returning the SAME value a field already holds\n// is skipped entirely — no write, no event — which is why a reducer with\n// overlapping inputs/outputs (like payment_split above) settles instead of\n// re-entering itself forever.\n//\n// CONVERGENCE REQUIREMENT: because an output write dispatches a REAL input\n// event (issue #76), recompute re-enters with changed = that OUTPUT field's\n// name (when it's also a declared input). A branching reducer must therefore\n// be convergent: the re-entrant pass must compute values EQUAL to what the\n// first pass already wrote to the DOM, so the change guard settles the chain.\n// The three_way_split above is: after `changed === \"field_a\"` writes field_c,\n// the re-entrant `changed === \"field_c\"` pass derives field_a back to the\n// value it already holds — no write, no event, settled in one bounce.\n\n// THE RESERVED `$ops` OUTPUT (issue #226). Besides field/text outputs, a\n// reducer may return the reserved key `$ops` holding a chain of client DOM\n// ops — built with the `ops` builder below, or a raw [[name, args], ...]\n// array. The controller consumes it as a PHASE 4 of the single-pass write set:\n// the ops run AFTER the field writes, text sinks, and phase-3 input dispatches\n// settle, through the SAME frozen CLIENT_OPS whitelist on_client uses (an\n// unknown op warns + is skipped). `null`/`undefined`/absent = no effect.\n//\n// RISING-EDGE semantics, keyed on CONTENT: the chain runs only when it\n// DIFFERS from the previous pass's chain (including from \"absent\"), and only\n// on EVENT-DRIVEN passes. Returning the SAME chain again is settled — no\n// re-fire (a 7th keystroke capped back to the same complete value can't\n// re-submit), mirroring the change-guarded field writes. Returning a\n// DIFFERENT chain fires again — a multi-box reducer advancing focus\n// box-by-box emits a new focus target per digit, each a new intent. The\n// connect/morph SEED pass (issue #199 — recompute with no event) ARMS the\n// latch but never fires — a form re-rendered with an already-complete value\n// (a validation-error morph, a browser restore) must not auto-fire; that is\n// what breaks the submit → error re-render → re-seed → submit loop. A later\n// pass returning no $ops re-arms. The canonical use — a one-time-code field\n// that normalizes on input and commits when complete:\n//\n// import { setComputeReducer, ops } from \"phlex/reactive/compute\"\n// setComputeReducer(\"otp\", ({ code }) => {\n// const digits = code.replace(/\\D/g, \"\").slice(0, 6)\n// return { code: digits, $ops: digits.length === 6 ? ops.submit() : null }\n// })\n//\n// `submit` commits the target's own form via requestSubmit() — the real submit\n// event fires, so an on(:verify, event: \"submit\") interception (or a native/\n// Turbo form) handles it exactly like a user submit. submit/focus are\n// ACTOR-ONLY: usable here and in on_client/reply.js, refused in broadcasts.\n\nconst reducers = new Map()\n\n// Register (or replace) the reducer for `key`. `fn` is\n// (values: Record<string, number>, meta: { changed: string | null })\n// => Record<string, unknown>.\nexport function setComputeReducer(key, fn) {\n reducers.set(key, fn)\n}\n\n// Look up a registered reducer; undefined when none — the controller then makes\n// #recompute a no-op rather than throwing (a missing reducer must not break the\n// page; it just means no client-side binding for that root).\nexport function computeReducer(key) {\n return reducers.get(key)\n}\n\n// Test seam: clear the registry so a reducer registered in one test can't leak.\nexport function __resetComputeRegistryForTest() {\n reducers.clear()\n}\n\n// --- The reducer-side op-chain builder (issue #226) --------------------------\n//\n// A thin, IMMUTABLE mirror of the Ruby Phlex::Reactive::JS builder: verbs carry\n// the WIRE op names (snake_case) and append [name, args] pairs; every verb\n// returns a NEW instance, so a chain held in a constant can never be mutated by\n// later use. `.ops` exposes the raw [[name, args], ...] list the controller\n// interprets (and toJSON serializes it, so a chain can also feed a hand-built\n// ops attr). Targets: a CSS selector string, or omit for \"@root\" (the\n// component's own root). No build-time attr validation here — the interpreter's\n// allowlist (guardAttr) is the enforcement point; this builder only shapes the\n// wire.\nconst ROOT_SENTINEL = \"@root\"\n\nfunction targetArgs(to, { global, transition } = {}) {\n const args = { to: to ?? ROOT_SENTINEL }\n if (global) args.global = true\n if (transition) args.transition = normalizeTransition(transition)\n return args\n}\n\n// Named legs { during, from, to } → the [during, from, to] wire array (the\n// issue #186 vocabulary). Loud at authoring time, like the Ruby builder.\n// Frozen, like every nested payload — the chain's immutability contract must\n// hold all the way down (the Ruby twin freezes its legs array too).\nfunction normalizeTransition(transition) {\n const named =\n transition && typeof transition === \"object\" && !Array.isArray(transition) &&\n [\"during\", \"from\", \"to\"].every((k) => k in transition)\n if (!named) throw new Error(\"[phlex-reactive] ops transition takes named legs { during, from, to }\")\n return Object.freeze([String(transition.during), String(transition.from), String(transition.to)])\n}\n\nclass OpsChain {\n constructor(list = Object.freeze([])) {\n this.ops = list\n Object.freeze(this)\n }\n\n show(to, opts) {\n return this.#append(\"show\", targetArgs(to, opts))\n }\n\n hide(to, opts) {\n return this.#append(\"hide\", targetArgs(to, opts))\n }\n\n toggle(to, opts) {\n return this.#append(\"toggle\", targetArgs(to, opts))\n }\n\n add_class(to, classes, opts) {\n return this.#append(\"add_class\", classArgs(to, classes, opts))\n }\n\n remove_class(to, classes, opts) {\n return this.#append(\"remove_class\", classArgs(to, classes, opts))\n }\n\n toggle_class(to, classes, opts) {\n return this.#append(\"toggle_class\", classArgs(to, classes, opts))\n }\n\n set_attr(to, name, value, opts) {\n return this.#append(\"set_attr\", { ...targetArgs(to, opts), name: String(name), value: String(value) })\n }\n\n remove_attr(to, name, opts) {\n return this.#append(\"remove_attr\", { ...targetArgs(to, opts), name: String(name) })\n }\n\n toggle_attr(to, name, opts) {\n return this.#append(\"toggle_attr\", { ...targetArgs(to, opts), name: String(name) })\n }\n\n focus(to, opts) {\n return this.#append(\"focus\", targetArgs(to, opts))\n }\n\n focus_first(to, opts) {\n return this.#append(\"focus_first\", targetArgs(to, opts))\n }\n\n text(to, value, opts) {\n return this.#append(\"text\", { ...targetArgs(to, opts), value: String(value ?? \"\") })\n }\n\n dispatch(name, { to, detail, global } = {}) {\n const args = { name: String(name), to: to ?? ROOT_SENTINEL, detail: detail ?? {} }\n if (global) args.global = true\n return this.#append(\"dispatch\", args)\n }\n\n submit(to, opts) {\n return this.#append(\"submit\", targetArgs(to, opts))\n }\n\n toJSON() {\n return this.ops\n }\n\n #append(name, args) {\n return new OpsChain(Object.freeze([...this.ops, Object.freeze([name, Object.freeze(args)])]))\n }\n}\n\n// classes: one class string or an array of them (never whitespace-split — a\n// classList token can't contain spaces, so splitting would only mask a bug).\n// Loud on an empty/missing list, and frozen (the Ruby twin freezes its class\n// list too) — a chain held in a constant must stay immutable all the way down.\nfunction classArgs(to, classes, opts) {\n const list = classes == null ? [] : (Array.isArray(classes) ? classes : [classes]).map(String)\n if (list.length === 0) throw new Error(\"[phlex-reactive] a class op needs at least one class\")\n return { ...targetArgs(to, opts), classes: Object.freeze(list) }\n}\n\n// The shared empty chain — start every reducer effect from here:\n// $ops: done ? ops.dispatch(\"code:complete\").submit() : null\nexport const ops = new OpsChain()\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "3CB48262B60C70DD64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|