phlex-reactive 0.4.5 → 0.4.7
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 +37 -0
- data/README.md +32 -0
- data/app/javascript/phlex/reactive/confirm.js +34 -0
- data/app/javascript/phlex/reactive/reactive_controller.js +50 -23
- data/lib/phlex/reactive/engine.rb +17 -1
- data/lib/phlex/reactive/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6a9dfa70408b2db2a88337edfc41963ead03b74cd83d919888b9e3b9bbf5eb99
|
|
4
|
+
data.tar.gz: 3adb9e76db58f23fd80ebbbc9cb50b08768bbf3473ee5f83375d89b781eda454
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3011d097ec4280eaac80d94d914287d9604a3413615875ac719100065ef20d694ac4025e2695483d782f0c99e5803027dc5d717d38a922f4d404f40d67d513d9
|
|
7
|
+
data.tar.gz: 35b1ec519c60fa7c2a4655b80ad2717d8eca2eb84f71f262ec67ca618b83582acbef81980ceb6125addfb6b47dc9dc5f732170291a507dd2246b1986ddb1097c
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,22 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
8
8
|
|
|
9
9
|
### Added
|
|
10
10
|
|
|
11
|
+
- **Overridable / async confirm resolver — reuse your themed dialog (#55).**
|
|
12
|
+
Follow-up to #52. The `confirm:` gate was hardcoded to the synchronous,
|
|
13
|
+
browser-native `window.confirm`, so a reactive trigger was the one interaction
|
|
14
|
+
a Hotwire app couldn't theme — every confirmable reactive action showed the
|
|
15
|
+
unstyled native chrome instead of the app's `Turbo.config.forms.confirm`
|
|
16
|
+
dialog. The client now resolves the confirmation through an overridable hook
|
|
17
|
+
(`confirmResolver`, set via `setConfirmResolver` from `phlex/reactive/confirm`),
|
|
18
|
+
defaulting to `window.confirm`. An app reuses its styled dialog in one line:
|
|
19
|
+
`setConfirmResolver((m) => window.Turbo.config.forms.confirm(m))`. The resolver
|
|
20
|
+
may be **async** — `dispatch` `preventDefault`s up front (preserving the #11
|
|
21
|
+
submit-trigger guarantee), then `await`s the resolver and enqueues only on a
|
|
22
|
+
truthy result; a falsy resolve or a rejection cancels the action and never
|
|
23
|
+
leaks an unhandled rejection. Unset, behavior is byte-for-byte identical to
|
|
24
|
+
0.4.5 (sync native confirm, no dependency); the `confirm:` markup/`on(...)` API
|
|
25
|
+
is unchanged. README documents the one-line opt-in.
|
|
26
|
+
|
|
11
27
|
- **`reactive_root` helper — the whole reactive root in one spread (#48).**
|
|
12
28
|
`reactive_attrs` doesn't emit `id:`, so an app could put `id:` on a *child*
|
|
13
29
|
element and leave the controller root's `id` empty — which silently re-opened
|
|
@@ -85,6 +101,27 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
85
101
|
|
|
86
102
|
### Fixed
|
|
87
103
|
|
|
104
|
+
- **`reactive_controller.js` used a relative `./confirm.js` import that 404'd under
|
|
105
|
+
importmap-rails + Propshaft — taking down every Stimulus controller on the page (#57).**
|
|
106
|
+
The #55 confirm resolver added `import { confirmResolver } from "./confirm.js"` to the
|
|
107
|
+
client controller. Under importmap + Propshaft the controller is served at its
|
|
108
|
+
**digested** URL, and a relative sibling import is left untouched (Propshaft's JS
|
|
109
|
+
compiler rewrites only `RAILS_ASSET_URL(...)`, and the import map resolves **only**
|
|
110
|
+
bare specifiers, never relative-resolved URLs). So the browser resolved `./confirm.js`
|
|
111
|
+
against the digested controller URL and requested an **undigested**
|
|
112
|
+
`/assets/phlex/reactive/confirm.js` → **404**. The throwing import meant
|
|
113
|
+
`reactive_controller.js` never evaluated, and in an app that eagerly registers it the
|
|
114
|
+
whole controllers entrypoint died — **every** Stimulus controller on the page stopped,
|
|
115
|
+
with no obvious link to phlex-reactive. The fix imports the **bare** specifier the
|
|
116
|
+
engine already pins (`phlex/reactive/confirm`), which resolves to the digested asset
|
|
117
|
+
through the import map and mirrors how the gem already expects apps to import the
|
|
118
|
+
module (`import { setConfirmResolver } from "phlex/reactive/confirm"`). Bundlers
|
|
119
|
+
(esbuild/webpack/bun) resolve the bare specifier the same way they already resolve
|
|
120
|
+
`phlex/reactive/reactive_controller`; the gem's bun JS suite resolves it via a new
|
|
121
|
+
`tsconfig.json` `paths` alias. Covered by a bun unit test (the bare import resolves,
|
|
122
|
+
and the source no longer carries the relative form). 0.4.5 was unaffected (inline
|
|
123
|
+
`window.confirm`, no relative import).
|
|
124
|
+
|
|
88
125
|
- **Client mirror of #44: collections of *reactive* rows were STILL add-once-only in
|
|
89
126
|
the browser — `#extractToken` read the FIRST token in the response, not this
|
|
90
127
|
controller's own (#46).** The server fix in 0.4.2 (#44) made the `add` response
|
data/README.md
CHANGED
|
@@ -311,6 +311,7 @@ Use in controllers: `render turbo_stream: Counter.replace(counter)`.
|
|
|
311
311
|
| `reactive_attrs` | Marks an element reactive + carries the signed token (no `id`). Spread alongside `id:` on the **same** element: `div(id:, **reactive_attrs)`. Prefer `reactive_root`, which can't split them. |
|
|
312
312
|
| `on(:action, event: "click", **params)` | Spread onto a trigger element. Adds `type=button` for clicks. |
|
|
313
313
|
| `on(:action, event: "input", debounce: 300)` | Coalesce rapid events into one round trip after a quiet period (live-as-you-type). |
|
|
314
|
+
| `on(:action, confirm: "Sure?")` | Gate a destructive trigger behind a confirmation. Defaults to `window.confirm`; override the dialog with [`setConfirmResolver`](#custom-confirmation-dialogs-setconfirmresolver). |
|
|
314
315
|
| `reactive_input(:param, **attrs)` / `reactive_select(:param, **attrs)` | Render a control already bound to an action param (no magic `name:`). |
|
|
315
316
|
| `reactive_field(:param, **attrs)` | The attribute hash behind the above — spread onto any control. |
|
|
316
317
|
| `nested_update!(:assoc, attrs)` | Map a nested param onto `<assoc>_attributes` with id preservation; update the record. |
|
|
@@ -478,6 +479,37 @@ end
|
|
|
478
479
|
`nested_attributes(:address, address)` returns the id-merged hash without
|
|
479
480
|
updating, if you need to combine it with other attributes.
|
|
480
481
|
|
|
482
|
+
### Custom confirmation dialogs (`setConfirmResolver`)
|
|
483
|
+
|
|
484
|
+
`on(:action, confirm: "Really delete this?")` gates a destructive trigger behind
|
|
485
|
+
a confirmation. Because the reactive controller preempts the event (its own
|
|
486
|
+
`preventDefault` + POST), Hotwire's `data-turbo-confirm` — which routes through
|
|
487
|
+
`Turbo.config.forms.confirm` — never runs for a reactive trigger. So by default
|
|
488
|
+
the gate uses the browser-native `window.confirm` (synchronous, no dependency,
|
|
489
|
+
screen-reader friendly).
|
|
490
|
+
|
|
491
|
+
If your app already themes confirmations (the common Hotwire setup —
|
|
492
|
+
`Turbo.config.forms.confirm = (message) => Promise<boolean>`, backed by a styled
|
|
493
|
+
modal), reuse that exact dialog for reactive triggers with one line at boot:
|
|
494
|
+
|
|
495
|
+
```js
|
|
496
|
+
import { setConfirmResolver } from "phlex/reactive/confirm"
|
|
497
|
+
|
|
498
|
+
// Reuse the same themed dialog the rest of the app already uses.
|
|
499
|
+
setConfirmResolver((message) => window.Turbo.config.forms.confirm(message))
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
The resolver receives the `confirm:` message and returns `true`/`false` (or a
|
|
503
|
+
`Promise` of one). It may be **async** — the controller `await`s it, then runs
|
|
504
|
+
the action only on a truthy result; a falsy result (or a rejected promise — e.g.
|
|
505
|
+
the user dismissed the dialog) cancels the action, exactly like declining the
|
|
506
|
+
native prompt. The native default is always prevented up front, so a `submit`
|
|
507
|
+
trigger never navigates while the dialog is open.
|
|
508
|
+
|
|
509
|
+
Unset, behavior is identical to the native `window.confirm` — the `confirm:`
|
|
510
|
+
markup and `on(...)` API are unchanged; only the client's resolution strategy
|
|
511
|
+
gains a seam.
|
|
512
|
+
|
|
481
513
|
### `reply` — controlling the action's reply
|
|
482
514
|
|
|
483
515
|
By default an action re-renders its component in place. To do more, **return**
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// The overridable confirmation resolver behind `on(:action, confirm: "…")`
|
|
2
|
+
// (issue #55, follow-up to #52).
|
|
3
|
+
//
|
|
4
|
+
// The reactive controller preempts the click event (its own preventDefault +
|
|
5
|
+
// POST), so Hotwire's `data-turbo-confirm` — which routes through
|
|
6
|
+
// `Turbo.config.forms.confirm` — never runs for a reactive trigger. That made
|
|
7
|
+
// the reactive path the ONE interaction a Hotwire app couldn't theme: every
|
|
8
|
+
// confirmable reactive action showed the unstyled native window.confirm chrome.
|
|
9
|
+
//
|
|
10
|
+
// This module is the seam. `dispatch` resolves the confirm message through
|
|
11
|
+
// `confirmResolver`, which defaults to a Promise-wrapped window.confirm (so the
|
|
12
|
+
// 0.4.5 behavior is byte-for-byte unchanged when nothing is configured: sync,
|
|
13
|
+
// no dependency, screen-reader friendly). An app reuses its themed dialog with
|
|
14
|
+
// one line at boot:
|
|
15
|
+
//
|
|
16
|
+
// import { setConfirmResolver } from "phlex/reactive/confirm"
|
|
17
|
+
// setConfirmResolver((message) => window.Turbo.config.forms.confirm(message))
|
|
18
|
+
//
|
|
19
|
+
// The resolver may be sync or async — the controller awaits it via
|
|
20
|
+
// Promise.resolve, so a bare boolean and a Promise<boolean> both work. It must
|
|
21
|
+
// resolve truthy to proceed; a falsy resolve (or a rejection) cancels the
|
|
22
|
+
// action — exactly like declining the native prompt.
|
|
23
|
+
|
|
24
|
+
// The default: wrap the synchronous native confirm in a Promise so the call
|
|
25
|
+
// site can always `await` it. Read window lazily (per call), not at module load
|
|
26
|
+
// — under SSR / test the global may not exist yet when this module is imported.
|
|
27
|
+
export let confirmResolver = (message) =>
|
|
28
|
+
Promise.resolve(typeof window !== "undefined" ? window.confirm(message) : true)
|
|
29
|
+
|
|
30
|
+
// Override the resolver. Pass a function `(message) => boolean | Promise<boolean>`.
|
|
31
|
+
// Truthy resolves the action through; falsy (or a rejected Promise) cancels it.
|
|
32
|
+
export function setConfirmResolver(fn) {
|
|
33
|
+
confirmResolver = fn
|
|
34
|
+
}
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
// Import the BARE specifier the engine already pins (phlex/reactive/confirm),
|
|
3
|
+
// NOT a relative "./confirm.js" (issue #57). Under importmap-rails + Propshaft
|
|
4
|
+
// the controller is served at its DIGESTED url; a relative sibling import is
|
|
5
|
+
// left untouched (Propshaft rewrites only RAILS_ASSET_URL(...), and the import
|
|
6
|
+
// map resolves ONLY bare specifiers), so "./confirm.js" resolves against the
|
|
7
|
+
// digested controller url → an undigested /assets/.../confirm.js that 404s, and
|
|
8
|
+
// the throwing import takes down every Stimulus controller on the page. The
|
|
9
|
+
// bare specifier resolves to the digested asset through the import map, and
|
|
10
|
+
// bundlers/bun resolve it the same way they already resolve
|
|
11
|
+
// "phlex/reactive/reactive_controller" (see tsconfig.json paths for the tests).
|
|
12
|
+
import { confirmResolver } from "phlex/reactive/confirm"
|
|
2
13
|
|
|
3
14
|
// The ONE generic controller behind every reactive Phlex component. It
|
|
4
15
|
// replaces the per-feature Stimulus controllers you'd otherwise hand-write
|
|
@@ -171,37 +182,53 @@ export default class extends Controller {
|
|
|
171
182
|
const { action, params, debounce, confirm } = event.params
|
|
172
183
|
if (!action) return
|
|
173
184
|
|
|
174
|
-
// Confirmation gate (issue #52). A destructive reactive trigger can't use
|
|
175
|
-
// Hotwire's data-turbo-confirm — this controller preempts the event — so a
|
|
176
|
-
// `confirm:` message threads a data-reactive-confirm-param and we prompt
|
|
177
|
-
// HERE, BEFORE any preventDefault/enqueue/debounce. On decline we still
|
|
178
|
-
// preventDefault (so a `submit`/click can't natively navigate on cancel)
|
|
179
|
-
// and bail — nothing is enqueued, no timer is scheduled. window.confirm is
|
|
180
|
-
// synchronous + screen-reader friendly and keeps the no-dependency default;
|
|
181
|
-
// a richer/async dialog can be layered on additively later.
|
|
182
|
-
if (confirm && !window.confirm(confirm)) {
|
|
183
|
-
event.preventDefault()
|
|
184
|
-
return
|
|
185
|
-
}
|
|
186
|
-
|
|
187
185
|
// Stop native behavior (button submit / FORM NAVIGATION) HERE, synchronously
|
|
188
|
-
// within the event dispatch
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
// before the reactive round trip runs
|
|
192
|
-
// there's no default to miss
|
|
186
|
+
// within the event dispatch — BEFORE the (possibly async) confirm gate below.
|
|
187
|
+
// preventDefault() only works while the event is still being handled; once we
|
|
188
|
+
// await the confirm resolver it's too late, and a `submit` trigger would
|
|
189
|
+
// natively POST the form and navigate before the reactive round trip runs
|
|
190
|
+
// (issue #11). For a `click` trigger there's no default to miss. This holds
|
|
193
191
|
// for debounced triggers too — the round trip is deferred, but the native
|
|
194
|
-
// default must still be prevented now.
|
|
192
|
+
// default must still be prevented now. (Moved ahead of the confirm branch in
|
|
193
|
+
// issue #55: an async resolver means we can't preventDefault after awaiting.)
|
|
195
194
|
event.preventDefault()
|
|
196
195
|
|
|
196
|
+
// Capture the trigger element now; #proceed runs in a later microtask (after
|
|
197
|
+
// the confirm resolver settles), by which point event.target may be reset.
|
|
198
|
+
const target = event.target
|
|
199
|
+
|
|
200
|
+
// No confirm message → proceed straight away (unchanged fast path).
|
|
201
|
+
if (!confirm) return this.#proceed(target, action, params, debounce)
|
|
202
|
+
|
|
203
|
+
// Confirmation gate (issue #52, made overridable + async in #55). A reactive
|
|
204
|
+
// trigger can't use Hotwire's data-turbo-confirm — this controller preempts
|
|
205
|
+
// the event — so a `confirm:` message routes through confirmResolver (default
|
|
206
|
+
// window.confirm; an app can override it to reuse Turbo.config.forms.confirm).
|
|
207
|
+
// The resolver may be sync or async; call it INSIDE the chain (via the leading
|
|
208
|
+
// .then) so even a SYNCHRONOUS override throw rejects this promise instead of
|
|
209
|
+
// escaping dispatch — a throwing dialog is treated as a cancel, like the user
|
|
210
|
+
// dismissing it. The .catch is scoped to the resolver step (→ false = cancel),
|
|
211
|
+
// so a dismissed/erroring dialog never surfaces as an unhandled rejection AND a
|
|
212
|
+
// genuine bug inside #proceed is NOT silently swallowed. Enqueue ONLY on a
|
|
213
|
+
// truthy resolution — nothing is enqueued, no timer scheduled, otherwise.
|
|
214
|
+
Promise.resolve()
|
|
215
|
+
.then(() => confirmResolver(confirm))
|
|
216
|
+
.catch(() => false)
|
|
217
|
+
.then((ok) => {
|
|
218
|
+
if (ok) this.#proceed(target, action, params, debounce)
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Enqueue the action — debounced if a debounce window is set, else immediately.
|
|
223
|
+
// Split out of dispatch so both the no-confirm fast path and the post-confirm
|
|
224
|
+
// microtask share one place (issue #55). `target` is captured up front because
|
|
225
|
+
// this can run in a later microtask, after event.target has been reset.
|
|
226
|
+
#proceed(target, action, params, debounce) {
|
|
197
227
|
// Debounced trigger (e.g. on(:update, event: "input", debounce: 300)):
|
|
198
228
|
// coalesce rapid events into ONE round trip after a quiet period, instead of
|
|
199
229
|
// one POST per keystroke (issue #17). A blur flushes a pending dispatch.
|
|
200
230
|
const ms = Number(debounce) || 0
|
|
201
|
-
if (ms > 0) return this.#debounceDispatch(
|
|
202
|
-
|
|
203
|
-
// Capture action/params now; the queued work runs in a later microtask, by
|
|
204
|
-
// which point the event object may have been reset by the browser.
|
|
231
|
+
if (ms > 0) return this.#debounceDispatch(target, ms, action, params)
|
|
205
232
|
return this.#enqueue(action, params)
|
|
206
233
|
}
|
|
207
234
|
|
|
@@ -24,7 +24,10 @@ module Phlex
|
|
|
24
24
|
initializer "phlex_reactive.assets" do
|
|
25
25
|
if it.config.respond_to?(:assets)
|
|
26
26
|
it.config.assets.paths << root.join("app/javascript").to_s
|
|
27
|
-
it.config.assets.precompile += %w[
|
|
27
|
+
it.config.assets.precompile += %w[
|
|
28
|
+
phlex/reactive/reactive_controller.js
|
|
29
|
+
phlex/reactive/confirm.js
|
|
30
|
+
]
|
|
28
31
|
end
|
|
29
32
|
end
|
|
30
33
|
|
|
@@ -38,6 +41,19 @@ module Phlex
|
|
|
38
41
|
to: "phlex/reactive/reactive_controller.js",
|
|
39
42
|
preload: true
|
|
40
43
|
)
|
|
44
|
+
# The overridable confirm resolver (issue #55). reactive_controller.js
|
|
45
|
+
# imports it by this BARE specifier — `import { confirmResolver } from
|
|
46
|
+
# "phlex/reactive/confirm"` — NOT a relative "./confirm.js" (issue #57:
|
|
47
|
+
# a relative sibling import inside the digested controller resolves to
|
|
48
|
+
# an undigested /assets/.../confirm.js that 404s under Propshaft). This
|
|
49
|
+
# pin maps the bare specifier to the digested asset, so the controller's
|
|
50
|
+
# own import AND an app's `import { setConfirmResolver } from
|
|
51
|
+
# "phlex/reactive/confirm"` both resolve through the import map.
|
|
52
|
+
it.importmap.pin(
|
|
53
|
+
"phlex/reactive/confirm",
|
|
54
|
+
to: "phlex/reactive/confirm.js",
|
|
55
|
+
preload: true
|
|
56
|
+
)
|
|
41
57
|
end
|
|
42
58
|
end
|
|
43
59
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: phlex-reactive
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -117,6 +117,7 @@ files:
|
|
|
117
117
|
- LICENSE.txt
|
|
118
118
|
- README.md
|
|
119
119
|
- app/controllers/phlex/reactive/actions_controller.rb
|
|
120
|
+
- app/javascript/phlex/reactive/confirm.js
|
|
120
121
|
- app/javascript/phlex/reactive/reactive_controller.js
|
|
121
122
|
- lib/generators/phlex/reactive/component/USAGE
|
|
122
123
|
- lib/generators/phlex/reactive/component/component_generator.rb
|