@wizzard-packages/vue 0.2.2-canary-20260908083257 โ 0.2.2
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 +70 -775
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,822 +1,117 @@
|
|
|
1
|
-
# @wizzard-packages/vue
|
|
1
|
+
# @wizzard-packages/vue
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|

|
|
5
5
|

|
|
6
6
|
|
|
7
|
-
Vue
|
|
8
|
-
|
|
7
|
+
The Vue binding for [`@wizzard-packages/core`](https://www.npmjs.com/package/@wizzard-packages/core).
|
|
8
|
+
It bridges the engine into Vue and does nothing else: navigation, guards and validation are the
|
|
9
|
+
engine's, so this package is one `provide` and eight composables. That is why it is 732 B gzipped
|
|
10
|
+
against 5.07 kB for its 0.x equivalent โ nothing was optimised, the logic moved.
|
|
9
11
|
|
|
10
|
-
##
|
|
11
|
-
|
|
12
|
-
- ๐ข **Vue 3 Native**: Built with Composition API and fine-grained reactivity.
|
|
13
|
-
- ๐ **Headless**: Total control over your UI/components.
|
|
14
|
-
- ๐งช **Strictly Typed**: Full TypeScript support with the Factory Pattern.
|
|
15
|
-
- โก **Atomic Updates**: Components re-render only when the specific data they use changes.
|
|
16
|
-
- ๐ **Core Powered**: Uses the same battle-tested engine as `@wizzard-packages/react`.
|
|
17
|
-
- ๐ **Feature Parity**: All React adapter features available in Vue (validation, guards, persistence, middleware).
|
|
18
|
-
|
|
19
|
-
## ๐ฆ Installation
|
|
12
|
+
## Install
|
|
20
13
|
|
|
21
14
|
```bash
|
|
22
|
-
pnpm add @wizzard-packages/
|
|
23
|
-
# or
|
|
24
|
-
npm install @wizzard-packages/vue @wizzard-packages/core
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
Optional add-ons:
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
pnpm add @wizzard-packages/persistence @wizzard-packages/middleware
|
|
31
|
-
pnpm add @wizzard-packages/adapter-zod zod
|
|
32
|
-
pnpm add @wizzard-packages/adapter-yup yup
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## ๐ Quick Start
|
|
36
|
-
|
|
37
|
-
### 1. Define your Schema and Factory
|
|
38
|
-
|
|
39
|
-
Create a typed wizard instance for your data schema.
|
|
40
|
-
|
|
41
|
-
```typescript
|
|
42
|
-
// wizard.ts
|
|
43
|
-
import { createWizardFactory } from '@wizzard-packages/vue';
|
|
44
|
-
|
|
45
|
-
export interface MySchema {
|
|
46
|
-
user: {
|
|
47
|
-
name: string;
|
|
48
|
-
email: string;
|
|
49
|
-
};
|
|
50
|
-
plan: 'basic' | 'pro';
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export type StepId = 'info' | 'plan' | 'review';
|
|
54
|
-
|
|
55
|
-
export const {
|
|
56
|
-
useProvideWizard,
|
|
57
|
-
useWizardActions,
|
|
58
|
-
useWizardValue,
|
|
59
|
-
useWizardState,
|
|
60
|
-
useWizardField,
|
|
61
|
-
useWizardSelector,
|
|
62
|
-
useWizardError,
|
|
63
|
-
} = createWizardFactory<MySchema>();
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### 2. Provide the Wizard in a Parent Component
|
|
67
|
-
|
|
68
|
-
```vue
|
|
69
|
-
<script setup lang="ts">
|
|
70
|
-
import { useProvideWizard } from './wizard';
|
|
71
|
-
import type { IWizardConfig } from '@wizzard-packages/core';
|
|
72
|
-
|
|
73
|
-
const config: IWizardConfig<'info' | 'plan' | 'review', any> = {
|
|
74
|
-
steps: [
|
|
75
|
-
{ id: 'info', label: 'Personal Info' },
|
|
76
|
-
{ id: 'plan', label: 'Select Plan' },
|
|
77
|
-
{ id: 'review', label: 'Review' },
|
|
78
|
-
],
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
useProvideWizard({
|
|
82
|
-
config,
|
|
83
|
-
initialData: {
|
|
84
|
-
user: { name: '', email: '' },
|
|
85
|
-
plan: 'basic',
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
</script>
|
|
89
|
-
|
|
90
|
-
<template>
|
|
91
|
-
<div class="wizard-container">
|
|
92
|
-
<StepRenderer />
|
|
93
|
-
<NavigationControls />
|
|
94
|
-
</div>
|
|
95
|
-
</template>
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
### 3. Consume State in Child Components
|
|
99
|
-
|
|
100
|
-
#### Reading and Writing Values (`useWizardField`)
|
|
101
|
-
|
|
102
|
-
```vue
|
|
103
|
-
<script setup lang="ts">
|
|
104
|
-
import { useWizardField } from './wizard';
|
|
105
|
-
|
|
106
|
-
// Reactive [ref, setter]
|
|
107
|
-
const [name, setName] = useWizardField('user.name');
|
|
108
|
-
const [email, setEmail] = useWizardField('user.email');
|
|
109
|
-
</script>
|
|
110
|
-
|
|
111
|
-
<template>
|
|
112
|
-
<div>
|
|
113
|
-
<input
|
|
114
|
-
:value="name"
|
|
115
|
-
@input="(e) => setName((e.target as HTMLInputElement).value)"
|
|
116
|
-
placeholder="Enter name"
|
|
117
|
-
/>
|
|
118
|
-
<input
|
|
119
|
-
:value="email"
|
|
120
|
-
@input="(e) => setEmail((e.target as HTMLInputElement).value)"
|
|
121
|
-
placeholder="Enter email"
|
|
122
|
-
type="email"
|
|
123
|
-
/>
|
|
124
|
-
</div>
|
|
125
|
-
</template>
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
#### Navigation Actions (`useWizardActions`)
|
|
129
|
-
|
|
130
|
-
```vue
|
|
131
|
-
<script setup lang="ts">
|
|
132
|
-
import { useWizardActions, useWizardState } from './wizard';
|
|
133
|
-
|
|
134
|
-
const state = useWizardState();
|
|
135
|
-
const { nextStep, prevStep, goToStep } = useWizardActions();
|
|
136
|
-
</script>
|
|
137
|
-
|
|
138
|
-
<template>
|
|
139
|
-
<div class="navigation">
|
|
140
|
-
<button @click="prevStep" :disabled="state.isFirstStep">Back</button>
|
|
141
|
-
<span>Step {{ state.currentStepIndex + 1 }} of {{ state.totalSteps }}</span>
|
|
142
|
-
<button @click="nextStep" :disabled="state.isLastStep">Next</button>
|
|
143
|
-
</div>
|
|
144
|
-
</template>
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
## ๐ API Reference
|
|
148
|
-
|
|
149
|
-
### `createWizardFactory<TSchema>()`
|
|
150
|
-
|
|
151
|
-
Returns a set of typed hooks for your schema:
|
|
152
|
-
|
|
153
|
-
#### `useProvideWizard(options)`
|
|
154
|
-
|
|
155
|
-
Initializes and provides the wizard store to the component tree.
|
|
156
|
-
|
|
157
|
-
```typescript
|
|
158
|
-
interface IWizardProviderOptions<StepId, TSchema> {
|
|
159
|
-
config: IWizardConfig<StepId, TSchema>;
|
|
160
|
-
initialData?: TSchema;
|
|
161
|
-
initialStepId?: StepId;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
useProvideWizard({
|
|
165
|
-
config: {
|
|
166
|
-
steps: [
|
|
167
|
-
{ id: 'step1', label: 'Step 1' },
|
|
168
|
-
{ id: 'step2', label: 'Step 2' },
|
|
169
|
-
],
|
|
170
|
-
validationMode: 'onChange', // 'onChange' | 'onBlur' | 'onSubmit'
|
|
171
|
-
validationDebounceTime: 300,
|
|
172
|
-
},
|
|
173
|
-
initialData: { name: '' },
|
|
174
|
-
initialStepId: 'step1',
|
|
175
|
-
});
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
#### `useWizardState()`
|
|
179
|
-
|
|
180
|
-
Returns a reactive computed ref with the complete wizard state:
|
|
181
|
-
|
|
182
|
-
```typescript
|
|
183
|
-
const state = useWizardState();
|
|
184
|
-
|
|
185
|
-
// Available properties:
|
|
186
|
-
state.currentStepId; // Current step ID
|
|
187
|
-
state.currentStepIndex; // Current step index (0-based)
|
|
188
|
-
state.totalSteps; // Total number of active steps
|
|
189
|
-
state.isFirstStep; // Boolean: is first step
|
|
190
|
-
state.isLastStep; // Boolean: is last step
|
|
191
|
-
state.canGoNext; // Boolean: can navigate forward
|
|
192
|
-
state.canGoPrev; // Boolean: can navigate backward
|
|
193
|
-
state.visitedSteps; // Set of visited step IDs
|
|
194
|
-
state.completedSteps; // Set of completed step IDs
|
|
195
|
-
state.errorSteps; // Set of steps with errors
|
|
196
|
-
state.progress; // Progress percentage (0-100)
|
|
197
|
-
state.isBusy; // Boolean: async operation in progress
|
|
198
|
-
state.data; // Current wizard data
|
|
199
|
-
state.activeSteps; // Array of currently active steps
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
#### `useWizardValue(path, defaultValue?)`
|
|
203
|
-
|
|
204
|
-
Returns a reactive ref for a specific path in the wizard data:
|
|
205
|
-
|
|
206
|
-
```typescript
|
|
207
|
-
const name = useWizardValue('user.name');
|
|
208
|
-
const email = useWizardValue('user.email', 'default@example.com');
|
|
209
|
-
|
|
210
|
-
// Automatically updates when the value changes
|
|
211
|
-
console.log(name.value); // 'John'
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
#### `useWizardField(path, defaultValue?)`
|
|
215
|
-
|
|
216
|
-
Returns `[ref, setter]` tuple for a specific field:
|
|
217
|
-
|
|
218
|
-
```typescript
|
|
219
|
-
const [name, setName] = useWizardField('user.name');
|
|
220
|
-
const [age, setAge] = useWizardField('user.age', 18);
|
|
221
|
-
|
|
222
|
-
// Use in template
|
|
223
|
-
<input :value="name" @input="e => setName(e.target.value)" />
|
|
224
|
-
|
|
225
|
-
// Or with v-model workaround
|
|
226
|
-
const nameModel = computed({
|
|
227
|
-
get: () => name.value,
|
|
228
|
-
set: (val) => setName(val)
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
<input v-model="nameModel" />
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
#### `useWizardActions()`
|
|
235
|
-
|
|
236
|
-
Returns navigation and data manipulation methods:
|
|
237
|
-
|
|
238
|
-
```typescript
|
|
239
|
-
const actions = useWizardActions();
|
|
240
|
-
|
|
241
|
-
// Navigation
|
|
242
|
-
actions.nextStep(); // Go to next step
|
|
243
|
-
actions.prevStep(); // Go to previous step
|
|
244
|
-
actions.goToStep('stepId'); // Go to specific step
|
|
245
|
-
actions.goToStepIndex(2); // Go to step by index
|
|
246
|
-
|
|
247
|
-
// Data manipulation
|
|
248
|
-
actions.setData('user.name', 'John'); // Set single value
|
|
249
|
-
actions.updateData({
|
|
250
|
-
// Update multiple values
|
|
251
|
-
user: { name: 'John', email: 'john@example.com' },
|
|
252
|
-
});
|
|
253
|
-
actions.getData('user.name'); // Get value by path
|
|
254
|
-
actions.getData('user.name', 'Default'); // With default value
|
|
255
|
-
|
|
256
|
-
// Validation
|
|
257
|
-
actions.validateStep('stepId'); // Validate specific step
|
|
258
|
-
actions.validateAll(); // Validate all steps
|
|
259
|
-
|
|
260
|
-
// Error management
|
|
261
|
-
actions.setErrors({
|
|
262
|
-
// Set errors for a step
|
|
263
|
-
stepId: {
|
|
264
|
-
fieldName: 'Error message',
|
|
265
|
-
},
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
// Reset
|
|
269
|
-
actions.reset(); // Reset to initial state
|
|
270
|
-
actions.reset({ name: 'New' }); // Reset with new data
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
#### `useWizardSelector(selector)`
|
|
274
|
-
|
|
275
|
-
Returns a reactive computed value derived from wizard state:
|
|
276
|
-
|
|
277
|
-
```typescript
|
|
278
|
-
const progress = useWizardSelector((state) => state.progress);
|
|
279
|
-
const isValid = useWizardSelector((state) => state.errorSteps.size === 0);
|
|
280
|
-
|
|
281
|
-
// Complex derived state
|
|
282
|
-
const wizardStatus = useWizardSelector((state) => ({
|
|
283
|
-
step: state.currentStepId,
|
|
284
|
-
progress: state.progress,
|
|
285
|
-
canProceed: state.canGoNext && !state.isBusy,
|
|
286
|
-
hasErrors: state.errorSteps.size > 0,
|
|
287
|
-
}));
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
#### `useWizardError(path)`
|
|
291
|
-
|
|
292
|
-
Returns a reactive ref with the error message for a specific field:
|
|
293
|
-
|
|
294
|
-
```typescript
|
|
295
|
-
const nameError = useWizardError('user.name');
|
|
296
|
-
const emailError = useWizardError('user.email');
|
|
297
|
-
|
|
298
|
-
// Use in template
|
|
299
|
-
<div v-if="nameError" class="error">{{ nameError }}</div>
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
## ๐ Advanced Examples
|
|
303
|
-
|
|
304
|
-
### Example 1: Step Guards (beforeLeave)
|
|
305
|
-
|
|
306
|
-
Prevent navigation until validation passes:
|
|
307
|
-
|
|
308
|
-
```typescript
|
|
309
|
-
const config: IWizardConfig<StepId, MySchema> = {
|
|
310
|
-
steps: [
|
|
311
|
-
{
|
|
312
|
-
id: 'info',
|
|
313
|
-
label: 'Personal Info',
|
|
314
|
-
beforeLeave: async (data) => {
|
|
315
|
-
if (!data.user.name) {
|
|
316
|
-
return { canLeave: false, error: 'Name is required' };
|
|
317
|
-
}
|
|
318
|
-
if (!data.user.email.includes('@')) {
|
|
319
|
-
return { canLeave: false, error: 'Valid email required' };
|
|
320
|
-
}
|
|
321
|
-
return { canLeave: true };
|
|
322
|
-
},
|
|
323
|
-
},
|
|
324
|
-
{ id: 'review', label: 'Review' },
|
|
325
|
-
],
|
|
326
|
-
};
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
### Example 2: Conditional Steps (isVisible)
|
|
330
|
-
|
|
331
|
-
Show/hide steps based on data:
|
|
332
|
-
|
|
333
|
-
```typescript
|
|
334
|
-
const config: IWizardConfig<StepId, MySchema> = {
|
|
335
|
-
steps: [
|
|
336
|
-
{ id: 'plan', label: 'Select Plan' },
|
|
337
|
-
{
|
|
338
|
-
id: 'payment',
|
|
339
|
-
label: 'Payment',
|
|
340
|
-
isVisible: (data) => data.plan === 'pro', // Only show for pro plan
|
|
341
|
-
},
|
|
342
|
-
{ id: 'review', label: 'Review' },
|
|
343
|
-
],
|
|
344
|
-
};
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
### Example 3: Async Step Conditions
|
|
348
|
-
|
|
349
|
-
Dynamic step visibility with async checks:
|
|
350
|
-
|
|
351
|
-
```typescript
|
|
352
|
-
const config: IWizardConfig<StepId, MySchema> = {
|
|
353
|
-
steps: [
|
|
354
|
-
{ id: 'account', label: 'Account' },
|
|
355
|
-
{
|
|
356
|
-
id: 'premium',
|
|
357
|
-
label: 'Premium Features',
|
|
358
|
-
isVisible: async (data) => {
|
|
359
|
-
const hasAccess = await checkPremiumAccess(data.user.id);
|
|
360
|
-
return hasAccess;
|
|
361
|
-
},
|
|
362
|
-
},
|
|
363
|
-
],
|
|
364
|
-
};
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
### Example 4: Zod Validation
|
|
368
|
-
|
|
369
|
-
Integrate with Zod for schema validation:
|
|
370
|
-
|
|
371
|
-
```vue
|
|
372
|
-
<script setup lang="ts">
|
|
373
|
-
import { z } from 'zod';
|
|
374
|
-
import { ZodAdapter } from '@wizzard-packages/adapter-zod';
|
|
375
|
-
import { useProvideWizard } from './wizard';
|
|
376
|
-
|
|
377
|
-
const userSchema = z.object({
|
|
378
|
-
name: z.string().min(2, 'Name must be at least 2 characters'),
|
|
379
|
-
email: z.string().email('Invalid email address'),
|
|
380
|
-
age: z.number().min(18, 'Must be 18 or older'),
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
const config = {
|
|
384
|
-
steps: [
|
|
385
|
-
{
|
|
386
|
-
id: 'info',
|
|
387
|
-
label: 'Info',
|
|
388
|
-
validate: ZodAdapter(userSchema),
|
|
389
|
-
},
|
|
390
|
-
],
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
useProvideWizard({ config, initialData: { name: '', email: '', age: 0 } });
|
|
394
|
-
</script>
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
### Example 5: LocalStorage Persistence
|
|
398
|
-
|
|
399
|
-
Persist wizard state across sessions:
|
|
400
|
-
|
|
401
|
-
```typescript
|
|
402
|
-
import { LocalStorageAdapter } from '@wizzard-packages/persistence';
|
|
403
|
-
|
|
404
|
-
const config = {
|
|
405
|
-
steps: [
|
|
406
|
-
{ id: 'step1', label: 'Step 1' },
|
|
407
|
-
{ id: 'step2', label: 'Step 2' },
|
|
408
|
-
],
|
|
409
|
-
persistence: LocalStorageAdapter('my-wizard-state'),
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
useProvideWizard({ config, initialData: {} });
|
|
15
|
+
pnpm add @wizzard-packages/core@canary @wizzard-packages/vue@canary
|
|
413
16
|
```
|
|
414
17
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
```typescript
|
|
420
|
-
import { loggerMiddleware } from '@wizzard-packages/middleware';
|
|
421
|
-
|
|
422
|
-
const config = {
|
|
423
|
-
steps: [{ id: 'step1', label: 'Step 1' }],
|
|
424
|
-
middlewares: [loggerMiddleware],
|
|
425
|
-
};
|
|
18
|
+
Vue 3.3 or newer. Reactivity rides on snapshot identity: the engine returns the same object
|
|
19
|
+
until a commit, so a `shallowRef` holding it invalidates exactly once per commit and every
|
|
20
|
+
`computed` derived from it caches for free.
|
|
426
21
|
|
|
427
|
-
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
### Example 7: Progress Indicator Component
|
|
431
|
-
|
|
432
|
-
```vue
|
|
433
|
-
<script setup lang="ts">
|
|
434
|
-
import { useWizardState } from './wizard';
|
|
435
|
-
|
|
436
|
-
const state = useWizardState();
|
|
437
|
-
</script>
|
|
438
|
-
|
|
439
|
-
<template>
|
|
440
|
-
<div class="progress-bar">
|
|
441
|
-
<div class="progress-fill" :style="{ width: `${state.progress}%` }" />
|
|
442
|
-
</div>
|
|
443
|
-
<div class="progress-text">
|
|
444
|
-
{{ state.progress }}% Complete ({{ state.currentStepIndex + 1 }} / {{ state.totalSteps }})
|
|
445
|
-
</div>
|
|
446
|
-
</template>
|
|
447
|
-
|
|
448
|
-
<style scoped>
|
|
449
|
-
.progress-bar {
|
|
450
|
-
width: 100%;
|
|
451
|
-
height: 8px;
|
|
452
|
-
background: #e0e0e0;
|
|
453
|
-
border-radius: 4px;
|
|
454
|
-
overflow: hidden;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
.progress-fill {
|
|
458
|
-
height: 100%;
|
|
459
|
-
background: linear-gradient(90deg, #4caf50, #8bc34a);
|
|
460
|
-
transition: width 0.3s ease;
|
|
461
|
-
}
|
|
462
|
-
</style>
|
|
463
|
-
```
|
|
22
|
+
## Use
|
|
464
23
|
|
|
465
|
-
|
|
24
|
+
`provideWizard` goes in a parent, because `inject` reads the parent chain โ the component that
|
|
25
|
+
provides the wizard is not the one that uses it. Pass a flow and the scope owns and destroys
|
|
26
|
+
the engine, or pass a `Wizard` you built yourself and it stays yours.
|
|
466
27
|
|
|
467
28
|
```vue
|
|
468
29
|
<script setup lang="ts">
|
|
469
|
-
import {
|
|
30
|
+
import { provideWizard } from '@wizzard-packages/vue/v1';
|
|
470
31
|
|
|
471
|
-
|
|
472
|
-
|
|
32
|
+
import { signup } from './flow';
|
|
33
|
+
import Wizard from './Wizard.vue';
|
|
473
34
|
|
|
474
|
-
|
|
475
|
-
return state.visitedSteps.has(stepId) || state.completedSteps.has(stepId);
|
|
476
|
-
};
|
|
35
|
+
provideWizard({ flow: signup });
|
|
477
36
|
</script>
|
|
478
37
|
|
|
479
38
|
<template>
|
|
480
|
-
<
|
|
481
|
-
<div
|
|
482
|
-
v-for="(step, index) in state.activeSteps"
|
|
483
|
-
:key="step.id"
|
|
484
|
-
class="breadcrumb"
|
|
485
|
-
:class="{
|
|
486
|
-
active: step.id === state.currentStepId,
|
|
487
|
-
completed: state.completedSteps.has(step.id),
|
|
488
|
-
error: state.errorSteps.has(step.id),
|
|
489
|
-
}"
|
|
490
|
-
@click="canNavigateToStep(step.id) && goToStep(step.id)"
|
|
491
|
-
>
|
|
492
|
-
<span class="number">{{ index + 1 }}</span>
|
|
493
|
-
<span class="label">{{ step.label }}</span>
|
|
494
|
-
</div>
|
|
495
|
-
</div>
|
|
39
|
+
<Wizard />
|
|
496
40
|
</template>
|
|
497
41
|
```
|
|
498
42
|
|
|
499
|
-
|
|
43
|
+
<!-- example:quickstart-vue -->
|
|
500
44
|
|
|
45
|
+
<!-- prettier-ignore -->
|
|
501
46
|
```vue
|
|
502
47
|
<script setup lang="ts">
|
|
503
|
-
import {
|
|
48
|
+
import { useField, useNavigation, useStep } from '@wizzard-packages/vue/v1';
|
|
504
49
|
|
|
505
|
-
const
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
const nameError = useWizardError('user.name');
|
|
509
|
-
const emailError = useWizardError('user.email');
|
|
510
|
-
|
|
511
|
-
const { validateStep, nextStep } = useWizardActions();
|
|
512
|
-
|
|
513
|
-
const handleSubmit = async () => {
|
|
514
|
-
const result = await validateStep('info');
|
|
515
|
-
if (result.isValid) {
|
|
516
|
-
await nextStep();
|
|
517
|
-
}
|
|
518
|
-
};
|
|
50
|
+
const { current, isLast } = useStep();
|
|
51
|
+
const { next, back, canBack } = useNavigation();
|
|
52
|
+
const full = useField<string>('name.full');
|
|
519
53
|
</script>
|
|
520
54
|
|
|
521
55
|
<template>
|
|
522
|
-
<form @submit.prevent
|
|
523
|
-
<
|
|
524
|
-
|
|
525
|
-
<input
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
</div>
|
|
532
|
-
|
|
533
|
-
<div class="field">
|
|
534
|
-
<label>Email</label>
|
|
535
|
-
<input
|
|
536
|
-
type="email"
|
|
537
|
-
:value="email"
|
|
538
|
-
@input="(e) => setEmail((e.target as HTMLInputElement).value)"
|
|
539
|
-
:class="{ error: emailError }"
|
|
540
|
-
/>
|
|
541
|
-
<span v-if="emailError" class="error-message">{{ emailError }}</span>
|
|
542
|
-
</div>
|
|
543
|
-
|
|
544
|
-
<button type="submit">Continue</button>
|
|
56
|
+
<form @submit.prevent>
|
|
57
|
+
<label v-if="current === 'name'">
|
|
58
|
+
Your name
|
|
59
|
+
<input v-model="full" />
|
|
60
|
+
</label>
|
|
61
|
+
<p v-else-if="current === 'review'">Hello, {{ full || 'stranger' }}.</p>
|
|
62
|
+
|
|
63
|
+
<button type="button" :disabled="!canBack" @click="back()">Back</button>
|
|
64
|
+
<button type="button" :disabled="isLast" @click="next()">Next</button>
|
|
545
65
|
</form>
|
|
546
66
|
</template>
|
|
547
67
|
```
|
|
548
68
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
For easier v-model integration:
|
|
552
|
-
|
|
553
|
-
```vue
|
|
554
|
-
<script setup lang="ts">
|
|
555
|
-
import { computed } from 'vue';
|
|
556
|
-
import { useWizardField } from './wizard';
|
|
557
|
-
|
|
558
|
-
const [nameRef, setName] = useWizardField('user.name');
|
|
559
|
-
const [emailRef, setEmail] = useWizardField('user.email');
|
|
560
|
-
|
|
561
|
-
// Create computed properties for v-model
|
|
562
|
-
const name = computed({
|
|
563
|
-
get: () => nameRef.value,
|
|
564
|
-
set: (val) => setName(val),
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
const email = computed({
|
|
568
|
-
get: () => emailRef.value,
|
|
569
|
-
set: (val) => setEmail(val),
|
|
570
|
-
});
|
|
571
|
-
</script>
|
|
572
|
-
|
|
573
|
-
<template>
|
|
574
|
-
<div>
|
|
575
|
-
<input v-model="name" placeholder="Name" />
|
|
576
|
-
<input v-model="email" type="email" placeholder="Email" />
|
|
577
|
-
</div>
|
|
578
|
-
</template>
|
|
579
|
-
```
|
|
580
|
-
|
|
581
|
-
### Example 11: Complex Selector with Performance
|
|
582
|
-
|
|
583
|
-
```vue
|
|
584
|
-
<script setup lang="ts">
|
|
585
|
-
import { useWizardSelector } from './wizard';
|
|
586
|
-
|
|
587
|
-
// Only re-renders when these specific values change
|
|
588
|
-
const wizardMeta = useWizardSelector((state) => ({
|
|
589
|
-
currentStep: state.activeSteps.find((s) => s.id === state.currentStepId),
|
|
590
|
-
isLastAndValid: state.isLastStep && state.errorSteps.size === 0,
|
|
591
|
-
completionRate: (state.completedSteps.size / state.totalSteps) * 100,
|
|
592
|
-
}));
|
|
593
|
-
</script>
|
|
594
|
-
|
|
595
|
-
<template>
|
|
596
|
-
<div class="wizard-meta">
|
|
597
|
-
<h2>{{ wizardMeta.currentStep?.label }}</h2>
|
|
598
|
-
<p v-if="wizardMeta.isLastAndValid">Ready to submit!</p>
|
|
599
|
-
<p>{{ wizardMeta.completionRate.toFixed(0) }}% of steps completed</p>
|
|
600
|
-
</div>
|
|
601
|
-
</template>
|
|
602
|
-
```
|
|
603
|
-
|
|
604
|
-
### Example 12: Dynamic Step Configuration
|
|
605
|
-
|
|
606
|
-
```vue
|
|
607
|
-
<script setup lang="ts">
|
|
608
|
-
import { ref, computed } from 'vue';
|
|
609
|
-
import { useProvideWizard } from './wizard';
|
|
610
|
-
import type { IStepConfig } from '@wizzard-packages/core';
|
|
611
|
-
|
|
612
|
-
const userType = ref<'personal' | 'business'>('personal');
|
|
613
|
-
|
|
614
|
-
const steps = computed(() => {
|
|
615
|
-
const baseSteps: IStepConfig<string, any>[] = [{ id: 'type', label: 'Account Type' }];
|
|
616
|
-
|
|
617
|
-
if (userType.value === 'business') {
|
|
618
|
-
baseSteps.push({ id: 'company', label: 'Company Info' }, { id: 'tax', label: 'Tax Details' });
|
|
619
|
-
} else {
|
|
620
|
-
baseSteps.push({ id: 'personal', label: 'Personal Info' });
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
baseSteps.push({ id: 'review', label: 'Review' });
|
|
624
|
-
|
|
625
|
-
return baseSteps;
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
const config = computed(() => ({
|
|
629
|
-
steps: steps.value,
|
|
630
|
-
}));
|
|
631
|
-
|
|
632
|
-
useProvideWizard({
|
|
633
|
-
config: config.value,
|
|
634
|
-
initialData: { type: userType.value },
|
|
635
|
-
});
|
|
636
|
-
</script>
|
|
637
|
-
```
|
|
638
|
-
|
|
639
|
-
## ๐ฏ Best Practices
|
|
640
|
-
|
|
641
|
-
### 1. Memoize Configuration
|
|
642
|
-
|
|
643
|
-
Use `computed` for step configuration to avoid unnecessary recalculations:
|
|
644
|
-
|
|
645
|
-
```typescript
|
|
646
|
-
const config = computed(() => ({
|
|
647
|
-
steps: [{ id: 'step1', label: 'Step 1' }],
|
|
648
|
-
middlewares: [loggerMiddleware],
|
|
649
|
-
validationMode: 'onChange',
|
|
650
|
-
}));
|
|
651
|
-
```
|
|
652
|
-
|
|
653
|
-
### 2. Granular Subscriptions
|
|
654
|
-
|
|
655
|
-
Prefer specific hooks over full state for better performance:
|
|
656
|
-
|
|
657
|
-
```typescript
|
|
658
|
-
// โ
Good: Only re-renders when 'user.name' changes
|
|
659
|
-
const name = useWizardValue('user.name');
|
|
660
|
-
|
|
661
|
-
// โ Avoid: Re-renders on any state change
|
|
662
|
-
const state = useWizardState();
|
|
663
|
-
const name = state.data.user.name;
|
|
664
|
-
```
|
|
665
|
-
|
|
666
|
-
### 3. Use Selectors for Derived State
|
|
667
|
-
|
|
668
|
-
Create computed values with `useWizardSelector`:
|
|
669
|
-
|
|
670
|
-
```typescript
|
|
671
|
-
// Only re-computes when progress or isBusy changes
|
|
672
|
-
const status = useWizardSelector((state) => ({
|
|
673
|
-
progress: state.progress,
|
|
674
|
-
loading: state.isBusy,
|
|
675
|
-
}));
|
|
676
|
-
```
|
|
677
|
-
|
|
678
|
-
### 4. Validation Timing
|
|
679
|
-
|
|
680
|
-
Choose appropriate validation mode:
|
|
681
|
-
|
|
682
|
-
- `onChange`: Real-time validation (best UX, more API calls)
|
|
683
|
-
- `onBlur`: Validate on field blur (balanced)
|
|
684
|
-
- `onSubmit`: Validate on step navigation (least intrusive)
|
|
685
|
-
|
|
686
|
-
### 5. Error Handling
|
|
687
|
-
|
|
688
|
-
Always handle async errors in guards and validation:
|
|
689
|
-
|
|
690
|
-
```typescript
|
|
691
|
-
{
|
|
692
|
-
id: 'step1',
|
|
693
|
-
beforeLeave: async (data) => {
|
|
694
|
-
try {
|
|
695
|
-
await validateAPI(data);
|
|
696
|
-
return { canLeave: true };
|
|
697
|
-
} catch (error) {
|
|
698
|
-
return {
|
|
699
|
-
canLeave: false,
|
|
700
|
-
error: error.message || 'Validation failed'
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
```
|
|
706
|
-
|
|
707
|
-
## ๐ Integration with Vue Router
|
|
708
|
-
|
|
709
|
-
```vue
|
|
710
|
-
<script setup lang="ts">
|
|
711
|
-
import { watch } from 'vue';
|
|
712
|
-
import { useRoute, useRouter } from 'vue-router';
|
|
713
|
-
import { useWizardActions, useWizardState } from './wizard';
|
|
69
|
+
<!-- /example -->
|
|
714
70
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
const { goToStep } = useWizardActions();
|
|
718
|
-
const state = useWizardState();
|
|
71
|
+
That file and the flow it imports are `examples/quickstart`, which CI runs โ this block is
|
|
72
|
+
generated from them, so what you paste is what is tested.
|
|
719
73
|
|
|
720
|
-
|
|
721
|
-
watch(
|
|
722
|
-
() => route.params.step,
|
|
723
|
-
(stepId) => {
|
|
724
|
-
if (stepId && stepId !== state.currentStepId) {
|
|
725
|
-
goToStep(stepId as string);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
);
|
|
729
|
-
|
|
730
|
-
// Sync route with wizard step
|
|
731
|
-
watch(
|
|
732
|
-
() => state.currentStepId,
|
|
733
|
-
(stepId) => {
|
|
734
|
-
if (route.params.step !== stepId) {
|
|
735
|
-
router.push({ params: { step: stepId } });
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
);
|
|
739
|
-
</script>
|
|
740
|
-
```
|
|
74
|
+
## Composables
|
|
741
75
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const wrapper = mount({
|
|
751
|
-
setup() {
|
|
752
|
-
useProvideWizard({
|
|
753
|
-
config: {
|
|
754
|
-
steps: [
|
|
755
|
-
{ id: 'step1', label: 'Step 1' },
|
|
756
|
-
{ id: 'step2', label: 'Step 2' },
|
|
757
|
-
],
|
|
758
|
-
},
|
|
759
|
-
initialData: {},
|
|
760
|
-
});
|
|
761
|
-
|
|
762
|
-
const state = useWizardState();
|
|
763
|
-
const { nextStep } = useWizardActions();
|
|
764
|
-
|
|
765
|
-
return { state, nextStep };
|
|
766
|
-
},
|
|
767
|
-
template: '<div>{{ state.currentStepId }}</div>',
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
expect(wrapper.text()).toBe('step1');
|
|
771
|
-
|
|
772
|
-
await wrapper.vm.nextStep();
|
|
773
|
-
await wrapper.vm.$nextTick();
|
|
774
|
-
|
|
775
|
-
expect(wrapper.text()).toBe('step2');
|
|
776
|
-
});
|
|
777
|
-
});
|
|
778
|
-
```
|
|
779
|
-
|
|
780
|
-
## ๐ SSR Support
|
|
781
|
-
|
|
782
|
-
The Vue adapter is SSR-friendly and works with Nuxt 3:
|
|
783
|
-
|
|
784
|
-
```typescript
|
|
785
|
-
// Avoid persistence adapters that access `window` during module initialization
|
|
786
|
-
// Use lazy initialization instead:
|
|
787
|
-
|
|
788
|
-
const config = computed(() => ({
|
|
789
|
-
steps: [...],
|
|
790
|
-
persistence: typeof window !== 'undefined'
|
|
791
|
-
? LocalStorageAdapter('key')
|
|
792
|
-
: undefined
|
|
793
|
-
}));
|
|
794
|
-
```
|
|
76
|
+
| Composable | Returns |
|
|
77
|
+
| ----------------------- | -------------------------------------------------------------------------------------------- |
|
|
78
|
+
| `useStep()` | `current`, `isFirst`, `isLast`, `progress`, `breadcrumbs` and the rest, each a `ComputedRef` |
|
|
79
|
+
| `useNavigation()` | `next`, `back`, `go`, `cancel`, and `canBack`, `isBusy`, `isLast` as `ComputedRef`s |
|
|
80
|
+
| `useField<T>(path)` | a `WritableComputedRef`, usable directly with `v-model` |
|
|
81
|
+
| `useErrors(stepId?)` | a `ComputedRef` of the error map for a step, or for the current one |
|
|
82
|
+
| `useWizardSelector(fn)` | one derived value as a `ComputedRef` |
|
|
83
|
+
| `useWizard()` | the engine itself, for anything the composables above do not cover |
|
|
795
84
|
|
|
796
|
-
|
|
85
|
+
`useOptionalWizard()` returns `null` outside a provider instead of throwing, which is what a
|
|
86
|
+
component rendered both inside and outside a wizard needs.
|
|
797
87
|
|
|
798
|
-
|
|
799
|
-
-
|
|
800
|
-
-
|
|
88
|
+
Navigation is async and returns a result, not a boolean: `await next()` gives
|
|
89
|
+
`{ ok: false, reason: 'blocked', by: 'age-check' }` when a guard refuses. Every `await` inside
|
|
90
|
+
the engine re-checks a navigation epoch, so a validator that resolves after the user pressed
|
|
91
|
+
Back cannot move them.
|
|
801
92
|
|
|
802
|
-
|
|
93
|
+
On the server `onMounted` never runs, so a wizard rendered by SSR navigates nowhere and hydrates
|
|
94
|
+
into its first step on the client.
|
|
803
95
|
|
|
804
|
-
|
|
805
|
-
- **Optional persistence**: `@wizzard-packages/persistence`
|
|
806
|
-
- **Optional middleware**: `@wizzard-packages/middleware`
|
|
807
|
-
- **Optional validation**: `@wizzard-packages/adapter-zod` or `@wizzard-packages/adapter-yup`
|
|
96
|
+
## Documentation
|
|
808
97
|
|
|
809
|
-
|
|
98
|
+
[Getting started](https://zizzx.github.io/wizzard-packages/docs/start/) ยท
|
|
99
|
+
[The flow](https://zizzx.github.io/wizzard-packages/docs/flow/) ยท
|
|
100
|
+
[Navigation](https://zizzx.github.io/wizzard-packages/docs/navigation/) ยท
|
|
101
|
+
[Validation](https://zizzx.github.io/wizzard-packages/docs/validation/) ยท
|
|
102
|
+
[Persistence](https://zizzx.github.io/wizzard-packages/docs/persistence/)
|
|
810
103
|
|
|
811
|
-
|
|
812
|
-
-
|
|
813
|
-
|
|
814
|
-
- **Changelog**: [CHANGELOG.md](./CHANGELOG.md)
|
|
104
|
+
The same names, as hooks, are in
|
|
105
|
+
[`@wizzard-packages/react`](https://www.npmjs.com/package/@wizzard-packages/react). A shared
|
|
106
|
+
contract suite runs against both, which is what stops them drifting apart.
|
|
815
107
|
|
|
816
|
-
##
|
|
108
|
+
## Upgrading from 0.x
|
|
817
109
|
|
|
818
|
-
|
|
110
|
+
0.x was a different library with the same name: `createWizardFactory<TSchema>()`,
|
|
111
|
+
`useProvideWizard`, `useWizardState`, `useWizardActions`. In v1 the names are shorter and the
|
|
112
|
+
import path carries the version โ `@wizzard-packages/vue/v1`. v1 is on the `canary` tag while
|
|
113
|
+
the launch lands; the 0.x line on `latest` is being retired.
|
|
819
114
|
|
|
820
|
-
|
|
115
|
+
## License
|
|
821
116
|
|
|
822
|
-
|
|
117
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wizzard-packages/vue",
|
|
3
|
-
"version": "0.2.2
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Vue 3 composition API and components for Wizzard Stepper.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wizard",
|
|
@@ -59,9 +59,9 @@
|
|
|
59
59
|
"vue": "^3.3.0"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@wizzard-packages/core": "0.5.0
|
|
63
|
-
"@wizzard-packages/middleware": "0.1.5
|
|
64
|
-
"@wizzard-packages/persistence": "0.1.5
|
|
62
|
+
"@wizzard-packages/core": "0.5.0",
|
|
63
|
+
"@wizzard-packages/middleware": "0.1.5",
|
|
64
|
+
"@wizzard-packages/persistence": "0.1.5"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@vitejs/plugin-vue": "^5.2.1",
|