phlex-reactive 0.15.5 → 0.16.1
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 +344 -142
- data/README.md +100 -3
- data/app/controllers/phlex/reactive/actions_controller.rb +10 -0
- data/app/controllers/phlex/reactive/fragments_controller.rb +4 -3
- 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/generators/phlex/reactive/install/templates/phlex_reactive.rb.erb +2 -1
- data/lib/phlex/reactive/apm/subscriber.rb +5 -0
- data/lib/phlex/reactive/apm.rb +5 -6
- data/lib/phlex/reactive/authorization.rb +31 -5
- data/lib/phlex/reactive/client_bindings.rb +19 -0
- 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 +15 -1
- data/lib/phlex/reactive/component/helpers.rb +9 -1
- data/lib/phlex/reactive/component/identity.rb +17 -6
- data/lib/phlex/reactive/component/lazy.rb +4 -0
- data/lib/phlex/reactive/component/presence_declaration.rb +4 -0
- data/lib/phlex/reactive/component/registry.rb +76 -6
- data/lib/phlex/reactive/component/sortable.rb +3 -0
- data/lib/phlex/reactive/component.rb +4 -0
- data/lib/phlex/reactive/defer.rb +4 -4
- data/lib/phlex/reactive/doctor/ractor_readiness.rb +65 -0
- data/lib/phlex/reactive/doctor.rb +2 -0
- data/lib/phlex/reactive/engine.rb +31 -4
- data/lib/phlex/reactive/fragment.rb +5 -4
- data/lib/phlex/reactive/held_fields.rb +21 -1
- data/lib/phlex/reactive/param_schema.rb +3 -2
- data/lib/phlex/reactive/pending/markup.rb +27 -9
- data/lib/phlex/reactive/pending.rb +4 -4
- data/lib/phlex/reactive/response.rb +55 -4
- data/lib/phlex/reactive/shareable.rb +87 -0
- data/lib/phlex/reactive/store.rb +73 -0
- data/lib/phlex/reactive/streamable.rb +49 -19
- data/lib/phlex/reactive/test_helpers.rb +6 -0
- data/lib/phlex/reactive/version.rb +1 -1
- data/lib/phlex/reactive.rb +293 -122
- data/sig/phlex/reactive/component.rbs +108 -0
- data/sig/phlex/reactive/param_schema.rbs +23 -0
- data/sig/phlex/reactive/response.rbs +73 -0
- data/sig/phlex/reactive/streamable.rbs +62 -0
- data/sig/phlex/reactive.rbs +231 -0
- metadata +14 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 860b397593c4d0a2d05d2485345bfc78564e784942c8413db1b67fbbacea6b41
|
|
4
|
+
data.tar.gz: '059b1148284859d6a69afdd4f62693f4dbedb0eab41370af843e30a4f3d27e4f'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0500ccbf1d82ea1039cf078dbe62e67efd6da9c9a2ecbeaace46d7d316f4a6c62ef479ea8b1a81e263ad1f738c25d9aa64f0dc53fc7af7a1bbadd6dab49e5021
|
|
7
|
+
data.tar.gz: eae4009ae39fba74bc6219c4b90df31589bed31ed70a457c7f240075ffd50b84662a39af475d878bc339d2e538611e248a287e11a5bbdacd52cb47d1bf73d45e
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,183 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
8
8
|
|
|
9
9
|
### Added
|
|
10
10
|
|
|
11
|
+
- **Draggable regions: move and resize a box, or move a point, inside a
|
|
12
|
+
frame, with the result in fields (#416).** `reactive_frame` marks the
|
|
13
|
+
coordinate space, `reactive_box(fields:, value:, aspect:, min:, action:,
|
|
14
|
+
busy:, messages:)` a box in it, `reactive_box_handle(edge)` an edge or corner
|
|
15
|
+
to resize from, and `reactive_point(fields:, value:, within:, …)` a point,
|
|
16
|
+
inside the frame or inside a box. The helpers render the position as
|
|
17
|
+
percentages from `value:`, so the first paint needs no JavaScript and a
|
|
18
|
+
re-render with new values moves the box. The box moves and resizes with a
|
|
19
|
+
mouse, a pen, a finger or the keyboard (arrows 1%, Shift 10%, Alt resizes,
|
|
20
|
+
Escape puts it back, each step announced in the words of `messages:`). It
|
|
21
|
+
stays inside the frame and never goes below `min:`. `aspect:` locks the
|
|
22
|
+
ratio (a Numeric, or a field such as a row of preset radios, which re-fits
|
|
23
|
+
the box when it changes). The fields hold fractions rounded to 4 decimals,
|
|
24
|
+
with `input` as the box moves (at most once per field a frame) and one
|
|
25
|
+
`change` on release. `action:` is sent once per release through the root's
|
|
26
|
+
pipeline, with `x`, `y`, `width`, `height`. Without it the binding is
|
|
27
|
+
client-only and works on a `ClientBindings` root. A morph that lands
|
|
28
|
+
mid-drag does not interrupt it. The helpers check their arguments' keys,
|
|
29
|
+
types and ranges at render (an undeclared action under `verbose_errors`).
|
|
30
|
+
Client: a new `drag` feature module (3,576 B gzipped): +2,789 B in the
|
|
31
|
+
default bundle, +51 B in the split core, which loads it only on a page with
|
|
32
|
+
a frame, a box or a point. Docs: a "Draggable regions" guide with a live demo.
|
|
33
|
+
|
|
34
|
+
- **Fields that feed CSS custom properties: `reactive_css_vars` (#417).** A
|
|
35
|
+
preview that follows a control (a slider that brightens an image, a select
|
|
36
|
+
that rotates it) needed app JavaScript or a request per input event.
|
|
37
|
+
`reactive_css_vars(to: "#sample", vars: { "--light" => "light",
|
|
38
|
+
"--turn" => { field: "turn", unit: "deg" } })` reads each field as
|
|
39
|
+
`reactive_show` does (a checkbox gives `1` or `0`, a radio group its checked
|
|
40
|
+
value), parses it as a finite number and writes `number + unit` on every
|
|
41
|
+
`to:` element with `style.setProperty`, on connect, on `input`/`change` and
|
|
42
|
+
after a morph, with no request. Anything that is not a number removes the
|
|
43
|
+
property, so the `var()` fallback applies: no text from a field reaches CSS.
|
|
44
|
+
Units come from a fixed list (`""`, `px`, `%`, `deg`, `turn`, `ms`, `s`,
|
|
45
|
+
`em`, `rem`, `fr`) and names match `--[a-zA-Z0-9-]+`, checked at render and in
|
|
46
|
+
the browser. `to:` resolves like a client op's (`:root`, inside the root,
|
|
47
|
+
`global: true`, an Array). It goes on the root or on any element inside it,
|
|
48
|
+
and works on a `ClientBindings` view. Client: +460 B gzipped in the default
|
|
49
|
+
bundle; +462 B in `features/bindings` and +16 B in the split core.
|
|
50
|
+
Docs: "Live CSS properties" on the client-ops page, with a demo.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- **A reply that refuses the posted values keeps the held marks (#413).**
|
|
55
|
+
Every reply to a root's own request released the fields a morph held
|
|
56
|
+
(#370), so after an action refused a save the user's next keystroke posted
|
|
57
|
+
no held names and went through over the other change. Return
|
|
58
|
+
`reply.morph.refused` (every mark) or `reply.morph.refused("title")` (only
|
|
59
|
+
those fields) and the client keeps the marks: the root's next action names
|
|
60
|
+
them in `reactive_held_fields` again, until the action stores the field or
|
|
61
|
+
the user takes the other value. The endpoint sends it as the
|
|
62
|
+
`X-Reactive-Refused` response header; the reply stays a 200, with no error
|
|
63
|
+
marker and no `reactive:error`. `run_reactive(…).refused?` and
|
|
64
|
+
`response.refused?` / `response.refused_fields` assert it in a unit test.
|
|
65
|
+
Client: +65 B gzipped in the default bundle, +60 B in the split core.
|
|
66
|
+
Docs: "Knowing what was held" on the architecture page, the `reply` table
|
|
67
|
+
in the README.
|
|
68
|
+
|
|
69
|
+
## [0.16.0] - 2026-10-16
|
|
70
|
+
|
|
71
|
+
### Upgrading from 0.15.x
|
|
72
|
+
|
|
73
|
+
- **The configuration is finalized automatically at boot (#364).** Nothing to
|
|
74
|
+
call: the engine runs `Phlex::Reactive.finalize!` once the app has booted,
|
|
75
|
+
after every initializer and every `after_initialize` block the app
|
|
76
|
+
registered. From then on every setting reads back as a frozen copy, and the
|
|
77
|
+
param-type and named-schema registries are closed (registering into either
|
|
78
|
+
after boot already raised). Booting the gem without the engine, call
|
|
79
|
+
`Phlex::Reactive.finalize!` at the end of your own boot.
|
|
80
|
+
|
|
81
|
+
- **The configuration's lists are frozen once the app has booted (#364).**
|
|
82
|
+
Append to `authorization_errors` in an initializer or an `after_initialize`
|
|
83
|
+
block, as before; after boot, assign a new list (`<<` raises `FrozenError`).
|
|
84
|
+
`around_action`, `on_action_error` and `register_token_upgrader` now replace
|
|
85
|
+
a frozen list instead of appending to it; `around_actions`,
|
|
86
|
+
`on_action_error_hooks` and `token_upgraders` are frozen, and
|
|
87
|
+
`authorization_methods`' default is a frozen list. A setting reads back
|
|
88
|
+
frozen too: change it by assigning, never in place.
|
|
89
|
+
|
|
90
|
+
- **Nothing an app configures makes boot raise (#364).** A value no Ractor can
|
|
91
|
+
share is kept as it is and works on the main Ractor only: a block written in
|
|
92
|
+
an initializer (`around_action`, `on_action_error`, `param_type`), a lambda
|
|
93
|
+
that closes over app state, a logger, an APM object, the verifier. An app
|
|
94
|
+
booting without a `secret_key_base` (a production rake task or build step)
|
|
95
|
+
keeps the verifier lazy, as before: the first token raises, not boot.
|
|
96
|
+
|
|
97
|
+
### Added
|
|
98
|
+
|
|
99
|
+
- **`Phlex::Reactive.finalize!`, `finalized?` and a test-only
|
|
100
|
+
`reset_finalization!` (#364).** `finalize!` freezes the param-type and
|
|
101
|
+
named-schema registries and stores every setting as a deep-frozen,
|
|
102
|
+
Ractor-shareable copy (the objects an app handed in are never frozen), so a
|
|
103
|
+
non-main Ractor can read the configuration. The engine calls it at boot (see
|
|
104
|
+
Upgrading). Writers keep working afterwards, and what they write is shared
|
|
105
|
+
too.
|
|
106
|
+
|
|
107
|
+
- **`Phlex::Reactive.renderer` takes a name (#364).** `renderer =
|
|
108
|
+
"ApplicationController"`, like `base_controller_name`, resolves on every read,
|
|
109
|
+
so a class Zeitwerk reloaded in development is picked up (and the per-thread
|
|
110
|
+
view contexts rebuild for it). A class keeps working, and keeps rendering the
|
|
111
|
+
class object it was given. The README and the install template show the name.
|
|
112
|
+
|
|
113
|
+
- **`phlex_reactive:doctor` reports Ractor readiness (#364).** An advisory line
|
|
114
|
+
(never a failure): eager loading on, the configuration finalized, and each
|
|
115
|
+
setting `finalize!` shares that still holds a value a non-main Ractor cannot
|
|
116
|
+
read, with its class (the verifier, never shareable, is left out).
|
|
117
|
+
|
|
118
|
+
- **RBS signatures (#364).** `sig/` covers the configuration and token surface
|
|
119
|
+
of `Phlex::Reactive`, a component's class-level DSL, Streamable (its instance
|
|
120
|
+
methods and its class methods), `Response`, `ParamSchema`, `Store` and
|
|
121
|
+
`Shareable`, and ships with the gem. Lint runs `rbs -I sig validate`; a spec
|
|
122
|
+
fails when a public method has no signature.
|
|
123
|
+
|
|
124
|
+
- **Concurrency conformance (#364).** conformance-kit 0.4's gates run in CI:
|
|
125
|
+
every `rubocop-thread_safety` cop, with no directory excludes (`NewThread`
|
|
126
|
+
is excluded only from the five spec files that start threads on purpose),
|
|
127
|
+
thread and Async contention examples over rendering, tokens, params and
|
|
128
|
+
broadcasts, a
|
|
129
|
+
Ruby 4.0 Ractor canary (param coercion, Stream wrapping, `Streamable#dom_id`,
|
|
130
|
+
configuration reads and token upgrades run in a non-main Ractor; rendering and
|
|
131
|
+
signing are pending on Phlex and ActiveSupport), and `conformance-audit
|
|
132
|
+
--strict`. A report-only kino smoke serves the dummy app in Ractor mode and
|
|
133
|
+
records the first blocker. Docs: a new "Concurrency & Ractors" guide page.
|
|
134
|
+
|
|
135
|
+
### Changed
|
|
136
|
+
|
|
137
|
+
- **No reader writes on first use (#364).** Every setting starts at its default
|
|
138
|
+
when the gem loads (the param-type registry is seeded then too; the verifier,
|
|
139
|
+
which needs `Rails.application`, is built by `finalize!` at boot when it can
|
|
140
|
+
be). With eager loading the engine also prepends `verify_authorized`'s
|
|
141
|
+
interceptor to every component and resolves every component's registries and
|
|
142
|
+
render memos at boot (a `ClientBindings`-only view's too), so a request
|
|
143
|
+
writes no class.
|
|
144
|
+
|
|
145
|
+
- **Per-Ractor state instead of `Concurrent::Map` and lock constants (#364).**
|
|
146
|
+
The fragment viewer warnings, the CSRF-name patterns, the once-per-process
|
|
147
|
+
config warnings, the APM adapter memo, the Streamable class registry, the
|
|
148
|
+
pending-markup variants and the registry's write lock live in
|
|
149
|
+
`Phlex::Reactive::Store` (`Ractor.store_if_absent`, with a Mutex-guarded
|
|
150
|
+
table on an engine without Ractors). `lib/` and `app/` no longer reference
|
|
151
|
+
`concurrent-ruby`, which was never a declared dependency.
|
|
152
|
+
|
|
153
|
+
### Fixed
|
|
154
|
+
|
|
155
|
+
- **Eager loading the gem raised `NoMethodError: undefined method 'to_json'`
|
|
156
|
+
in a process that had not loaded json yet (#364).** `Component::Sortable`
|
|
157
|
+
calls `Hash#to_json` while it loads; it now requires json itself.
|
|
158
|
+
|
|
159
|
+
- **`verify_authorized` could prepend two interceptors to one class (#364).**
|
|
160
|
+
The lazy, request-path `Authorization.instrument!` checked and prepended
|
|
161
|
+
without a lock; two threads first-calling it for one class both prepended.
|
|
162
|
+
It now holds a lock across the check and the prepend.
|
|
163
|
+
|
|
164
|
+
## [0.15.5] - 2026-10-15
|
|
165
|
+
|
|
166
|
+
### Fixed
|
|
167
|
+
|
|
168
|
+
- **A server trigger inside a client-only root reaches the component around it
|
|
169
|
+
(#410).** A `ClientBindings` component (a `reactive_filter` list, a
|
|
170
|
+
`reactive_show` wrapper, an `on_client` menu) renders a root with no token.
|
|
171
|
+
Rendered inside a server component, it swallowed that component's `on(...)`
|
|
172
|
+
triggers: Stimulus sends the action to the nearest root, which had no token,
|
|
173
|
+
and the outer root's field collection skipped every control under it (the
|
|
174
|
+
#15 rule). A tokenless client-only root is now transparent to server round
|
|
175
|
+
trips: `dispatch` forwards to the nearest token-bearing root above (skipping
|
|
176
|
+
further client-only roots, waking a dormant one), and that root's action
|
|
177
|
+
collects the controls under nested client-only roots. A nested server
|
|
178
|
+
component keeps its own fields and triggers, and client bindings stay on
|
|
179
|
+
their own root. With no token-bearing root above, nothing is sent, and
|
|
180
|
+
`verbose_errors`/debug logs one warning. Lazy and deferred shells are not
|
|
181
|
+
client-only. Client: +414 B gzipped in the default bundle, +415 B in the
|
|
182
|
+
split core.
|
|
183
|
+
|
|
184
|
+
## [0.15.4] - 2026-10-14
|
|
185
|
+
|
|
186
|
+
### Added
|
|
187
|
+
|
|
11
188
|
- **A server deadline counted down on the client — `reactive_countdown(until:, format: :ms, waiting:, done:)` (#400).**
|
|
12
189
|
`span(**reactive_countdown(until: deadline.iso8601, waiting: "#wait", done: "#done")) { "2:00" }`
|
|
13
190
|
puts the deadline on the element; the client paints the time left from
|
|
@@ -176,6 +353,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
176
353
|
anything else raises at render. Client: +27 B gzipped in the default
|
|
177
354
|
bundle, +20 B in the persist module; the split core unchanged.
|
|
178
355
|
|
|
356
|
+
## [0.15.3] - 2026-10-13
|
|
357
|
+
|
|
358
|
+
### Added
|
|
359
|
+
|
|
179
360
|
- **An image fallback that recovers — `reactive_image_fallback(hide: true, show_alt: nil)` (#383).**
|
|
180
361
|
`img(**reactive_image_fallback)` marks an `<img>` whose `src` fails with
|
|
181
362
|
`data-reactive-image-state="error"` and hides it; `show_alt: "#bank-name"`
|
|
@@ -196,6 +377,91 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
196
377
|
+550 B in the split core. Docs: "An image that falls back, and recovers" on
|
|
197
378
|
the client-ops example.
|
|
198
379
|
|
|
380
|
+
### Fixed
|
|
381
|
+
|
|
382
|
+
- **`reactive_media` waits for `take` characters before calling a key a miss
|
|
383
|
+
(#380).** A value shorter than `take:` sliced to a short key, missed the
|
|
384
|
+
pool and wrote `blank_label` while the user was still typing. It is now
|
|
385
|
+
*pending*: `blank_label` only names a full-length key that misses. New
|
|
386
|
+
options: `match:` (a Regexp, anchored to the whole key; a key it rejects is
|
|
387
|
+
pending too) and `pending: :clear` (default: clear the preview, empty the
|
|
388
|
+
label) or `:keep` (leave both alone). The preview carries
|
|
389
|
+
`data-reactive-media-state="pending|hit|miss"`, so a row can hide itself
|
|
390
|
+
with CSS. `match:` reaches the browser as a JS pattern: its `\A`/`\z`
|
|
391
|
+
anchors are dropped (a trailing `\Z` becomes `\n?`), and what JS reads
|
|
392
|
+
differently raises at render (options, escapes such as `\h`/`\p{…}`/`\s`/
|
|
393
|
+
`\u{…}`, POSIX and nested classes, `&&`, `(?i)`/`(?>…)`, characters above
|
|
394
|
+
U+FFFF). **Behaviour change:** with `take:`, a short value now leaves the
|
|
395
|
+
label empty instead of writing `blank_label`. Without `take:` or `match:`
|
|
396
|
+
nothing changes. Client: +160 B gzipped in the default bundle (+152 B on
|
|
397
|
+
top of #381/#382), +152 B in the bindings module, the split core unchanged. Docs: "Media
|
|
398
|
+
previews" on the client-ops example.
|
|
399
|
+
|
|
400
|
+
- **A pool `<img>` with an `alt` and no `src` keeps its name (#381).**
|
|
401
|
+
`js.copy_media` and `reactive_media` treated a source `<img>` without `src`
|
|
402
|
+
as a clear: the preview's `alt` was emptied and `label:` got `blank_label`,
|
|
403
|
+
so a known entry with no logo file lost its name. Such a source is now a hit
|
|
404
|
+
without an image: the target's `src` and `srcset` are removed, its `alt` is
|
|
405
|
+
set, and `label:` gets that `alt`. Only a pool miss, a selector that matches
|
|
406
|
+
nothing, or a source that is not an `<img>` clears. Client: +1 B gzipped in
|
|
407
|
+
the default bundle, −2 B in the split core; the bindings module is
|
|
408
|
+
unchanged.
|
|
409
|
+
|
|
410
|
+
- **`js.preview_file` follows an app's upload validation (#382).** A preview
|
|
411
|
+
ran once, on `change`, so a file the app then rejected (cleared with
|
|
412
|
+
`input.value = ""`, no new `change`) stayed on screen. Two pieces fix it.
|
|
413
|
+
`js.preview_file(…, valid: true)` treats an input whose `validity.valid` is
|
|
414
|
+
false (`setCustomValidity`, `required`) as empty: it never gets a preview,
|
|
415
|
+
and one it had is cleared and its URL revoked. And `reactive:file-changed`
|
|
416
|
+
is the documented event for "the app changed this input's files without a
|
|
417
|
+
`change` event": bind the op to it as well,
|
|
418
|
+
`mix(on_client(:change, op), on_client("reactive:file-changed", op))`, and
|
|
419
|
+
the app dispatches it after it clears or invalidates the field. That works
|
|
420
|
+
whichever `change` listener runs first. Without `valid:` the op behaves as
|
|
421
|
+
before. Client: +22 B gzipped in the default bundle, +34 B in the split
|
|
422
|
+
core (on top of #381). Docs: "Preview a chosen image" on the client-ops example.
|
|
423
|
+
|
|
424
|
+
### Performance
|
|
425
|
+
|
|
426
|
+
- **Guarded and persisted forms connect in linear time.** Each `warn_unsaved`
|
|
427
|
+
root added its own `beforeunload` / `turbo:before-visit` pair on `window`,
|
|
428
|
+
and each persisted root its own `turbo:submit-end` listener on `document`.
|
|
429
|
+
A browser scans a target's listeners on every add and remove, so connecting
|
|
430
|
+
and disconnecting N of them was quadratic. One shared listener per window or
|
|
431
|
+
document now serves them all, added with the first root and removed with
|
|
432
|
+
the last; behaviour is unchanged (live dirty count, one prompt per visit,
|
|
433
|
+
#298). Headless Chromium, insert + connect: 4,000 guarded roots ~306 ->
|
|
434
|
+
~227 ms, 4,000 persisted roots ~280 -> ~230 ms. +194 B gzipped in the
|
|
435
|
+
default bundle (110 B for the guard, 84 B for persist).
|
|
436
|
+
- **A record-backed token no longer builds a GlobalID URI.** For a record
|
|
437
|
+
whose `to_gid` is GlobalID's own and whose id is an Integer, the gid is
|
|
438
|
+
built directly as `gid://<app>/<Class>/<id>` (byte-identical, pinned by
|
|
439
|
+
`identity_gid_spec.rb`); anything else still calls `to_gid`. The token goes
|
|
440
|
+
from ~11.4 µs and 54 objects to ~5.2 µs and 20 (in-place A/B).
|
|
441
|
+
- **`rake bench:browser`.** Connect, disconnect and a heap soak for the
|
|
442
|
+
shipped client in headless Chromium. The happy-dom connect bench is
|
|
443
|
+
engine-relative and missed the quadratic above; this is the one to trust.
|
|
444
|
+
- **The doctor's authorization check is linear, not quadratic.** It rebuilt
|
|
445
|
+
the Inspector's whole component list (a Prism scan per action) once for
|
|
446
|
+
every component. It now reads it once: `doctor.checks` over the dummy app's
|
|
447
|
+
90 components drops from 1,337 ms to 55 ms, and an app with a few hundred
|
|
448
|
+
components no longer waits tens of seconds on `rake phlex_reactive:doctor`.
|
|
449
|
+
- **`on()` checks for removed keywords without building arrays.** Every
|
|
450
|
+
trigger ran `params.keys & REMOVED_ON_KWARGS.keys`. It now returns at once
|
|
451
|
+
when there are no extra keywords. `on(:increment)` allocates 5 objects
|
|
452
|
+
instead of 8; a 7-trigger component's `render_component` 118 instead of 136
|
|
453
|
+
(~5% faster in an in-place A/B, a method-level win that shows on broadcast
|
|
454
|
+
fan-out, not request latency).
|
|
455
|
+
- **The request bench measures production, not a file watcher.** The dummy
|
|
456
|
+
app's test env left code reloading on, so every request globbed `app/`:
|
|
457
|
+
`rake bench:request` read ~1.7-2.0 ms and 1,272 objects per state-backed
|
|
458
|
+
action, against ~0.30-0.35 ms and 824 objects with it off (same machine,
|
|
459
|
+
interleaved). Test-env only; the gem is unchanged.
|
|
460
|
+
|
|
461
|
+
## [0.15.2] - 2026-10-13
|
|
462
|
+
|
|
463
|
+
### Added
|
|
464
|
+
|
|
199
465
|
- **Media previews — `js.copy_media` and `reactive_media` (#373).**
|
|
200
466
|
`js.copy_media("#logo", from: "#logo-007")` copies `src`, `srcset` and `alt`
|
|
201
467
|
from an `<img>` the server rendered onto a preview `<img>`; the URL is read
|
|
@@ -285,6 +551,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
285
551
|
Client: +251 B gzipped in the default bundle, +247 B in the split core.
|
|
286
552
|
Docs: "Keeping client state through a morph" on the client-ops example.
|
|
287
553
|
|
|
554
|
+
## [0.15.1] - 2026-10-12
|
|
555
|
+
|
|
556
|
+
### Added
|
|
557
|
+
|
|
288
558
|
- **Modal dialogs — `js.show_modal` / `js.close_dialog` and
|
|
289
559
|
`data-reactive-modal` (#365).** `js.show_modal("#picker")` calls
|
|
290
560
|
`HTMLDialogElement#showModal()`, so an `on_client` dialog gets the browser's
|
|
@@ -310,6 +580,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
310
580
|
every other key alone (+134 B gzipped). `once:` raises on these keys. Existing
|
|
311
581
|
chords render unchanged.
|
|
312
582
|
|
|
583
|
+
## [0.15.0] - 2026-10-12
|
|
584
|
+
|
|
585
|
+
### Added
|
|
586
|
+
|
|
313
587
|
- **Presence — `reactive_presence`, `presence`, `js.presence` (#358).** A
|
|
314
588
|
roster component declares the presence stream it shows
|
|
315
589
|
(`reactive_presence :viewers, state: { selected: :integer }, ttl: 45`) and
|
|
@@ -353,6 +627,40 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
353
627
|
true` without `shortcut:` behaves exactly as before (never prevented, no
|
|
354
628
|
editable guard).
|
|
355
629
|
|
|
630
|
+
### Fixed
|
|
631
|
+
|
|
632
|
+
- **A stream that renders into a connected root wires what its content needs
|
|
633
|
+
(#356).** A root read its feature markers only at connect and after a morph
|
|
634
|
+
of itself, so a `reactive_show` target, select-all/count group, filter,
|
|
635
|
+
nested list, dirty field or paste trigger that an `append`, `prepend`,
|
|
636
|
+
`update`, `before`, `after` or child `replace` brought into it stayed dead
|
|
637
|
+
until the root reconnected. Now, once such a stream has rendered, the root
|
|
638
|
+
re-runs the seeds of the features it has (a new show target takes the
|
|
639
|
+
field's current value) and connects any feature its new content needs, in
|
|
640
|
+
both clients; bindings and form also wire a binding whose gate reads true
|
|
641
|
+
only then. As after a morph, this covers a root with an action token or a
|
|
642
|
+
lazy shell; a tokenless, client-only root does not re-read. On the split
|
|
643
|
+
client the stream's render waits for the module the content needs, as it
|
|
644
|
+
already did for an incoming root, so the content lands wired; the wait is
|
|
645
|
+
capped at 1 s, after which Turbo renders and the root connects the module
|
|
646
|
+
when it arrives. A draft
|
|
647
|
+
restore no longer marks fields `data-reactive-dirty` on a root that never
|
|
648
|
+
opted into dirty tracking (one that only gates a paste trigger).
|
|
649
|
+
|
|
650
|
+
- **An editable is editable in an engine without `contenteditable="plaintext-only"`,
|
|
651
|
+
however it reaches the page (#355).** Such an engine reads the server's
|
|
652
|
+
`plaintext-only` as invalid, so the element cannot even be focused. The
|
|
653
|
+
bindings feature switched it to `"true"` only when it connected, or after a
|
|
654
|
+
morph of the root, which left an editable that a stream appended, a frame
|
|
655
|
+
loaded or an unfocused morph rewrote uneditable. The runtime now does the
|
|
656
|
+
switch with one observer, installed only in such an engine (elsewhere the
|
|
657
|
+
probe is all that runs). Bundle: default +157 B gzip, split core +241 B,
|
|
658
|
+
`features/bindings` -138 B.
|
|
659
|
+
|
|
660
|
+
## [0.14.4] - 2026-10-11
|
|
661
|
+
|
|
662
|
+
### Added
|
|
663
|
+
|
|
356
664
|
- **Sortable lists — `reactive_sortable`, `reactive_sortable_source`,
|
|
357
665
|
`reactive_sortable_item` (#350).** Rows drag to reorder a list with a mouse,
|
|
358
666
|
a finger (a 200 ms press) or the keyboard (Space picks up, arrows move,
|
|
@@ -390,104 +698,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
390
698
|
client the stream that brings it starts the import). Bundle: default
|
|
391
699
|
+977 B gzip, split core +412 B, `features/bindings` +524 B.
|
|
392
700
|
|
|
393
|
-
|
|
394
|
-
payload qualifies (#346).** A binding can require top-level `event.detail`
|
|
395
|
-
keys to strictly equal JSON scalars, so `on_client("turbo:submit-end",
|
|
396
|
-
js.check_group("ids[]", false), detail: { success: true })` clears a bulk
|
|
397
|
-
selection after a successful submit and keeps it after a failed one. A
|
|
398
|
-
missing detail or key never matches; a miss runs no confirm and no
|
|
399
|
-
`preventDefault`. Each binding record carries its own filter, so two bindings
|
|
400
|
-
on one element can split success and failure. A non-scalar value, an empty
|
|
401
|
-
Hash, or `detail:` with `once:` raises at render. Records without `detail`
|
|
402
|
-
are byte-identical.
|
|
701
|
+
## [0.14.3] - 2026-10-07
|
|
403
702
|
|
|
404
703
|
### Fixed
|
|
405
704
|
|
|
406
|
-
- **A server trigger inside a client-only root reaches the component around it
|
|
407
|
-
(#410).** A `ClientBindings` component (a `reactive_filter` list, a
|
|
408
|
-
`reactive_show` wrapper, an `on_client` menu) renders a root with no token.
|
|
409
|
-
Rendered inside a server component, it swallowed that component's `on(...)`
|
|
410
|
-
triggers: Stimulus sends the action to the nearest root, which had no token,
|
|
411
|
-
and the outer root's field collection skipped every control under it (the
|
|
412
|
-
#15 rule). A tokenless client-only root is now transparent to server round
|
|
413
|
-
trips: `dispatch` forwards to the nearest token-bearing root above (skipping
|
|
414
|
-
further client-only roots, waking a dormant one), and that root's action
|
|
415
|
-
collects the controls under nested client-only roots. A nested server
|
|
416
|
-
component keeps its own fields and triggers, and client bindings stay on
|
|
417
|
-
their own root. With no token-bearing root above, nothing is sent, and
|
|
418
|
-
`verbose_errors`/debug logs one warning. Lazy and deferred shells are not
|
|
419
|
-
client-only. Client: +414 B gzipped in the default bundle, +415 B in the
|
|
420
|
-
split core.
|
|
421
|
-
|
|
422
|
-
- **`reactive_media` waits for `take` characters before calling a key a miss
|
|
423
|
-
(#380).** A value shorter than `take:` sliced to a short key, missed the
|
|
424
|
-
pool and wrote `blank_label` while the user was still typing. It is now
|
|
425
|
-
*pending*: `blank_label` only names a full-length key that misses. New
|
|
426
|
-
options: `match:` (a Regexp, anchored to the whole key; a key it rejects is
|
|
427
|
-
pending too) and `pending: :clear` (default: clear the preview, empty the
|
|
428
|
-
label) or `:keep` (leave both alone). The preview carries
|
|
429
|
-
`data-reactive-media-state="pending|hit|miss"`, so a row can hide itself
|
|
430
|
-
with CSS. `match:` reaches the browser as a JS pattern: its `\A`/`\z`
|
|
431
|
-
anchors are dropped (a trailing `\Z` becomes `\n?`), and what JS reads
|
|
432
|
-
differently raises at render (options, escapes such as `\h`/`\p{…}`/`\s`/
|
|
433
|
-
`\u{…}`, POSIX and nested classes, `&&`, `(?i)`/`(?>…)`, characters above
|
|
434
|
-
U+FFFF). **Behaviour change:** with `take:`, a short value now leaves the
|
|
435
|
-
label empty instead of writing `blank_label`. Without `take:` or `match:`
|
|
436
|
-
nothing changes. Client: +160 B gzipped in the default bundle (+152 B on
|
|
437
|
-
top of #381/#382), +152 B in the bindings module, the split core unchanged. Docs: "Media
|
|
438
|
-
previews" on the client-ops example.
|
|
439
|
-
|
|
440
|
-
- **A pool `<img>` with an `alt` and no `src` keeps its name (#381).**
|
|
441
|
-
`js.copy_media` and `reactive_media` treated a source `<img>` without `src`
|
|
442
|
-
as a clear: the preview's `alt` was emptied and `label:` got `blank_label`,
|
|
443
|
-
so a known entry with no logo file lost its name. Such a source is now a hit
|
|
444
|
-
without an image: the target's `src` and `srcset` are removed, its `alt` is
|
|
445
|
-
set, and `label:` gets that `alt`. Only a pool miss, a selector that matches
|
|
446
|
-
nothing, or a source that is not an `<img>` clears. Client: +1 B gzipped in
|
|
447
|
-
the default bundle, −2 B in the split core; the bindings module is
|
|
448
|
-
unchanged.
|
|
449
|
-
|
|
450
|
-
- **`js.preview_file` follows an app's upload validation (#382).** A preview
|
|
451
|
-
ran once, on `change`, so a file the app then rejected (cleared with
|
|
452
|
-
`input.value = ""`, no new `change`) stayed on screen. Two pieces fix it.
|
|
453
|
-
`js.preview_file(…, valid: true)` treats an input whose `validity.valid` is
|
|
454
|
-
false (`setCustomValidity`, `required`) as empty: it never gets a preview,
|
|
455
|
-
and one it had is cleared and its URL revoked. And `reactive:file-changed`
|
|
456
|
-
is the documented event for "the app changed this input's files without a
|
|
457
|
-
`change` event": bind the op to it as well,
|
|
458
|
-
`mix(on_client(:change, op), on_client("reactive:file-changed", op))`, and
|
|
459
|
-
the app dispatches it after it clears or invalidates the field. That works
|
|
460
|
-
whichever `change` listener runs first. Without `valid:` the op behaves as
|
|
461
|
-
before. Client: +22 B gzipped in the default bundle, +34 B in the split
|
|
462
|
-
core (on top of #381). Docs: "Preview a chosen image" on the client-ops example.
|
|
463
|
-
|
|
464
|
-
- **A stream that renders into a connected root wires what its content needs
|
|
465
|
-
(#356).** A root read its feature markers only at connect and after a morph
|
|
466
|
-
of itself, so a `reactive_show` target, select-all/count group, filter,
|
|
467
|
-
nested list, dirty field or paste trigger that an `append`, `prepend`,
|
|
468
|
-
`update`, `before`, `after` or child `replace` brought into it stayed dead
|
|
469
|
-
until the root reconnected. Now, once such a stream has rendered, the root
|
|
470
|
-
re-runs the seeds of the features it has (a new show target takes the
|
|
471
|
-
field's current value) and connects any feature its new content needs, in
|
|
472
|
-
both clients; bindings and form also wire a binding whose gate reads true
|
|
473
|
-
only then. As after a morph, this covers a root with an action token or a
|
|
474
|
-
lazy shell; a tokenless, client-only root does not re-read. On the split
|
|
475
|
-
client the stream's render waits for the module the content needs, as it
|
|
476
|
-
already did for an incoming root, so the content lands wired; the wait is
|
|
477
|
-
capped at 1 s, after which Turbo renders and the root connects the module
|
|
478
|
-
when it arrives. A draft
|
|
479
|
-
restore no longer marks fields `data-reactive-dirty` on a root that never
|
|
480
|
-
opted into dirty tracking (one that only gates a paste trigger).
|
|
481
|
-
- **An editable is editable in an engine without `contenteditable="plaintext-only"`,
|
|
482
|
-
however it reaches the page (#355).** Such an engine reads the server's
|
|
483
|
-
`plaintext-only` as invalid, so the element cannot even be focused. The
|
|
484
|
-
bindings feature switched it to `"true"` only when it connected, or after a
|
|
485
|
-
morph of the root, which left an editable that a stream appended, a frame
|
|
486
|
-
loaded or an unfocused morph rewrote uneditable. The runtime now does the
|
|
487
|
-
switch with one observer, installed only in such an engine (elsewhere the
|
|
488
|
-
probe is all that runs). Bundle: default +157 B gzip, split core +241 B,
|
|
489
|
-
`features/bindings` -138 B.
|
|
490
|
-
|
|
491
705
|
- **A `checked:`-only `reactive_show_targets` entry hides at zero boxes
|
|
492
706
|
(#348).** `"#bulk-bar" => { if: { "ids[]" => { checked: 1.. } } }` was left
|
|
493
707
|
visible once the owning root held no box of the group (the last ticked row
|
|
@@ -497,42 +711,20 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
497
711
|
first paint already were. A target with any value term keeps the skip when
|
|
498
712
|
none of its fields is owned.
|
|
499
713
|
|
|
500
|
-
|
|
714
|
+
## [0.14.2] - 2026-10-07
|
|
501
715
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
built directly as `gid://<app>/<Class>/<id>` (byte-identical, pinned by
|
|
515
|
-
`identity_gid_spec.rb`); anything else still calls `to_gid`. The token goes
|
|
516
|
-
from ~11.4 µs and 54 objects to ~5.2 µs and 20 (in-place A/B).
|
|
517
|
-
- **`rake bench:browser`.** Connect, disconnect and a heap soak for the
|
|
518
|
-
shipped client in headless Chromium. The happy-dom connect bench is
|
|
519
|
-
engine-relative and missed the quadratic above; this is the one to trust.
|
|
520
|
-
- **The doctor's authorization check is linear, not quadratic.** It rebuilt
|
|
521
|
-
the Inspector's whole component list (a Prism scan per action) once for
|
|
522
|
-
every component. It now reads it once: `doctor.checks` over the dummy app's
|
|
523
|
-
90 components drops from 1,337 ms to 55 ms, and an app with a few hundred
|
|
524
|
-
components no longer waits tens of seconds on `rake phlex_reactive:doctor`.
|
|
525
|
-
- **`on()` checks for removed keywords without building arrays.** Every
|
|
526
|
-
trigger ran `params.keys & REMOVED_ON_KWARGS.keys`. It now returns at once
|
|
527
|
-
when there are no extra keywords. `on(:increment)` allocates 5 objects
|
|
528
|
-
instead of 8; a 7-trigger component's `render_component` 118 instead of 136
|
|
529
|
-
(~5% faster in an in-place A/B, a method-level win that shows on broadcast
|
|
530
|
-
fan-out, not request latency).
|
|
531
|
-
- **The request bench measures production, not a file watcher.** The dummy
|
|
532
|
-
app's test env left code reloading on, so every request globbed `app/`:
|
|
533
|
-
`rake bench:request` read ~1.7-2.0 ms and 1,272 objects per state-backed
|
|
534
|
-
action, against ~0.30-0.35 ms and 824 objects with it off (same machine,
|
|
535
|
-
interleaved). Test-env only; the gem is unchanged.
|
|
716
|
+
### Added
|
|
717
|
+
|
|
718
|
+
- **`on_client(…, detail: { success: true })` — run ops only when the event's
|
|
719
|
+
payload qualifies (#346).** A binding can require top-level `event.detail`
|
|
720
|
+
keys to strictly equal JSON scalars, so `on_client("turbo:submit-end",
|
|
721
|
+
js.check_group("ids[]", false), detail: { success: true })` clears a bulk
|
|
722
|
+
selection after a successful submit and keeps it after a failed one. A
|
|
723
|
+
missing detail or key never matches; a miss runs no confirm and no
|
|
724
|
+
`preventDefault`. Each binding record carries its own filter, so two bindings
|
|
725
|
+
on one element can split success and failure. A non-scalar value, an empty
|
|
726
|
+
Hash, or `detail:` with `once:` raises at render. Records without `detail`
|
|
727
|
+
are byte-identical.
|
|
536
728
|
|
|
537
729
|
## [0.14.1] - 2026-10-07
|
|
538
730
|
|
|
@@ -564,7 +756,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
564
756
|
|
|
565
757
|
## [0.14.0] - 2026-10-07
|
|
566
758
|
|
|
567
|
-
This section also covers 0.
|
|
759
|
+
This section also covers 0.9.1 through 0.13.4, which were released without
|
|
568
760
|
changelog sections of their own.
|
|
569
761
|
|
|
570
762
|
### Added
|
|
@@ -2324,7 +2516,7 @@ changelog sections of their own.
|
|
|
2324
2516
|
(`spec/requests/deferred_latency_spec.rb`) pins that a 120 ms segment cost
|
|
2325
2517
|
moves OFF the actor's reply and onto the deferred leg; it never disappears.
|
|
2326
2518
|
|
|
2327
|
-
## [0.9.0] - 2026-07-
|
|
2519
|
+
## [0.9.0] - 2026-07-04
|
|
2328
2520
|
|
|
2329
2521
|
Consolidates everything since 0.2.6; tags 0.2.7–0.4.8 shipped without changelog
|
|
2330
2522
|
sections, so this section spans the whole run to 0.9.0. The jump from 0.4.8 to
|
|
@@ -3760,7 +3952,7 @@ room for dogfood fixes.
|
|
|
3760
3952
|
Outer flat fields and per-row reactive editing compose cleanly, with no
|
|
3761
3953
|
name-disjointness workarounds.
|
|
3762
3954
|
|
|
3763
|
-
## [0.2.6]
|
|
3955
|
+
## [0.2.6] - 2026-06-25
|
|
3764
3956
|
|
|
3765
3957
|
### Added
|
|
3766
3958
|
|
|
@@ -3787,7 +3979,7 @@ room for dogfood fixes.
|
|
|
3787
3979
|
for non-remove/redirect responses, and never double-prepends when the action
|
|
3788
3980
|
already included a self-targeted stream.
|
|
3789
3981
|
|
|
3790
|
-
## [0.2.5]
|
|
3982
|
+
## [0.2.5] - 2026-06-25
|
|
3791
3983
|
|
|
3792
3984
|
### Fixed
|
|
3793
3985
|
|
|
@@ -3803,7 +3995,7 @@ room for dogfood fixes.
|
|
|
3803
3995
|
bun unit test (`spec/javascript/reactive_controller.test.js`) that asserts the
|
|
3804
3996
|
synchronous call and fails on the pre-fix deferred code. Closes #11.
|
|
3805
3997
|
|
|
3806
|
-
## [0.2.4] - 2026-06-
|
|
3998
|
+
## [0.2.4] - 2026-06-25
|
|
3807
3999
|
|
|
3808
4000
|
### Fixed
|
|
3809
4001
|
|
|
@@ -3821,6 +4013,12 @@ room for dogfood fixes.
|
|
|
3821
4013
|
byte-identical to source by a guard spec, so the browser tests never validate
|
|
3822
4014
|
stale client code again. Closes #8.
|
|
3823
4015
|
|
|
4016
|
+
### Documentation
|
|
4017
|
+
|
|
4018
|
+
- Documented the combined record + state identity (the `{c, gid, s}` token
|
|
4019
|
+
shape): the README, `docs/architecture.md`, and `docs/security.md` no longer
|
|
4020
|
+
frame `reactive_record` and `reactive_state` as mutually exclusive. (#9)
|
|
4021
|
+
|
|
3824
4022
|
## [0.2.3] - 2026-06-24
|
|
3825
4023
|
|
|
3826
4024
|
### Fixed
|
|
@@ -3840,12 +4038,6 @@ room for dogfood fixes.
|
|
|
3840
4038
|
round trip (only a genuinely absent value falls back to the `initialize`
|
|
3841
4039
|
default). No API changes. Closes #6.
|
|
3842
4040
|
|
|
3843
|
-
### Documentation
|
|
3844
|
-
|
|
3845
|
-
- Documented the combined record + state identity (the `{c, gid, s}` token
|
|
3846
|
-
shape): the README, `docs/architecture.md`, and `docs/security.md` no longer
|
|
3847
|
-
frame `reactive_record` and `reactive_state` as mutually exclusive. (#9)
|
|
3848
|
-
|
|
3849
4041
|
## [0.2.2] - 2026-06-24
|
|
3850
4042
|
|
|
3851
4043
|
### Fixed
|
|
@@ -3922,7 +4114,17 @@ room for dogfood fixes.
|
|
|
3922
4114
|
scaffolds a reactive component (and an RSpec spec when the app uses RSpec),
|
|
3923
4115
|
state-backed by default or record-backed with `--record`.
|
|
3924
4116
|
|
|
3925
|
-
[Unreleased]: https://github.com/mhenrixon/phlex-reactive/compare/v0.
|
|
4117
|
+
[Unreleased]: https://github.com/mhenrixon/phlex-reactive/compare/v0.16.0...HEAD
|
|
4118
|
+
[0.16.0]: https://github.com/mhenrixon/phlex-reactive/compare/v0.15.5...v0.16.0
|
|
4119
|
+
[0.15.5]: https://github.com/mhenrixon/phlex-reactive/compare/v0.15.4...v0.15.5
|
|
4120
|
+
[0.15.4]: https://github.com/mhenrixon/phlex-reactive/compare/v0.15.3...v0.15.4
|
|
4121
|
+
[0.15.3]: https://github.com/mhenrixon/phlex-reactive/compare/v0.15.2...v0.15.3
|
|
4122
|
+
[0.15.2]: https://github.com/mhenrixon/phlex-reactive/compare/v0.15.1...v0.15.2
|
|
4123
|
+
[0.15.1]: https://github.com/mhenrixon/phlex-reactive/compare/v0.15.0...v0.15.1
|
|
4124
|
+
[0.15.0]: https://github.com/mhenrixon/phlex-reactive/compare/v0.14.4...v0.15.0
|
|
4125
|
+
[0.14.4]: https://github.com/mhenrixon/phlex-reactive/compare/v0.14.3...v0.14.4
|
|
4126
|
+
[0.14.3]: https://github.com/mhenrixon/phlex-reactive/compare/v0.14.2...v0.14.3
|
|
4127
|
+
[0.14.2]: https://github.com/mhenrixon/phlex-reactive/compare/v0.14.1...v0.14.2
|
|
3926
4128
|
[0.14.1]: https://github.com/mhenrixon/phlex-reactive/compare/v0.14.0...v0.14.1
|
|
3927
4129
|
[0.14.0]: https://github.com/mhenrixon/phlex-reactive/compare/v0.9.0...v0.14.0
|
|
3928
4130
|
[0.9.0]: https://github.com/mhenrixon/phlex-reactive/compare/v0.2.6...v0.9.0
|