phlex-reactive 0.16.0 → 0.16.2
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 +282 -145
- data/README.md +85 -1
- data/app/controllers/phlex/reactive/actions_controller.rb +10 -0
- data/app/javascript/phlex/reactive/core.js +1 -0
- data/app/javascript/phlex/reactive/core.min.js +4 -4
- data/app/javascript/phlex/reactive/core.min.js.map +4 -4
- data/app/javascript/phlex/reactive/features/bindings.js +81 -4
- data/app/javascript/phlex/reactive/features/bindings.min.js +2 -2
- data/app/javascript/phlex/reactive/features/bindings.min.js.map +3 -3
- data/app/javascript/phlex/reactive/features/drag.js +522 -0
- data/app/javascript/phlex/reactive/features/drag.min.js +4 -0
- data/app/javascript/phlex/reactive/features/drag.min.js.map +10 -0
- data/app/javascript/phlex/reactive/reactive_controller.js +2 -0
- data/app/javascript/phlex/reactive/reactive_controller.min.js +4 -4
- data/app/javascript/phlex/reactive/reactive_controller.min.js.map +7 -6
- data/app/javascript/phlex/reactive/runtime.js +57 -23
- data/lib/phlex/reactive/component/css_vars_binding.rb +118 -0
- data/lib/phlex/reactive/component/drag.rb +270 -0
- data/lib/phlex/reactive/component/dsl.rb +3 -0
- data/lib/phlex/reactive/component/helpers.rb +5 -1
- data/lib/phlex/reactive/component/registry.rb +15 -5
- data/lib/phlex/reactive/engine.rb +14 -5
- data/lib/phlex/reactive/held_fields.rb +21 -1
- data/lib/phlex/reactive/param_schema.rb +5 -1
- data/lib/phlex/reactive/response.rb +55 -4
- data/lib/phlex/reactive/shareable.rb +18 -3
- data/lib/phlex/reactive/test_helpers.rb +6 -0
- data/lib/phlex/reactive/version.rb +1 -1
- data/sig/phlex/reactive/response.rbs +3 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 96630c84c36efda322bafa8792e9e00bc0827d727a8d9db9c0f3191916f3a561
|
|
4
|
+
data.tar.gz: 2fc0570f0888b0905f04573b0ee8ecc71210ad08e5e10f430067b7141d1c468f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c8487e66e0f93336442b6edbaaaa2cd2afdf0e18c809783e9001f25b1aaaf7525baab624c8d6b20a3acc896ccb1213423123d4ef914d335dcd147a4b19ab4927
|
|
7
|
+
data.tar.gz: 02d982c7a73c156d52d5a707e1745ea4de5df759f8d7c9d402c51c1ddabd68800d12a8972bf18fe9f5d10e074844922c6b31eec228508ebd14acb8ccac85d58b
|
data/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,100 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
-
## [0.16.
|
|
9
|
+
## [0.16.2] - 2026-10-17
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **An app on Rails main boots in Ractor mode with phlex-reactive mounted.**
|
|
14
|
+
Rails main's `RouteSet#finalize!` makes the route set Ractor-shareable, the
|
|
15
|
+
blocks engines append included. The engine's route block had the engine as
|
|
16
|
+
its `self`, so with `ActiveSupport::Ractors.unshareable_proc_action = :raise`
|
|
17
|
+
boot raised `Ractor::IsolationError: Proc's self is not shareable`. On Ruby
|
|
18
|
+
4.0+ the block is now built with `Ractor.shareable_proc`; the routes it
|
|
19
|
+
draws are unchanged, and older Rubies get the same block as before.
|
|
20
|
+
|
|
21
|
+
- **A warmed component's class can be read from a non-main Ractor.** Boot's
|
|
22
|
+
`Component::Registry.warm!` froze each class's resolution cache only at the
|
|
23
|
+
top level, so the action definitions in it (their `params` Hash, their
|
|
24
|
+
compiled `ParamSchema`) stayed mutable, and the class's own declarations
|
|
25
|
+
(`@reactive_own_actions`, `@reactive_own_state_keys`, …) were not frozen at
|
|
26
|
+
all. Every reactive render or action in a non-main Ractor raised
|
|
27
|
+
`Ractor::IsolationError: can not get unshareable values from instance
|
|
28
|
+
variables of classes/modules`. warm! now shares the cache, every
|
|
29
|
+
`@reactive_own_*` declaration and the render memos deeply
|
|
30
|
+
(`Phlex::Reactive::Shareable.share`, which learned to copy `Data` values
|
|
31
|
+
that keep Data's own `initialize`; one with its own is kept as it is);
|
|
32
|
+
a compiled `ParamSchema` is frozen with a deep-frozen copy of its schema,
|
|
33
|
+
and `action` keeps a frozen copy of `params:`, so the app's own Hash is
|
|
34
|
+
never frozen. A value no Ractor can share (a `reactive_collection size:`
|
|
35
|
+
lambda that assigns what it captures) is kept as it is and works on the
|
|
36
|
+
main Ractor; boot never raises for it. A declaration after warm! (a
|
|
37
|
+
development reload) writes to a fresh copy, as before. The Ractor canary
|
|
38
|
+
now reads a warmed component with an action that takes params.
|
|
39
|
+
|
|
40
|
+
## [0.16.1] - 2026-10-17
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- **Draggable regions: move and resize a box, or move a point, inside a
|
|
45
|
+
frame, with the result in fields (#416).** `reactive_frame` marks the
|
|
46
|
+
coordinate space, `reactive_box(fields:, value:, aspect:, min:, action:,
|
|
47
|
+
busy:, messages:)` a box in it, `reactive_box_handle(edge)` an edge or corner
|
|
48
|
+
to resize from, and `reactive_point(fields:, value:, within:, …)` a point,
|
|
49
|
+
inside the frame or inside a box. The helpers render the position as
|
|
50
|
+
percentages from `value:`, so the first paint needs no JavaScript and a
|
|
51
|
+
re-render with new values moves the box. The box moves and resizes with a
|
|
52
|
+
mouse, a pen, a finger or the keyboard (arrows 1%, Shift 10%, Alt resizes,
|
|
53
|
+
Escape puts it back, each step announced in the words of `messages:`). It
|
|
54
|
+
stays inside the frame and never goes below `min:`. `aspect:` locks the
|
|
55
|
+
ratio (a Numeric, or a field such as a row of preset radios, which re-fits
|
|
56
|
+
the box when it changes). The fields hold fractions rounded to 4 decimals,
|
|
57
|
+
with `input` as the box moves (at most once per field a frame) and one
|
|
58
|
+
`change` on release. `action:` is sent once per release through the root's
|
|
59
|
+
pipeline, with `x`, `y`, `width`, `height`. Without it the binding is
|
|
60
|
+
client-only and works on a `ClientBindings` root. A morph that lands
|
|
61
|
+
mid-drag does not interrupt it. The helpers check their arguments' keys,
|
|
62
|
+
types and ranges at render (an undeclared action under `verbose_errors`).
|
|
63
|
+
Client: a new `drag` feature module (3,576 B gzipped): +2,789 B in the
|
|
64
|
+
default bundle, +51 B in the split core, which loads it only on a page with
|
|
65
|
+
a frame, a box or a point. Docs: a "Draggable regions" guide with a live demo.
|
|
66
|
+
|
|
67
|
+
- **Fields that feed CSS custom properties: `reactive_css_vars` (#417).** A
|
|
68
|
+
preview that follows a control (a slider that brightens an image, a select
|
|
69
|
+
that rotates it) needed app JavaScript or a request per input event.
|
|
70
|
+
`reactive_css_vars(to: "#sample", vars: { "--light" => "light",
|
|
71
|
+
"--turn" => { field: "turn", unit: "deg" } })` reads each field as
|
|
72
|
+
`reactive_show` does (a checkbox gives `1` or `0`, a radio group its checked
|
|
73
|
+
value), parses it as a finite number and writes `number + unit` on every
|
|
74
|
+
`to:` element with `style.setProperty`, on connect, on `input`/`change` and
|
|
75
|
+
after a morph, with no request. Anything that is not a number removes the
|
|
76
|
+
property, so the `var()` fallback applies: no text from a field reaches CSS.
|
|
77
|
+
Units come from a fixed list (`""`, `px`, `%`, `deg`, `turn`, `ms`, `s`,
|
|
78
|
+
`em`, `rem`, `fr`) and names match `--[a-zA-Z0-9-]+`, checked at render and in
|
|
79
|
+
the browser. `to:` resolves like a client op's (`:root`, inside the root,
|
|
80
|
+
`global: true`, an Array). It goes on the root or on any element inside it,
|
|
81
|
+
and works on a `ClientBindings` view. Client: +460 B gzipped in the default
|
|
82
|
+
bundle; +462 B in `features/bindings` and +16 B in the split core.
|
|
83
|
+
Docs: "Live CSS properties" on the client-ops page, with a demo.
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
|
|
87
|
+
- **A reply that refuses the posted values keeps the held marks (#413).**
|
|
88
|
+
Every reply to a root's own request released the fields a morph held
|
|
89
|
+
(#370), so after an action refused a save the user's next keystroke posted
|
|
90
|
+
no held names and went through over the other change. Return
|
|
91
|
+
`reply.morph.refused` (every mark) or `reply.morph.refused("title")` (only
|
|
92
|
+
those fields) and the client keeps the marks: the root's next action names
|
|
93
|
+
them in `reactive_held_fields` again, until the action stores the field or
|
|
94
|
+
the user takes the other value. The endpoint sends it as the
|
|
95
|
+
`X-Reactive-Refused` response header; the reply stays a 200, with no error
|
|
96
|
+
marker and no `reactive:error`. `run_reactive(…).refused?` and
|
|
97
|
+
`response.refused?` / `response.refused_fields` assert it in a unit test.
|
|
98
|
+
Client: +65 B gzipped in the default bundle, +60 B in the split core.
|
|
99
|
+
Docs: "Knowing what was held" on the architecture page, the `reply` table
|
|
100
|
+
in the README.
|
|
101
|
+
|
|
102
|
+
## [0.16.0] - 2026-10-16
|
|
10
103
|
|
|
11
104
|
### Upgrading from 0.15.x
|
|
12
105
|
|
|
@@ -103,8 +196,25 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
103
196
|
|
|
104
197
|
## [0.15.5] - 2026-10-15
|
|
105
198
|
|
|
106
|
-
|
|
107
|
-
|
|
199
|
+
### Fixed
|
|
200
|
+
|
|
201
|
+
- **A server trigger inside a client-only root reaches the component around it
|
|
202
|
+
(#410).** A `ClientBindings` component (a `reactive_filter` list, a
|
|
203
|
+
`reactive_show` wrapper, an `on_client` menu) renders a root with no token.
|
|
204
|
+
Rendered inside a server component, it swallowed that component's `on(...)`
|
|
205
|
+
triggers: Stimulus sends the action to the nearest root, which had no token,
|
|
206
|
+
and the outer root's field collection skipped every control under it (the
|
|
207
|
+
#15 rule). A tokenless client-only root is now transparent to server round
|
|
208
|
+
trips: `dispatch` forwards to the nearest token-bearing root above (skipping
|
|
209
|
+
further client-only roots, waking a dormant one), and that root's action
|
|
210
|
+
collects the controls under nested client-only roots. A nested server
|
|
211
|
+
component keeps its own fields and triggers, and client bindings stay on
|
|
212
|
+
their own root. With no token-bearing root above, nothing is sent, and
|
|
213
|
+
`verbose_errors`/debug logs one warning. Lazy and deferred shells are not
|
|
214
|
+
client-only. Client: +414 B gzipped in the default bundle, +415 B in the
|
|
215
|
+
split core.
|
|
216
|
+
|
|
217
|
+
## [0.15.4] - 2026-10-14
|
|
108
218
|
|
|
109
219
|
### Added
|
|
110
220
|
|
|
@@ -276,6 +386,10 @@ changelog sections, so this section spans the whole run to 0.15.5.
|
|
|
276
386
|
anything else raises at render. Client: +27 B gzipped in the default
|
|
277
387
|
bundle, +20 B in the persist module; the split core unchanged.
|
|
278
388
|
|
|
389
|
+
## [0.15.3] - 2026-10-13
|
|
390
|
+
|
|
391
|
+
### Added
|
|
392
|
+
|
|
279
393
|
- **An image fallback that recovers — `reactive_image_fallback(hide: true, show_alt: nil)` (#383).**
|
|
280
394
|
`img(**reactive_image_fallback)` marks an `<img>` whose `src` fails with
|
|
281
395
|
`data-reactive-image-state="error"` and hides it; `show_alt: "#bank-name"`
|
|
@@ -296,6 +410,91 @@ changelog sections, so this section spans the whole run to 0.15.5.
|
|
|
296
410
|
+550 B in the split core. Docs: "An image that falls back, and recovers" on
|
|
297
411
|
the client-ops example.
|
|
298
412
|
|
|
413
|
+
### Fixed
|
|
414
|
+
|
|
415
|
+
- **`reactive_media` waits for `take` characters before calling a key a miss
|
|
416
|
+
(#380).** A value shorter than `take:` sliced to a short key, missed the
|
|
417
|
+
pool and wrote `blank_label` while the user was still typing. It is now
|
|
418
|
+
*pending*: `blank_label` only names a full-length key that misses. New
|
|
419
|
+
options: `match:` (a Regexp, anchored to the whole key; a key it rejects is
|
|
420
|
+
pending too) and `pending: :clear` (default: clear the preview, empty the
|
|
421
|
+
label) or `:keep` (leave both alone). The preview carries
|
|
422
|
+
`data-reactive-media-state="pending|hit|miss"`, so a row can hide itself
|
|
423
|
+
with CSS. `match:` reaches the browser as a JS pattern: its `\A`/`\z`
|
|
424
|
+
anchors are dropped (a trailing `\Z` becomes `\n?`), and what JS reads
|
|
425
|
+
differently raises at render (options, escapes such as `\h`/`\p{…}`/`\s`/
|
|
426
|
+
`\u{…}`, POSIX and nested classes, `&&`, `(?i)`/`(?>…)`, characters above
|
|
427
|
+
U+FFFF). **Behaviour change:** with `take:`, a short value now leaves the
|
|
428
|
+
label empty instead of writing `blank_label`. Without `take:` or `match:`
|
|
429
|
+
nothing changes. Client: +160 B gzipped in the default bundle (+152 B on
|
|
430
|
+
top of #381/#382), +152 B in the bindings module, the split core unchanged. Docs: "Media
|
|
431
|
+
previews" on the client-ops example.
|
|
432
|
+
|
|
433
|
+
- **A pool `<img>` with an `alt` and no `src` keeps its name (#381).**
|
|
434
|
+
`js.copy_media` and `reactive_media` treated a source `<img>` without `src`
|
|
435
|
+
as a clear: the preview's `alt` was emptied and `label:` got `blank_label`,
|
|
436
|
+
so a known entry with no logo file lost its name. Such a source is now a hit
|
|
437
|
+
without an image: the target's `src` and `srcset` are removed, its `alt` is
|
|
438
|
+
set, and `label:` gets that `alt`. Only a pool miss, a selector that matches
|
|
439
|
+
nothing, or a source that is not an `<img>` clears. Client: +1 B gzipped in
|
|
440
|
+
the default bundle, −2 B in the split core; the bindings module is
|
|
441
|
+
unchanged.
|
|
442
|
+
|
|
443
|
+
- **`js.preview_file` follows an app's upload validation (#382).** A preview
|
|
444
|
+
ran once, on `change`, so a file the app then rejected (cleared with
|
|
445
|
+
`input.value = ""`, no new `change`) stayed on screen. Two pieces fix it.
|
|
446
|
+
`js.preview_file(…, valid: true)` treats an input whose `validity.valid` is
|
|
447
|
+
false (`setCustomValidity`, `required`) as empty: it never gets a preview,
|
|
448
|
+
and one it had is cleared and its URL revoked. And `reactive:file-changed`
|
|
449
|
+
is the documented event for "the app changed this input's files without a
|
|
450
|
+
`change` event": bind the op to it as well,
|
|
451
|
+
`mix(on_client(:change, op), on_client("reactive:file-changed", op))`, and
|
|
452
|
+
the app dispatches it after it clears or invalidates the field. That works
|
|
453
|
+
whichever `change` listener runs first. Without `valid:` the op behaves as
|
|
454
|
+
before. Client: +22 B gzipped in the default bundle, +34 B in the split
|
|
455
|
+
core (on top of #381). Docs: "Preview a chosen image" on the client-ops example.
|
|
456
|
+
|
|
457
|
+
### Performance
|
|
458
|
+
|
|
459
|
+
- **Guarded and persisted forms connect in linear time.** Each `warn_unsaved`
|
|
460
|
+
root added its own `beforeunload` / `turbo:before-visit` pair on `window`,
|
|
461
|
+
and each persisted root its own `turbo:submit-end` listener on `document`.
|
|
462
|
+
A browser scans a target's listeners on every add and remove, so connecting
|
|
463
|
+
and disconnecting N of them was quadratic. One shared listener per window or
|
|
464
|
+
document now serves them all, added with the first root and removed with
|
|
465
|
+
the last; behaviour is unchanged (live dirty count, one prompt per visit,
|
|
466
|
+
#298). Headless Chromium, insert + connect: 4,000 guarded roots ~306 ->
|
|
467
|
+
~227 ms, 4,000 persisted roots ~280 -> ~230 ms. +194 B gzipped in the
|
|
468
|
+
default bundle (110 B for the guard, 84 B for persist).
|
|
469
|
+
- **A record-backed token no longer builds a GlobalID URI.** For a record
|
|
470
|
+
whose `to_gid` is GlobalID's own and whose id is an Integer, the gid is
|
|
471
|
+
built directly as `gid://<app>/<Class>/<id>` (byte-identical, pinned by
|
|
472
|
+
`identity_gid_spec.rb`); anything else still calls `to_gid`. The token goes
|
|
473
|
+
from ~11.4 µs and 54 objects to ~5.2 µs and 20 (in-place A/B).
|
|
474
|
+
- **`rake bench:browser`.** Connect, disconnect and a heap soak for the
|
|
475
|
+
shipped client in headless Chromium. The happy-dom connect bench is
|
|
476
|
+
engine-relative and missed the quadratic above; this is the one to trust.
|
|
477
|
+
- **The doctor's authorization check is linear, not quadratic.** It rebuilt
|
|
478
|
+
the Inspector's whole component list (a Prism scan per action) once for
|
|
479
|
+
every component. It now reads it once: `doctor.checks` over the dummy app's
|
|
480
|
+
90 components drops from 1,337 ms to 55 ms, and an app with a few hundred
|
|
481
|
+
components no longer waits tens of seconds on `rake phlex_reactive:doctor`.
|
|
482
|
+
- **`on()` checks for removed keywords without building arrays.** Every
|
|
483
|
+
trigger ran `params.keys & REMOVED_ON_KWARGS.keys`. It now returns at once
|
|
484
|
+
when there are no extra keywords. `on(:increment)` allocates 5 objects
|
|
485
|
+
instead of 8; a 7-trigger component's `render_component` 118 instead of 136
|
|
486
|
+
(~5% faster in an in-place A/B, a method-level win that shows on broadcast
|
|
487
|
+
fan-out, not request latency).
|
|
488
|
+
- **The request bench measures production, not a file watcher.** The dummy
|
|
489
|
+
app's test env left code reloading on, so every request globbed `app/`:
|
|
490
|
+
`rake bench:request` read ~1.7-2.0 ms and 1,272 objects per state-backed
|
|
491
|
+
action, against ~0.30-0.35 ms and 824 objects with it off (same machine,
|
|
492
|
+
interleaved). Test-env only; the gem is unchanged.
|
|
493
|
+
|
|
494
|
+
## [0.15.2] - 2026-10-13
|
|
495
|
+
|
|
496
|
+
### Added
|
|
497
|
+
|
|
299
498
|
- **Media previews — `js.copy_media` and `reactive_media` (#373).**
|
|
300
499
|
`js.copy_media("#logo", from: "#logo-007")` copies `src`, `srcset` and `alt`
|
|
301
500
|
from an `<img>` the server rendered onto a preview `<img>`; the URL is read
|
|
@@ -385,6 +584,10 @@ changelog sections, so this section spans the whole run to 0.15.5.
|
|
|
385
584
|
Client: +251 B gzipped in the default bundle, +247 B in the split core.
|
|
386
585
|
Docs: "Keeping client state through a morph" on the client-ops example.
|
|
387
586
|
|
|
587
|
+
## [0.15.1] - 2026-10-12
|
|
588
|
+
|
|
589
|
+
### Added
|
|
590
|
+
|
|
388
591
|
- **Modal dialogs — `js.show_modal` / `js.close_dialog` and
|
|
389
592
|
`data-reactive-modal` (#365).** `js.show_modal("#picker")` calls
|
|
390
593
|
`HTMLDialogElement#showModal()`, so an `on_client` dialog gets the browser's
|
|
@@ -410,6 +613,10 @@ changelog sections, so this section spans the whole run to 0.15.5.
|
|
|
410
613
|
every other key alone (+134 B gzipped). `once:` raises on these keys. Existing
|
|
411
614
|
chords render unchanged.
|
|
412
615
|
|
|
616
|
+
## [0.15.0] - 2026-10-12
|
|
617
|
+
|
|
618
|
+
### Added
|
|
619
|
+
|
|
413
620
|
- **Presence — `reactive_presence`, `presence`, `js.presence` (#358).** A
|
|
414
621
|
roster component declares the presence stream it shows
|
|
415
622
|
(`reactive_presence :viewers, state: { selected: :integer }, ttl: 45`) and
|
|
@@ -453,6 +660,40 @@ changelog sections, so this section spans the whole run to 0.15.5.
|
|
|
453
660
|
true` without `shortcut:` behaves exactly as before (never prevented, no
|
|
454
661
|
editable guard).
|
|
455
662
|
|
|
663
|
+
### Fixed
|
|
664
|
+
|
|
665
|
+
- **A stream that renders into a connected root wires what its content needs
|
|
666
|
+
(#356).** A root read its feature markers only at connect and after a morph
|
|
667
|
+
of itself, so a `reactive_show` target, select-all/count group, filter,
|
|
668
|
+
nested list, dirty field or paste trigger that an `append`, `prepend`,
|
|
669
|
+
`update`, `before`, `after` or child `replace` brought into it stayed dead
|
|
670
|
+
until the root reconnected. Now, once such a stream has rendered, the root
|
|
671
|
+
re-runs the seeds of the features it has (a new show target takes the
|
|
672
|
+
field's current value) and connects any feature its new content needs, in
|
|
673
|
+
both clients; bindings and form also wire a binding whose gate reads true
|
|
674
|
+
only then. As after a morph, this covers a root with an action token or a
|
|
675
|
+
lazy shell; a tokenless, client-only root does not re-read. On the split
|
|
676
|
+
client the stream's render waits for the module the content needs, as it
|
|
677
|
+
already did for an incoming root, so the content lands wired; the wait is
|
|
678
|
+
capped at 1 s, after which Turbo renders and the root connects the module
|
|
679
|
+
when it arrives. A draft
|
|
680
|
+
restore no longer marks fields `data-reactive-dirty` on a root that never
|
|
681
|
+
opted into dirty tracking (one that only gates a paste trigger).
|
|
682
|
+
|
|
683
|
+
- **An editable is editable in an engine without `contenteditable="plaintext-only"`,
|
|
684
|
+
however it reaches the page (#355).** Such an engine reads the server's
|
|
685
|
+
`plaintext-only` as invalid, so the element cannot even be focused. The
|
|
686
|
+
bindings feature switched it to `"true"` only when it connected, or after a
|
|
687
|
+
morph of the root, which left an editable that a stream appended, a frame
|
|
688
|
+
loaded or an unfocused morph rewrote uneditable. The runtime now does the
|
|
689
|
+
switch with one observer, installed only in such an engine (elsewhere the
|
|
690
|
+
probe is all that runs). Bundle: default +157 B gzip, split core +241 B,
|
|
691
|
+
`features/bindings` -138 B.
|
|
692
|
+
|
|
693
|
+
## [0.14.4] - 2026-10-11
|
|
694
|
+
|
|
695
|
+
### Added
|
|
696
|
+
|
|
456
697
|
- **Sortable lists — `reactive_sortable`, `reactive_sortable_source`,
|
|
457
698
|
`reactive_sortable_item` (#350).** Rows drag to reorder a list with a mouse,
|
|
458
699
|
a finger (a 200 ms press) or the keyboard (Space picks up, arrows move,
|
|
@@ -490,104 +731,10 @@ changelog sections, so this section spans the whole run to 0.15.5.
|
|
|
490
731
|
client the stream that brings it starts the import). Bundle: default
|
|
491
732
|
+977 B gzip, split core +412 B, `features/bindings` +524 B.
|
|
492
733
|
|
|
493
|
-
|
|
494
|
-
payload qualifies (#346).** A binding can require top-level `event.detail`
|
|
495
|
-
keys to strictly equal JSON scalars, so `on_client("turbo:submit-end",
|
|
496
|
-
js.check_group("ids[]", false), detail: { success: true })` clears a bulk
|
|
497
|
-
selection after a successful submit and keeps it after a failed one. A
|
|
498
|
-
missing detail or key never matches; a miss runs no confirm and no
|
|
499
|
-
`preventDefault`. Each binding record carries its own filter, so two bindings
|
|
500
|
-
on one element can split success and failure. A non-scalar value, an empty
|
|
501
|
-
Hash, or `detail:` with `once:` raises at render. Records without `detail`
|
|
502
|
-
are byte-identical.
|
|
734
|
+
## [0.14.3] - 2026-10-07
|
|
503
735
|
|
|
504
736
|
### Fixed
|
|
505
737
|
|
|
506
|
-
- **A server trigger inside a client-only root reaches the component around it
|
|
507
|
-
(#410).** A `ClientBindings` component (a `reactive_filter` list, a
|
|
508
|
-
`reactive_show` wrapper, an `on_client` menu) renders a root with no token.
|
|
509
|
-
Rendered inside a server component, it swallowed that component's `on(...)`
|
|
510
|
-
triggers: Stimulus sends the action to the nearest root, which had no token,
|
|
511
|
-
and the outer root's field collection skipped every control under it (the
|
|
512
|
-
#15 rule). A tokenless client-only root is now transparent to server round
|
|
513
|
-
trips: `dispatch` forwards to the nearest token-bearing root above (skipping
|
|
514
|
-
further client-only roots, waking a dormant one), and that root's action
|
|
515
|
-
collects the controls under nested client-only roots. A nested server
|
|
516
|
-
component keeps its own fields and triggers, and client bindings stay on
|
|
517
|
-
their own root. With no token-bearing root above, nothing is sent, and
|
|
518
|
-
`verbose_errors`/debug logs one warning. Lazy and deferred shells are not
|
|
519
|
-
client-only. Client: +414 B gzipped in the default bundle, +415 B in the
|
|
520
|
-
split core.
|
|
521
|
-
|
|
522
|
-
- **`reactive_media` waits for `take` characters before calling a key a miss
|
|
523
|
-
(#380).** A value shorter than `take:` sliced to a short key, missed the
|
|
524
|
-
pool and wrote `blank_label` while the user was still typing. It is now
|
|
525
|
-
*pending*: `blank_label` only names a full-length key that misses. New
|
|
526
|
-
options: `match:` (a Regexp, anchored to the whole key; a key it rejects is
|
|
527
|
-
pending too) and `pending: :clear` (default: clear the preview, empty the
|
|
528
|
-
label) or `:keep` (leave both alone). The preview carries
|
|
529
|
-
`data-reactive-media-state="pending|hit|miss"`, so a row can hide itself
|
|
530
|
-
with CSS. `match:` reaches the browser as a JS pattern: its `\A`/`\z`
|
|
531
|
-
anchors are dropped (a trailing `\Z` becomes `\n?`), and what JS reads
|
|
532
|
-
differently raises at render (options, escapes such as `\h`/`\p{…}`/`\s`/
|
|
533
|
-
`\u{…}`, POSIX and nested classes, `&&`, `(?i)`/`(?>…)`, characters above
|
|
534
|
-
U+FFFF). **Behaviour change:** with `take:`, a short value now leaves the
|
|
535
|
-
label empty instead of writing `blank_label`. Without `take:` or `match:`
|
|
536
|
-
nothing changes. Client: +160 B gzipped in the default bundle (+152 B on
|
|
537
|
-
top of #381/#382), +152 B in the bindings module, the split core unchanged. Docs: "Media
|
|
538
|
-
previews" on the client-ops example.
|
|
539
|
-
|
|
540
|
-
- **A pool `<img>` with an `alt` and no `src` keeps its name (#381).**
|
|
541
|
-
`js.copy_media` and `reactive_media` treated a source `<img>` without `src`
|
|
542
|
-
as a clear: the preview's `alt` was emptied and `label:` got `blank_label`,
|
|
543
|
-
so a known entry with no logo file lost its name. Such a source is now a hit
|
|
544
|
-
without an image: the target's `src` and `srcset` are removed, its `alt` is
|
|
545
|
-
set, and `label:` gets that `alt`. Only a pool miss, a selector that matches
|
|
546
|
-
nothing, or a source that is not an `<img>` clears. Client: +1 B gzipped in
|
|
547
|
-
the default bundle, −2 B in the split core; the bindings module is
|
|
548
|
-
unchanged.
|
|
549
|
-
|
|
550
|
-
- **`js.preview_file` follows an app's upload validation (#382).** A preview
|
|
551
|
-
ran once, on `change`, so a file the app then rejected (cleared with
|
|
552
|
-
`input.value = ""`, no new `change`) stayed on screen. Two pieces fix it.
|
|
553
|
-
`js.preview_file(…, valid: true)` treats an input whose `validity.valid` is
|
|
554
|
-
false (`setCustomValidity`, `required`) as empty: it never gets a preview,
|
|
555
|
-
and one it had is cleared and its URL revoked. And `reactive:file-changed`
|
|
556
|
-
is the documented event for "the app changed this input's files without a
|
|
557
|
-
`change` event": bind the op to it as well,
|
|
558
|
-
`mix(on_client(:change, op), on_client("reactive:file-changed", op))`, and
|
|
559
|
-
the app dispatches it after it clears or invalidates the field. That works
|
|
560
|
-
whichever `change` listener runs first. Without `valid:` the op behaves as
|
|
561
|
-
before. Client: +22 B gzipped in the default bundle, +34 B in the split
|
|
562
|
-
core (on top of #381). Docs: "Preview a chosen image" on the client-ops example.
|
|
563
|
-
|
|
564
|
-
- **A stream that renders into a connected root wires what its content needs
|
|
565
|
-
(#356).** A root read its feature markers only at connect and after a morph
|
|
566
|
-
of itself, so a `reactive_show` target, select-all/count group, filter,
|
|
567
|
-
nested list, dirty field or paste trigger that an `append`, `prepend`,
|
|
568
|
-
`update`, `before`, `after` or child `replace` brought into it stayed dead
|
|
569
|
-
until the root reconnected. Now, once such a stream has rendered, the root
|
|
570
|
-
re-runs the seeds of the features it has (a new show target takes the
|
|
571
|
-
field's current value) and connects any feature its new content needs, in
|
|
572
|
-
both clients; bindings and form also wire a binding whose gate reads true
|
|
573
|
-
only then. As after a morph, this covers a root with an action token or a
|
|
574
|
-
lazy shell; a tokenless, client-only root does not re-read. On the split
|
|
575
|
-
client the stream's render waits for the module the content needs, as it
|
|
576
|
-
already did for an incoming root, so the content lands wired; the wait is
|
|
577
|
-
capped at 1 s, after which Turbo renders and the root connects the module
|
|
578
|
-
when it arrives. A draft
|
|
579
|
-
restore no longer marks fields `data-reactive-dirty` on a root that never
|
|
580
|
-
opted into dirty tracking (one that only gates a paste trigger).
|
|
581
|
-
- **An editable is editable in an engine without `contenteditable="plaintext-only"`,
|
|
582
|
-
however it reaches the page (#355).** Such an engine reads the server's
|
|
583
|
-
`plaintext-only` as invalid, so the element cannot even be focused. The
|
|
584
|
-
bindings feature switched it to `"true"` only when it connected, or after a
|
|
585
|
-
morph of the root, which left an editable that a stream appended, a frame
|
|
586
|
-
loaded or an unfocused morph rewrote uneditable. The runtime now does the
|
|
587
|
-
switch with one observer, installed only in such an engine (elsewhere the
|
|
588
|
-
probe is all that runs). Bundle: default +157 B gzip, split core +241 B,
|
|
589
|
-
`features/bindings` -138 B.
|
|
590
|
-
|
|
591
738
|
- **A `checked:`-only `reactive_show_targets` entry hides at zero boxes
|
|
592
739
|
(#348).** `"#bulk-bar" => { if: { "ids[]" => { checked: 1.. } } }` was left
|
|
593
740
|
visible once the owning root held no box of the group (the last ticked row
|
|
@@ -597,42 +744,20 @@ changelog sections, so this section spans the whole run to 0.15.5.
|
|
|
597
744
|
first paint already were. A target with any value term keeps the skip when
|
|
598
745
|
none of its fields is owned.
|
|
599
746
|
|
|
600
|
-
|
|
747
|
+
## [0.14.2] - 2026-10-07
|
|
601
748
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
built directly as `gid://<app>/<Class>/<id>` (byte-identical, pinned by
|
|
615
|
-
`identity_gid_spec.rb`); anything else still calls `to_gid`. The token goes
|
|
616
|
-
from ~11.4 µs and 54 objects to ~5.2 µs and 20 (in-place A/B).
|
|
617
|
-
- **`rake bench:browser`.** Connect, disconnect and a heap soak for the
|
|
618
|
-
shipped client in headless Chromium. The happy-dom connect bench is
|
|
619
|
-
engine-relative and missed the quadratic above; this is the one to trust.
|
|
620
|
-
- **The doctor's authorization check is linear, not quadratic.** It rebuilt
|
|
621
|
-
the Inspector's whole component list (a Prism scan per action) once for
|
|
622
|
-
every component. It now reads it once: `doctor.checks` over the dummy app's
|
|
623
|
-
90 components drops from 1,337 ms to 55 ms, and an app with a few hundred
|
|
624
|
-
components no longer waits tens of seconds on `rake phlex_reactive:doctor`.
|
|
625
|
-
- **`on()` checks for removed keywords without building arrays.** Every
|
|
626
|
-
trigger ran `params.keys & REMOVED_ON_KWARGS.keys`. It now returns at once
|
|
627
|
-
when there are no extra keywords. `on(:increment)` allocates 5 objects
|
|
628
|
-
instead of 8; a 7-trigger component's `render_component` 118 instead of 136
|
|
629
|
-
(~5% faster in an in-place A/B, a method-level win that shows on broadcast
|
|
630
|
-
fan-out, not request latency).
|
|
631
|
-
- **The request bench measures production, not a file watcher.** The dummy
|
|
632
|
-
app's test env left code reloading on, so every request globbed `app/`:
|
|
633
|
-
`rake bench:request` read ~1.7-2.0 ms and 1,272 objects per state-backed
|
|
634
|
-
action, against ~0.30-0.35 ms and 824 objects with it off (same machine,
|
|
635
|
-
interleaved). Test-env only; the gem is unchanged.
|
|
749
|
+
### Added
|
|
750
|
+
|
|
751
|
+
- **`on_client(…, detail: { success: true })` — run ops only when the event's
|
|
752
|
+
payload qualifies (#346).** A binding can require top-level `event.detail`
|
|
753
|
+
keys to strictly equal JSON scalars, so `on_client("turbo:submit-end",
|
|
754
|
+
js.check_group("ids[]", false), detail: { success: true })` clears a bulk
|
|
755
|
+
selection after a successful submit and keeps it after a failed one. A
|
|
756
|
+
missing detail or key never matches; a miss runs no confirm and no
|
|
757
|
+
`preventDefault`. Each binding record carries its own filter, so two bindings
|
|
758
|
+
on one element can split success and failure. A non-scalar value, an empty
|
|
759
|
+
Hash, or `detail:` with `once:` raises at render. Records without `detail`
|
|
760
|
+
are byte-identical.
|
|
636
761
|
|
|
637
762
|
## [0.14.1] - 2026-10-07
|
|
638
763
|
|
|
@@ -664,7 +789,7 @@ changelog sections, so this section spans the whole run to 0.15.5.
|
|
|
664
789
|
|
|
665
790
|
## [0.14.0] - 2026-10-07
|
|
666
791
|
|
|
667
|
-
This section also covers 0.
|
|
792
|
+
This section also covers 0.9.1 through 0.13.4, which were released without
|
|
668
793
|
changelog sections of their own.
|
|
669
794
|
|
|
670
795
|
### Added
|
|
@@ -2424,7 +2549,7 @@ changelog sections of their own.
|
|
|
2424
2549
|
(`spec/requests/deferred_latency_spec.rb`) pins that a 120 ms segment cost
|
|
2425
2550
|
moves OFF the actor's reply and onto the deferred leg; it never disappears.
|
|
2426
2551
|
|
|
2427
|
-
## [0.9.0] - 2026-07-
|
|
2552
|
+
## [0.9.0] - 2026-07-04
|
|
2428
2553
|
|
|
2429
2554
|
Consolidates everything since 0.2.6; tags 0.2.7–0.4.8 shipped without changelog
|
|
2430
2555
|
sections, so this section spans the whole run to 0.9.0. The jump from 0.4.8 to
|
|
@@ -3860,7 +3985,7 @@ room for dogfood fixes.
|
|
|
3860
3985
|
Outer flat fields and per-row reactive editing compose cleanly, with no
|
|
3861
3986
|
name-disjointness workarounds.
|
|
3862
3987
|
|
|
3863
|
-
## [0.2.6]
|
|
3988
|
+
## [0.2.6] - 2026-06-25
|
|
3864
3989
|
|
|
3865
3990
|
### Added
|
|
3866
3991
|
|
|
@@ -3887,7 +4012,7 @@ room for dogfood fixes.
|
|
|
3887
4012
|
for non-remove/redirect responses, and never double-prepends when the action
|
|
3888
4013
|
already included a self-targeted stream.
|
|
3889
4014
|
|
|
3890
|
-
## [0.2.5]
|
|
4015
|
+
## [0.2.5] - 2026-06-25
|
|
3891
4016
|
|
|
3892
4017
|
### Fixed
|
|
3893
4018
|
|
|
@@ -3903,7 +4028,7 @@ room for dogfood fixes.
|
|
|
3903
4028
|
bun unit test (`spec/javascript/reactive_controller.test.js`) that asserts the
|
|
3904
4029
|
synchronous call and fails on the pre-fix deferred code. Closes #11.
|
|
3905
4030
|
|
|
3906
|
-
## [0.2.4] - 2026-06-
|
|
4031
|
+
## [0.2.4] - 2026-06-25
|
|
3907
4032
|
|
|
3908
4033
|
### Fixed
|
|
3909
4034
|
|
|
@@ -3921,6 +4046,12 @@ room for dogfood fixes.
|
|
|
3921
4046
|
byte-identical to source by a guard spec, so the browser tests never validate
|
|
3922
4047
|
stale client code again. Closes #8.
|
|
3923
4048
|
|
|
4049
|
+
### Documentation
|
|
4050
|
+
|
|
4051
|
+
- Documented the combined record + state identity (the `{c, gid, s}` token
|
|
4052
|
+
shape): the README, `docs/architecture.md`, and `docs/security.md` no longer
|
|
4053
|
+
frame `reactive_record` and `reactive_state` as mutually exclusive. (#9)
|
|
4054
|
+
|
|
3924
4055
|
## [0.2.3] - 2026-06-24
|
|
3925
4056
|
|
|
3926
4057
|
### Fixed
|
|
@@ -3940,12 +4071,6 @@ room for dogfood fixes.
|
|
|
3940
4071
|
round trip (only a genuinely absent value falls back to the `initialize`
|
|
3941
4072
|
default). No API changes. Closes #6.
|
|
3942
4073
|
|
|
3943
|
-
### Documentation
|
|
3944
|
-
|
|
3945
|
-
- Documented the combined record + state identity (the `{c, gid, s}` token
|
|
3946
|
-
shape): the README, `docs/architecture.md`, and `docs/security.md` no longer
|
|
3947
|
-
frame `reactive_record` and `reactive_state` as mutually exclusive. (#9)
|
|
3948
|
-
|
|
3949
4074
|
## [0.2.2] - 2026-06-24
|
|
3950
4075
|
|
|
3951
4076
|
### Fixed
|
|
@@ -4022,7 +4147,19 @@ room for dogfood fixes.
|
|
|
4022
4147
|
scaffolds a reactive component (and an RSpec spec when the app uses RSpec),
|
|
4023
4148
|
state-backed by default or record-backed with `--record`.
|
|
4024
4149
|
|
|
4025
|
-
[Unreleased]: https://github.com/mhenrixon/phlex-reactive/compare/v0.
|
|
4150
|
+
[Unreleased]: https://github.com/mhenrixon/phlex-reactive/compare/v0.16.2...HEAD
|
|
4151
|
+
[0.16.2]: https://github.com/mhenrixon/phlex-reactive/compare/v0.16.1...v0.16.2
|
|
4152
|
+
[0.16.1]: https://github.com/mhenrixon/phlex-reactive/compare/v0.16.0...v0.16.1
|
|
4153
|
+
[0.16.0]: https://github.com/mhenrixon/phlex-reactive/compare/v0.15.5...v0.16.0
|
|
4154
|
+
[0.15.5]: https://github.com/mhenrixon/phlex-reactive/compare/v0.15.4...v0.15.5
|
|
4155
|
+
[0.15.4]: https://github.com/mhenrixon/phlex-reactive/compare/v0.15.3...v0.15.4
|
|
4156
|
+
[0.15.3]: https://github.com/mhenrixon/phlex-reactive/compare/v0.15.2...v0.15.3
|
|
4157
|
+
[0.15.2]: https://github.com/mhenrixon/phlex-reactive/compare/v0.15.1...v0.15.2
|
|
4158
|
+
[0.15.1]: https://github.com/mhenrixon/phlex-reactive/compare/v0.15.0...v0.15.1
|
|
4159
|
+
[0.15.0]: https://github.com/mhenrixon/phlex-reactive/compare/v0.14.4...v0.15.0
|
|
4160
|
+
[0.14.4]: https://github.com/mhenrixon/phlex-reactive/compare/v0.14.3...v0.14.4
|
|
4161
|
+
[0.14.3]: https://github.com/mhenrixon/phlex-reactive/compare/v0.14.2...v0.14.3
|
|
4162
|
+
[0.14.2]: https://github.com/mhenrixon/phlex-reactive/compare/v0.14.1...v0.14.2
|
|
4026
4163
|
[0.14.1]: https://github.com/mhenrixon/phlex-reactive/compare/v0.14.0...v0.14.1
|
|
4027
4164
|
[0.14.0]: https://github.com/mhenrixon/phlex-reactive/compare/v0.9.0...v0.14.0
|
|
4028
4165
|
[0.9.0]: https://github.com/mhenrixon/phlex-reactive/compare/v0.2.6...v0.9.0
|