Statuses
wizard.statusesis a per-stepFormStatusrollup that mirrors each form'smeta. Read it drillably for templates, call it for a snapshot, or seed it up-front withdefaultStatusesfor resume flows that need to render filled rails before per-form data lands.
- Category
- Reactive surface
- Shape
{ valid, dirty, submitted, errorCount }- Read patterns
- drillable · callable · called with a key
- Seeding
defaultStatuses (object, sync factory, async factory)
The FormStatus shape
type FormStatus = {
readonly valid: boolean
readonly dirty: boolean
readonly submitted: boolean
readonly errorCount: number
}
Each field tracks the per-step form's meta. A step's status flips when its meta does. The four scalars are deliberately small. They're what step indicators, navigation gates, and submit summaries reach for.
Reading patterns
wizard.statuses is both a drillable record and a callable accessor. Same data, three call shapes:
import { useForm, useWizard } from 'attaform/zod'
import { z } from 'zod'
const accountSchema = z.object({ email: z.email() })
const profileSchema = z.object({ name: z.string().min(1) })
const account = useForm({ schema: accountSchema, key: 'signup-account' })
const profile = useForm({ schema: profileSchema, key: 'signup-profile' })
const wizard = useWizard({ steps: [account, profile] })
wizard.statuses // drillable record
wizard.statuses() // { 'signup-account': FormStatus, 'signup-profile': FormStatus }
wizard.statuses('signup-account') // FormStatus for one step
wizard.statuses['signup-account'] // FormStatus for one step (drillable)
wizard.statuses['signup-account'].valid // boolean
The drillable form is the template-friendly read; the callable form is convenient in script for one-off reads or destructured snapshots.
Status rails
The classic use case is a step indicator: one dot per form, painted with its current state. wizard.steps walks the compiled positions; wizard.statuses[step.key] reads each one's status:
<script setup lang="ts">
import { useForm, useWizard } from 'attaform/zod'
const wizard = useWizard({ steps: [account, profile, review] })
</script>
<template>
<ol class="wizard-rail">
<li v-for="step in wizard.steps" :key="step.key">
<span
class="dot"
:class="{
done: wizard.statuses[step.key].valid,
dirty: wizard.statuses[step.key].dirty,
current: wizard.currentStep === step.key,
}"
/>
{{ step.key }}
</li>
</ol>
</template>
Affordance steps (bare-string slots) carry an always-valid status, so the rail can paint every position without special-casing them. See Step slots for the affordance-slot story.
Seeding with defaultStatuses
Resume flows (e-commerce checkouts reopened mid-purchase, partially-completed onboarding, draft restore) often need to render filled rails before any per-form data has loaded. The defaultStatuses option seeds wizard.statuses up-front. Three shapes mirror the defaultValues trichotomy.
A plain object for compile-time-known seeds:
const wizard = useWizard({
steps: [account, profile, review],
defaultStatuses: {
'signup-account': { valid: true, dirty: false, submitted: true, errorCount: 0 },
'signup-profile': { valid: false, dirty: true, submitted: false, errorCount: 1 },
'signup-review': { valid: false, dirty: false, submitted: false, errorCount: 0 },
},
})
A sync factory for seeds derived from synchronous state (a draft snapshot in a Pinia store, a URL parameter, a cookie):
const wizard = useWizard({
steps: [account, profile, review],
defaultStatuses: () => buildStatusesFromDraft(draftStore.snapshot),
})
An async factory for seeds that need a server round-trip (saved flow state, a server-rendered status payload):
const wizard = useWizard({
steps: [account, profile, review],
defaultStatuses: async () => fetchSavedFlowStatuses(userId),
})
The seed fills in until the real form data lands. Resolution priority per step:
- The step's form has
defaultsResolved === true(its async / sync defaults have settled). Status derives fromform.meta. - The step is an affordance (noop form). The built-in always-valid status renders.
- The step has a seed entry from
defaultStatuses. The seed value renders. - Otherwise, a pending status renders (
valid: false, dirty: false, submitted: false, errorCount: 0).
Unknown keys in the seed object dev-warn at construction; the wizard ignores them. Known keys still apply, so a partial seed is fine.
Reacting to status changes
wizard.statuses is reactive, so Vue's watch is the right tool for one-off side effects (analytics, autosave, a celebration toast when the last step flips valid). The status proxy plugs into Vue's reactivity the same way form.meta does:
import { watch } from 'vue'
import { useForm, useWizard } from 'attaform/zod'
const wizard = useWizard({ steps: [account, profile, review] })
watch(
() => wizard.statuses['signup-profile'].valid,
(isValid) => {
if (isValid) analytics.track('profile_complete', { user: userId })
}
)
For a whole-wizard sweep, watch the callable form and diff against the previous snapshot:
watch(
() => wizard.statuses(),
(next, prev) => {
for (const [key, status] of Object.entries(next)) {
if (status.valid && !prev[key]?.valid) {
analytics.track('step_valid', { key })
}
}
},
{ deep: true }
)
Where to next
useWizardfor the construction signature and the wizard's full reactive surface.- Aggregates for
wizard.allValuesandwizard.allErrors. - handleSubmit for the submission pipeline that flips per-form
submitted.