phlex-reactive 0.4.5 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +32 -0
- data/app/javascript/phlex/reactive/confirm.js +34 -0
- data/app/javascript/phlex/reactive/reactive_controller.js +40 -23
- data/lib/phlex/reactive/engine.rb +12 -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: 57f49d14a96bb92d0e65f8b6dc8331390538331630858145ed6a42ad80cc4ba5
|
|
4
|
+
data.tar.gz: 5709021682d0741191df8000fd1b90c10927d1e9bb0f59a8e459728e39a39bc6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d69d38d3f339502c69caad92d5730c43f2b995d6e12202823ac62da69e02967e14eaa46dfb57536f74c11f5bd8181c1ef8bc9b4cc12067d2f7eb072b4d996cc2
|
|
7
|
+
data.tar.gz: 30e84a8827a9cfdd515d7a2f9daec8c70b76ea5faffd6bf302f406b6aafc00daddfbf2ba0f4a1e070421e44e34ad3525fe09ecd692b5a009bcd4b0473f1eb087
|
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
|
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,5 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { confirmResolver } from "./confirm.js"
|
|
2
3
|
|
|
3
4
|
// The ONE generic controller behind every reactive Phlex component. It
|
|
4
5
|
// replaces the per-feature Stimulus controllers you'd otherwise hand-write
|
|
@@ -171,37 +172,53 @@ export default class extends Controller {
|
|
|
171
172
|
const { action, params, debounce, confirm } = event.params
|
|
172
173
|
if (!action) return
|
|
173
174
|
|
|
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
175
|
// 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
|
|
176
|
+
// within the event dispatch — BEFORE the (possibly async) confirm gate below.
|
|
177
|
+
// preventDefault() only works while the event is still being handled; once we
|
|
178
|
+
// await the confirm resolver it's too late, and a `submit` trigger would
|
|
179
|
+
// natively POST the form and navigate before the reactive round trip runs
|
|
180
|
+
// (issue #11). For a `click` trigger there's no default to miss. This holds
|
|
193
181
|
// for debounced triggers too — the round trip is deferred, but the native
|
|
194
|
-
// default must still be prevented now.
|
|
182
|
+
// default must still be prevented now. (Moved ahead of the confirm branch in
|
|
183
|
+
// issue #55: an async resolver means we can't preventDefault after awaiting.)
|
|
195
184
|
event.preventDefault()
|
|
196
185
|
|
|
186
|
+
// Capture the trigger element now; #proceed runs in a later microtask (after
|
|
187
|
+
// the confirm resolver settles), by which point event.target may be reset.
|
|
188
|
+
const target = event.target
|
|
189
|
+
|
|
190
|
+
// No confirm message → proceed straight away (unchanged fast path).
|
|
191
|
+
if (!confirm) return this.#proceed(target, action, params, debounce)
|
|
192
|
+
|
|
193
|
+
// Confirmation gate (issue #52, made overridable + async in #55). A reactive
|
|
194
|
+
// trigger can't use Hotwire's data-turbo-confirm — this controller preempts
|
|
195
|
+
// the event — so a `confirm:` message routes through confirmResolver (default
|
|
196
|
+
// window.confirm; an app can override it to reuse Turbo.config.forms.confirm).
|
|
197
|
+
// The resolver may be sync or async; call it INSIDE the chain (via the leading
|
|
198
|
+
// .then) so even a SYNCHRONOUS override throw rejects this promise instead of
|
|
199
|
+
// escaping dispatch — a throwing dialog is treated as a cancel, like the user
|
|
200
|
+
// dismissing it. The .catch is scoped to the resolver step (→ false = cancel),
|
|
201
|
+
// so a dismissed/erroring dialog never surfaces as an unhandled rejection AND a
|
|
202
|
+
// genuine bug inside #proceed is NOT silently swallowed. Enqueue ONLY on a
|
|
203
|
+
// truthy resolution — nothing is enqueued, no timer scheduled, otherwise.
|
|
204
|
+
Promise.resolve()
|
|
205
|
+
.then(() => confirmResolver(confirm))
|
|
206
|
+
.catch(() => false)
|
|
207
|
+
.then((ok) => {
|
|
208
|
+
if (ok) this.#proceed(target, action, params, debounce)
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Enqueue the action — debounced if a debounce window is set, else immediately.
|
|
213
|
+
// Split out of dispatch so both the no-confirm fast path and the post-confirm
|
|
214
|
+
// microtask share one place (issue #55). `target` is captured up front because
|
|
215
|
+
// this can run in a later microtask, after event.target has been reset.
|
|
216
|
+
#proceed(target, action, params, debounce) {
|
|
197
217
|
// Debounced trigger (e.g. on(:update, event: "input", debounce: 300)):
|
|
198
218
|
// coalesce rapid events into ONE round trip after a quiet period, instead of
|
|
199
219
|
// one POST per keystroke (issue #17). A blur flushes a pending dispatch.
|
|
200
220
|
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.
|
|
221
|
+
if (ms > 0) return this.#debounceDispatch(target, ms, action, params)
|
|
205
222
|
return this.#enqueue(action, params)
|
|
206
223
|
}
|
|
207
224
|
|
|
@@ -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,14 @@ 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 relatively (./confirm.js), and an app reuses its themed
|
|
46
|
+
# dialog via `import { setConfirmResolver } from "phlex/reactive/confirm"`.
|
|
47
|
+
it.importmap.pin(
|
|
48
|
+
"phlex/reactive/confirm",
|
|
49
|
+
to: "phlex/reactive/confirm.js",
|
|
50
|
+
preload: true
|
|
51
|
+
)
|
|
41
52
|
end
|
|
42
53
|
end
|
|
43
54
|
|
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.6
|
|
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
|