Продвинутые паттерны
Параллельные состояния
Состояние может объявлять регионы parallel — набор независимых под-машин, которые все становятся активными при входе в родительское состояние и уничтожаются при выходе из него.
const editor = defineMachine({
id: 'editor',
initial: 'editing',
states: {
editing: {
parallel: {
saving: {
initial: 'idle',
states: {
idle: { on: { START_SAVE: { target: 'saving' } } },
saving: { on: { SAVE_DONE: { target: 'saved' } } },
saved: {},
},
},
validation: {
initial: 'valid',
states: {
valid: { on: { INVALIDATE: { target: 'invalid' } } },
invalid: { on: { VALIDATE: { target: 'valid' } } },
},
},
},
},
idle: {},
},
})send() доставляет каждое событие во все активные регионы. Каждый регион обрабатывает его независимо.
const { matches, send } = useMachine(editor)
matches('editing') // основное состояние
matches({ saving: 'idle' }) // проверка региона
matches({ validation: 'valid' }) // другой регион
await send('INVALIDATE')
matches({ validation: 'invalid' }) // true
matches({ saving: 'idle' }) // всё ещё true — не затронутоРазрешение конфликтов контекста: когда два региона возвращают Partial<context>, затрагивающий одно и то же поле, побеждает последний регион в порядке объявления. В dev-режиме выводится console.warn с именами конфликтующих регионов и полем.
Ограничение: параллельные регионы поддерживают один уровень вложенности. Регионы не могут сами содержать parallel. Это осознанное решение для контроля сложности.
useWizard
Composable для многошаговых форм, построенный поверх defineMachine. Машина wizard'а генерируется автоматически из массива шагов.
function useWizard<TContext>(
steps: WizardStep<TContext>[],
options?: WizardOptions,
): WizardInstance<TContext>WizardStep
| Поле | Тип | Описание |
|---|---|---|
id | string | Уникальный идентификатор шага (внутри становится именем состояния) |
label | string? | Отображаемая метка |
component | Component? | Vue-компонент, рендерящийся для этого шага |
canProceed | (ctx) => boolean | Promise<boolean> | Условие для next() и goTo() вперёд; может быть асинхронным |
onEnter | (ctx) => void | Вызывается, когда wizard входит в этот шаг |
onLeave | (ctx) => void | Вызывается, когда wizard покидает этот шаг |
WizardOptions
| Опция | Тип | По умолчанию | Описание |
|---|---|---|---|
initialStep | number | 0 | Индекс начального шага |
allowSkip | boolean | false | Пропускать canProceed при goTo() вперёд |
circular | boolean | false | next() возвращается от последнего шага к первому |
Возвращаемое значение
| Свойство | Тип | Описание |
|---|---|---|
currentStep | Ref<WizardStep> | Объект текущего активного шага |
currentIndex | ComputedRef<number> | Индекс текущего шага (от нуля) |
totalSteps | number | Общее число шагов |
progress | ComputedRef<number> | От 0 до 1 на основе текущего индекса |
isFirst | ComputedRef<boolean> | true на первом шаге |
isLast | ComputedRef<boolean> | true на последнем шаге |
history | Ref<string[]> | ID посещённых шагов |
next() | Promise<boolean> | Продвигает вперёд; сначала вызывает canProceed; возвращает false, если заблокировано |
prev() | void | Назад (без guard) |
goTo(id) | Promise<boolean> | Переход к шагу по id; учитывает canProceed, если не allowSkip |
reset() | void | Возврат к начальному шагу |
Пример
<script setup lang="ts">
import { useWizard } from 'vue-state-machine'
import type { WizardStep } from 'vue-state-machine'
import StepInfo from './StepInfo.vue'
import StepAddress from './StepAddress.vue'
import StepPayment from './StepPayment.vue'
interface CheckoutCtx {
name: string
email: string
address: string
}
const steps: WizardStep<CheckoutCtx>[] = [
{
id: 'info',
label: 'Your info',
component: StepInfo,
canProceed: (ctx) => !!ctx.name && !!ctx.email,
},
{
id: 'address',
label: 'Delivery',
component: StepAddress,
canProceed: (ctx) => !!ctx.address,
},
{
id: 'payment',
label: 'Payment',
component: StepPayment,
onEnter: () => trackEvent('payment_step_entered'),
},
]
const { currentStep, progress, isFirst, isLast, next, prev } = useWizard(steps)
</script>
<template>
<div>
<progress :value="progress" max="1" />
<component :is="currentStep.component" />
<nav>
<button :disabled="isFirst" @click="prev">Back</button>
<button v-if="!isLast" @click="next">Next</button>
<button v-else @click="submit">Place order</button>
</nav>
</div>
</template>Правила canProceed
- Может возвращать
booleanилиPromise<boolean> - Если возвращает
false,next()/goTo()вперёд возвращаютfalse, и wizard остаётся на текущем шаге - Если выбрасывает исключение, результат тот же — возвращается
false, ошибка логируется вconsole.errorв dev-режиме prev()иgoTo()назад никогда не проверяютcanProceedallowSkip: trueотключаетcanProceedтолько дляgoTo();next()проверяет его всегда