phlex-reactive 0.4.4 → 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 +41 -11
- data/lib/phlex/reactive/component.rb +11 -1
- 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
|
|
@@ -168,27 +169,56 @@ export default class extends Controller {
|
|
|
168
169
|
// a per-controller promise makes each dispatch wait for the previous one, so
|
|
169
170
|
// it always uses the freshest token.
|
|
170
171
|
dispatch(event) {
|
|
171
|
-
const { action, params, debounce } = event.params
|
|
172
|
+
const { action, params, debounce, confirm } = event.params
|
|
172
173
|
if (!action) return
|
|
173
174
|
|
|
174
175
|
// Stop native behavior (button submit / FORM NAVIGATION) HERE, synchronously
|
|
175
|
-
// within the event dispatch
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
// before the reactive round trip runs
|
|
179
|
-
// 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
|
|
180
181
|
// for debounced triggers too — the round trip is deferred, but the native
|
|
181
|
-
// 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.)
|
|
182
184
|
event.preventDefault()
|
|
183
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) {
|
|
184
217
|
// Debounced trigger (e.g. on(:update, event: "input", debounce: 300)):
|
|
185
218
|
// coalesce rapid events into ONE round trip after a quiet period, instead of
|
|
186
219
|
// one POST per keystroke (issue #17). A blur flushes a pending dispatch.
|
|
187
220
|
const ms = Number(debounce) || 0
|
|
188
|
-
if (ms > 0) return this.#debounceDispatch(
|
|
189
|
-
|
|
190
|
-
// Capture action/params now; the queued work runs in a later microtask, by
|
|
191
|
-
// which point the event object may have been reset by the browser.
|
|
221
|
+
if (ms > 0) return this.#debounceDispatch(target, ms, action, params)
|
|
192
222
|
return this.#enqueue(action, params)
|
|
193
223
|
}
|
|
194
224
|
|
|
@@ -304,13 +304,22 @@ module Phlex
|
|
|
304
304
|
# live-update-as-you-type doesn't POST per keystroke. A blur flushes a
|
|
305
305
|
# pending dispatch so the last edit is never dropped. Omit it for the
|
|
306
306
|
# immediate-dispatch default.
|
|
307
|
+
#
|
|
308
|
+
# `confirm:` (a message string) gates the action behind a confirmation
|
|
309
|
+
# prompt (issue #52). Destructive reactive triggers can't use Hotwire's
|
|
310
|
+
# `data-turbo-confirm` — the reactive controller calls preventDefault and
|
|
311
|
+
# enqueues the POST itself, so Turbo's confirm handling never runs. The
|
|
312
|
+
# client shows window.confirm(message) FIRST and bails before any
|
|
313
|
+
# enqueue/debounce if the user declines (and prevents the native default so
|
|
314
|
+
# a `submit` trigger can't navigate on cancel). Omit it for no prompt.
|
|
315
|
+
# button(**on(:destroy, confirm: "Really delete this item?")) { "Delete" }
|
|
307
316
|
# The verbatim JSON for an empty explicit-params payload. The common
|
|
308
317
|
# trigger (on(:increment), no params) hits this on EVERY render — skipping
|
|
309
318
|
# params.to_json (which re-serializes {} to the same "{}" each time) avoids
|
|
310
319
|
# a per-render allocation while keeping the wire format byte-identical.
|
|
311
320
|
EMPTY_PARAMS_JSON = "{}"
|
|
312
321
|
|
|
313
|
-
def on(action_name, event: "click", debounce: nil, **params)
|
|
322
|
+
def on(action_name, event: "click", debounce: nil, confirm: nil, **params)
|
|
314
323
|
attrs = {
|
|
315
324
|
data: {
|
|
316
325
|
action: "#{event}->reactive#dispatch",
|
|
@@ -319,6 +328,7 @@ module Phlex
|
|
|
319
328
|
}
|
|
320
329
|
}
|
|
321
330
|
attrs[:data][:reactive_debounce_param] = debounce if debounce
|
|
331
|
+
attrs[:data][:reactive_confirm_param] = confirm if confirm
|
|
322
332
|
attrs[:type] = "button" if event == "click"
|
|
323
333
|
attrs
|
|
324
334
|
end
|
|
@@ -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
|