Skip to content

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
vue
<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

vue
<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:

text
initialValues < request result < URL values < modelValue

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

ts
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

TypeGenerated control/structure
text / textarea / passwordInput / Textarea / Password
digit / money / percentInputNumber
select / treeSelect / radioSelect / TreeSelect / RadioGroup
checkbox / switchCheckbox(Group) / Switch
slider / segmentedSlider / Segmented
date / dateTimeDatePicker
dateRange / dateTimeRangeDateRangePicker
time / timeRangeTimePicker / TimeRangePicker
group / formSetField groups
formListDynamic add/remove list
dividerDivider
dependencyDependency 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:

ts
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

layoutTypeScenario
FormStandard form
EmbedEmbedded form without an overlay
ModalFormModal with v-model:open
DrawerFormDrawer with v-model:open
QueryFilterGrid-based inline query form
LightFilterLightweight inline filter
StepFormSingle step view with v-model:current
StepsFormMulti-step form with Steps navigation

Set layout-type or import a named layout component:

vue
<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

ts
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) → renderFormItemcolumn.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.

ts
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

vue
<!-- 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

PropTypeDescription
columnsSchemaFormColumn<T>[]The single schema entry point
modelValuePartial<T>Standard v-model value
initialValuesPartial<T>Initialization and reset baseline
request(params?) => Promise<Partial<T>>Asynchronous initial values
paramsRecord<string, unknown>Initialization request params
layoutTypeSchemaFormLayoutTypeForm layout; defaults to Form
openbooleanModal/Drawer state; supports v-model:open
currentnumberStep index; supports v-model:current
title / widthText or sizeOverlay title and width
labelCol / wrapperColAntdv Next Form configLabel and control layout
gridbooleanResponsive Row/Col grid
readonlybooleanWhole-form readonly mode
urlSyncboolean | { key?, mode? }Query/hash synchronization
submitterfalse | { submitText?, resetText? }Default action area
styleCSSPropertiesRoot style

Columns also accept component, colProps, rowProps, tooltip, and extra.

Events

EventArgumentsDescription
update:model-valuevaluesDefault v-model update
update:openopenOverlay two-way binding
update:currentcurrentStep two-way binding
changevaluesAny field change
values-changechanged, valueschanged contains path and value
submit / finishvaluesValidated, transformed result
resetvaluesRestored initialization snapshot
open / closenoneComponent method or overlay interaction
current-changecurrentActive step change
request-errorerrorAsync initial value failure
errorerrorInitialization or validation failure

Slots

Field paths are dot-joined, so ['profile', 'name'] becomes profile.name.

vue
<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>
SlotProps
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

ts
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()
MethodDescription
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

Released under the MIT License