@wizzard-packages/vue 0.2.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/LICENSE +21 -0
- package/README.md +836 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +107 -0
- package/dist/index.d.ts +107 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
# @wizzard-packages/vue ๐งโโ๏ธ
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
Vue 3 Composition API bindings for **Wizzard Stepper**.
|
|
8
|
+
Build flexible, headless, and strictly typed multi-step wizards with Vue 3.
|
|
9
|
+
|
|
10
|
+
## โจ Features
|
|
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
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm add @wizzard-packages/vue @wizzard-packages/core
|
|
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">
|
|
141
|
+
Back
|
|
142
|
+
</button>
|
|
143
|
+
<span>Step {{ state.currentStepIndex + 1 }} of {{ state.totalSteps }}</span>
|
|
144
|
+
<button @click="nextStep" :disabled="state.isLastStep">
|
|
145
|
+
Next
|
|
146
|
+
</button>
|
|
147
|
+
</div>
|
|
148
|
+
</template>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## ๐ API Reference
|
|
152
|
+
|
|
153
|
+
### `createWizardFactory<TSchema>()`
|
|
154
|
+
|
|
155
|
+
Returns a set of typed hooks for your schema:
|
|
156
|
+
|
|
157
|
+
#### `useProvideWizard(options)`
|
|
158
|
+
|
|
159
|
+
Initializes and provides the wizard store to the component tree.
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
interface IWizardProviderOptions<StepId, TSchema> {
|
|
163
|
+
config: IWizardConfig<StepId, TSchema>;
|
|
164
|
+
initialData?: TSchema;
|
|
165
|
+
initialStepId?: StepId;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
useProvideWizard({
|
|
169
|
+
config: {
|
|
170
|
+
steps: [
|
|
171
|
+
{ id: 'step1', label: 'Step 1' },
|
|
172
|
+
{ id: 'step2', label: 'Step 2' }
|
|
173
|
+
],
|
|
174
|
+
validationMode: 'onChange', // 'onChange' | 'onBlur' | 'onSubmit'
|
|
175
|
+
validationDebounceTime: 300,
|
|
176
|
+
},
|
|
177
|
+
initialData: { name: '' },
|
|
178
|
+
initialStepId: 'step1'
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
#### `useWizardState()`
|
|
183
|
+
|
|
184
|
+
Returns a reactive computed ref with the complete wizard state:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
const state = useWizardState();
|
|
188
|
+
|
|
189
|
+
// Available properties:
|
|
190
|
+
state.currentStepId // Current step ID
|
|
191
|
+
state.currentStepIndex // Current step index (0-based)
|
|
192
|
+
state.totalSteps // Total number of active steps
|
|
193
|
+
state.isFirstStep // Boolean: is first step
|
|
194
|
+
state.isLastStep // Boolean: is last step
|
|
195
|
+
state.canGoNext // Boolean: can navigate forward
|
|
196
|
+
state.canGoPrev // Boolean: can navigate backward
|
|
197
|
+
state.visitedSteps // Set of visited step IDs
|
|
198
|
+
state.completedSteps // Set of completed step IDs
|
|
199
|
+
state.errorSteps // Set of steps with errors
|
|
200
|
+
state.progress // Progress percentage (0-100)
|
|
201
|
+
state.isBusy // Boolean: async operation in progress
|
|
202
|
+
state.data // Current wizard data
|
|
203
|
+
state.activeSteps // Array of currently active steps
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### `useWizardValue(path, defaultValue?)`
|
|
207
|
+
|
|
208
|
+
Returns a reactive ref for a specific path in the wizard data:
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
const name = useWizardValue('user.name');
|
|
212
|
+
const email = useWizardValue('user.email', 'default@example.com');
|
|
213
|
+
|
|
214
|
+
// Automatically updates when the value changes
|
|
215
|
+
console.log(name.value); // 'John'
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
#### `useWizardField(path, defaultValue?)`
|
|
219
|
+
|
|
220
|
+
Returns `[ref, setter]` tuple for a specific field:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
const [name, setName] = useWizardField('user.name');
|
|
224
|
+
const [age, setAge] = useWizardField('user.age', 18);
|
|
225
|
+
|
|
226
|
+
// Use in template
|
|
227
|
+
<input :value="name" @input="e => setName(e.target.value)" />
|
|
228
|
+
|
|
229
|
+
// Or with v-model workaround
|
|
230
|
+
const nameModel = computed({
|
|
231
|
+
get: () => name.value,
|
|
232
|
+
set: (val) => setName(val)
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
<input v-model="nameModel" />
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
#### `useWizardActions()`
|
|
239
|
+
|
|
240
|
+
Returns navigation and data manipulation methods:
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
const actions = useWizardActions();
|
|
244
|
+
|
|
245
|
+
// Navigation
|
|
246
|
+
actions.nextStep() // Go to next step
|
|
247
|
+
actions.prevStep() // Go to previous step
|
|
248
|
+
actions.goToStep('stepId') // Go to specific step
|
|
249
|
+
actions.goToStepIndex(2) // Go to step by index
|
|
250
|
+
|
|
251
|
+
// Data manipulation
|
|
252
|
+
actions.setData('user.name', 'John') // Set single value
|
|
253
|
+
actions.updateData({ // Update multiple values
|
|
254
|
+
user: { name: 'John', email: 'john@example.com' }
|
|
255
|
+
})
|
|
256
|
+
actions.getData('user.name') // Get value by path
|
|
257
|
+
actions.getData('user.name', 'Default') // With default value
|
|
258
|
+
|
|
259
|
+
// Validation
|
|
260
|
+
actions.validateStep('stepId') // Validate specific step
|
|
261
|
+
actions.validateAll() // Validate all steps
|
|
262
|
+
|
|
263
|
+
// Error management
|
|
264
|
+
actions.setErrors({ // Set errors for a step
|
|
265
|
+
stepId: {
|
|
266
|
+
fieldName: 'Error message'
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
// Reset
|
|
271
|
+
actions.reset() // Reset to initial state
|
|
272
|
+
actions.reset({ name: 'New' }) // Reset with new data
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
#### `useWizardSelector(selector)`
|
|
276
|
+
|
|
277
|
+
Returns a reactive computed value derived from wizard state:
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
const progress = useWizardSelector(state => state.progress);
|
|
281
|
+
const isValid = useWizardSelector(state =>
|
|
282
|
+
state.errorSteps.size === 0
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// Complex derived state
|
|
286
|
+
const wizardStatus = useWizardSelector(state => ({
|
|
287
|
+
step: state.currentStepId,
|
|
288
|
+
progress: state.progress,
|
|
289
|
+
canProceed: state.canGoNext && !state.isBusy,
|
|
290
|
+
hasErrors: state.errorSteps.size > 0
|
|
291
|
+
}));
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
#### `useWizardError(path)`
|
|
295
|
+
|
|
296
|
+
Returns a reactive ref with the error message for a specific field:
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
const nameError = useWizardError('user.name');
|
|
300
|
+
const emailError = useWizardError('user.email');
|
|
301
|
+
|
|
302
|
+
// Use in template
|
|
303
|
+
<div v-if="nameError" class="error">{{ nameError }}</div>
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## ๐ Advanced Examples
|
|
307
|
+
|
|
308
|
+
### Example 1: Step Guards (beforeLeave)
|
|
309
|
+
|
|
310
|
+
Prevent navigation until validation passes:
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
const config: IWizardConfig<StepId, MySchema> = {
|
|
314
|
+
steps: [
|
|
315
|
+
{
|
|
316
|
+
id: 'info',
|
|
317
|
+
label: 'Personal Info',
|
|
318
|
+
beforeLeave: async (data) => {
|
|
319
|
+
if (!data.user.name) {
|
|
320
|
+
return { canLeave: false, error: 'Name is required' };
|
|
321
|
+
}
|
|
322
|
+
if (!data.user.email.includes('@')) {
|
|
323
|
+
return { canLeave: false, error: 'Valid email required' };
|
|
324
|
+
}
|
|
325
|
+
return { canLeave: true };
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
{ id: 'review', label: 'Review' }
|
|
329
|
+
]
|
|
330
|
+
};
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Example 2: Conditional Steps (isVisible)
|
|
334
|
+
|
|
335
|
+
Show/hide steps based on data:
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
const config: IWizardConfig<StepId, MySchema> = {
|
|
339
|
+
steps: [
|
|
340
|
+
{ id: 'plan', label: 'Select Plan' },
|
|
341
|
+
{
|
|
342
|
+
id: 'payment',
|
|
343
|
+
label: 'Payment',
|
|
344
|
+
isVisible: (data) => data.plan === 'pro' // Only show for pro plan
|
|
345
|
+
},
|
|
346
|
+
{ id: 'review', label: 'Review' }
|
|
347
|
+
]
|
|
348
|
+
};
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Example 3: Async Step Conditions
|
|
352
|
+
|
|
353
|
+
Dynamic step visibility with async checks:
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
const config: IWizardConfig<StepId, MySchema> = {
|
|
357
|
+
steps: [
|
|
358
|
+
{ id: 'account', label: 'Account' },
|
|
359
|
+
{
|
|
360
|
+
id: 'premium',
|
|
361
|
+
label: 'Premium Features',
|
|
362
|
+
isVisible: async (data) => {
|
|
363
|
+
const hasAccess = await checkPremiumAccess(data.user.id);
|
|
364
|
+
return hasAccess;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
]
|
|
368
|
+
};
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Example 4: Zod Validation
|
|
372
|
+
|
|
373
|
+
Integrate with Zod for schema validation:
|
|
374
|
+
|
|
375
|
+
```vue
|
|
376
|
+
<script setup lang="ts">
|
|
377
|
+
import { z } from 'zod';
|
|
378
|
+
import { ZodAdapter } from '@wizzard-packages/adapter-zod';
|
|
379
|
+
import { useProvideWizard } from './wizard';
|
|
380
|
+
|
|
381
|
+
const userSchema = z.object({
|
|
382
|
+
name: z.string().min(2, 'Name must be at least 2 characters'),
|
|
383
|
+
email: z.string().email('Invalid email address'),
|
|
384
|
+
age: z.number().min(18, 'Must be 18 or older')
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const config = {
|
|
388
|
+
steps: [
|
|
389
|
+
{
|
|
390
|
+
id: 'info',
|
|
391
|
+
label: 'Info',
|
|
392
|
+
validate: ZodAdapter(userSchema)
|
|
393
|
+
}
|
|
394
|
+
]
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
useProvideWizard({ config, initialData: { name: '', email: '', age: 0 } });
|
|
398
|
+
</script>
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Example 5: LocalStorage Persistence
|
|
402
|
+
|
|
403
|
+
Persist wizard state across sessions:
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
import { LocalStorageAdapter } from '@wizzard-packages/persistence';
|
|
407
|
+
|
|
408
|
+
const config = {
|
|
409
|
+
steps: [
|
|
410
|
+
{ id: 'step1', label: 'Step 1' },
|
|
411
|
+
{ id: 'step2', label: 'Step 2' }
|
|
412
|
+
],
|
|
413
|
+
persistence: LocalStorageAdapter('my-wizard-state')
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
useProvideWizard({ config, initialData: {} });
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Example 6: Middleware (Logger)
|
|
420
|
+
|
|
421
|
+
Add logging middleware for debugging:
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
import { loggerMiddleware } from '@wizzard-packages/middleware';
|
|
425
|
+
|
|
426
|
+
const config = {
|
|
427
|
+
steps: [
|
|
428
|
+
{ id: 'step1', label: 'Step 1' }
|
|
429
|
+
],
|
|
430
|
+
middlewares: [loggerMiddleware]
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
useProvideWizard({ config, initialData: {} });
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### Example 7: Progress Indicator Component
|
|
437
|
+
|
|
438
|
+
```vue
|
|
439
|
+
<script setup lang="ts">
|
|
440
|
+
import { useWizardState } from './wizard';
|
|
441
|
+
|
|
442
|
+
const state = useWizardState();
|
|
443
|
+
</script>
|
|
444
|
+
|
|
445
|
+
<template>
|
|
446
|
+
<div class="progress-bar">
|
|
447
|
+
<div
|
|
448
|
+
class="progress-fill"
|
|
449
|
+
:style="{ width: `${state.progress}%` }"
|
|
450
|
+
/>
|
|
451
|
+
</div>
|
|
452
|
+
<div class="progress-text">
|
|
453
|
+
{{ state.progress }}% Complete
|
|
454
|
+
({{ state.currentStepIndex + 1 }} / {{ state.totalSteps }})
|
|
455
|
+
</div>
|
|
456
|
+
</template>
|
|
457
|
+
|
|
458
|
+
<style scoped>
|
|
459
|
+
.progress-bar {
|
|
460
|
+
width: 100%;
|
|
461
|
+
height: 8px;
|
|
462
|
+
background: #e0e0e0;
|
|
463
|
+
border-radius: 4px;
|
|
464
|
+
overflow: hidden;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.progress-fill {
|
|
468
|
+
height: 100%;
|
|
469
|
+
background: linear-gradient(90deg, #4caf50, #8bc34a);
|
|
470
|
+
transition: width 0.3s ease;
|
|
471
|
+
}
|
|
472
|
+
</style>
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### Example 8: Step Breadcrumbs
|
|
476
|
+
|
|
477
|
+
```vue
|
|
478
|
+
<script setup lang="ts">
|
|
479
|
+
import { useWizardState, useWizardActions } from './wizard';
|
|
480
|
+
|
|
481
|
+
const state = useWizardState();
|
|
482
|
+
const { goToStep } = useWizardActions();
|
|
483
|
+
|
|
484
|
+
const canNavigateToStep = (stepId: string) => {
|
|
485
|
+
return state.visitedSteps.has(stepId) ||
|
|
486
|
+
state.completedSteps.has(stepId);
|
|
487
|
+
};
|
|
488
|
+
</script>
|
|
489
|
+
|
|
490
|
+
<template>
|
|
491
|
+
<div class="breadcrumbs">
|
|
492
|
+
<div
|
|
493
|
+
v-for="(step, index) in state.activeSteps"
|
|
494
|
+
:key="step.id"
|
|
495
|
+
class="breadcrumb"
|
|
496
|
+
:class="{
|
|
497
|
+
active: step.id === state.currentStepId,
|
|
498
|
+
completed: state.completedSteps.has(step.id),
|
|
499
|
+
error: state.errorSteps.has(step.id)
|
|
500
|
+
}"
|
|
501
|
+
@click="canNavigateToStep(step.id) && goToStep(step.id)"
|
|
502
|
+
>
|
|
503
|
+
<span class="number">{{ index + 1 }}</span>
|
|
504
|
+
<span class="label">{{ step.label }}</span>
|
|
505
|
+
</div>
|
|
506
|
+
</div>
|
|
507
|
+
</template>
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Example 9: Form with Validation Errors
|
|
511
|
+
|
|
512
|
+
```vue
|
|
513
|
+
<script setup lang="ts">
|
|
514
|
+
import { useWizardField, useWizardError, useWizardActions } from './wizard';
|
|
515
|
+
|
|
516
|
+
const [name, setName] = useWizardField('user.name');
|
|
517
|
+
const [email, setEmail] = useWizardField('user.email');
|
|
518
|
+
|
|
519
|
+
const nameError = useWizardError('user.name');
|
|
520
|
+
const emailError = useWizardError('user.email');
|
|
521
|
+
|
|
522
|
+
const { validateStep, nextStep } = useWizardActions();
|
|
523
|
+
|
|
524
|
+
const handleSubmit = async () => {
|
|
525
|
+
const result = await validateStep('info');
|
|
526
|
+
if (result.isValid) {
|
|
527
|
+
await nextStep();
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
</script>
|
|
531
|
+
|
|
532
|
+
<template>
|
|
533
|
+
<form @submit.prevent="handleSubmit">
|
|
534
|
+
<div class="field">
|
|
535
|
+
<label>Name</label>
|
|
536
|
+
<input
|
|
537
|
+
:value="name"
|
|
538
|
+
@input="e => setName((e.target as HTMLInputElement).value)"
|
|
539
|
+
:class="{ error: nameError }"
|
|
540
|
+
/>
|
|
541
|
+
<span v-if="nameError" class="error-message">{{ nameError }}</span>
|
|
542
|
+
</div>
|
|
543
|
+
|
|
544
|
+
<div class="field">
|
|
545
|
+
<label>Email</label>
|
|
546
|
+
<input
|
|
547
|
+
type="email"
|
|
548
|
+
:value="email"
|
|
549
|
+
@input="e => setEmail((e.target as HTMLInputElement).value)"
|
|
550
|
+
:class="{ error: emailError }"
|
|
551
|
+
/>
|
|
552
|
+
<span v-if="emailError" class="error-message">{{ emailError }}</span>
|
|
553
|
+
</div>
|
|
554
|
+
|
|
555
|
+
<button type="submit">Continue</button>
|
|
556
|
+
</form>
|
|
557
|
+
</template>
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### Example 10: Computed v-model Wrapper
|
|
561
|
+
|
|
562
|
+
For easier v-model integration:
|
|
563
|
+
|
|
564
|
+
```vue
|
|
565
|
+
<script setup lang="ts">
|
|
566
|
+
import { computed } from 'vue';
|
|
567
|
+
import { useWizardField } from './wizard';
|
|
568
|
+
|
|
569
|
+
const [nameRef, setName] = useWizardField('user.name');
|
|
570
|
+
const [emailRef, setEmail] = useWizardField('user.email');
|
|
571
|
+
|
|
572
|
+
// Create computed properties for v-model
|
|
573
|
+
const name = computed({
|
|
574
|
+
get: () => nameRef.value,
|
|
575
|
+
set: (val) => setName(val)
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
const email = computed({
|
|
579
|
+
get: () => emailRef.value,
|
|
580
|
+
set: (val) => setEmail(val)
|
|
581
|
+
});
|
|
582
|
+
</script>
|
|
583
|
+
|
|
584
|
+
<template>
|
|
585
|
+
<div>
|
|
586
|
+
<input v-model="name" placeholder="Name" />
|
|
587
|
+
<input v-model="email" type="email" placeholder="Email" />
|
|
588
|
+
</div>
|
|
589
|
+
</template>
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
### Example 11: Complex Selector with Performance
|
|
593
|
+
|
|
594
|
+
```vue
|
|
595
|
+
<script setup lang="ts">
|
|
596
|
+
import { useWizardSelector } from './wizard';
|
|
597
|
+
|
|
598
|
+
// Only re-renders when these specific values change
|
|
599
|
+
const wizardMeta = useWizardSelector(state => ({
|
|
600
|
+
currentStep: state.activeSteps.find(s => s.id === state.currentStepId),
|
|
601
|
+
isLastAndValid: state.isLastStep && state.errorSteps.size === 0,
|
|
602
|
+
completionRate: (state.completedSteps.size / state.totalSteps) * 100
|
|
603
|
+
}));
|
|
604
|
+
</script>
|
|
605
|
+
|
|
606
|
+
<template>
|
|
607
|
+
<div class="wizard-meta">
|
|
608
|
+
<h2>{{ wizardMeta.currentStep?.label }}</h2>
|
|
609
|
+
<p v-if="wizardMeta.isLastAndValid">Ready to submit!</p>
|
|
610
|
+
<p>{{ wizardMeta.completionRate.toFixed(0) }}% of steps completed</p>
|
|
611
|
+
</div>
|
|
612
|
+
</template>
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
### Example 12: Dynamic Step Configuration
|
|
616
|
+
|
|
617
|
+
```vue
|
|
618
|
+
<script setup lang="ts">
|
|
619
|
+
import { ref, computed } from 'vue';
|
|
620
|
+
import { useProvideWizard } from './wizard';
|
|
621
|
+
import type { IStepConfig } from '@wizzard-packages/core';
|
|
622
|
+
|
|
623
|
+
const userType = ref<'personal' | 'business'>('personal');
|
|
624
|
+
|
|
625
|
+
const steps = computed(() => {
|
|
626
|
+
const baseSteps: IStepConfig<string, any>[] = [
|
|
627
|
+
{ id: 'type', label: 'Account Type' }
|
|
628
|
+
];
|
|
629
|
+
|
|
630
|
+
if (userType.value === 'business') {
|
|
631
|
+
baseSteps.push(
|
|
632
|
+
{ id: 'company', label: 'Company Info' },
|
|
633
|
+
{ id: 'tax', label: 'Tax Details' }
|
|
634
|
+
);
|
|
635
|
+
} else {
|
|
636
|
+
baseSteps.push(
|
|
637
|
+
{ id: 'personal', label: 'Personal Info' }
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
baseSteps.push({ id: 'review', label: 'Review' });
|
|
642
|
+
|
|
643
|
+
return baseSteps;
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
const config = computed(() => ({
|
|
647
|
+
steps: steps.value
|
|
648
|
+
}));
|
|
649
|
+
|
|
650
|
+
useProvideWizard({
|
|
651
|
+
config: config.value,
|
|
652
|
+
initialData: { type: userType.value }
|
|
653
|
+
});
|
|
654
|
+
</script>
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
## ๐ฏ Best Practices
|
|
658
|
+
|
|
659
|
+
### 1. Memoize Configuration
|
|
660
|
+
|
|
661
|
+
Use `computed` for step configuration to avoid unnecessary recalculations:
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
const config = computed(() => ({
|
|
665
|
+
steps: [
|
|
666
|
+
{ id: 'step1', label: 'Step 1' }
|
|
667
|
+
],
|
|
668
|
+
middlewares: [loggerMiddleware],
|
|
669
|
+
validationMode: 'onChange'
|
|
670
|
+
}));
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### 2. Granular Subscriptions
|
|
674
|
+
|
|
675
|
+
Prefer specific hooks over full state for better performance:
|
|
676
|
+
|
|
677
|
+
```typescript
|
|
678
|
+
// โ
Good: Only re-renders when 'user.name' changes
|
|
679
|
+
const name = useWizardValue('user.name');
|
|
680
|
+
|
|
681
|
+
// โ Avoid: Re-renders on any state change
|
|
682
|
+
const state = useWizardState();
|
|
683
|
+
const name = state.data.user.name;
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
### 3. Use Selectors for Derived State
|
|
687
|
+
|
|
688
|
+
Create computed values with `useWizardSelector`:
|
|
689
|
+
|
|
690
|
+
```typescript
|
|
691
|
+
// Only re-computes when progress or isBusy changes
|
|
692
|
+
const status = useWizardSelector(state => ({
|
|
693
|
+
progress: state.progress,
|
|
694
|
+
loading: state.isBusy
|
|
695
|
+
}));
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
### 4. Validation Timing
|
|
699
|
+
|
|
700
|
+
Choose appropriate validation mode:
|
|
701
|
+
|
|
702
|
+
- `onChange`: Real-time validation (best UX, more API calls)
|
|
703
|
+
- `onBlur`: Validate on field blur (balanced)
|
|
704
|
+
- `onSubmit`: Validate on step navigation (least intrusive)
|
|
705
|
+
|
|
706
|
+
### 5. Error Handling
|
|
707
|
+
|
|
708
|
+
Always handle async errors in guards and validation:
|
|
709
|
+
|
|
710
|
+
```typescript
|
|
711
|
+
{
|
|
712
|
+
id: 'step1',
|
|
713
|
+
beforeLeave: async (data) => {
|
|
714
|
+
try {
|
|
715
|
+
await validateAPI(data);
|
|
716
|
+
return { canLeave: true };
|
|
717
|
+
} catch (error) {
|
|
718
|
+
return {
|
|
719
|
+
canLeave: false,
|
|
720
|
+
error: error.message || 'Validation failed'
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
## ๐ Integration with Vue Router
|
|
728
|
+
|
|
729
|
+
```vue
|
|
730
|
+
<script setup lang="ts">
|
|
731
|
+
import { watch } from 'vue';
|
|
732
|
+
import { useRoute, useRouter } from 'vue-router';
|
|
733
|
+
import { useWizardActions, useWizardState } from './wizard';
|
|
734
|
+
|
|
735
|
+
const route = useRoute();
|
|
736
|
+
const router = useRouter();
|
|
737
|
+
const { goToStep } = useWizardActions();
|
|
738
|
+
const state = useWizardState();
|
|
739
|
+
|
|
740
|
+
// Sync wizard step with route
|
|
741
|
+
watch(() => route.params.step, (stepId) => {
|
|
742
|
+
if (stepId && stepId !== state.currentStepId) {
|
|
743
|
+
goToStep(stepId as string);
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
// Sync route with wizard step
|
|
748
|
+
watch(() => state.currentStepId, (stepId) => {
|
|
749
|
+
if (route.params.step !== stepId) {
|
|
750
|
+
router.push({ params: { step: stepId } });
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
</script>
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
## ๐งช Testing
|
|
757
|
+
|
|
758
|
+
```typescript
|
|
759
|
+
import { mount } from '@vue/test-utils';
|
|
760
|
+
import { useProvideWizard, useWizardState } from './wizard';
|
|
761
|
+
|
|
762
|
+
describe('Wizard', () => {
|
|
763
|
+
it('navigates between steps', async () => {
|
|
764
|
+
const wrapper = mount({
|
|
765
|
+
setup() {
|
|
766
|
+
useProvideWizard({
|
|
767
|
+
config: {
|
|
768
|
+
steps: [
|
|
769
|
+
{ id: 'step1', label: 'Step 1' },
|
|
770
|
+
{ id: 'step2', label: 'Step 2' }
|
|
771
|
+
]
|
|
772
|
+
},
|
|
773
|
+
initialData: {}
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
const state = useWizardState();
|
|
777
|
+
const { nextStep } = useWizardActions();
|
|
778
|
+
|
|
779
|
+
return { state, nextStep };
|
|
780
|
+
},
|
|
781
|
+
template: '<div>{{ state.currentStepId }}</div>'
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
expect(wrapper.text()).toBe('step1');
|
|
785
|
+
|
|
786
|
+
await wrapper.vm.nextStep();
|
|
787
|
+
await wrapper.vm.$nextTick();
|
|
788
|
+
|
|
789
|
+
expect(wrapper.text()).toBe('step2');
|
|
790
|
+
});
|
|
791
|
+
});
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
## ๐ SSR Support
|
|
795
|
+
|
|
796
|
+
The Vue adapter is SSR-friendly and works with Nuxt 3:
|
|
797
|
+
|
|
798
|
+
```typescript
|
|
799
|
+
// Avoid persistence adapters that access `window` during module initialization
|
|
800
|
+
// Use lazy initialization instead:
|
|
801
|
+
|
|
802
|
+
const config = computed(() => ({
|
|
803
|
+
steps: [...],
|
|
804
|
+
persistence: typeof window !== 'undefined'
|
|
805
|
+
? LocalStorageAdapter('key')
|
|
806
|
+
: undefined
|
|
807
|
+
}));
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
## ๐ฆ Package Structure
|
|
811
|
+
|
|
812
|
+
- **ESM**: `dist/index.js` (tree-shakeable)
|
|
813
|
+
- **CJS**: `dist/index.cjs` (Node.js compatibility)
|
|
814
|
+
- **Types**: `dist/index.d.ts` (TypeScript definitions)
|
|
815
|
+
|
|
816
|
+
## ๐ How It Fits
|
|
817
|
+
|
|
818
|
+
- **Core engine**: `@wizzard-packages/core` (shared with React)
|
|
819
|
+
- **Optional persistence**: `@wizzard-packages/persistence`
|
|
820
|
+
- **Optional middleware**: `@wizzard-packages/middleware`
|
|
821
|
+
- **Optional validation**: `@wizzard-packages/adapter-zod` or `@wizzard-packages/adapter-yup`
|
|
822
|
+
|
|
823
|
+
## ๐ Links
|
|
824
|
+
|
|
825
|
+
- **Repository**: https://github.com/ZizzX/wizzard-packages
|
|
826
|
+
- **Documentation**: https://zizzx.github.io/wizzard-packages/
|
|
827
|
+
- **Issues**: https://github.com/ZizzX/wizzard-packages/issues
|
|
828
|
+
- **Changelog**: [CHANGELOG.md](./CHANGELOG.md)
|
|
829
|
+
|
|
830
|
+
## ๐ License
|
|
831
|
+
|
|
832
|
+
MIT ยฉ [ZizzX](https://github.com/ZizzX)
|
|
833
|
+
|
|
834
|
+
---
|
|
835
|
+
|
|
836
|
+
**Made with โค๏ธ for the Vue community**
|