Layout
Customize field layout via the layout config, supporting grid, tabs, accordion containers and collapsible nested fields.
Examples
Simple Container Layout
Use afz.layout to create a basic container layout, controlling styles via the class attribute:
<script setup lang="ts">
import type { FormSubmitEvent } from '@nuxt/ui'
import type { z } from 'zod'
const { afz } = useAutoForm()
const toast = useToast()
const schema = afz.object({
$layout: afz.layout({
class: 'space-y-4',
fields: {
firstName: afz.string().meta({ label: '名字' }),
lastName: afz.string().meta({ label: '姓氏' }),
email: afz.email().meta({ label: '邮箱地址' })
}
})
})
type Schema = z.output<typeof schema>
const form = ref<Partial<Schema>>({})
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({
title: '提交成功',
color: 'success',
description: JSON.stringify(event.data, null, 2)
})
}
</script>
<template>
<MAutoForm :schema="schema" :state="form" @submit="onSubmit" />
</template>
It is conventional for layout field keys (e.g.
$layout) to start with $, but it is not required. You can use any field name as long as its value is afz.layout().Grid Layout
Use CSS Grid to create multi-column layouts, controlling column spans via field-level class:
<script setup lang="ts">
import type { FormSubmitEvent } from '@nuxt/ui'
import type { z } from 'zod'
const { afz } = useAutoForm()
const toast = useToast()
const schema = afz.object({
$grid: afz.layout({
class: 'grid grid-cols-2 gap-4',
fields: {
firstName: afz.string().meta({ label: '名字' }),
lastName: afz.string().meta({ label: '姓氏' }),
email: afz.email().meta({
class: 'col-span-2',
label: '邮箱地址',
description: '此字段占据两列宽度'
}),
phone: afz.string().meta({ label: '电话' }),
age: afz.number().int().min(0).meta({ label: '年龄' })
}
})
})
type Schema = z.output<typeof schema>
const form = ref<Partial<Schema>>({})
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({
title: '提交成功',
color: 'success',
description: JSON.stringify(event.data, null, 2)
})
}
</script>
<template>
<MAutoForm :schema="schema" :state="form" @submit="onSubmit" />
</template>
Responsive Multi-Column Layout
Use Tailwind's responsive prefixes to create layouts that adapt to different screen sizes:
<script setup lang="ts">
import type { FormSubmitEvent } from '@nuxt/ui'
import type { z } from 'zod'
const { afz } = useAutoForm()
const toast = useToast()
const schema = afz.object({
$responsive: afz.layout({
// 移动端 1 列,平板 2 列,桌面端 3 列
class: 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4',
fields: {
title: afz.string().meta({
class: 'col-span-full', // 所有屏幕尺寸都占满整行
label: '标题'
}),
firstName: afz.string().meta({ label: '名字' }),
lastName: afz.string().meta({ label: '姓氏' }),
email: afz.email().meta({ label: '邮箱' }),
phone: afz.string().meta({ label: '电话' }),
age: afz.number().int().min(0).meta({ label: '年龄' }),
description: afz.string({ type: 'textarea' }).meta({
class: 'col-span-full',
label: '描述'
})
}
})
})
type Schema = z.output<typeof schema>
const form = ref<Partial<Schema>>({})
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({
title: '提交成功',
color: 'success',
description: JSON.stringify(event.data, null, 2)
})
}
</script>
<template>
<MAutoForm :schema="schema" :state="form" @submit="onSubmit" />
</template>
Use Tailwind's responsive prefixes (
md:, lg:) to easily achieve cross-device adaptive layouts. Fields can span the full row with col-span-full.Nested Layout
Layout fields can be nested infinitely to build complex hierarchical structures:
<script setup lang="ts">
import type { FormSubmitEvent } from '@nuxt/ui'
import type { z } from 'zod'
const { afz } = useAutoForm()
const toast = useToast()
const schema = afz.object({
$personalInfo: afz.layout({
class: 'grid grid-cols-2 gap-4',
fields: {
firstName: afz.string().meta({ label: '名字' }),
lastName: afz.string().meta({ label: '姓氏' })
}
}),
$contactInfo: afz.layout({
class: 'space-y-4 mt-10',
fields: {
email: afz.email().meta({ label: '邮箱地址' }),
$phoneLayout: afz.layout({
class: 'grid grid-cols-3 gap-2',
fields: {
countryCode: afz.string().default('+86').meta({ label: '国家代码' }),
phoneNumber: afz.string().meta({
class: 'col-span-2',
label: '电话号码',
hint: '此字段占据两列宽度'
})
}
})
}
})
})
type Schema = z.output<typeof schema>
const form = ref<Partial<Schema>>({})
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({
title: '提交成功',
color: 'success',
description: JSON.stringify(event.data, null, 2)
})
}
</script>
<template>
<MAutoForm :schema="schema" :state="form" @submit="onSubmit" />
</template>
There is no limit to nested layout depth, but deep nesting can hurt readability. Keeping it to 2-3 levels is recommended.
Accordion Layout
Use Nuxt UI's UAccordion component to organize fields in collapsible panels:
<script setup lang="ts">
import type { FormSubmitEvent } from '@nuxt/ui'
import type { z } from 'zod'
import { UAccordion } from '#components'
const { afz } = useAutoForm()
const toast = useToast()
const schema = afz.object({
$accordion: afz.layout({
component: UAccordion,
props: {
type: 'multiple',
ui: {
content: 'space-y-4'
},
items: [
{ label: '基本信息', icon: 'i-lucide-user', slot: 'item-0' },
{ label: '详细信息', icon: 'i-lucide-square-pen', slot: 'item-1' }
]
},
fieldSlots: {
name: 'item-0',
email: 'item-0',
bio: 'item-1'
},
fields: {
name: afz.string().meta({ label: '姓名' }),
email: afz.email().meta({ label: '邮箱' }),
bio: afz.string({ type: 'textarea' }).meta({ label: '个人简介' }).optional()
}
})
})
type Schema = z.output<typeof schema>
const form = ref<Partial<Schema>>({})
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({
title: '提交成功',
color: 'success',
description: JSON.stringify(event.data, null, 2)
})
}
</script>
<template>
<MAutoForm :schema="schema" :state="form" @submit="onSubmit" />
</template>
fieldSlots is used to assign different fields to different slots of the layout component. Slot names must match the slot defined in items.Tabs Layout
Use the UTabs component to create a tabbed form with dynamic responsive configuration:
<script setup lang="ts">
import { UTabs } from '#components'
import type { FormSubmitEvent } from '@nuxt/ui'
import type { z } from 'zod'
import type { AutoFormFieldContext } from '#movk/types'
const { afz } = useAutoForm()
const toast = useToast()
const schema = afz.object({
userType: afz.enum(['personal', 'company'])
.default('personal')
.meta({ label: '用户类型' }),
$tabs: afz.layout({
component: UTabs,
props: ({ state }) => {
const isCompany = state?.userType === 'company'
return {
ui: { content: 'space-y-4' },
items: isCompany
? [
{ label: '公司信息', icon: 'i-lucide-building', slot: 'tab-0' },
{ label: '联系方式', icon: 'i-lucide-phone', slot: 'tab-1' }
]
: [
{ label: '个人信息', icon: 'i-lucide-user', slot: 'tab-0' },
{ label: '联系方式', icon: 'i-lucide-phone', slot: 'tab-1' }
]
}
},
fieldSlots: ({ state }) => {
if (state?.userType === 'company') {
return {
companyName: 'tab-0',
registrationNumber: 'tab-0',
phone: 'tab-1',
email: 'tab-1',
address: 'tab-1'
}
}
return {
username: 'tab-0',
age: 'tab-0',
phone: 'tab-1',
email: 'tab-1',
address: 'tab-1'
}
},
fields: {
username: afz.string().meta({
label: '姓名',
if: ({ state }: AutoFormFieldContext) => state?.userType === 'personal'
}).optional(),
age: afz.number().int().min(0).meta({
label: '年龄',
if: ({ state }: AutoFormFieldContext) => state?.userType === 'personal'
}).optional(),
companyName: afz.string().meta({
label: '公司名称',
if: ({ state }: AutoFormFieldContext) => state?.userType === 'company'
}).optional(),
registrationNumber: afz.string().meta({
label: '注册号',
if: ({ state }: AutoFormFieldContext) => state?.userType === 'company'
}).optional(),
phone: afz.string().meta({ label: '联系电话' }),
email: afz.email().meta({ label: '邮箱地址' }),
address: afz.string().meta({ label: '地址' })
}
})
})
type Schema = z.output<typeof schema>
const form = ref<Partial<Schema>>({})
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({
title: '提交成功',
color: 'success',
description: JSON.stringify(event.data, null, 2)
})
}
</script>
<template>
<MAutoForm :schema="schema" :state="form" @submit="onSubmit" />
</template>
The tabs layout demonstrates how to dynamically adjust the layout structure based on form state. When switching user types, tab titles and field distribution update automatically.
When using conditional rendering (
if), it is recommended to mark fields as .optional() to avoid validation errors.API
AutoFormLayoutConfig
afz.layout accepts a configuration object with the following attributes:
| Attribute | Type | Description |
|---|---|---|
component | Component | string | Layout container component (default: 'div') |
class | string | (context) => string | CSS class name, supports reactive function |
props | object | (context) => object | Component attributes, supports reactive function |
slots | Record<string, () => VNode> | (context) => ... | Component slot content |
fields | Record<string, ZodType> | Contained field definitions |
fieldSlot | string | (context) => string | Render all fields into the specified slot |
fieldSlots | Record<string, string> | (context) => ... | Assign different fields to different slots |
AutoFormNestedCollapsible
The collapsible attribute accepts a configuration object with the following options:
| Attribute | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Whether to enable collapsible functionality |
defaultOpen | boolean | Whether to be open by default | |
open | boolean | Control expand/collapse state (controlled mode) | |
disabled | boolean | Disable collapsible (always expanded) | |
unmountOnHide | boolean | true | Unmount content when hidden |
as | string | 'div' | Rendered element type |
class | ClassNameValue | CSS class name | |
ui | { root?: ClassNameValue; content?: ClassNameValue; } | UI style configuration |