Slots
Overview
The AutoForm slot system is organized into three levels:
- Form slots - Customize the overall form structure (
header,footer,submit) - Field slots - Fine-tune individual parts of each field (
field-label,field-hint,field-error, etc.) - Content slots - Take over the rendering of object and array fields, or insert content before/after fields (
field-content,field-before,field-after)
field-{type} and the named patternfield-{type}:{path}, allowing you to unify styles while precisely controlling specific fields. Named patterns take priority over generic patterns.Examples
Form Level
header, footer, and submit take over the form's head, footer and submit area — all can read loading, errors, state to customize the outer layer.
submit slot, set :submit="false" to disable the default submit button, otherwise it will be rendered twice.<script lang="ts" setup>
import type { z } from 'zod'
const { afz } = useAutoForm()
const schema = afz.object({
email: afz.email().meta({ label: '邮箱' }),
password: afz.string({ type: 'withPasswordToggle' }).min(6).meta({ label: '密码' }),
remember: afz.boolean({ type: 'switch', controlProps: { label: '记住登录状态' } }).default(true).meta({ label: '记住我' })
})
const form = reactive<Partial<z.output<typeof schema>>>({})
async function onSubmit() {
await new Promise(resolve => setTimeout(resolve, 1200))
}
</script>
<template>
<MAutoForm
:schema="schema"
:state="form"
:submit="false"
@submit="onSubmit"
>
<template #header="{ loading }">
<UAlert
color="info"
variant="soft"
icon="i-lucide-log-in"
title="header 状态展示"
description="header 在字段渲染前接收 loading,可同步说明与提交状态"
>
<template v-if="loading" #actions>
<UBadge color="info" variant="subtle">
提交中
</UBadge>
</template>
</UAlert>
</template>
<template #footer="{ errors, state }">
<div class="mt-3 flex flex-wrap items-center gap-2 text-sm">
<UBadge :color="errors.length ? 'error' : 'success'" variant="subtle">
{{ errors.length ? `${errors.length} 个错误` : '校验通过' }}
</UBadge>
<UBadge color="neutral" variant="subtle">
{{ state.remember ? '保持登录' : '本次登录' }}
</UBadge>
</div>
</template>
<template #submit="{ errors, loading }">
<div class="mt-4 flex gap-2">
<UButton type="submit" icon="i-lucide-send" :loading="loading" :disabled="errors.length > 0">
登录
</UButton>
<UButton variant="outline" color="neutral" :disabled="loading">
创建账号
</UButton>
</div>
</template>
</MAutoForm>
</template>
Generic Field
field-label, field-description, field-hint, field-error uniformly override the label, description, hint and error areas of all fields.
<script lang="ts" setup>
import type { z } from 'zod'
const { afz } = useAutoForm()
const schema = afz.object({
username: afz.string().min(3).meta({ label: '用户名', hint: '至少 3 个字符' }),
email: afz.email().meta({ label: '邮箱', description: '用于接收通知' }),
role: afz.enum(['admin', 'user', 'guest']).meta({ label: '角色' }),
active: afz.boolean({ controlProps: { label: '启用账户' } }).default(true).meta({ label: '状态' })
})
const state = reactive<Partial<z.output<typeof schema>>>({})
</script>
<template>
<MAutoForm :schema="schema" :state="state" :validate-on="['input', 'blur']">
<template #field-label="{ label, path }">
<span class="inline-flex items-center gap-2">
<UBadge color="primary" variant="subtle" size="xs">
{{ label }}
</UBadge>
<span class="text-xs text-muted">
{{ path }}
</span>
</span>
</template>
<template #field-description="{ description }">
<span
v-if="description"
class="text-xs text-toned"
>
{{ description }}
</span>
</template>
<template #field-hint="{ hint }">
<span class="inline-flex items-center gap-1 text-xs text-muted">
<UIcon name="i-lucide-info" class="size-3" />
<span>{{ hint || '通用 hint slot' }}</span>
</span>
</template>
<template #field-error="{ error }">
<UAlert
v-if="error"
color="error"
variant="subtle"
icon="i-lucide-triangle-alert"
:description="String(error)"
class="mt-2"
/>
</template>
</MAutoForm>
</template>
Named Field
field-{type}:{path} replaces only the specified area of a given field; unmatched fields keep their default rendering. field-default:{path} completely replaces the control for that field.
field-label and field-label:username exist, the username field uses the latter.<script lang="ts" setup>
import type { z } from 'zod'
const { afz } = useAutoForm()
const schema = afz.object({
username: afz.string().min(2).meta({ label: '用户名' }),
password: afz.string({ type: 'withPasswordToggle' }).min(8).meta({ label: '密码' }),
bio: afz.string({ type: 'textarea' }).default('一个简单的自我介绍').meta({ label: '简介' })
})
const state = reactive<Partial<z.output<typeof schema>>>({})
</script>
<template>
<MAutoForm :schema="schema" :state="state">
<template #field-label:username="{ label }">
<UBadge icon="i-lucide-user" color="success" variant="subtle" size="xs">
{{ label }}
</UBadge>
</template>
<template #field-help:password>
<UAlert
color="warning"
variant="subtle"
icon="i-lucide-shield-check"
description="field-help:password 只替换 password 字段的帮助说明,并保留字段默认控件"
class="mt-2"
/>
</template>
<template #field-default:bio="{ setValue, value }">
<UTextarea
:model-value="value"
:rows="5"
placeholder="field-default:bio 完全替换默认 textarea"
class="ring-2 ring-primary/30 rounded w-full"
@update:model-value="setValue"
/>
</template>
</MAutoForm>
</template>
Before / After Field
field-before:{path} and field-after:{path} append content before and after the default field content without taking over child field rendering and interaction.
<script lang="ts" setup>
import type { z } from 'zod'
const { afz } = useAutoForm()
const schema = afz.object({
settings: afz.object({
notifications: afz.boolean({ type: 'switch', controlProps: { label: '接收通知' } }).default(true).meta({ label: '通知' }),
digest: afz.enum(['daily', 'weekly', 'monthly']).default('weekly').meta({ label: '摘要频率' }),
notes: afz.string({ type: 'textarea' }).default('保留默认字段渲染,只在前后插入 slot 内容。').meta({ label: '备注' })
}).default({
notifications: true,
digest: 'weekly',
notes: '保留默认字段渲染,只在前后插入 slot 内容。'
}).meta({ label: '通知设置', collapsible: { defaultOpen: true } })
})
const state = reactive<Partial<z.output<typeof schema>>>({})
</script>
<template>
<MAutoForm :schema="schema" :state="state">
<template #field-before:settings="{ path }">
<UAlert
color="neutral"
variant="subtle"
icon="i-lucide-settings"
title="field-before 插入点"
:description="`field-before:${path} 不接管子字段,只在默认内容前追加说明。`"
class="mb-4"
/>
</template>
<template #field-after:settings="{ value }">
<div class="mt-4 flex flex-wrap items-center gap-2">
<UBadge :color="value?.notifications ? 'success' : 'neutral'" variant="subtle">
{{ value?.notifications ? '通知开启' : '通知关闭' }}
</UBadge>
<UBadge color="info" variant="subtle">
{{ value?.digest || '未选择摘要频率' }}
</UBadge>
<span class="text-xs text-muted">
field-after 保留默认字段交互,只追加汇总状态。
</span>
</div>
</template>
</MAutoForm>
</template>
Object Content
field-content:{path} completely replaces the rendering of an object field; use setValue('key', value) with a relative key to update child fields.
<script lang="ts" setup>
import type { z } from 'zod'
const { afz } = useAutoForm()
const schema = afz.object({
profile: afz.object({
displayName: afz.string().meta({ label: '显示名称' }),
website: afz.url().meta({ label: '个人网站' }),
bio: afz.string({ type: 'textarea' }).meta({ label: '简介' })
}).default({
displayName: 'Movk User',
website: 'https://movk.dev',
bio: '使用 field-content:profile 接管对象字段渲染。'
}).meta({ label: '个人资料', collapsible: { defaultOpen: true } })
})
const state = reactive<Partial<z.output<typeof schema>>>({})
</script>
<template>
<MAutoForm :schema="schema" :state="state">
<template #field-content:profile="{ path, value, setValue }">
<div class="space-y-4 rounded-md border border-default p-4">
<div class="flex items-center justify-between">
<div>
<p class="font-medium">
自定义个人资料
</p>
<p class="text-sm text-muted">
每个控件都通过相对 key 更新 profile 子字段
</p>
</div>
<UBadge color="info" variant="subtle">
object
</UBadge>
</div>
<UFormField label="显示名称" :name="`${path}.displayName`" required>
<UInput
:model-value="value?.displayName"
icon="i-lucide-user"
placeholder="显示名称"
@update:model-value="setValue('displayName', $event)"
/>
</UFormField>
<UFormField label="个人网站">
<UInput
:model-value="value?.website"
icon="i-lucide-link"
placeholder="https://movk.dev"
@update:model-value="setValue('website', $event)"
/>
</UFormField>
<UFormField label="简介">
<UTextarea
:model-value="value?.bio"
:rows="4"
placeholder="简介"
@update:model-value="setValue('bio', $event)"
/>
</UFormField>
</div>
</template>
</MAutoForm>
</template>
Array Content
field-content:{path} replaces the rendering of an array field; setValue can write the entire list or update a single item using an [index].key path.
<script lang="ts" setup>
import type { z } from 'zod'
const { afz } = useAutoForm()
const schema = afz.object({
todos: afz.array(
afz.object({
title: afz.string().min(1).meta({ label: '标题' }),
priority: afz.enum(['low', 'medium', 'high']).default('medium').meta({ label: '优先级' }),
done: afz.boolean().default(false).meta({ label: '完成' })
})
).default([
{ title: '整理 slot 示例', priority: 'high', done: false },
{ title: '梳理 setValue 路径', priority: 'medium', done: false }
]).meta({ label: '待办列表', collapsible: { defaultOpen: true } })
})
const state = reactive<Partial<z.output<typeof schema>>>({})
const priorityItems = [
{ label: '低', value: 'low' },
{ label: '中', value: 'medium' },
{ label: '高', value: 'high' }
]
function createTodo() {
return { title: '', priority: 'medium' as const, done: false }
}
</script>
<template>
<MAutoForm :schema="schema" :state="state">
<template #field-content:todos="{ value, setValue }">
<div class="space-y-3">
<div
v-for="(todo, index) in value || []"
:key="index"
class="rounded-md border border-default p-4 space-y-3"
>
<div class="flex items-center justify-between gap-3">
<UBadge color="neutral" variant="subtle">
#{{ index + 1 }}
</UBadge>
<UButton
icon="i-lucide-trash-2"
color="error"
variant="ghost"
size="xs"
@click="setValue((value || []).filter((_, current) => current !== index))"
/>
</div>
<UFormField label="标题">
<UInput
:model-value="todo?.title"
placeholder="任务标题"
@update:model-value="setValue(`[${index}].title`, $event)"
/>
</UFormField>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<UFormField label="优先级">
<USelect
:model-value="todo?.priority"
:items="priorityItems"
@update:model-value="setValue(`[${index}].priority`, $event)"
/>
</UFormField>
<UFormField label="完成状态">
<USwitch
:model-value="todo?.done"
:label="todo?.done ? '已完成' : '进行中'"
@update:model-value="setValue(`[${index}].done`, $event)"
/>
</UFormField>
</div>
</div>
<UButton
icon="i-lucide-plus"
color="info"
variant="soft"
size="sm"
@click="setValue([...(value || []), createTodo()])"
>
添加任务
</UButton>
</div>
</template>
</MAutoForm>
</template>
API
Form-Level Slots
| Name | Description |
|---|---|
header | Above the fields area, suitable for titles, descriptions or status hints |
footer | Below the fields area, above the submit button |
submit | Takes over the submit area, use with :submit="false" |
Field Slots
| Slot | Parameter | Type | Description |
|---|---|---|---|
field-label | label | string | Label text |
field-hint | hint | string | Hint text |
field-description | description | string | Description text |
field-help | help | string | Help text |
field-error | error | string | boolean | Error message |
field-default | error | string | boolean | Takes over control rendering for primitive type fields (with field context) |
field-default is only for customizing the control of primitive type fields (string, number, boolean, etc.). For object and array fields, use field-content.Content Slots
| Name | Description |
|---|---|
field-content:{path} | Completely takes over the rendering of an object/array field |
field-before:{path} | Inserts before the default content without taking over child fields |
field-after:{path} | Inserts after the default content without taking over child fields |
setValue Signature
setValue supports the following overload forms:
| Signature | Return | Description |
|---|---|---|
setValue(value) | void | Set the entire field value |
setValue(relativePath, value) | void | Update a child field by relative path: use key for objects (auto-inferred), [index].key for arrays |
// Set the current field value directly
setValue(newValue)
<template #field-content:profile="{ value, setValue }">
<!-- Set the entire object -->
<UButton @click="setValue({ name: 'John', email: 'test@example.com' })">
Fill Defaults
</UButton>
<!-- Set a child field using a relative path -->
<UInput
:model-value="value?.name"
@update:model-value="setValue('name', $event)"
/>
</template>
<template #field-content:tasks="{ value, setValue }">
<div v-for="(task, index) in value" :key="index">
<!-- Set an array element's property -->
<UInput
:model-value="task?.title"
@update:model-value="setValue(`[${index}].title`, $event)"
/>
<!-- Remove an array element -->
<UButton
@click="setValue(value.filter((_, i) => i !== index))"
icon="i-lucide-trash-2"
/>
</div>
<!-- Add a new element -->
<UButton
@click="setValue([...value, { title: '', completed: false }])"
icon="i-lucide-plus"
/>
</template>
AutoFormFieldContext
Field context type, available in slots and reactive functions:
export interface AutoFormFieldContext<S = any, P extends string = string> {
/** Form data */
readonly state: S
/** Field path */
readonly path: P
/** Field value (may be undefined) */
readonly value: FieldValueType<S, P>
/**
* Callback function to set the field value
* @description Supports multiple invocation styles:
* 1. setValue(value) - set the entire value of the current field
* 2. setValue(key, value) - set a child field value (object) or element property (array)
* 3. setValue(path, value) - set a value at an arbitrary path (string fallback)
* @example
* // Object field profile: { name, email, bio }
* setValue({ name: 'John', email: 'test@example.com' }) // set the entire object
* setValue('name', 'John') // key auto-inferred as 'name' | 'email' | 'bio'
*
* // Array field tasks: [{ title, priority, completed }]
* setValue([...tasks, newTask]) // set the entire array
* setValue('[0].title', 'New Title') // use index path string
*
* // Or use property name directly when iterating (recommended)
* v-for="(task, index) in tasks"
* setValue(`[${index}].title`, value) // dynamic index
*/
setValue: {
(value: FieldValueType<S, P>): void
<K extends RelativePath<NonUndefinedFieldValue<S, P>>>(
relativePath: K extends never ? string : K,
value: any
): void
(relativePath: string, value: any): void
}
/** Field error list */
readonly errors: unknown[]
/** Form submit loading state */
readonly loading: boolean
/** Field collapsed state */
readonly open?: boolean
/** Array element index */
readonly count?: number
}