SearchForm
Introduction
MSearchForm is a schema-driven search form component with built-in grid layout, search/reset buttons, and collapse behavior. When there are many search fields, items exceeding the visible row count are automatically collapsed. Users can expand or collapse them using the toggle button.
Usage
Renders fields from an AutoForm schema. cols controls the grid. Built-in search and reset buttons — clicking "Search" triggers validation and emits @submit:
<script setup lang="ts">
import type { FormSubmitEvent } from '@nuxt/ui'
import type z from 'zod'
const { afz } = useAutoForm()
const schema = afz.object({
name: afz.string({ controlProps: { placeholder: '请输入姓名' } }).meta({ label: '姓名' }).optional(),
status: afz.enum(['启用', '禁用', '待审核']).meta({ label: '状态' }).optional(),
department: afz.string({ controlProps: { placeholder: '请输入部门' } }).meta({ label: '部门' }).optional(),
keyword: afz.string({ controlProps: { placeholder: '请输入关键词' } }).meta({ label: '关键词' }).optional(),
email: afz.email({ controlProps: { placeholder: '请输入邮箱' } }).meta({ label: '邮箱' }).optional()
})
const state = ref<Partial<z.output<typeof schema>>>({})
const result = ref('')
function handleSearch(event: FormSubmitEvent<Record<string, unknown>>) {
result.value = JSON.stringify(event.data, null, 2)
}
function handleReset() {
result.value = ''
}
</script>
<template>
<div class="space-y-4">
<MSearchForm
v-model="state"
:schema="schema"
@submit="handleSearch"
@reset="handleReset"
/>
<pre v-if="result" class="text-sm bg-muted p-3 rounded-(--ui-radius)">{{ result }}</pre>
</div>
</template>
v-model Binding
v-model binds form data bidirectionally. The initial value passed in is recorded as the reset baseline — clicking "Reset" restores to that initial value rather than clearing the form.
{
"name": "张三",
"status": "启用"
}<script setup lang="ts">
import type z from 'zod'
const { afz } = useAutoForm()
const schema = afz.object({
name: afz.string({ controlProps: { placeholder: '请输入姓名' } }).meta({ label: '姓名' }).optional(),
status: afz.enum(['启用', '禁用', '待审核']).meta({ label: '状态' }).optional(),
keyword: afz.string({ controlProps: { placeholder: '请输入关键词' } }).meta({ label: '关键词' }).optional()
})
const state = ref<Partial<z.output<typeof schema>>>({
name: '张三',
status: '启用'
})
</script>
<template>
<div class="space-y-4">
<MSearchForm v-model="state" :schema="schema" />
<pre class="text-sm bg-muted p-3 rounded-(--ui-radius)">{{ JSON.stringify(state, null, 2) }}</pre>
<UButton
size="sm"
color="neutral"
variant="outline"
@click="state = { name: '李四', status: '禁用', keyword: '测试' }"
>
外部设值
</UButton>
</div>
</template>
cols Grid Columns
cols controls the number of grid columns: pass a number for a fixed count, or a breakpoint object to switch columns across sm, md, lg, xl breakpoints.
<script setup lang="ts">
const { afz } = useAutoForm()
const schema = afz.object({
name: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '姓名' }).optional(),
status: afz.enum(['启用', '禁用']).meta({ label: '状态' }).optional(),
department: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '部门' }).optional(),
role: afz.enum(['管理员', '编辑', '查看者']).meta({ label: '角色' }).optional(),
keyword: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '关键词' }).optional()
})
</script>
<template>
<MSearchForm :schema="schema" />
</template>
调整浏览器窗口宽度查看效果:sm=1, md=2, lg=3, xl=4
<script setup lang="ts">
const { afz } = useAutoForm()
const schema = afz.object({
name: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '姓名' }).optional(),
status: afz.enum(['启用', '禁用', '待审核']).meta({ label: '状态' }).optional(),
department: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '部门' }).optional(),
email: afz.email({ controlProps: { placeholder: '请输入' } }).meta({ label: '邮箱' }).optional(),
keyword: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '关键词' }).optional()
})
const params = ref({})
</script>
<template>
<div class="space-y-2">
<p class="text-sm text-muted">
调整浏览器窗口宽度查看效果:sm=1, md=2, lg=3, xl=4
</p>
<MSearchForm
v-model="params"
:schema="schema"
:cols="{ sm: 1, md: 2, lg: 3, xl: 4 }"
/>
</div>
</template>
visibleRows Collapse Behavior
visibleRows controls the number of visible rows. Fields exceeding this count collapse into the expanded area. Drag cols and visibleRows below to change the collapse threshold in real time.
<script setup lang="ts">
const { afz } = useAutoForm()
const schema = afz.object({
name: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '姓名' }).optional(),
status: afz.enum(['启用', '禁用', '待审核']).meta({ label: '状态' }).optional(),
department: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '部门' }).optional(),
role: afz.enum(['管理员', '编辑', '查看者']).meta({ label: '角色' }).optional(),
email: afz.email({ controlProps: { placeholder: '请输入' } }).meta({ label: '邮箱' }).optional(),
phone: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '手机号' }).optional(),
keyword: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '关键词' }).optional()
})
</script>
<template>
<MSearchForm :schema="schema" />
</template>
expanded Controlled Expand
v-model:expanded takes over the expanded state, taking priority over defaultExpanded. It can be driven by external buttons or logic.
<script setup lang="ts">
const { afz } = useAutoForm()
const schema = afz.object({
name: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '姓名' }).optional(),
status: afz.enum(['启用', '禁用', '待审核']).meta({ label: '状态' }).optional(),
department: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '部门' }).optional(),
role: afz.enum(['管理员', '编辑', '查看者']).meta({ label: '角色' }).optional(),
email: afz.email({ controlProps: { placeholder: '请输入' } }).meta({ label: '邮箱' }).optional(),
phone: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '手机号' }).optional(),
keyword: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '关键词' }).optional()
})
const expanded = ref(false)
</script>
<template>
<div class="space-y-3">
<div class="flex items-center gap-2 text-sm text-muted">
<UButton size="sm" color="neutral" variant="outline" @click="expanded = !expanded">
外部{{ expanded ? '收起' : '展开' }}
</UButton>
<span>expanded = {{ expanded }}</span>
</div>
<MSearchForm v-model:expanded="expanded" :schema="schema" />
</div>
</template>
expandText / collapseText / icon Toggle Button
expandText and collapseText customize the expand/collapse label text. icon switches the button icon. collapseButtonProps passes through button attributes.
<script setup lang="ts">
const { afz } = useAutoForm()
const schema = afz.object({
name: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '姓名' }).optional(),
status: afz.enum(['启用', '禁用', '待审核']).meta({ label: '状态' }).optional(),
department: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '部门' }).optional(),
role: afz.enum(['管理员', '编辑', '查看者']).meta({ label: '角色' }).optional(),
email: afz.email({ controlProps: { placeholder: '请输入' } }).meta({ label: '邮箱' }).optional(),
phone: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '手机号' }).optional(),
keyword: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '关键词' }).optional()
})
</script>
<template>
<MSearchForm :schema="schema" :collapse-button-props="{ color: 'primary', variant: 'soft' }" />
</template>
actions Action Buttons
The actions array extends or trims buttons:
- Built-in
key: searchautomatically binds to submit;key: resetautomatically binds to reset - Custom
keyrequires anonClick(ctx)handler wherectxcontainsstate,errors,search,reset,clear,toggle,loading, andexpanded - Pass
actions: []to disable all built-in buttons and fully customize via theactionsslot
仅保留搜索按钮(actions 只传 search)
关闭全部内置按钮(actions: []),通过 actions slot 完全自定义
<script setup lang="ts">
const { afz } = useAutoForm()
const schema = afz.object({
name: afz
.string({ controlProps: { placeholder: '请输入' } })
.meta({ label: '姓名' })
.optional(),
status: afz.enum(['启用', '禁用']).meta({ label: '状态' }).optional(),
department: afz
.string({ controlProps: { placeholder: '请输入' } })
.meta({ label: '部门' })
.optional(),
keyword: afz
.string({ controlProps: { placeholder: '请输入' } })
.meta({ label: '关键词' })
.optional()
})
</script>
<template>
<div class="space-y-6">
<div class="space-y-2">
<p class="text-sm text-muted">仅保留搜索按钮(actions 只传 search)</p>
<MSearchForm
:schema="schema"
:actions="[{ key: 'search', label: '搜索', icon: 'i-lucide-search', type: 'submit' }]"
/>
</div>
<div class="space-y-2">
<p class="text-sm text-muted">
关闭全部内置按钮(actions: []),通过 actions slot 完全自定义
</p>
<MSearchForm :schema="schema" :actions="[]">
<template #actions="{ search, reset }">
<div class="flex items-end gap-2 justify-end">
<UButton color="primary" variant="solid" icon="i-lucide-filter" @click="search">
筛选
</UButton>
<UButton color="neutral" variant="ghost" icon="i-lucide-x" @click="reset">
清空
</UButton>
</div>
</template>
</MSearchForm>
</div>
</div>
</template>
<script setup lang="ts">
const { afz } = useAutoForm()
const schema = afz.object({
name: afz
.string({ controlProps: { placeholder: '请输入' } })
.meta({ label: '姓名' })
.optional(),
status: afz.enum(['启用', '禁用']).meta({ label: '状态' }).optional(),
keyword: afz
.string({ controlProps: { placeholder: '请输入' } })
.meta({ label: '关键词' })
.optional(),
department: afz
.string({ controlProps: { placeholder: '请输入' } })
.meta({ label: '部门' })
.optional()
})
function onExport() {
console.log('导出')
}
</script>
<template>
<MSearchForm
:schema="schema"
:actions="[
{
key: 'search',
label: '查询',
icon: 'i-lucide-search',
type: 'submit',
color: 'primary',
variant: 'solid'
},
{
key: 'reset',
label: '清空',
icon: 'i-lucide-rotate-ccw',
color: 'error',
variant: 'outline'
},
{
key: 'export',
label: '导出',
icon: 'i-lucide-download',
color: 'primary',
variant: 'soft',
onClick: onExport
}
]"
/>
</template>
Examples
Taking Over the Actions Area
The #actions slot exposes search, clear, and loading, allowing custom buttons to replace the default actions area:
<script setup lang="ts">
import type z from 'zod'
const { afz } = useAutoForm()
const schema = afz.object({
name: afz
.string({ controlProps: { placeholder: '请输入' } })
.meta({ label: '姓名' })
.optional(),
status: afz.enum(['启用', '禁用']).meta({ label: '状态' }).optional()
})
const state = ref<Partial<z.output<typeof schema>>>({})
</script>
<template>
<MSearchForm v-model="state" :schema="schema" :cols="3">
<template #actions="{ search, clear, loading }">
<div class="flex items-end gap-2">
<UButton
color="primary"
variant="solid"
icon="i-lucide-filter"
:loading="loading"
@click="search"
>
筛选
</UButton>
<UButton color="neutral" variant="ghost" icon="i-lucide-x" @click="clear"> 清空 </UButton>
</div>
</template>
</MSearchForm>
</template>
Extending Layout Areas
header, footer, and extraActions insert supplementary content. Slot props update with expand state and form values:
<script setup lang="ts">
const { afz } = useAutoForm()
const schema = afz.object({
name: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '姓名' }).optional(),
status: afz.enum(['启用', '禁用']).meta({ label: '状态' }).optional(),
keyword: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '关键词' }).optional()
})
</script>
<template>
<MSearchForm :schema="schema" :cols="3">
<template #header="{ expanded }">
<div class="rounded border border-dashed border-primary/40 bg-primary/5 px-3 py-2 text-xs text-primary">
#header · expanded={{ expanded }}
</div>
</template>
<template #extraActions>
<UButton size="sm" color="neutral" variant="outline" icon="i-lucide-save">
保存方案
</UButton>
</template>
<template #footer="{ state: formState }">
<div class="mt-2 rounded border border-dashed border-success/40 bg-success/5 px-3 py-2 text-xs text-success">
#footer · 当前关键词: {{ (formState as Record<string, unknown>).keyword ?? '—' }}
</div>
</template>
</MSearchForm>
</template>
Async Submit and Validation
loading controls the button loading state. @error returns the list of Zod validation failures:
<script setup lang="ts">
import type { FormErrorEvent, FormSubmitEvent } from '@nuxt/ui'
import type z from 'zod'
const { afz } = useAutoForm()
const schema = afz.object({
name: afz.string({ controlProps: { placeholder: '请输入' } }).meta({ label: '姓名' }),
email: afz
.email({ controlProps: { placeholder: '请输入合法邮箱' } })
.meta({ label: '邮箱' })
.optional()
})
type Schema = z.output<typeof schema>
const state = ref<Partial<Schema>>({})
const loading = ref(false)
const toast = useToast()
function onSearch(event: FormSubmitEvent<Schema>) {
loading.value = true
setTimeout(() => {
loading.value = false
toast.add({ title: '查询完成', description: JSON.stringify(event.data), color: 'success' })
}, 1500)
}
function onError(event: FormErrorEvent) {
toast.add({
title: '校验失败',
description: `共 ${event.errors?.length ?? 0} 项错误`,
color: 'error'
})
}
</script>
<template>
<MSearchForm
v-model="state"
:schema="schema"
:loading="loading"
:validate-on="['blur']"
@submit="onSearch"
@error="onError"
/>
</template>
API
Props
| Prop | Default | Type |
|---|---|---|
schema | SZod 对象 schema,定义表单字段 | |
cols | 3 | number | { sm?: number; md?: number; lg?: number; xl?: number; }网格列数 |
visibleRows | 1 | number可见行数(折叠时显示的行数) |
actions | [{ key: 'search', ... }, { key: 'reset', ... }] | SearchFormAction[]动作按钮配置;不传时使用默认 search, reset;传 则关闭所有内置按钮 |
collapseButtonProps | ButtonProps收起按钮属性 | |
icon | 'i-lucide-chevron-down' | any展开/收起按钮图标 |
expandText | '展开' | string展开按钮文本 |
collapseText | '收起' | string收起按钮文本 |
controls | AutoFormControls自定义控件映射 | |
globalMeta | ZodAutoFormFieldMeta全局字段元数据配置 | |
validateOn | [] | FormInputEvents[]表单验证时机,详见 UForm 的 validateOn 属性 |
id | string | number | |
validate | (state: Partial<InferInput<S>>) => FormError<string>[] | Promise<FormError<string>[]>Custom validation function to validate the form state. | |
name | stringThe | |
validateOnInputDelay | `300` | numberDelay in milliseconds before validating the form on input events. |
transform | `true` | trueIf true, applies schema transformations on submit. |
nested | `false` | falseIf true, this form will attach to its parent Form and validate at the same time. |
onSubmit | (): void | Promise<void> | (event: FormSubmitEvent<InferOutput<S>>): void | Promise<void> | |
acceptcharset | string | |
action | string | |
autocomplete | string | |
enctype | string | |
method | string | |
novalidate | false | true | "true" | "false" | |
target | string | |
modelValue | {} | Partial<InferInput<S>> |
loading | boolean搜索按钮加载状态(作用于 type==='submit' 或 key==='search' 的按钮) | |
expanded | boolean受控展开状态;优先级高于 defaultExpanded | |
defaultExpanded | false | boolean默认展开状态 |
loadingAuto | true | boolean是否启用自动 loading 功能。 |
disabled | booleanDisable all inputs inside the form. | |
ui | Record<string, ClassNameValue> & { root?: SlotClass; form?: SlotClass; visible?: SlotClass; grid?: SlotClass; header?: SlotClass; footer?: SlotClass; actions?: SlotClass; toggleWrapper?: SlotClass; toggle?: SlotClass; toggleIcon?: SlotClass; collapsed?: SlotClass; } |
Emits
| Event | Type |
|---|---|
reset | [state: Partial<InferInput<S>>] |
clear | [state: Partial<InferInput<S>>] |
expand | [expanded: boolean] |
update:expanded | [expanded: boolean] |
error | [event: FormErrorEvent] |
update:modelValue | [value: Partial<InferInput<S>>] |
@submit is forwarded from the underlying UForm and is not listed in the table above. It fires after successful validation and returns a FormSubmitEvent where search conditions are in event.data — the primary outlet for receiving query parameters.Slots
| Slot | Type |
|---|---|
header | SearchFormSlotProps<S> |
footer | SearchFormSlotProps<S> |
actions | SearchFormSlotProps<S> |
extraActions | SearchFormSlotProps<S> |
field-label | { label?: string; } & AutoFormFieldContext<Partial<InferInput<S>>, string> |
field-hint | { hint?: string; } & AutoFormFieldContext<Partial<InferInput<S>>, string> |
field-description | { description?: string; } & AutoFormFieldContext<Partial<InferInput<S>>, string> |
field-help | { help?: string; } & AutoFormFieldContext<Partial<InferInput<S>>, string> |
field-error | { error?: string | boolean; } & AutoFormFieldContext<Partial<InferInput<S>>, string> |
field-default | { error?: string | boolean; } & AutoFormFieldContext<Partial<InferInput<S>>, string> |
Expose
You can access the typed component instance via useTemplateRef.
| Name | Type |
|---|---|
formRef | Ref<InstanceType<typeof UForm>> Reference to the UForm component |
submit() | void Programmatically trigger form submission (equivalent to clicking the search button) |
reset() | void Restore to baseline (the v-model snapshot from initial mount) and fire the |
clear() | void Clear all form fields to empty values and fire the |
setBaseline(value?) | void Set the restore baseline for |
expanded | ComputedRef<boolean> Current expanded/collapsed state |
toggle() | void Toggle expand/collapse and emit |
Theme
export default defineAppConfig({
ui: {
searchForm: {
slots: {
root: 'group/search',
form: 'space-y-4',
visible: 'relative',
grid: 'grid gap-4 [grid-template-columns:repeat(var(--m-search-cols,1),minmax(0,1fr))] sm:[grid-template-columns:repeat(var(--m-search-cols-sm,var(--m-search-cols)),minmax(0,1fr))] md:[grid-template-columns:repeat(var(--m-search-cols-md,var(--m-search-cols-sm,var(--m-search-cols))),minmax(0,1fr))] lg:[grid-template-columns:repeat(var(--m-search-cols-lg,var(--m-search-cols-md,var(--m-search-cols-sm,var(--m-search-cols)))),minmax(0,1fr))] xl:[grid-template-columns:repeat(var(--m-search-cols-xl,var(--m-search-cols-lg,var(--m-search-cols-md,var(--m-search-cols-sm,var(--m-search-cols))))),minmax(0,1fr))]',
header: '',
footer: '',
actions: 'flex items-end gap-2 justify-end',
toggleWrapper: 'flex justify-center pt-2',
toggle: 'group opacity-75 hover:opacity-100 active:opacity-90 transition-all duration-200',
toggleIcon: 'size-3.5 transition-all duration-200 opacity-80 group-data-[state=open]:rotate-180 group-data-[state=open]:opacity-100',
collapsed: 'mt-4'
}
}
}
})