SchemaForm
SchemaForm generates fields from the single columns schema and manages its model with Vue v-model. It shares dataIndex, valueType, valueEnum, validation, and transforms with the table components.
When to use
Use it when one columns schema should describe standard forms, query filters, overlay forms, or step forms while sharing initialization, validation, transformation, and submission behavior.
Examples
Async options, URL synchronization, and slots
View full code
<script setup lang="ts">
import { ref } from 'vue'
import { SchemaForm, type SchemaFormColumn, type SchemaFormInstance } from 'antdv-next-pro'
type Brief = Record<string, unknown> & {
project?: string
owner?: string
channel?: 'web' | 'mobile' | 'both'
enabled?: boolean
}
const formRef = ref<SchemaFormInstance<Brief>>()
const model = ref<Partial<Brief>>({ channel: 'both', enabled: true })
const submitted = ref('等待提交')
const columns: SchemaFormColumn<Brief>[] = [
{
title: '项目名称',
dataIndex: 'project',
valueType: 'text',
formItemProps: { rules: [{ required: true, message: '请输入项目名称' }] },
},
{
title: '负责人',
dataIndex: 'owner',
valueType: 'text',
dependencies: ['channel'],
},
{
title: '发布渠道',
dataIndex: 'channel',
valueType: 'select',
request: async () => {
await new Promise((resolve) => setTimeout(resolve, 120))
return [
{ label: 'Web', value: 'web' },
{ label: '移动端', value: 'mobile' },
{ label: '双端同步', value: 'both' },
]
},
},
{ title: '启用监测', dataIndex: 'enabled', valueType: 'switch' },
]
const loadInitialValues = async () => {
await new Promise((resolve) => setTimeout(resolve, 100))
return { owner: '林默' }
}
const onOwnerInput = (update: (value: unknown) => void, event: Event) => {
update((event.target as HTMLInputElement).value)
}
const submit = async () => {
try {
const values = await formRef.value?.submit()
submitted.value = JSON.stringify(values)
} catch {
submitted.value = '校验未通过'
}
}
const fillExample = () => {
formRef.value?.setFieldsValue({ project: '秋季增长实验', owner: 'Ada' })
}
</script>
<template>
<div class="demo-frame vp-raw">
<p class="demo-label">LIVE · ASYNC OPTIONS + URL SYNC + SLOTS</p>
<SchemaForm
ref="formRef"
v-model="model"
:columns="columns"
:request="loadInitialValues"
:url-sync="{ key: 'schema-demo' }"
:grid="true"
@request-error="submitted = '异步初始值加载失败'"
>
<template #label-project="{ column }"> {{ column.title }} · 必填 </template>
<template #field-owner="{ value, update, dependencies }">
<label class="owner-field">
<input
:value="String(value ?? '')"
placeholder="命名字段插槽"
@input="onOwnerInput(update, $event)"
/>
<small>依赖渠道:{{ dependencies?.[0] ?? '未选择' }}</small>
</label>
</template>
<template #submitter>
<div class="custom-submitter">
<button type="button" @click="fillExample">ref 填充</button>
<button type="button" @click="formRef?.reset()">重置</button>
<button type="button" class="primary" @click="submit">ref 提交</button>
</div>
</template>
</SchemaForm>
<p class="submit-result"><strong>结果:</strong>{{ submitted }}</p>
</div>
</template>
<style scoped>
.owner-field {
display: grid;
gap: 4px;
}
.owner-field input {
width: 100%;
padding: 6px 11px;
border: 1px solid #d9d9d9;
border-radius: 6px;
font: inherit;
}
.owner-field small,
.submit-result {
color: #64748b;
font-size: 12px;
}
.custom-submitter {
display: flex;
gap: 8px;
}
.custom-submitter button {
padding: 6px 11px;
border: 1px solid #bfd2e7;
border-radius: 6px;
background: #fff;
color: #1768d3;
cursor: pointer;
}
.custom-submitter .primary {
border-color: #1768d3;
background: #1768d3;
color: #fff;
}
</style>Basic usage
<SchemaForm ref="formRef" v-model="form" :columns="columns" @finish="save" />initialValues, asynchronous request(params), URL values, and the controlled value merge in this precedence order, with the right side winning:
initialValues < request result < URL values < modelValueThe merged snapshot is also the target of reset(). Changes to params, request, initialValues, or urlSync initialize again. Concurrent initializations use the latest request only. A failure falls back to initialValues + modelValue and emits both request-error and error.
const loadInitialValues = async (params?: Record<string, unknown>) => {
const project = await api.project(params?.projectId)
return { owner: project.owner, channel: project.channel }
}valueType and async options
| Type | Generated control/structure |
|---|---|
text / textarea / password | Input / Textarea / Password |
digit / money / percent | InputNumber |
select / treeSelect / radio | Select / TreeSelect / RadioGroup |
checkbox / switch | Checkbox(Group) / Switch |
slider / segmented | Slider / Segmented |
date / dateTime | DatePicker |
dateRange / dateTimeRange | DateRangePicker |
time / timeRange | TimePicker / TimeRangePicker |
group / formSet | Field groups |
formList | Dynamic add/remove list |
divider | Divider |
dependency | Dependency or custom reactive region |
select, treeSelect, radio, checkbox, and segmented can use valueEnum, top-level/fieldProps options, or a column-level async request. TreeSelect uses treeData at the underlying control boundary:
const channelColumn: SchemaFormColumn<Brief> = {
title: 'Channel',
dataIndex: 'channel',
valueType: 'select',
params: { enabled: true },
request: async (params) => {
const items = await api.channels(params)
return items.map((item) => ({
label: item.name,
value: item.code,
disabled: !item.available,
}))
},
}Options reload when request or params changes, and only the latest concurrent result is accepted.
SchemaForm, ProTable search/edit controls, and the standalone ProFormFields share the same field registry and control core. Option requests, readonly output, and the treeSelect, slider, and segmented types therefore behave consistently. timeRange also generates a time-range control. Captcha, UploadButton, and UploadDragger remain standalone components and are not mapped to valueType.
Layout types
layoutType | Scenario |
|---|---|
Form | Standard form |
Embed | Embedded form without an overlay |
ModalForm | Modal with v-model:open |
DrawerForm | Drawer with v-model:open |
QueryFilter | Grid-based inline query form |
LightFilter | Lightweight inline filter |
StepForm | Single step view with v-model:current |
StepsForm | Multi-step form with Steps navigation |
Set layout-type or import a named layout component:
<script setup lang="ts">
import { ModalForm, StepsForm } from 'antdv-next-pro'
</script>
<template>
<ModalForm v-model="form" v-model:open="open" :columns="columns" />
<StepsForm v-model="form" v-model:current="current" :columns="stepColumns" />
</template>For multiple steps, every top-level column must be a group or formSet with child columns. Each top-level group becomes one step. next() validates the current step before advancing; the last step submits the complete result. A Steps title may navigate directly to an earlier step. Clicking a later step invokes one validated next() transition and cannot bypass the current step.
Composition and dynamic schemas
const columns: SchemaFormColumn<Project>[] = [
{
title: 'Basics',
valueType: 'group',
columns: [
{ title: 'Name', dataIndex: 'name', valueType: 'text' },
{ title: 'Kind', dataIndex: 'kind', valueType: 'select', valueEnum: kinds },
],
},
{
title: 'Contacts',
dataIndex: 'contacts',
valueType: 'formList',
fieldProps: {
creatorButtonText: 'Add contact',
removeText: 'Remove',
initialValue: { name: '', email: '' },
},
columns: [
{ title: 'Name', dataIndex: 'name', valueType: 'text' },
{ title: 'Email', dataIndex: 'email', valueType: 'text' },
],
},
]columns may be computed; adding or removing columns from external state creates dynamic fields. dependencies passes dependency values to field slots. A dependency column with renderFormItem creates a custom reactive region.
For a regular field, extension precedence is: dynamic field slot (field-${path}, ${path}, or the column key) → renderFormItem → column.component → the default valueType control. A column.component is still wrapped by SchemaForm's FormItem and receives value, modelValue, disabled, and matching update listeners. Use a field slot or renderFormItem when the custom content should take over completely.
Value transforms
convertValue only processes values entering the form, including initialization, external v-model updates, and setFieldsValue. transform only processes submission output when submit() runs. validate() validates and returns the raw form values without applying transform.
const columns = [
{
title: 'Window',
dataIndex: 'range',
valueType: 'dateRange',
convertValue: (value) => value?.map(dayjs),
transform: (value) => ({
startedAt: value?.[0]?.toISOString(),
endedAt: value?.[1]?.toISOString(),
}),
},
]An object returned by transform merges into the final submit() result. A scalar remains under the original dataIndex. Use validate() when you need validated raw form values and submit() when you need the transformed API payload.
URL synchronization
<!-- one query parameter per field -->
<SchemaForm :url-sync="true" />
<!-- complete model JSON in the filters parameter -->
<SchemaForm :url-sync="{ key: 'filters' }" />
<!-- one hash parameter per field -->
<SchemaForm :url-sync="{ mode: 'hash' }" />Per-field mode removes empty URL values; named-key mode stores the complete model. The component listens to popstate / hashchange and hydrates again, which is useful for shareable filters. URL values override the request result during initialization but are still overridden by explicit modelValue.
API
Props
| Prop | Type | Description |
|---|---|---|
columns | SchemaFormColumn<T>[] | The single schema entry point |
modelValue | Partial<T> | Standard v-model value |
initialValues | Partial<T> | Initialization and reset baseline |
request | (params?) => Promise<Partial<T>> | Asynchronous initial values |
params | Record<string, unknown> | Initialization request params |
layoutType | SchemaFormLayoutType | Form layout; defaults to Form |
open | boolean | Modal/Drawer state; supports v-model:open |
current | number | Step index; supports v-model:current |
title / width | Text or size | Overlay title and width |
labelCol / wrapperCol | Antdv Next Form config | Label and control layout |
grid | boolean | Responsive Row/Col grid |
readonly | boolean | Whole-form readonly mode |
urlSync | boolean | { key?, mode? } | Query/hash synchronization |
submitter | false | { submitText?, resetText? } | Default action area |
style | CSSProperties | Root style |
Columns also accept component, colProps, rowProps, tooltip, and extra.
Events
| Event | Arguments | Description |
|---|---|---|
update:model-value | values | Default v-model update |
update:open | open | Overlay two-way binding |
update:current | current | Step two-way binding |
change | values | Any field change |
values-change | changed, values | changed contains path and value |
submit / finish | values | Validated, transformed result |
reset | values | Restored initialization snapshot |
open / close | none | Component method or overlay interaction |
current-change | current | Active step change |
request-error | error | Async initial value failure |
error | error | Initialization or validation failure |
Slots
Field paths are dot-joined, so ['profile', 'name'] becomes profile.name.
<SchemaForm ref="formRef" v-model="form" :columns="columns">
<template #label-project="{ column }">
{{ column.title }} *
</template>
<template #field-owner="{ value, update, dependencies }">
<OwnerPicker
:model-value="value"
:channel="dependencies[0]"
@update:model-value="update"
/>
</template>
<template #submitter="{ current }">
<button @click="formRef?.prev()">Previous</button>
<button @click="formRef?.submit()">Submit step {{ current + 1 }}</button>
</template>
</SchemaForm>| Slot | Props |
|---|---|
field-${path}, ${path}, or column key | { value, record, column, dependencies, update } |
label-${path} | { column, record } |
submitter | { values, current } |
trigger | { open, openForm, closeForm } |
title | { title, open, values, close } |
footer | { values, submitting, submit, reset, close } |
step-title | { title, index, current, step, steps, values } |
step-content | { current, step, steps, columns, values, content } |
step-actions | { current, step, steps, values, hasPrevious, hasNext, submitting, next, prev, submit, reset } |
update(nextValue) inside a field slot updates the form, v-model, URL, and related events. trigger, title, and footer apply to ModalForm / DrawerForm. step-title, step-content, and step-actions apply to StepForm / StepsForm. Call content() from step-content to render the current step's default fields.
Component instance
const formRef = ref<SchemaFormInstance<Brief>>()
formRef.value?.setFieldsValue({ owner: 'Ada' })
const raw = formRef.value?.getFieldsValue()
const output = await formRef.value?.submit()
await formRef.value?.next()| Method | Description |
|---|---|
validate() | Validate and return raw form values without transforms |
reset() | Restore the initialization snapshot |
getFieldsValue() | Read current raw form values |
setFieldsValue(values) | Merge incoming nested values |
submit() | Validate, transform, emit submit / finish, and return the transformed result |
open() / close() | Control Modal/Drawer |
next() / prev() | Control steps; next returns whether it advanced |