Multi-step / nested UForm

I'm trying to implement a multi-step form using the form nesting mentioned in the documentation but I'm having a hard time understanding how to get it to work and validate properly at each step. More specifically, I'm confused about the following: - Should the parent form define the whole schema or is each child form responsible of specifying each sub-schema? - How can I customize each step's validation, for example calling an API for checking if an email is available?
6 Replies
kapa.ai
kapa.ai2mo ago
Hi @Pascal Martineau. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- To implement a multi-step (nested) form with Nuxt UI's UForm, here’s how you should approach your questions based on the official documentation: 1. Should the parent form define the whole schema or is each child form responsible for its sub-schema? Each child (nested) form is responsible for specifying its own sub-schema. The parent form manages the overall state, but each nested UForm should receive its own schema (and optionally a name prop to map its state within the parent). This allows for modular validation and state management per step. Example:
<script setup lang="ts">
import * as z from 'zod'

const parentSchema = z.object({
customer: z.string().min(2)
})

const itemSchema = z.object({
description: z.string().min(1),
price: z.number().min(0)
})

const state = reactive({
customer: '',
items: [{ description: '', price: 0 }]
})
</script>

<template>
<UForm :state="state" :schema="parentSchema">
<UFormField label="Customer" name="customer">
<UInput v-model="state.customer" />
</UFormField>

<UForm
v-for="(item, index) in state.items"
:key="index"
:name="`items.${index}`"
:schema="itemSchema"
nested
>
<UFormField label="Description" name="description">
<UInput v-model="item.description" />
</UFormField>
<UFormField label="Price" name="price">
<UInput v-model="item.price" type="number" />
</UFormField>
</UForm>
</UForm>
</template>
<script setup lang="ts">
import * as z from 'zod'

const parentSchema = z.object({
customer: z.string().min(2)
})

const itemSchema = z.object({
description: z.string().min(1),
price: z.number().min(0)
})

const state = reactive({
customer: '',
items: [{ description: '', price: 0 }]
})
</script>

<template>
<UForm :state="state" :schema="parentSchema">
<UFormField label="Customer" name="customer">
<UInput v-model="state.customer" />
</UFormField>

<UForm
v-for="(item, index) in state.items"
:key="index"
:name="`items.${index}`"
:schema="itemSchema"
nested
>
<UFormField label="Description" name="description">
<UInput v-model="item.description" />
</UFormField>
<UFormField label="Price" name="price">
<UInput v-model="item.price" type="number" />
</UFormField>
</UForm>
</UForm>
</template>
In this setup, the parent form handles the overall state, while each nested form (step) handles its own schema and validation logic. This is the recommended approach for multi-step forms in Nuxt UI v4 Docs: Input events & Nesting forms. 2. How can I customize each step's validation (e.g., call an API to check if an email is available)? You can use the validate prop on any UForm (parent or nested) to provide custom validation logic, including asynchronous checks like API calls. The validate function should return an array of errors (or a Promise resolving to such an array). This can be used alongside the schema prop for more complex scenarios. Example:
<script setup lang="ts">
import type { FormError } from '@nuxt/ui'

const state = reactive({ email: '' })

const validate = async (state: any): Promise<FormError[]> => {
const errors = []
if (!state.email) {
errors.push({ name: 'email', message: 'Required' })
} else {
// Example async API check
const available = await checkEmailAvailability(state.email)
if (!available) {
errors.push({ name: 'email', message: 'Email is already taken' })
}
}
return errors
}
</script>

<template>
<UForm :validate="validate" :state="state">
<UFormField label="Email" name="email">
<UInput v-model="state.email" />
</UFormField>
</UForm>
</template>
<script setup lang="ts">
import type { FormError } from '@nuxt/ui'

const state = reactive({ email: '' })

const validate = async (state: any): Promise<FormError[]> => {
const errors = []
if (!state.email) {
errors.push({ name: 'email', message: 'Required' })
} else {
// Example async API check
const available = await checkEmailAvailability(state.email)
if (!available) {
errors.push({ name: 'email', message: 'Email is already taken' })
}
}
return errors
}
</script>

<template>
<UForm :validate="validate" :state="state">
<UFormField label="Email" name="email">
<UInput v-model="state.email" />
</UFormField>
</UForm>
</template>
You can use this approach for any step by placing the validate prop on the relevant nested UForm Docs: Custom validation. --- Summary:
- Each nested UForm should define its own schema (the parent does not need to define the entire schema). - Use the validate prop for custom (including async) validation per step or form Docs: Custom validation. - The parent manages the overall state, and nested forms map to sub-objects via the name prop. If you need more advanced control, you can combine both schema and custom validation as needed. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai2mo ago
<@449034510227734529> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Pascal Martineau
Pascal MartineauOP2mo ago
@kapa.ai I get validation errors displayed even if the state seems ok
kapa.ai
kapa.ai2mo ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- If you are seeing validation errors displayed even though your form state appears correct, here are some possible causes and troubleshooting steps based on the official Nuxt UI documentation and related sources: 1. State Initialization Issues
Make sure your form state is initialized with the correct types and default values. For example, if your schema expects a string but your state is undefined, validation may fail. For Zod and similar schema libraries, initializing with undefined is common, but if your schema uses .default(''), your state should match that expectation (Docs: Schema validation). 2. Schema Transformations
In Nuxt UI v4, schema transformations (like .default() or .transform()) are only applied to the data on submit, not to the form state itself. This means your state may not reflect the transformed or defaulted values until submission, which can cause validation errors to appear if the state is not initialized as expected (Migration guide). 3. Custom Validation Logic
If you use the validate prop, ensure your function correctly checks the state and returns errors only when appropriate. Double-check that your validation logic matches the expected state structure (Docs: Custom validation). 4. Nested Forms and State Mapping
When using nested forms, ensure that the name prop on each nested form matches the structure of your state object. Mismatches can cause validation to fail even if the state looks correct (Docs: Input events & Nesting forms). 5. Model Modifiers
If you are using model modifiers like nullable or optional, ensure you are using the correct modifier for your use case. For example, nullable converts empty values to null, and optional to undefined. Using the wrong one can cause type mismatches and validation errors (Migration guide). 6. Hydration or SSR Issues
If you are using SSR and see hydration mismatches, it could be due to non-deterministic initial values (e.g., using Date.now() in your state). Always initialize such values in onMounted instead of directly in the state (Stack Overflow: Hydration mismatches). Summary:
- Double-check your state initialization and schema expectations. - Remember that schema transformations only apply on submit. - Ensure custom validation and nested form mapping are correct. - Watch out for SSR/hydration issues with non-deterministic values. If you provide a code sample, I can help pinpoint the exact issue! ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai2mo ago
<@449034510227734529> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Pascal Martineau
Pascal MartineauOP2mo ago
It was the "name" property that was missing on the children form.

Did you find this page helpful?