---
title: "SearchForm"
description: "A schema-driven, collapsible search form component."
canonical_url: "https://nuxt.mhaibaraai.cn/en/docs/components/search-form"
---
# SearchForm

> A schema-driven, collapsible search form component.

## 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.

> [!NOTE]
> See: /docs/auto-form/quickstart
> 
> Reuses the AutoForm infrastructure (schema introspection, control mapping, field renderers) and defines search fields via Zod schema.

## Usage

Renders fields from an AutoForm schema. `cols` controls the grid. Built-in search and reset buttons — clicking "Search" triggers validation and emits `@submit`:

```vue [ComponentsSearchFormBasicExample.vue]
<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.

```vue [ComponentsSearchFormModelExample.vue]
<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.

```vue [ComponentsSearchFormColsExample.vue]
<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>
```

```vue [ComponentsSearchFormResponsiveExample.vue]
<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.

```vue [ComponentsSearchFormExpandExample.vue]
<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.

```vue [ComponentsSearchFormExpandedControlledExample.vue]
<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.

```vue [ComponentsSearchFormToggleButtonExample.vue]
<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: search` automatically binds to submit; `key: reset` automatically binds to reset
- Custom `key` requires an `onClick(ctx)` handler where `ctx` contains `state`, `errors`, `search`, `reset`, `clear`, `toggle`, `loading`, and `expanded`
- Pass `actions: []` to disable all built-in buttons and fully customize via the `actions` slot

```vue [ComponentsSearchFormHideButtonsExample.vue]
<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>
```

```vue [ComponentsSearchFormCustomExample.vue]
<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:

```vue [ComponentsSearchFormActionsSlotExample.vue]
<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:

```vue [ComponentsSearchFormLayoutSlotsExample.vue]
<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:

```vue [ComponentsSearchFormAsyncExample.vue]
<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

```ts
/**
 * Props for the MSearchForm component
 */
interface MSearchFormProps {
  /**
   * Zod 对象 schema，定义表单字段
   */
  schema: S;
  /**
   * 网格列数
   * @default 3
   */
  cols?: number | { sm?: number | undefined; md?: number | undefined; lg?: number | undefined; xl?: number | undefined; } | undefined;
  /**
   * 可见行数（折叠时显示的行数）
   * @default 1
   */
  visibleRows?: number | undefined;
  /**
   * 动作按钮配置；不传时使用默认 [search, reset]；传 [] 则关闭所有内置按钮
   * @default [{ key: 'search', ... }, { key: 'reset', ... }]
   */
  actions?: SearchFormAction[] | undefined;
  /**
   * 搜索按钮加载状态（作用于 type==='submit' 或 key==='search' 的按钮）
   */
  loading?: boolean | undefined;
  /**
   * 收起按钮属性
   */
  collapseButtonProps?: ButtonProps | undefined;
  /**
   * 展开/收起按钮图标
   * @default 'i-lucide-chevron-down'
   */
  icon?: any;
  /**
   * 展开按钮文本
   * @default '展开'
   */
  expandText?: string | undefined;
  /**
   * 收起按钮文本
   * @default '收起'
   */
  collapseText?: string | undefined;
  /**
   * 受控展开状态；优先级高于 defaultExpanded
   */
  expanded?: boolean | undefined;
  /**
   * 默认展开状态
   * @default false
   */
  defaultExpanded?: boolean | undefined;
  /**
   * 自定义控件映射
   */
  controls?: AutoFormControls | undefined;
  /**
   * 全局字段元数据配置
   */
  globalMeta?: ZodAutoFormFieldMeta | undefined;
  /**
   * 是否启用自动 loading 功能。
   * @default true
   */
  loadingAuto?: boolean | undefined;
  /**
   * 表单验证时机，详见 UForm 的 validateOn 属性
   * @default []
   */
  validateOn?: FormInputEvents[] | undefined;
  ui?: Record<string, C> & { root?: SlotClass; form?: SlotClass; visible?: SlotClass; grid?: SlotClass; header?: SlotClass; footer?: SlotClass; actions?: SlotClass; toggleWrapper?: SlotClass; toggle?: SlotClass; toggleIcon?: SlotClass; collapsed?: SlotClass; } | undefined;
  id?: string | number | undefined;
  /**
   * Custom validation function to validate the form state.
   */
  validate?: (state: Partial<InferInput<S>>): FormError<string>[] | Promise<FormError<string>[]> | undefined;
  /**
   * Disable all inputs inside the form.
   */
  disabled?: boolean | undefined;
  /**
   * The `name` attribute of the form element.
   * For nested forms (`nested` is true), this is also used as the path of the form's state within its parent form.
   */
  name?: string | undefined;
  /**
   * Delay in milliseconds before validating the form on input events.
   * @default `300`
   */
  validateOnInputDelay?: number | undefined;
  /**
   * If true, applies schema transformations on submit.
   * @default `true`
   */
  transform?: true | undefined;
  /**
   * If true, this form will attach to its parent Form and validate at the same time.
   * @default `false`
   */
  nested?: false | undefined;
  onSubmit?: (): void | (event: FormSubmitEvent<InferOutput<S>>): void | undefined;
  acceptcharset?: string | undefined;
  action?: string | undefined;
  autocomplete?: string | undefined;
  enctype?: string | undefined;
  method?: string | undefined;
  novalidate?: false | true | "true" | "false" | undefined;
  target?: string | undefined;
  /**
   * @default {}
   */
  modelValue?: Partial<InferInput<S>> | undefined;
}
```

### Emits

```ts
/**
 * Emitted events for the MSearchForm component
 */
interface MSearchFormEmits {
  reset: (payload: [state: Partial<InferInput<S>>]) => void;
  clear: (payload: [state: Partial<InferInput<S>>]) => void;
  expand: (payload: [expanded: boolean]) => void;
  update:expanded: (payload: [expanded: boolean]) => void;
  error: (payload: [event: FormErrorEvent]) => void;
  update:expanded: (payload: [value: boolean | undefined]) => void;
  update:modelValue: (payload: [value: Partial<InferInput<S>>]) => void;
}
```

> [!NOTE]
> 
> @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

```ts
/**
 * Slots for the MSearchForm component
 */
interface MSearchFormSlots {
  header(): any;
  footer(): any;
  actions(): any;
  extraActions(): any;
  field-label(): any;
  field-hint(): any;
  field-description(): any;
  field-help(): any;
  field-error(): any;
  field-default(): any;
}
```

### Expose

You can access the typed component instance via [`useTemplateRef`](https://vuejs.org/api/composition-api-helpers.html#usetemplateref).

| Name | Type |
| --- | --- |
| `formRef` | `Ref<InstanceType<typeof UForm>>` <br> Reference to the UForm component |
| `submit()` | `void` <br> Programmatically trigger form submission (equivalent to clicking the search button) |
| `reset()` | `void` <br> Restore to baseline (the v-model snapshot from initial mount) and fire the `reset` event |
| `clear()` | `void` <br> Clear all form fields to empty values and fire the `clear` event |
| `setBaseline(value?)` | `void` <br> Set the restore baseline for `reset()`; uses the current v-model value if no argument is passed |
| `expanded` | `ComputedRef<boolean>` <br> Current expanded/collapsed state |
| `toggle()` | `void` <br> Toggle expand/collapse and emit `expand` and `update:expanded` |

## Theme

<component-theme>



</component-theme>

## Changelog

See commit history for [src/runtime/components/SearchForm.vue](https://github.com/mhaibaraai/movk-nuxt/commits/main/src/runtime/components/SearchForm.vue).


## Sitemap

See the full [sitemap](https://nuxt.mhaibaraai.cn/sitemap.md) for all pages.
