@retronew/call-vue 0.2.0 → 0.4.0
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.
- package/README.md +218 -1
- package/dist/index.d.mts +8 -9
- package/dist/index.mjs +9 -7
- package/dist/mutation-flow/index.d.mts +29 -0
- package/dist/mutation-flow/index.mjs +29 -0
- package/package.json +8 -4
- package/skills/call-vue/SKILL.md +24 -6
package/README.md
CHANGED
|
@@ -10,6 +10,26 @@ Call & await Vue components like async functions. A Vue 3 port of
|
|
|
10
10
|
(`createCallable`, `call`/`upsert`/`end`/`update`) built on native Vue
|
|
11
11
|
reactivity — no context providers, no global store to wire up.
|
|
12
12
|
|
|
13
|
+
[Documentation with live demos](https://call-vue.retronew.dev) ·
|
|
14
|
+
[Examples](https://call-vue.retronew.dev/examples) ·
|
|
15
|
+
[Concepts](https://call-vue.retronew.dev/concepts) ·
|
|
16
|
+
[Full API reference](https://call-vue.retronew.dev/api)
|
|
17
|
+
|
|
18
|
+
## Contents
|
|
19
|
+
|
|
20
|
+
- [Install](#install)
|
|
21
|
+
- [Quick start](#quick-start)
|
|
22
|
+
- [API](#api)
|
|
23
|
+
- [Exit transitions](#exit-transitions)
|
|
24
|
+
- [Root props and TypeScript](#root-props-and-typescript)
|
|
25
|
+
- [Mutation flow](#mutation-flow)
|
|
26
|
+
- [Async components](#async-components)
|
|
27
|
+
- [SSR](#ssr)
|
|
28
|
+
- [Stacking](#stacking)
|
|
29
|
+
- [Errors and troubleshooting](#errors-and-troubleshooting)
|
|
30
|
+
- [Capability matrix](#capability-matrix)
|
|
31
|
+
- [FAQ](#faq)
|
|
32
|
+
|
|
13
33
|
## Why
|
|
14
34
|
|
|
15
35
|
Confirmation dialogs, prompts, and toasts are usually one-off components you
|
|
@@ -30,6 +50,9 @@ No global state, no extra store — `<Confirm />` mounted once *is* the stack.
|
|
|
30
50
|
pnpm add @retronew/call-vue
|
|
31
51
|
```
|
|
32
52
|
|
|
53
|
+
Use `npm install @retronew/call-vue`, `yarn add @retronew/call-vue`, or
|
|
54
|
+
`bun add @retronew/call-vue` with another package manager.
|
|
55
|
+
|
|
33
56
|
## Quick start
|
|
34
57
|
|
|
35
58
|
1. Define the component. It receives your own props **plus** an injected
|
|
@@ -43,7 +66,7 @@ import type { PropsWithCall } from '@retronew/call-vue'
|
|
|
43
66
|
type Props = { message: string }
|
|
44
67
|
type Response = boolean
|
|
45
68
|
|
|
46
|
-
defineProps<PropsWithCall<Props, Response,
|
|
69
|
+
defineProps<PropsWithCall<Props, Response, {}>>()
|
|
47
70
|
</script>
|
|
48
71
|
|
|
49
72
|
<template>
|
|
@@ -109,10 +132,37 @@ async function handleDelete() {
|
|
|
109
132
|
instance's position in the stack, how many are open, and whatever props
|
|
110
133
|
were passed to `<Confirm rootProp="…" />`.
|
|
111
134
|
|
|
135
|
+
For a `void` response, the two external forms are intentionally distinct:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const promise = Toast.upsert({ text: 'Uploading…' })
|
|
139
|
+
|
|
140
|
+
Toast.end(promise, undefined) // end only this call
|
|
141
|
+
Toast.end() // end every open call
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Do not write `Toast.end(promise)`: JavaScript would otherwise interpret that
|
|
145
|
+
single argument as the response for the broadcast overload. The public type
|
|
146
|
+
rejects this form. Inside the user component, `call.end()` remains the natural
|
|
147
|
+
way to finish its own `void` call.
|
|
148
|
+
|
|
112
149
|
See the [Claude Code skill](skills/call-vue/SKILL.md) for the stacking model,
|
|
113
150
|
`unmountingDelay` exit-transition pattern, and the single-`<Root>` constraint
|
|
114
151
|
in depth.
|
|
115
152
|
|
|
153
|
+
### Public types
|
|
154
|
+
|
|
155
|
+
All public types are flat named exports:
|
|
156
|
+
|
|
157
|
+
| Type | Purpose |
|
|
158
|
+
| --- | --- |
|
|
159
|
+
| `CallFunction<Props, Response>` | The typed `call()` method. |
|
|
160
|
+
| `UpsertFunction<Props, Response>` | The typed singleton `upsert()` method. |
|
|
161
|
+
| `CallContext<Props, Response, RootProps>` | The injected `call` prop. |
|
|
162
|
+
| `PropsWithCall<Props, Response, RootProps>` | Your props merged with `call`. |
|
|
163
|
+
| `UserComponent<Props, Response, RootProps>` | A component accepted by `createCallable`. |
|
|
164
|
+
| `Callable<Props, Response, RootProps>` | The Root component plus its imperative methods. |
|
|
165
|
+
|
|
116
166
|
## Exit transitions
|
|
117
167
|
|
|
118
168
|
Pass a second argument to `createCallable` to keep an ended call mounted
|
|
@@ -125,6 +175,106 @@ export const Toast = createCallable<Props, Response>(ToastCard, 200 /* ms */)
|
|
|
125
175
|
|
|
126
176
|
Inside `ToastCard`, branch on `call.ended` to trigger the leave state.
|
|
127
177
|
|
|
178
|
+
## Root props and TypeScript
|
|
179
|
+
|
|
180
|
+
The third generic is the type of props mounted on the Callable Root. A normal
|
|
181
|
+
interface works directly; it does not need to extend `Record<string, unknown>`:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
interface RootProps {
|
|
185
|
+
accent: string
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export const Notice = createCallable<NoticeProps, void, RootProps>(NoticeCard)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
```vue
|
|
192
|
+
<Notice accent="#6366f1" />
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Every active card reads the current value through `call.root.accent`. Updating
|
|
196
|
+
the Root prop re-renders active calls. For cross-file `.vue` prop validation,
|
|
197
|
+
run `vue-tsc --noEmit` in addition to a plain TypeScript check; plain `tsc`
|
|
198
|
+
cannot inspect generated SFC props by itself.
|
|
199
|
+
|
|
200
|
+
The Callable itself is the Root component. Mount `<Notice />`; there is no
|
|
201
|
+
separate `.Root` alias. Its public type is Vue's general `Component` shape, so
|
|
202
|
+
it remains valid whether the internal Root is represented as an options object
|
|
203
|
+
or a functional component.
|
|
204
|
+
|
|
205
|
+
## Mutation flow
|
|
206
|
+
|
|
207
|
+
For the common “submit → await a side effect → close only on success” flow,
|
|
208
|
+
import the opt-in composable from its own subpath:
|
|
209
|
+
|
|
210
|
+
```vue
|
|
211
|
+
<script setup lang="ts">
|
|
212
|
+
import { toRef } from 'vue'
|
|
213
|
+
import type { PropsWithCall } from '@retronew/call-vue'
|
|
214
|
+
import { useMutationFlow, type MutationFn } from '@retronew/call-vue/mutation-flow'
|
|
215
|
+
|
|
216
|
+
type Props = { mutationFn: MutationFn<boolean> }
|
|
217
|
+
const props = defineProps<PropsWithCall<Props, boolean, {}>>()
|
|
218
|
+
const submit = useMutationFlow(props.call, toRef(props, 'mutationFn'))
|
|
219
|
+
</script>
|
|
220
|
+
|
|
221
|
+
<template>
|
|
222
|
+
<button :disabled="submit.pending" @click="submit()">Save</button>
|
|
223
|
+
<button :disabled="submit.pending" @click="props.call.end(false)">Cancel</button>
|
|
224
|
+
</template>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`MutationFn<Response, Payload>` receives only `{ end }` and decides when to
|
|
228
|
+
close the Call. If it returns or rejects without calling `end`, the Call stays
|
|
229
|
+
open and `pending` clears, so the user can retry. Errors are not swallowed.
|
|
230
|
+
|
|
231
|
+
When `mutationFn` is optional, `submit(payload).orEnd(value)` supplies a
|
|
232
|
+
per-button fallback response only when no handler was provided. Omitting the
|
|
233
|
+
chain intentionally leaves the Call open for another explicit close path.
|
|
234
|
+
|
|
235
|
+
Pass a `Ref` such as `toRef(props, 'mutationFn')` when a live Call can receive
|
|
236
|
+
an updated handler through `Callable.update()`.
|
|
237
|
+
|
|
238
|
+
## Async components
|
|
239
|
+
|
|
240
|
+
`defineAsyncComponent()` can be passed directly to `createCallable`. The loader
|
|
241
|
+
is not invoked until the first call creates a component instance:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import { defineAsyncComponent } from 'vue'
|
|
245
|
+
import PickerLoadError from './PickerLoadError.vue'
|
|
246
|
+
import PickerLoading from './PickerLoading.vue'
|
|
247
|
+
|
|
248
|
+
const AsyncPicker = defineAsyncComponent({
|
|
249
|
+
loader: () => import('./PickerDialog.vue'),
|
|
250
|
+
loadingComponent: PickerLoading,
|
|
251
|
+
errorComponent: PickerLoadError,
|
|
252
|
+
delay: 0,
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
export const Picker = createCallable<PickerProps, PickedItem>(AsyncPicker)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Use Vue's `loadingComponent`/`errorComponent` options when a call may be added
|
|
259
|
+
after its surrounding `<Suspense>` has already resolved. Ending a call while
|
|
260
|
+
its component is still loading does not resurrect it when the loader finishes.
|
|
261
|
+
|
|
262
|
+
## SSR
|
|
263
|
+
|
|
264
|
+
Callable Roots are safe to render repeatedly with Vue SSR: server-side setup
|
|
265
|
+
does not register a live Root or leak the Root count into later requests. The
|
|
266
|
+
user component is not rendered on the server while the stack is empty.
|
|
267
|
+
`call()` and `upsert()` remain client-imperative APIs and throw
|
|
268
|
+
`No <Root> found!` until the Root's client `onMounted` hook has run.
|
|
269
|
+
|
|
270
|
+
## Accessibility responsibility
|
|
271
|
+
|
|
272
|
+
`call-vue` controls stack and Promise lifecycles; it is deliberately headless.
|
|
273
|
+
Dialog semantics remain the user component's responsibility. A production
|
|
274
|
+
dialog should at least provide an accessible name/description, move focus
|
|
275
|
+
inside on open, trap Tab, close with Escape when appropriate, restore focus on
|
|
276
|
+
unmount, and respect reduced-motion preferences.
|
|
277
|
+
|
|
128
278
|
## Stacking
|
|
129
279
|
|
|
130
280
|
Every `call()` while a previous one is still open stacks on top of it —
|
|
@@ -141,6 +291,73 @@ stack, or rendering all of them with a depth-based transform).
|
|
|
141
291
|
- Unmounting `<Confirm />` resets its stack — a fresh mount always starts
|
|
142
292
|
empty.
|
|
143
293
|
|
|
294
|
+
## Errors and troubleshooting
|
|
295
|
+
|
|
296
|
+
| Error or symptom | Cause and solution |
|
|
297
|
+
| --- | --- |
|
|
298
|
+
| `No <Root> found!` | Mount the returned Callable once and wait for client `onMounted` before calling it. During SSR, move the call to a client interaction. |
|
|
299
|
+
| `Multiple instances of <Root> found!` | The same Callable is mounted in more than one live location. Keep exactly one Root for that Callable. |
|
|
300
|
+
| The Promise never resolves | Every success, cancel, Escape, and backdrop path must explicitly run `call.end(response)` or an external `Callable.end(...)`. Hiding the UI is not enough. |
|
|
301
|
+
| Exit animation is cut off | Match `createCallable(component, unmountingDelay)` to the CSS leave duration and style against `call.ended`. |
|
|
302
|
+
| A targeted `void` end fails to type-check | Use `Toast.end(promise, undefined)`. `Toast.end()` is the broadcast form. |
|
|
303
|
+
| Root data appears missing | Read mounted Root props from `call.root`; normal call props remain top-level component props. |
|
|
304
|
+
|
|
305
|
+
The documentation site has the expanded
|
|
306
|
+
[troubleshooting guide](https://call-vue.retronew.dev/troubleshooting).
|
|
307
|
+
|
|
308
|
+
## Capability matrix
|
|
309
|
+
|
|
310
|
+
This package targets `react-call`'s framework-neutral core semantics, while
|
|
311
|
+
using Vue-native components and lifecycle primitives.
|
|
312
|
+
|
|
313
|
+
| Capability | `call-vue` | Notes |
|
|
314
|
+
| --- | --- | --- |
|
|
315
|
+
| `createCallable`, `call`, `end` | Supported | Promise and broadcast/targeted semantics match the upstream core. |
|
|
316
|
+
| Concurrent Stack | Supported | Every normal call remains independently active. |
|
|
317
|
+
| `upsert`, `update` | Supported | Singleton Promise identity and targeted/broadcast updates are covered. |
|
|
318
|
+
| Root props | Supported | Available reactively through `call.root`. |
|
|
319
|
+
| Exit lifecycle | Supported | `call.ended` plus `unmountingDelay`. |
|
|
320
|
+
| Vue async components | Supported | Use `defineAsyncComponent`; empty stacks stay lazy. |
|
|
321
|
+
| SSR-safe Root creation | Supported | Calling remains client-only. |
|
|
322
|
+
| `<Callable.Root />` alias | Not provided | The direct `<Callable />` Root is the only API; the legacy alias was removed rather than soft-deprecated. |
|
|
323
|
+
| Mutation-flow helper subpath | Supported | Import `useMutationFlow` and its types from `@retronew/call-vue/mutation-flow`. |
|
|
324
|
+
| Vite HMR transform | Not published | Normal Vue HMR applies, but open-call preservation is not promised yet. |
|
|
325
|
+
| Multi-preview host helper | Not published | Mount one Callable Root outside repeated Storybook/Histoire previews manually. |
|
|
326
|
+
|
|
327
|
+
Unsupported entries are deliberate capability boundaries, not hidden aliases.
|
|
328
|
+
Do not import `react-call`-specific subpaths from this package.
|
|
329
|
+
|
|
330
|
+
## FAQ
|
|
331
|
+
|
|
332
|
+
### What if more than one call is active?
|
|
333
|
+
|
|
334
|
+
The Root renders all Calls as a Stack in insertion order. Your component may
|
|
335
|
+
show all of them, position them by `call.index`, or visually prioritize the
|
|
336
|
+
latest using `call.stackSize`.
|
|
337
|
+
|
|
338
|
+
### Can I place more than one Root?
|
|
339
|
+
|
|
340
|
+
Not for the same Callable. You may mount one `Confirm`, one `Toast`, and one
|
|
341
|
+
`Picker` together because those are three independent Callable values.
|
|
342
|
+
|
|
343
|
+
### Does `upsert()` replace normal calls?
|
|
344
|
+
|
|
345
|
+
No. The singleton upsert instance coexists with normal `call()` instances.
|
|
346
|
+
Repeated upserts update only the singleton and return its original Promise.
|
|
347
|
+
|
|
348
|
+
### Is mutation flow just an async function?
|
|
349
|
+
|
|
350
|
+
The domain work is an async function. `useMutationFlow` additionally
|
|
351
|
+
standardizes pending state, duplicate-submit behavior, payload typing, retry
|
|
352
|
+
after failure, and the rule that only an explicit `call.end()` closes the Call.
|
|
353
|
+
Those semantics are why it ships as an optional subpath rather than in the
|
|
354
|
+
core entry.
|
|
355
|
+
|
|
356
|
+
### Can I use Teleport?
|
|
357
|
+
|
|
358
|
+
Yes. Teleport is presentation owned by your user component. Mount the Callable
|
|
359
|
+
Root once, then Teleport each rendered dialog or toast to the desired target.
|
|
360
|
+
|
|
144
361
|
## Claude Code skill
|
|
145
362
|
|
|
146
363
|
This package ships a [Claude Code skill](skills/call-vue/SKILL.md) covering
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Component
|
|
1
|
+
import { Component } from "vue";
|
|
2
2
|
//#region src/createCallable/types.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Properties every `call.*` context carries, regardless of stack position.
|
|
@@ -40,19 +40,18 @@ type PropsWithCall<Props, Response, RootProps> = Props & {
|
|
|
40
40
|
* or functional) whose props satisfy `PropsWithCall<Props, Response, RootProps>`.
|
|
41
41
|
*/
|
|
42
42
|
type UserComponent<Props, Response, RootProps> = Component<PropsWithCall<Props, Response, RootProps>>;
|
|
43
|
+
type EndFunction<Response> = ((promise: Promise<Response>, response: [Response] extends [void] ? undefined : Response) => void) & ([Response] extends [void] ? (response?: undefined) => void : (response: Response) => void);
|
|
43
44
|
/**
|
|
44
45
|
* What `createCallable` returns.
|
|
45
46
|
*
|
|
46
47
|
* The callable is the Root component itself — mount it with `<Confirm />`
|
|
47
|
-
*
|
|
48
|
-
*
|
|
48
|
+
* and use the imperative methods (`call`, `upsert`, `end`, `update`) as
|
|
49
|
+
* properties on the very same object.
|
|
49
50
|
*/
|
|
50
|
-
type Callable<Props, Response, RootProps> =
|
|
51
|
-
/** Alias for the callable itself — `Confirm.Root === Confirm`. */
|
|
52
|
-
Root: DefineComponent<RootProps>;
|
|
51
|
+
type Callable<Props, Response, RootProps> = Component<RootProps> & {
|
|
53
52
|
call: CallFunction<Props, Response>;
|
|
54
53
|
upsert: UpsertFunction<Props, Response>;
|
|
55
|
-
end:
|
|
54
|
+
end: EndFunction<Response>;
|
|
56
55
|
update: ((promise: Promise<Response>, props: Partial<Props>) => void) & ((props: Partial<Props>) => void);
|
|
57
56
|
};
|
|
58
57
|
//#endregion
|
|
@@ -65,8 +64,8 @@ type Callable<Props, Response, RootProps> = DefineComponent<RootProps> & {
|
|
|
65
64
|
*
|
|
66
65
|
* This is a Vue-native port of react-call's `createCallable` — see
|
|
67
66
|
* `skills/call-vue/SKILL.md` for the full mental model (stack, upsert,
|
|
68
|
-
*
|
|
67
|
+
* lifecycle constraints).
|
|
69
68
|
*/
|
|
70
|
-
declare function createCallable<Props = void, Response = void, RootProps extends
|
|
69
|
+
declare function createCallable<Props = void, Response = void, RootProps extends object = {}>(UserComponent: UserComponent<Props, Response, RootProps>, unmountingDelay?: number): Callable<Props, Response, RootProps>;
|
|
71
70
|
//#endregion
|
|
72
71
|
export { type CallContext, type CallFunction, type Callable, type PropsWithCall, type UpsertFunction, type UserComponent, createCallable };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineComponent, h, onUnmounted, shallowRef } from "vue";
|
|
1
|
+
import { defineComponent, h, onMounted, onUnmounted, shallowRef } from "vue";
|
|
2
2
|
//#region src/createCallable/store.ts
|
|
3
3
|
/**
|
|
4
4
|
* Vue-native replacement for react-call's `useSyncExternalStore`-backed
|
|
@@ -58,7 +58,7 @@ function createStackStore() {
|
|
|
58
58
|
*
|
|
59
59
|
* This is a Vue-native port of react-call's `createCallable` — see
|
|
60
60
|
* `skills/call-vue/SKILL.md` for the full mental model (stack, upsert,
|
|
61
|
-
*
|
|
61
|
+
* lifecycle constraints).
|
|
62
62
|
*/
|
|
63
63
|
function createCallable(UserComponent, unmountingDelay = 0) {
|
|
64
64
|
const store = createStackStore();
|
|
@@ -121,13 +121,13 @@ function createCallable(UserComponent, unmountingDelay = 0) {
|
|
|
121
121
|
});
|
|
122
122
|
return promise;
|
|
123
123
|
};
|
|
124
|
-
const end = (...args) => {
|
|
124
|
+
const end = ((...args) => {
|
|
125
125
|
const targeted = args.length === 2;
|
|
126
126
|
const promise = targeted ? args[0] : null;
|
|
127
127
|
const response = targeted ? args[1] : args[0];
|
|
128
128
|
if (!targeted || promise === store.getUpsertPromise()) store.setUpsertPromise(null);
|
|
129
129
|
return createEnd(promise)(response);
|
|
130
|
-
};
|
|
130
|
+
});
|
|
131
131
|
const update = (...args) => {
|
|
132
132
|
const targeted = args.length === 2;
|
|
133
133
|
store.set(targeted ? args[0] : null, (c) => ({
|
|
@@ -142,8 +142,11 @@ function createCallable(UserComponent, unmountingDelay = 0) {
|
|
|
142
142
|
name: "CallableRoot",
|
|
143
143
|
inheritAttrs: false,
|
|
144
144
|
setup(_, { attrs }) {
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
let unmountRoot;
|
|
146
|
+
onMounted(() => {
|
|
147
|
+
unmountRoot = store.mountRoot();
|
|
148
|
+
});
|
|
149
|
+
onUnmounted(() => unmountRoot?.());
|
|
147
150
|
return () => store.stack.value.map((item, index, stack) => h(UserComponent, {
|
|
148
151
|
...item.props,
|
|
149
152
|
key: item.key,
|
|
@@ -159,7 +162,6 @@ function createCallable(UserComponent, unmountingDelay = 0) {
|
|
|
159
162
|
}
|
|
160
163
|
});
|
|
161
164
|
return Object.assign(Root, {
|
|
162
|
-
Root,
|
|
163
165
|
call,
|
|
164
166
|
upsert,
|
|
165
167
|
end,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Ref } from "vue";
|
|
2
|
+
//#region src/mutation-flow/index.d.ts
|
|
3
|
+
/** The narrow call view exposed to a mutation handler. */
|
|
4
|
+
type MutationCall<Response> = {
|
|
5
|
+
end: (response: Response) => void;
|
|
6
|
+
};
|
|
7
|
+
/** An async side-effect that decides when its Call should close. */
|
|
8
|
+
type MutationFn<Response, Payload = void> = (call: MutationCall<Response>, payload: Payload) => Promise<void>;
|
|
9
|
+
/** Runs a required mutation and exposes its in-flight state. */
|
|
10
|
+
type Trigger<Payload> = ((payload: Payload) => void) & {
|
|
11
|
+
pending: boolean;
|
|
12
|
+
};
|
|
13
|
+
/** Adds a per-callsite fallback for an optional mutation. */
|
|
14
|
+
type ChainTrigger<Payload, Response> = ((payload: Payload) => {
|
|
15
|
+
orEnd: (value: Response) => void;
|
|
16
|
+
}) & {
|
|
17
|
+
pending: boolean;
|
|
18
|
+
};
|
|
19
|
+
type MutationSource<Response, Payload> = MutationFn<Response, Payload> | Readonly<Ref<MutationFn<Response, Payload> | undefined>> | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Coordinates an async submission with a Call's lifecycle.
|
|
22
|
+
*
|
|
23
|
+
* Pass a `Ref` when the handler can change while the Call is mounted, such as
|
|
24
|
+
* when `Callable.update()` replaces a `mutationFn` prop.
|
|
25
|
+
*/
|
|
26
|
+
declare function useMutationFlow<Response, Payload = void>(call: MutationCall<Response>, mutationFn: MutationFn<Response, Payload> | Readonly<Ref<MutationFn<Response, Payload>>>): Trigger<Payload>;
|
|
27
|
+
declare function useMutationFlow<Response, Payload = void>(call: MutationCall<Response>, mutationFn: MutationSource<Response, Payload>): ChainTrigger<Payload, Response>;
|
|
28
|
+
//#endregion
|
|
29
|
+
export { ChainTrigger, MutationCall, MutationFn, Trigger, useMutationFlow };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { isRef, ref } from "vue";
|
|
2
|
+
//#region src/mutation-flow/index.ts
|
|
3
|
+
const noopChain = { orEnd: () => {} };
|
|
4
|
+
function resolveMutation(source) {
|
|
5
|
+
return isRef(source) ? source.value : source;
|
|
6
|
+
}
|
|
7
|
+
function useMutationFlow(call, mutationSource) {
|
|
8
|
+
const pending = ref(false);
|
|
9
|
+
let inFlight = false;
|
|
10
|
+
const trigger = ((payload) => {
|
|
11
|
+
if (inFlight) return noopChain;
|
|
12
|
+
const mutationFn = resolveMutation(mutationSource);
|
|
13
|
+
if (!mutationFn) return { orEnd: (value) => call.end(value) };
|
|
14
|
+
inFlight = true;
|
|
15
|
+
pending.value = true;
|
|
16
|
+
mutationFn(call, payload).finally(() => {
|
|
17
|
+
inFlight = false;
|
|
18
|
+
pending.value = false;
|
|
19
|
+
});
|
|
20
|
+
return noopChain;
|
|
21
|
+
});
|
|
22
|
+
Object.defineProperty(trigger, "pending", {
|
|
23
|
+
enumerable: true,
|
|
24
|
+
get: () => pending.value
|
|
25
|
+
});
|
|
26
|
+
return trigger;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { useMutationFlow };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retronew/call-vue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Call & await Vue components like async functions — a Vue 3 port of react-call.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"async",
|
|
@@ -32,19 +32,23 @@
|
|
|
32
32
|
"types": "./dist/index.d.mts",
|
|
33
33
|
"default": "./dist/index.mjs"
|
|
34
34
|
},
|
|
35
|
+
"./mutation-flow": {
|
|
36
|
+
"types": "./dist/mutation-flow/index.d.mts",
|
|
37
|
+
"default": "./dist/mutation-flow/index.mjs"
|
|
38
|
+
},
|
|
35
39
|
"./package.json": "./package.json"
|
|
36
40
|
},
|
|
37
41
|
"publishConfig": {
|
|
38
42
|
"access": "public"
|
|
39
43
|
},
|
|
40
44
|
"devDependencies": {
|
|
41
|
-
"@types/node": "^26.
|
|
45
|
+
"@types/node": "^26.3.0",
|
|
42
46
|
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
43
47
|
"@vue/test-utils": "^2.4.11",
|
|
44
48
|
"jsdom": "^30.0.1",
|
|
45
49
|
"typescript": "^7.0.2",
|
|
46
|
-
"vite-plus": "^0.
|
|
47
|
-
"vitest": "4.1.
|
|
50
|
+
"vite-plus": "^0.3.0",
|
|
51
|
+
"vitest": "4.1.11",
|
|
48
52
|
"vue": "^3.5.41"
|
|
49
53
|
},
|
|
50
54
|
"peerDependencies": {
|
package/skills/call-vue/SKILL.md
CHANGED
|
@@ -33,7 +33,7 @@ served by routing.
|
|
|
33
33
|
`upsert`, `end`, `update`) attached to the same object. Don't call it a
|
|
34
34
|
"modal/dialog/component".
|
|
35
35
|
- **Root** — the mounting form of the Callable: the bare `<Confirm />`
|
|
36
|
-
|
|
36
|
+
itself. There is no separate `.Root` alias. Not a "provider/portal".
|
|
37
37
|
- **Call** — one imperative invocation (`Confirm.call({...})`), resolves to a
|
|
38
38
|
**Response**.
|
|
39
39
|
- **Stack** — the ordered list of active Calls a Root renders (not a
|
|
@@ -53,7 +53,7 @@ type Props = { message: string }
|
|
|
53
53
|
type Response = boolean
|
|
54
54
|
|
|
55
55
|
// `call` is the special injected prop (the CallContext)
|
|
56
|
-
defineProps<PropsWithCall<Props, Response,
|
|
56
|
+
defineProps<PropsWithCall<Props, Response, {}>>()
|
|
57
57
|
</script>
|
|
58
58
|
|
|
59
59
|
<template>
|
|
@@ -86,7 +86,8 @@ const accepted = await Confirm.call({ message: 'Continue?' })
|
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
Generics are `createCallable<Props, Response, RootProps>` (all optional,
|
|
89
|
-
default to `void`/`void`/`
|
|
89
|
+
default to `void`/`void`/`{}`). `RootProps` accepts a normal interface without
|
|
90
|
+
extending `Record<string, unknown>`.
|
|
90
91
|
|
|
91
92
|
Plain functional components work too — `createCallable` accepts anything
|
|
92
93
|
matching Vue's `Component<Props>` type, including a bare
|
|
@@ -109,6 +110,10 @@ tiny callable.
|
|
|
109
110
|
- **End / update from the caller** — `Confirm.end(promise, value)` /
|
|
110
111
|
`Confirm.update(promise, partialProps)` target one Call; omit the promise
|
|
111
112
|
argument to affect every currently active Call instead.
|
|
113
|
+
- **Void responses** — externally, use `Toast.end(promise, undefined)` to
|
|
114
|
+
target one Call and `Toast.end()` to end all Calls. Never write
|
|
115
|
+
`Toast.end(promise)`: a single argument is the broadcast response position.
|
|
116
|
+
Inside the component, `call.end()` is valid and ends only that instance.
|
|
112
117
|
|
|
113
118
|
## Hard rules (the common failures)
|
|
114
119
|
|
|
@@ -136,6 +141,16 @@ tiny callable.
|
|
|
136
141
|
swap into an exit-animation state; the instance stays mounted for 200ms
|
|
137
142
|
after `end()` so a CSS transition (or Vue `<Transition>` wrapping the
|
|
138
143
|
Callable's stack render) has time to play before removal.
|
|
144
|
+
- **SSR only registers on the client.** Repeated server renders do not count as
|
|
145
|
+
mounted Roots. `call()`/`upsert()` before client `onMounted` throw
|
|
146
|
+
*"No `<Root>` found!"*.
|
|
147
|
+
- **Async components load on demand.** Pass `defineAsyncComponent()` directly
|
|
148
|
+
to `createCallable`; an empty stack does not invoke the loader. Prefer its
|
|
149
|
+
`loadingComponent` and `errorComponent` options for Calls inserted after an
|
|
150
|
+
already-resolved `<Suspense>` boundary.
|
|
151
|
+
- **SFC types need Vue tooling.** Use `vue-tsc --noEmit` to validate cross-file
|
|
152
|
+
`.vue` props; a plain TypeScript shim cannot prove that the business props
|
|
153
|
+
and injected `call` prop match.
|
|
139
154
|
|
|
140
155
|
## Anti-patterns
|
|
141
156
|
|
|
@@ -146,9 +161,12 @@ tiny callable.
|
|
|
146
161
|
- Treating the Callable as a plain component to render with data props — the
|
|
147
162
|
props you pass to `<Confirm />` itself are **RootProps** (shared context),
|
|
148
163
|
never the per-instance data; that always goes through `.call(props)`.
|
|
149
|
-
- Reaching into `call.root` for data that changes per-Call —
|
|
150
|
-
|
|
151
|
-
|
|
164
|
+
- Reaching into `call.root` for data that changes per-Call — Root props are
|
|
165
|
+
shared by every active Call (and react to Root prop updates); put data that
|
|
166
|
+
differs per Call in the component's own props instead.
|
|
167
|
+
- Treating a dialog Callable as accessible by default — the library is
|
|
168
|
+
headless. The user component owns naming, initial focus, Tab trapping,
|
|
169
|
+
Escape behavior, focus restoration, and reduced motion.
|
|
152
170
|
|
|
153
171
|
## Quick reference
|
|
154
172
|
|