表单(Form)
- 按需重绘,避免了不必要的全量渲染, 性能更高
- 简单易用,结构极简,避免了不必要的层级嵌套
- 完善的无障碍支持
- 在 Form 外部可方便地获取 formState / fieldState
提供在外部对表单内部进行操作的方法:formApi / fieldApi - 支持将自定义组件封装成表单控件,你可以通过 Form 提供的扩展机制(withField)快捷接入自己团队的组件
- 支持 Form level / Field level 级别的赋值、校验(同步/异步)
表单控件(Field)
Semi 将所有自带的输入控件(文本输入框、下拉选择、复选框、单选框等)都使用 withField 封装了一次。接管了他们的数据流(modelValue & change)
使用的时候,需要使用以 Form 开头的控件(注意:只有这些控件才具有数据同步功能),它们是具名导出,不挂载在 Form 上
目前 Form 提供了如下表单控件
- 表单控件:
FormInput、FormInputNumber、FormTextArea、FormSelect、FormCheckbox、FormCheckboxGroup、FormRadio、FormRadioGroup、FormSwitch、FormDatePicker、FormTimePicker、FormSlider、FormTreeSelect、FormCascader、FormRating、FormAutoComplete、FormUpload、FormPinCode、FormTagInput - 其他部件:
ArrayField、FormInputGroup、FormLabel、FormErrorMessage、FormSection、FormSlot
<script setup lang="ts">
// 具有数据同步功能的表单控件,在 <Form> 内使用时,数据流会被 Form 自动接管
import { Form, FormInput, FormSelect } from '@fibrous/semi-ui-vue';
// 普通 Input,在 <Form> 内部使用时,Form 不会对其做任何处理
import { Input } from '@fibrous/semi-ui-vue';
</script>
<template>
<Form>
<FormInput field="name" />
<FormSelect field="role">
<FormSelect.Option value="rd">开发</FormSelect.Option>
</FormSelect>
</Form>
</template>
Form 提供的 Field 级别组件,它的 modelValue(或者 valueKey 指定的其他属性)、change 事件(或 onKeyChangeFnName 指定的其他事件)都会被 Form 接管,所以
代码演示
声明表单的多种写法
Semi Form 同时支持多种写法
基本写法
使用 FormInput、FormSelect 等以 Form 开头的表单控件,给表单控件添加field属性,将其放置于 Form 内部即可
还可以给每个表单控件设置label属性,不传入时默认与 field 相同
label可以直接传入字符串,亦可以以 object 方式声明,配置 required、optional、align、width 等属性应对更复杂的场景。需要放置节点时使用插槽:#fieldLabel 替换 label 内容,#labelExtra 放置紧随其后的内容(如下例的帮助图标),插槽优先于 label 属性
代码
<script setup lang="ts">
import { IconHelpCircle } from '@fibrous/semi-icons-vue';
import { Form, FormInput, FormSelect, Tooltip } from '@fibrous/semi-ui-vue';
</script>
<template>
<Form layout="horizontal" @valueChange="(values) => console.log(values)">
<FormInput field="UserName" label="用户名" :style="{ width: '80px' }" />
<FormInput field="Password" label="密码" :style="{ width: '176px' }">
<template #labelExtra>
<Tooltip content="详情">
<IconHelpCircle aria-label="帮助" :style="{ color: 'var(--semi-color-text-2)' }" />
</Tooltip>
</template>
</FormInput>
<FormSelect field="Role" :label="{ text: '角色', optional: true }" :style="{ width: '176px' }">
<FormSelect.Option value="admin">管理员</FormSelect.Option>
<FormSelect.Option value="user">普通用户</FormSelect.Option>
<FormSelect.Option value="guest">访客</FormSelect.Option>
</FormSelect>
</Form>
</template>支持的其他写法
当你需要在 Form 结构内部直接获取到 formState、formApi、values 等值时,你还可以使用以下的写法
通过默认作用域插槽
Form 的默认插槽是作用域插槽,参数为 { formState, formApi, values },用 #default="{ formState }" 解构需要的部分。React 中的 render props、child render function、props.component 三种写法,在 Vue 中都对应这一种,不再区分
代码
<script setup lang="ts">
import { Form, FormInput, FormSelect } from '@fibrous/semi-ui-vue';
</script>
<template>
<Form layout="horizontal" @valueChange="(values) => console.log(values)">
<template #default="{ formState }">
<FormSelect field="Role" label="角色" :style="{ width: '176px' }">
<FormSelect.Option value="admin">管理员</FormSelect.Option>
<FormSelect.Option value="user">普通用户</FormSelect.Option>
<FormSelect.Option value="guest">访客</FormSelect.Option>
</FormSelect>
<FormInput field="UserName" label="用户名" :style="{ width: '80px' }" />
<FormInput field="Password" label="密码" :style="{ width: '176px' }" />
<code :style="{ marginTop: '24px' }">{{ JSON.stringify(formState) }}</code>
</template>
</Form>
</template>已支持的表单控件
代码
<script setup lang="ts">
import type { StyleValue } from 'vue';
import { IconUpload } from '@fibrous/semi-icons-vue';
import {
Button,
Col,
Form,
FormCascader,
FormCheckbox,
FormCheckboxGroup,
FormDatePicker,
FormInput,
FormInputNumber,
FormRadio,
FormRadioGroup,
FormRating,
FormSection,
FormSelect,
FormSlider,
FormSwitch,
FormTagInput,
FormTextArea,
FormTimePicker,
FormTreeSelect,
FormUpload,
Row,
Space,
} from '@fibrous/semi-ui-vue';
const today = new Date(2024, 4, 6, 10, 30, 0);
const initValues = {
name: 'semi',
business: ['ulikeCam'],
role: 'ued',
switch: true,
files: [
{
uid: '1',
name: 'vigo.png',
status: 'success',
size: '130KB',
preview: true,
url: '/assets/placeholder/avatar-d.svg',
},
{
uid: '2',
name: 'resso.jpeg',
status: 'validateFail',
size: '222KB',
percent: 50,
preview: true,
fileInstance: new File([new ArrayBuffer(2048)], 'resso.jpeg', { type: 'image/jpeg' }),
url: '/assets/placeholder/avatar-b.svg',
},
{
uid: '3',
name: 'douyin.jpeg',
status: 'uploading',
size: '222KB',
percent: 50,
preview: true,
fileInstance: new File([new ArrayBuffer(2048)], 'dy.jpeg', { type: 'image/jpeg' }),
url: '/assets/placeholder/avatar-a.svg',
},
],
};
const treeData = [
{
label: '亚洲',
value: 'Asia',
key: '0',
children: [
{
label: '中国',
value: 'China',
key: '0-0',
children: [
{
label: '北京',
value: 'Beijing',
key: '0-0-0',
},
{
label: '上海',
value: 'Shanghai',
key: '0-0-1',
},
],
},
],
},
{
label: '北美洲',
value: 'North America',
key: '1',
},
];
const style = { width: '90%' };
const extraTextStyle: StyleValue = {
color: 'var(--semi-blue-5)',
fontSize: '14px',
userSelect: 'none',
cursor: 'pointer',
};
</script>
<template>
<Form :initValues="initValues" :style="{ padding: '10px', width: '100%' }" @valueChange="(values) => console.log(values)">
<FormSection>
<template #text>基本信息</template>
<Row>
<Col :span="12">
<FormInput field="name" initValue="mikeya" label="名称(Input)" :style="style" trigger="blur" />
</Col>
<Col :span="12">
<FormDatePicker field="date" :initValue="today" label="日期(DatePicker)" placeholder="请选择生效日期" :style="style" />
</Col>
</Row>
<Row>
<Col :span="12">
<FormSelect field="role" label="角色(Select)" placeholder="请选择你的角色" :style="style">
<FormSelect.Option value="operate">运营</FormSelect.Option>
<FormSelect.Option value="rd">开发</FormSelect.Option>
<FormSelect.Option value="pm">产品</FormSelect.Option>
<FormSelect.Option value="ued">设计</FormSelect.Option>
</FormSelect>
</Col>
<Col :span="12">
<FormSelect field="business" label="业务线(多选Select)" multiple placeholder="请选择业务线" :style="style">
<template #extraText>
<div :style="extraTextStyle">没有找到合适的业务线?</div>
</template>
<FormSelect.Option value="abc">Semi</FormSelect.Option>
<FormSelect.Option value="ulikeCam">轻颜相机</FormSelect.Option>
<FormSelect.Option value="toutiao">今日头条</FormSelect.Option>
</FormSelect>
</Col>
</Row>
<Row>
<Col :span="12">
<FormCascader field="area" label="地区(Cascader)" placeholder="请选择所在地区" :style="style" :treeData="treeData" />
</Col>
<Col :span="12">
<FormTreeSelect field="tree" filterTreeNode label="节点(TreeSelect)" placeholder="请选择服务节点" :style="style" :treeData="treeData" />
</Col>
</Row>
<Row>
<Col :span="12">
<FormTagInput field="product" :initValue="['abc', 'ulikeCam']" label="产品(TagInput)" placeholder="请输入产品" :style="style" />
</Col>
</Row>
<Row>
<Col :span="24">
<FormUpload action="//semi.design/api/upload" field="files" label="证明文件(Upload)">
<Button theme="light">
<template #icon>
<IconUpload />
</template>
点击上传
</Button>
</FormUpload>
</Col>
</Row>
</FormSection>
<FormSection>
<template #text>资源详情</template>
<Row>
<Col :span="12">
<FormTextArea field="description" label="申请理由(TextArea)" placeholder="请填写申请资源理由" :style="{ ...style, height: '120px' }" />
</Col>
<Col :span="12">
<FormCheckboxGroup direction="horizontal" field="type" :initValue="['user', 'admin']" label="申请类型(CheckboxGroup)" :rules="[{ required: true }]">
<FormCheckbox value="admin">admin</FormCheckbox>
<FormCheckbox value="user">user</FormCheckbox>
<FormCheckbox value="guest">guest</FormCheckbox>
<FormCheckbox value="root">root</FormCheckbox>
</FormCheckboxGroup>
<FormRadioGroup field="isMonopolize" label="是否独占资源(Radio)" :rules="[{ type: 'boolean' }, { required: true, message: '必须选择是否独占 ' }]">
<FormRadio :value="1">是</FormRadio>
<FormRadio :value="0">否</FormRadio>
</FormRadioGroup>
</Col>
</Row>
<Row>
<Col :span="12">
<FormTimePicker field="time" label="截止时刻(TimePicker)" :style="style" />
</Col>
<Col :span="12">
<FormInputNumber field="number" :initValue="20" label="申请数量(InputNumber)" :style="style" />
</Col>
</Row>
<Row>
<Col :span="12">
<FormSlider field="range" :initValue="10" label="资源使用报警阈值(%)(Slider)" :style="style" />
</Col>
<Col :span="12">
<FormSwitch field="switch" label="开关(Switch)" />
</Col>
</Row>
<Row>
<Col :span="12">
<FormRating field="rating" :initValue="2" label="满意度(Rating)" :style="style" />
</Col>
</Row>
</FormSection>
<FormCheckbox field="agree" noLabel value="false">我已阅读并清楚相关规定(Checkbox)</FormCheckbox>
<Space>
<Button class="btn-margin-right" htmlType="submit" type="primary">提交(submit)</Button>
<Button htmlType="reset">重置(reset)</Button>
</Space>
</Form>
</template>表单控件值的绑定
每个表单控件都需要以field属性绑定一个字段名称,用于将表单项的值正确映射到FormState values / errors / touched 中
字段可以是简单的字符串,可以是包含.或者[]的字符串, 支持多级嵌套
下面是字段名称以及他们在 FormState 中的映射路径的示例
| Field | Resolution |
|---|---|
| username | formState.values.username |
| user[0] | formState.values.user[0] |
| siblings.1 | formState.values.siblings[1] |
| siblings['2'] | formState.values.siblings[2] |
| parents[0].name | formState.values.parents[0].name |
| parents[1]['name'] | formState.values.parents[1].name |
代码
<script setup lang="ts">
import { Col, Form, FormInput, FormLabel, Row, TextArea, Toast } from '@fibrous/semi-ui-vue';
const onSubmit = (values: Record<string, unknown>) => Toast.info({ content: JSON.stringify(values) });
</script>
<template>
<Form @submit="onSubmit">
<template #default="{ formState }">
<Row>
<Col :span="12">
<FormInput aria-label="username" field="username" placeholder="请尝试输入值" />
<FormInput aria-label="user[0]" field="user[0]" placeholder="请尝试输入值" />
<FormInput aria-label="siblings.1" field="siblings.1" placeholder="请尝试输入值" />
<FormInput aria-label="siblings['2']" field="siblings['2']" placeholder="请尝试输入值" />
<FormInput aria-label="parents[0].name" field="parents[0].name" placeholder="请尝试输入值" />
<FormInput aria-label="parents[1]['name']" field="parents[1]['name']" placeholder="请尝试输入值" />
</Col>
<Col :offset="1" :span="10" :style="{ marginTop: '12px' }">
<FormLabel text="FormState实时映射值:" />
<TextArea aria-label="FormState实时映射值" :modelValue="JSON.stringify(formState.values)" />
</Col>
</Row>
</template>
</Form>
</template>表单布局
- 垂直布局:表单控件之间上下垂直排列(默认)
Semi Design Next 更推荐表单采用垂直布局
代码
<script setup lang="ts">
import { Button, Form, FormCheckbox, FormInput, Toast } from '@fibrous/semi-ui-vue';
const handleSubmit = (values: Record<string, unknown>) => {
console.log(values);
Toast.info('表单已提交');
};
</script>
<template>
<Form :style="{ width: '400px' }" @submit="handleSubmit">
<template #default="{ values }">
<FormInput field="phone" label="PhoneNumber" placeholder="Enter your phone number" :style="{ width: '100%' }" />
<FormInput field="password" label="Password" placeholder="Enter your password" :style="{ width: '100%' }" />
<FormCheckbox field="agree" noLabel>I have read and agree to the terms of service</FormCheckbox>
<div :style="{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }">
<p>
<span>Or</span>
<Button :style="{ color: 'var(--semi-color-primary)', marginLeft: '10px', cursor: 'pointer' }" theme="borderless"> Sign up </Button>
</p>
<Button :disabled="!values.agree" htmlType="submit" type="tertiary">Log in</Button>
</div>
</template>
</Form>
</template>- 水平布局:表单控件之间水平排列你可以通过设置
layout='horizontal'来使用水平布局
代码
<script setup lang="ts">
import { Form, FormInput } from '@fibrous/semi-ui-vue';
</script>
<template>
<Form layout="horizontal">
<FormInput field="phone" label="PhoneNumber" placeholder="Enter your phone number" />
<FormInput field="password" label="Password" placeholder="Enter your password" />
</Form>
</template>- labelPosition、labelAlign
你可以通过设置 labelPosition、labelAlign 控制 label 在 Field 中出现的位置,文本对齐的方向
代码
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Form, FormCheckbox, FormCheckboxGroup, FormInput, FormInputNumber, FormLabel, FormRadio, FormRadioGroup, FormSelect, FormSwitch, Select, type FormProps, type RuleItem } from '@fibrous/semi-ui-vue';
const labelPosition = ref<FormProps['labelPosition']>('left');
const labelAlign = ref<FormProps['labelAlign']>('left');
const labelWidth = computed(() => (labelPosition.value === 'left' ? '180px' : 'auto'));
const rules: RuleItem[] = [
{ required: true, message: 'required error' },
{ type: 'string', message: 'type error' },
{ validator: (_rule, value) => value === 'semi', message: 'should be semi' },
];
</script>
<template>
<div :style="{ borderBottom: '1px solid var(--semi-color-border)', paddingBottom: '12px' }">
<FormLabel :style="{ marginLeft: '10px' }">切换Label位置:</FormLabel>
<Select v-model="labelPosition" aria-label="labelPosition" :style="{ width: '200px' }">
<template #prefix>labelPosition</template>
<Select.Option value="top">top</Select.Option>
<Select.Option value="left">left</Select.Option>
</Select>
<FormLabel :style="{ marginLeft: '10px' }">切换Label文本对齐方向:</FormLabel>
<Select v-model="labelAlign" aria-label="labelAlign" :style="{ width: '200px' }">
<template #prefix>labelAlign</template>
<Select.Option value="left">left</Select.Option>
<Select.Option value="right">right</Select.Option>
</Select>
</div>
<Form :key="`${labelPosition}${labelAlign}`" :labelAlign="labelAlign" :labelPosition="labelPosition" :labelWidth="labelWidth" :style="{ padding: '10px', width: '600px' }">
<FormInput field="input" label="手机号码" :rules="rules" :style="{ width: '200px' }" trigger="blur" />
<FormSwitch field="agree" label="是否同意" />
<FormInputNumber field="price" label="价格" :style="{ width: '200px' }" />
<FormSelect field="name" label="姓名" :style="{ width: '200px' }">
<FormSelect.Option value="mike">mike</FormSelect.Option>
<FormSelect.Option value="jane">jane</FormSelect.Option>
<FormSelect.Option value="kate">kate</FormSelect.Option>
</FormSelect>
<FormCheckboxGroup direction="horizontal" field="role" label="角色">
<FormCheckbox value="admin">admin</FormCheckbox>
<FormCheckbox value="user">user</FormCheckbox>
<FormCheckbox value="guest">guest</FormCheckbox>
<FormCheckbox value="root">root</FormCheckbox>
</FormCheckboxGroup>
<FormRadioGroup field="性别">
<FormRadio value="1">man</FormRadio>
<FormRadio value="2">woman</FormRadio>
</FormRadioGroup>
</Form>
</template>- 更复杂的布局你还可以结合 Grid 提供的 Row、Col,来对表单进行你想要的排列
代码
<script setup lang="ts">
import { Col, Form, FormDatePicker, FormInput, FormSelect, Row, type RuleItem } from '@fibrous/semi-ui-vue';
const nickNameRules: RuleItem[] = [
{ required: true, message: 'required error' },
{ type: 'string', message: 'type error' },
{ validator: (_rule, value) => value === 'semi', message: 'should be semi' },
];
</script>
<template>
<Form labelPosition="top" :style="{ padding: '10px' }">
<Row>
<Col :span="8">
<FormInput field="nickName1" label="用户名" :rules="nickNameRules" :style="{ width: '250px' }" trigger="blur" />
</Col>
<Col :span="8">
<FormDatePicker field="date1" label="有效日期" :style="{ width: '250px' }" />
</Col>
<Col :span="8">
<FormSelect field="business1" label="业务线" :style="{ width: '250px' }">
<FormSelect.Option value="abc">Semi</FormSelect.Option>
<FormSelect.Option value="ulikeCam">轻颜相机</FormSelect.Option>
<FormSelect.Option value="toutiao">今日头条</FormSelect.Option>
</FormSelect>
</Col>
</Row>
<Row>
<Col :span="6">
<FormInput field="nickName2" label="用户名" :rules="nickNameRules" :style="{ width: '200px' }" trigger="blur" />
</Col>
<Col :span="6">
<FormDatePicker field="date2" label="有效日期" :style="{ width: '200px' }" />
</Col>
<Col :span="6">
<FormSelect field="business2" label="业务线" :style="{ width: '200px' }">
<FormSelect.Option value="abc">Semi</FormSelect.Option>
<FormSelect.Option value="ulikeCam">轻颜相机</FormSelect.Option>
<FormSelect.Option value="toutiao">今日头条</FormSelect.Option>
</FormSelect>
</Col>
<Col :span="6">
<FormSelect field="role" label="角色" :style="{ width: '200px' }">
<FormSelect.Option value="operate">运营</FormSelect.Option>
<FormSelect.Option value="rd">开发</FormSelect.Option>
<FormSelect.Option value="pm">产品</FormSelect.Option>
<FormSelect.Option value="ued">设计</FormSelect.Option>
</FormSelect>
</Col>
</Row>
</Form>
</template>表单分组
字段数量较多的表单应考虑对字段进行分组,可以使用FormSection对 Fields 进行分组,分组标题放在 #text 插槽(仅影响布局,不会影响数据结构)
代码
<script setup lang="ts">
import { Button, Form, FormCheckbox, FormDatePicker, FormInput, FormInputNumber, FormRadio, FormRadioGroup, FormSection, FormSelect, FormSwitch, Space } from '@fibrous/semi-ui-vue';
const today = new Date(2024, 4, 6, 10, 30, 0);
// initValue is not a declared Boolean prop, so a bare attribute would pass an empty string
const autoSubmit = true;
</script>
<template>
<Form style="width: 560px">
<FormSection>
<template #text>基本信息</template>
<FormInput field="name" initValue="TCS任务平台使用" label="考试名称" style="width: 560px" />
</FormSection>
<FormSection>
<template #text>合格标准</template>
<div style="display: flex">
<FormInputNumber field="pass" :initValue="60" :label="{ text: '及格正确率', required: true }" style="width: 80px" />
<FormInputNumber field="number" :initValue="10" :label="{ text: '合格人数', required: true }" style="width: 80px" />
</div>
</FormSection>
<FormSection>
<template #text>考试时间</template>
<FormDatePicker field="date" :initValue="today" :label="{ text: '开始时间', required: true }" style="width: 272px" type="dateTime" />
<div style="display: flex">
<FormInput field="time" initValue="60" label="考试时长" style="width: 176px">
<template #addonAfter>分钟</template>
</FormInput>
<FormCheckbox field="auto" :initValue="autoSubmit" noLabel style="padding-top: 30px; margin-inline-start: 12px"> 到时间自动交卷 </FormCheckbox>
</div>
<FormRadioGroup direction="vertical" field="type" initValue="always" label="有效时间">
<FormRadio value="always">永久有效</FormRadio>
<FormRadio value="user">自定义有效期</FormRadio>
</FormRadioGroup>
<FormRadioGroup direction="vertical" field="answerTime" initValue="always" label="答案放出时间" :rules="[{ required: true }]">
<FormRadio value="always">自动放出</FormRadio>
<FormRadio value="user">
<div style="display: inline-block">
自定义放出时间
<FormDatePicker field="customTime" noLabel style="width: 464px; display: inline-block" type="dateTimeRange" />
</div>
</FormRadio>
</FormRadioGroup>
</FormSection>
<FormSection>
<template #text>考试人员</template>
<div style="display: flex">
<FormSwitch field="open" :label="{ text: '对外开放', required: true }">
<template #checkedText>开</template>
<template #uncheckedText>关</template>
</FormSwitch>
</div>
<FormSelect field="users" :initValue="['1', '2', '3', '4']" :label="{ text: '考生', required: true }" multiple style="width: 560px">
<FormSelect.Option value="1">曲晨一</FormSelect.Option>
<FormSelect.Option value="2">夏可曼</FormSelect.Option>
<FormSelect.Option value="3">曲晨三</FormSelect.Option>
<FormSelect.Option value="4">蔡妍</FormSelect.Option>
</FormSelect>
</FormSection>
<Space>
<Button style="width: 120px; margin-top: 12px; margin-inline-end: 4px" theme="solid" type="primary"> 创建考试 </Button>
<Button style="margin-top: 12px">预览</Button>
</Space>
</Form>
</template>wrapperCol / labelCol
需要为 Form 内的所有 Field 设置统一的布局时,可以在 Form 上设置 wrapperCol 、labelCol 快速生成布局,无需手动使用 Row、Col 手动布局
wrapperCol、labelCol属性配置参考Col 组件
代码
<script setup lang="ts">
import { Form, FormInput, FormSelect } from '@fibrous/semi-ui-vue';
</script>
<template>
<Form :labelCol="{ span: 2 }" labelAlign="right" labelPosition="left" :wrapperCol="{ span: 20 }">
<FormInput field="name" label="姓名" placeholder="请输入姓名" style="width: 250px" trigger="blur" />
<FormSelect field="role" label="角色" placeholder="请选择角色" style="width: 250px">
<FormSelect.Option value="operate">运营</FormSelect.Option>
<FormSelect.Option value="rd">开发</FormSelect.Option>
<FormSelect.Option value="pm">产品</FormSelect.Option>
<FormSelect.Option value="ued">设计</FormSelect.Option>
</FormSelect>
</Form>
</template>隐藏Label
Form 会自动为 Field 控件插入 Label。如果你不需要自动插入 Label 模块, 可以通过在 Field 中设置noLabel=true将自动插入 Label 功能关闭(此时 Field 仍然具备自动展示 ErrorMessage 的能力,因此 DOM 结构与原始控件依然会有区别)
如果你希望与原始控件保持 DOM 结构一致,可以使用 pure=true,此时除了数据流被接管外,DOM结构不会有任何变化(你需要自行负责 ErrorMessage的渲染,同时它也无法被 formProps.wrapperCol 属性影响)
代码
<script setup lang="ts">
import { Form, FormInput } from '@fibrous/semi-ui-vue';
</script>
<template>
<Form :style="{ width: '400px' }" @submit="(values) => console.log(values)">
<FormInput field="name" label="姓名" noLabel placeholder="请输入姓名" :style="{ width: '250px' }" trigger="blur" />
<FormInput field="purename" placeholder="DOM结构与普通 Input 组件完全一致" pure />
</Form>
</template>内嵌 Label
通过将 labelPosition 设为inset,可以将 Label 内嵌在表单控件中。目前支持这项功能的组件有Input、InputNumber、DatePicker、TimePicker、Select、TreeSelect、Cascader、TagInput、AutoComplete。其余字段在 labelPosition 为inset时按top布局,并在开发模式下给出警告。
用 withField 封装的自定义组件,声明了 insetLabelId 属性才会按内嵌标签布局:此时组件通过 #insetLabel 插槽收到标签内容,并把 insetLabelId 写到承载标签的元素上;没有声明的组件同样按 top 布局。
代码
<script setup lang="ts">
import { Form, FormDatePicker, FormInput, FormSelect } from '@fibrous/semi-ui-vue';
const today = new Date(2024, 4, 6, 10, 30, 0);
</script>
<template>
<Form labelPosition="inset" layout="horizontal">
<FormInput field="name" initValue="semi" label="姓名" placeholder="请输入姓名" :style="{ width: '250px' }" trigger="blur" />
<FormSelect field="role" initValue="rd" label="角色" :style="{ width: '250px' }">
<FormSelect.Option value="operate">运营</FormSelect.Option>
<FormSelect.Option value="rd">开发</FormSelect.Option>
<FormSelect.Option value="pm">产品</FormSelect.Option>
<FormSelect.Option value="ued">设计</FormSelect.Option>
</FormSelect>
<FormDatePicker field="date" :initValue="today" label="开始日期" :style="{ width: '250px' }" />
</Form>
</template>导出 Label、ErrorMessage 使用
如果你需要 FormLabel、FormErrorMessage 模块自行组合使用,可以直接导入
- FormLabel 的 API 详见 FormLabel
- FormErrorMessage 的 API 详见 FormErrorMessage
例如:当自带的 Label、ErrorMessage 布局不满足业务需求,需要自行组合位置,但又希望能直接使用 Label、ErrorMessage 的默认样式时
import { FormErrorMessage, FormLabel } from '@fibrous/semi-ui-vue';
使用 FormSlot 放置自定义组件
当你的自定义组件,需要与 Field 组件保持同样的布局样式时,你可以通过 FormSlot 放置你的自定义组件
在 Form 组件上设置的 labelWidth、labelAlign、wrapperCol、labelCol 会自动作用在 FormSlot 上
FormSlot 属性配置详见FormSlot
代码
<script setup lang="ts">
import { Form, FormInput, FormSlot } from '@fibrous/semi-ui-vue';
const log = (value: unknown) => console.log(value);
</script>
<template>
<Form :labelWidth="100" labelPosition="left" style="width: 600px" @change="log" @submit="log">
<FormInput field="特效名称" style="width: 250px" />
<FormSlot error="我是SlotA的ErrorMessage" :label="{ text: 'SlotA' }">
<div style="display: flex; align-items: center; height: 32px; margin-top: 8px">我是Semi Form SlotA, 我是自定义的节点</div>
</FormSlot>
<FormSlot :label="{ text: 'SlotB', width: 160, align: 'right' }">
<div style="display: flex; align-items: center; height: 100%">我是Semi Form SlotB, 我的Label Align、Width与众不同</div>
</FormSlot>
</Form>
</template>使用 helpText、extraText 放置提示信息
可以通过#helpText插槽放置自定义提示信息,与校验信息(error)公用同一区块展示,两者均有值时,优先展示校验信息。
可以通过#extraText插槽放置额外的提示信息,当需要错误信息和提示文案同时出现时,可以使用这个插槽,常显,位于 helpText/error 后
当传入 validateStatus 时,优先展示 validateStatus 值对应的 UI 样式。不传入时,以 field 内部校验状态为准。
代码
<script setup lang="ts">
import { ref, useTemplateRef } from 'vue';
import { Form, FormInput, type FormHandle, type InputValidateStatus as ValidateStatus } from '@fibrous/semi-ui-vue';
const helpText = ref('');
const validateStatus = ref<ValidateStatus>('default');
const form = useTemplateRef<FormHandle>('form');
const validator = (val?: string) => {
if (!val) {
validateStatus.value = 'error';
return '密码不能为空';
} else if (val.length <= 3) {
validateStatus.value = 'warning';
helpText.value = '密码强度:弱'; // 展示 helpText
return ''; // 校验通过
}
helpText.value = '';
validateStatus.value = 'success';
return '';
};
const onSubmit = () => console.log('submit success');
const onSubmitFail = (errors: unknown) => console.log(errors);
const random = () => {
const pw = (Math.random() * 100000).toString().slice(0, 5);
form.value?.formApi.setValue('Password', pw);
form.value?.formApi.setError('Password', '');
helpText.value = '';
validateStatus.value = 'success';
};
</script>
<template>
<Form ref="form" showValidateIcon @submit="onSubmit" @submitFail="onSubmitFail">
<FormInput field="Password" :validateStatus="validateStatus" :validator="validator">
<template v-if="helpText" #helpText>
<span :style="{ color: 'var(--semi-color-warning)' }">{{ helpText }}</span>
</template>
<template #extraText>
<button
:style="{
padding: 0,
border: 'none',
background: 'none',
font: 'inherit',
color: 'var(--semi-color-link)',
fontSize: '14px',
userSelect: 'none',
cursor: 'pointer',
}"
type="button"
@click="random"
>
没有想到合适的密码?点击随机生成一个
</button>
</template>
</FormInput>
</Form>
</template>通过配置 extraTextPosition,你可以控制 extraText 的显示位置。可选值 bottom、middle
例如如当你希望将 extraText 提示信息显示在 Label 与 Field 控件中间时
该属性可在 Form 上统一配置,亦可在每个 Field 上单独配置,同时传入时,以 Field 的配置为准。
代码
<script setup lang="ts">
import { Form, FormCheckboxGroup, FormInputNumber } from '@fibrous/semi-ui-vue';
const options = [
{ label: '飞书通知', value: 'lark' },
{ label: '邮件通知', value: 'email' },
{ label: '顶部横幅通知', value: 'notification' },
];
const notifyText = '未勾选时,默认为红点提醒,消息默认进入收件人消息列表。对于重要通知,可同时勾选相应的通知方式。';
const forceText = '对于对话框通知,可指定该消息必须在指定时长后才可置为已读。';
</script>
<template>
<Form extraTextPosition="middle">
<FormCheckboxGroup direction="horizontal" field="notify" label="通知方式" :options="options">
<template #extraText>{{ notifyText }}</template>
</FormCheckboxGroup>
<FormInputNumber extraTextPosition="bottom" field="force" label="强制读取(可选)" placeholder="秒">
<template #extraText>{{ forceText }}</template>
</FormInputNumber>
</Form>
</template>使用 InputGroup 组合多个 Field
当你需要将一些表单控件组合起来使用时,你可以用FormInputGroup将其包裹起来,组内只有设置了 field 的直接子项会被渲染
当你给FormSelect、FormInput等表单控件加上 field 属性时,Form会默认给每个 Field 控件自动插入Label
而在FormInputGroup中一般仅需要一个属于整个 Group 的 Label,你可以在 FormInputGroup 中设置 label 属性,插入一个属于 Group 的Label
label可配置属性详见FormLabel
代码
<script setup lang="ts">
import { Button, Form, FormInput, FormInputGroup, FormSelect } from '@fibrous/semi-ui-vue';
const onSubmit = (values: unknown) => console.log(values);
</script>
<template>
<Form labelPosition="top" :style="{ width: '400px' }" @submit="onSubmit">
<FormInputGroup :label="{ text: '手机号码', required: true }" labelPosition="top">
<FormSelect field="phonePrefix" initValue="+86" :rules="[{ required: true }]" showClear :style="{ width: '150px' }">
<FormSelect.Option value="+1">美国+1</FormSelect.Option>
<FormSelect.Option value="+852">香港+852</FormSelect.Option>
<FormSelect.Option value="+86">中国+86</FormSelect.Option>
<FormSelect.Option value="+81">日本+81</FormSelect.Option>
</FormSelect>
<FormInput field="phoneNumber" initValue="18912345678" :rules="[{ required: true }]" showClear :style="{ width: '250px' }" />
</FormInputGroup>
<FormInput field="姓名" initValue="Semi" trigger="blur" />
<Button htmlType="submit">提交</Button>
</Form>
</template>Modal 弹出层中的表单
你可以将 Form 放置于 Modal 中,以弹窗形式承载
在提交时,通过 formApi.validate()对 Field 进行集中校验
代码
<script setup lang="ts">
import { ref } from 'vue';
import { Button, Col, Form, FormInput, FormSelect, Modal, Row, type FormApi } from '@fibrous/semi-ui-vue';
const message = '该项为必填项';
const rules = [{ required: true, message }];
const regions = [
{ value: 'China', label: '中国' },
{ value: 'US', label: '美国' },
{ value: 'Europe', label: '欧洲' },
{ value: 'Japan', label: '日本' },
];
const visible = ref(false);
let formApi: FormApi | undefined;
const handleOk = () => {
formApi
?.validate()
.then((values) => {
console.log(values);
})
.catch((errors) => {
console.log(errors);
});
};
</script>
<template>
<Button @click="visible = true">打开弹窗</Button>
<Modal v-model:visible="visible" :style="{ width: '600px' }" title="新建" @ok="handleOk">
<Form @getFormApi="(api) => (formApi = api)">
<Row>
<Col :span="5">
<FormSelect field="region" label="国家/地区" placeholder="请选择" :rules="rules" :style="{ width: '100%' }">
<FormSelect.Option v-for="item in regions" :key="item.value" :value="item.value">
{{ item.label }}
</FormSelect.Option>
</FormSelect>
</Col>
<Col :offset="2" :span="15">
<FormInput field="owner" label="业务执行人" :rules="rules" trigger="blur" />
</Col>
</Row>
<Row>
<Col :span="5">
<FormSelect field="area" label="投放区域" placeholder="请选择" :rules="rules" :style="{ width: '100%' }">
<FormSelect.Option v-for="item in regions" :key="item.value" :value="item.value">
{{ item.label }}
</FormSelect.Option>
</FormSelect>
</Col>
<Col :offset="2" :span="15">
<FormInput field="department" label="业务执行部门" :rules="rules" trigger="blur" />
</Col>
</Row>
</Form>
</Modal>
</template>配置初始值与校验规则
- 你可以通过
rules为每个 Field 表单控件配置校验规则
Form 内部的校验库基于 async-validator,更多配置规则可查阅其官方文档 - 你可以通过 form 的
initValues为整个表单统一设置初始值,也可以在每个 field 中通过initValue设置初始值(后者优先级更高) - 可以通过 trigger 为每个 Field 配置不同的校验触发时机,默认为 change(即值变化时,自动进行校验)。还支持 change、blur、mount、custom 或以上的组合。也支持通过 FormProps 统一配置,若都配置时,以 FieldProps 为准
- 可以通过 stopValidateWithError 开关,决定使用 rules 校验时,当碰到第一个检验不通过的 rules 后,是否继续触发后续 rules 的校验。也支持通过 FormProps 统一配置,若都配置时,以 FieldProps 为准
代码
<script setup lang="ts">
import { Button, Form, FormInput, type RuleItem } from '@fibrous/semi-ui-vue';
const initValues = {
name: 'semi',
shortcut: 'se',
};
const style = { width: '100%' };
const rules: RuleItem[] = [
{ required: true, message: 'required error' },
{ type: 'string', message: 'type error' },
{ validator: (_rule, value) => value === 'semi', message: 'should be semi' },
{
validator: (_rule, value) => Boolean(value && value.startsWith('se')),
message: 'should startsWith se',
},
];
</script>
<template>
<Form :initValues="initValues">
<FormInput field="name" :rules="rules" :style="style" trigger="blur" />
<FormInput field="shortcut" :rules="rules" stopValidateWithError :style="style" />
<Button htmlType="submit">提交</Button>
</Form>
</template>自定义校验(Form 级别)
你可以通过 validator 给Form整体设置自定义校验函数。submit 或调用 formApi.validate() 时会进行调用
同步校验
校验通过时,你应该返回一个空字符串;
校验失败时,你应该返回错误信息(Object,key 为 fieldName,value 为对应的错误信息)
代码
<script setup lang="ts">
import { Button, Form, FormInput } from '@fibrous/semi-ui-vue';
interface FormValues {
name?: string;
sex?: string;
}
const syncValidate = (values: FormValues) => {
const errors: Record<string, unknown> = {};
if (values.name !== 'mike') {
errors.name = 'you must name mike';
}
if (values.sex !== 'female') {
errors.sex = 'must be woman';
}
errors.familyName = [{ before: 'before error balabala ', after: 'after error balabala' }, 'familyName[1] error balabala'];
return errors;
};
</script>
<template>
<Form layout="horizontal" :validator="syncValidate">
<FormInput field="name" trigger="blur" />
<FormInput field="familyName[0].before" trigger="blur" />
<FormInput field="familyName[0].after" trigger="blur" />
<FormInput field="familyName[1]" trigger="blur" />
<div :style="{ display: 'flex', alignItems: 'flex-end' }">
<Button class="btn-margin-right" htmlType="submit" type="primary">Submit</Button>
<Button htmlType="reset">reset</Button>
</div>
</Form>
</template>异步校验
异步校验时,你应当返回一个 promise,在 promise.then()中 你需要 return 对应的错误信息
代码
<script setup lang="ts">
import { Button, Form, FormInput } from '@fibrous/semi-ui-vue';
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
interface FormValues {
name?: string;
sex?: string;
}
const asyncValidate = (values: FormValues) =>
sleep(2000).then(() => {
const errors: FormValues = {};
if (values.name !== 'mike') {
errors.name = 'you must name mike';
}
if (values.sex !== 'female') {
errors.sex = 'sex not valid';
}
return errors;
});
</script>
<template>
<Form layout="horizontal" :validator="asyncValidate">
<FormInput field="name" trigger="blur" />
<FormInput field="familyName[0].before" trigger="blur" />
<FormInput field="familyName[1]" trigger="blur" />
<FormInput field="sex" trigger="blur" />
<div :style="{ display: 'flex', alignItems: 'flex-end' }">
<Button class="btn-margin-right" htmlType="submit" type="primary">Submit</Button>
<Button htmlType="reset">reset</Button>
</div>
</Form>
</template>自定义校验(Field 级别)
你可以通过 validator 指定单个表单控件的自定义校验函数,支持同步、异步校验(通过返回 promise)
代码
<script setup lang="ts">
import { Button, Form, FormInput } from '@fibrous/semi-ui-vue';
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const validateName = (val?: string) => {
if (!val) {
return "【sync】can't be empty";
} else if (val.length <= 5) {
return '【sync】must more than 5';
}
return '';
};
const asyncValidate = (val?: string) =>
sleep(2000).then(() => {
if (!val) {
return "【async】can't be empty";
} else if (val.length <= 5) {
return '【async】must more than 5';
} else {
return '';
}
});
</script>
<template>
<Form>
<FormInput field="name" label="【name】asyncValidate after 2s" trigger="blur" :validator="asyncValidate" />
<FormInput field="familyName" label="【familyName】syncValidate" trigger="blur" :validator="validateName" />
<Button htmlType="reset">reset</Button>
</Form>
</template>手动触发指定 Field 校验
当你希望手动触发某些特定 Field 的校验操作时,可以通过 formApi.validate 完成。不传入参数时,默认对全部 Field 进行校验,传入参数时,以参数指定为准
静默校验
当你需要获取校验结果但不想触发 UI 更新时(如不显示错误提示、不设置 touched 状态),可以通过 formApi.validate({ silent: true }) 实现。这在根据校验结果决定是否发起后端请求等场景中非常有用。
代码
<script setup lang="ts">
import { shallowRef } from 'vue';
import { Button, Form, FormInput, Toast, type FormApi } from '@fibrous/semi-ui-vue';
const formApi = shallowRef<FormApi>();
const usernameRules = [
{ required: true, message: '用户名不能为空' },
{ min: 3, message: '用户名至少 3 个字符' },
];
const emailRules = [
{ required: true, message: '邮箱不能为空' },
{ type: 'email' as const, message: '邮箱格式不正确' },
];
const handleSilentValidate = () => {
formApi.value
?.validate({ silent: true })
.then((values) => {
Toast.success('表单校验通过,准备发起请求');
console.log('发起后端请求:', values);
})
.catch((errors) => {
Toast.error('表单校验未通过,但不显示错误提示');
console.log('校验错误:', errors);
});
};
const handleNormalValidate = () => {
formApi.value
?.validate()
.then(() => {
Toast.success('表单校验通过');
})
.catch(() => {
Toast.error('表单校验未通过,显示错误提示');
});
};
</script>
<template>
<Form layout="horizontal" @getFormApi="formApi = $event">
<FormInput field="username" label="用户名" :rules="usernameRules" />
<FormInput field="email" label="邮箱" :rules="emailRules" />
<div style="display: flex; gap: 12px; margin-top: 12px">
<Button type="primary" @click="handleSilentValidate">静默校验(不显示错误)</Button>
<Button @click="handleNormalValidate">普通校验(显示错误)</Button>
</div>
</Form>
</template>代码
<script setup lang="ts">
import { shallowRef } from 'vue';
import { Button, Form, FormCheckboxGroup, FormInput, Space, Toast, type FormApi } from '@fibrous/semi-ui-vue';
const allFields = ['a', 'b', 'c', 'd', 'b.name'];
const options = allFields.map((item) => ({ label: item, value: item }));
const validate = (val?: string) => {
if (!val) {
return "can't be empty";
} else if (val.length <= 5) {
return '我是传入的节点';
}
return undefined;
};
const formApi = shallowRef<FormApi>();
const validatePartial = (type?: 'all') => {
const scope = type === 'all' ? allFields : (formApi.value?.getValue('validateScope') ?? []);
formApi.value
?.validate(scope)
.then((values) => {
console.log(values);
Toast.success('pass');
})
.catch((error) => {
Toast.error('error');
console.log(error);
});
};
const resetPartial = () => {
formApi.value?.reset(formApi.value.getValue('resetScope'));
};
</script>
<template>
<Form autoScrollToError layout="horizontal" @getFormApi="(api) => (formApi = api)">
<template #default="{ values }">
<div>
<FormInput field="a[1]" trigger="blur" :validator="validate" />
<FormInput field="a[0]" trigger="blur" :validator="validate" />
<FormInput field="b.name[0]" trigger="blur" :validator="validate" />
<FormInput field="b.name[1]" trigger="blur" :validator="validate" />
<FormInput field="b.type" trigger="blur" :validator="validate" />
<FormInput field="c" trigger="blur" :validator="validate" />
<FormInput field="d" trigger="blur" :validator="validate" />
</div>
<div>
<FormCheckboxGroup direction="horizontal" field="validateScope" :initValue="['a', 'b']" label="当前希望校验的Field" :options="options" />
<FormCheckboxGroup direction="horizontal" field="resetScope" label="当前需要Reset的Field" :options="options" />
<Space>
<Button htmlType="reset">reset</Button>
<Button @click="validatePartial('all')">all validate</Button>
<Button @click="validatePartial()">partial validate {{ JSON.stringify(values.validateScope) }}</Button>
<Button @click="resetPartial">partial reset</Button>
</Space>
</div>
</template>
</Form>
</template>表单联动
你可以通过监听 Field 的 @change 事件,然后使用 formApi 进行相关修改,来使 Field 之间达到联动
代码
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { Button, Form, FormInput, FormSelect, Row } from '@fibrous/semi-ui-vue';
const form = useTemplateRef('form');
const handleSelectChange = (value: unknown) => {
const text = value === 'male' ? 'Hi male' : 'Hi female!';
form.value?.formApi.setValue('Note', text);
};
</script>
<template>
<Form ref="form" :style="{ width: '250px' }" @valueChange="(values) => console.log(values)">
<span>Note will change after Sex select</span>
<FormInput field="Note" :style="{ width: '250px' }" />
<FormSelect field="Sex" :style="{ width: '250px' }" @change="handleSelectChange">
<FormSelect.Option value="female">female</FormSelect.Option>
<FormSelect.Option value="male">male</FormSelect.Option>
</FormSelect>
<Row>
<Button class="btn-margin-right" htmlType="submit" type="primary">Submit</Button>
<Button htmlType="reset">reset</Button>
</Row>
</Form>
</template>动态表单
动态删减表单项
代码
<script setup lang="ts">
import { Button, Form, FormInput, FormRadio, FormRadioGroup } from '@fibrous/semi-ui-vue';
</script>
<template>
<Form :style="{ width: '450px' }">
<template #default="{ formState }">
<FormInput field="name" label="用户名称:" />
<FormRadioGroup field="isAnchor" label="是否已注册主播">
<FormRadio value="yes">yes</FormRadio>
<FormRadio value="no">no</FormRadio>
</FormRadioGroup>
<FormInput v-if="formState.values?.isAnchor === 'yes'" field="liveRoom" label="直播间名称" />
<Button htmlType="submit">提交</Button>
</template>
</Form>
</template>保留空值(allowEmpty)
默认情况下,值变为空的 field 会从 values 中移除。在 Field 上设置 allowEmpty 可以保留它的 key(值为 undefined),在 Form 上设置则对所有 field 生效。清空下面两个选择框,对比 values 中的 key。
代码
<script setup lang="ts">
import { Form, FormSelect } from '@fibrous/semi-ui-vue';
const roleOptions = [
{ label: '管理员', value: 'admin' },
{ label: '访客', value: 'guest' },
];
</script>
<template>
<Form :initValues="{ keep: 'admin', drop: 'admin' }">
<template #default="{ values }">
<FormSelect allowEmpty field="keep" label="设置了 allowEmpty" :optionList="roleOptions" showClear :style="{ width: '176px' }" />
<FormSelect field="drop" label="未设置" :optionList="roleOptions" showClear :style="{ width: '176px' }" />
<code>{{ JSON.stringify(Object.keys(values)) }}</code>
</template>
</Form>
</template>动态表单保留状态(keepState)
默认情况下,当 Field 组件卸载后,其对应的值(value)、校验信息(error)、交互状态(touched)都会被重置。如果你希望 Field 卸载后保留这些状态(例如在条件渲染的场景中),可以使用 keepState 属性。
代码
<script setup lang="ts">
import { ref } from 'vue';
import { Button, Form, FormInput, FormSwitch, Switch } from '@fibrous/semi-ui-vue';
const show = ref(true);
</script>
<template>
<Form :style="{ width: '450px' }">
<FormSwitch field="alwaysShow" label="始终显示的字段" />
<Switch v-model="show" aria-labelledby="form-keep-state-toggle" :style="{ marginBottom: '12px' }" />
<span id="form-keep-state-toggle" :style="{ marginLeft: '8px', color: 'var(--semi-color-text-2)' }"> 显示条件字段 </span>
<FormInput v-if="show" field="conditionalField" keepState label="条件字段(keepState)" />
<FormInput v-if="show" field="noKeepState" label="条件字段(无 keepState)" />
<Button htmlType="submit">提交</Button>
</Form>
</template>在上面的示例中:
- 设置了
keepState的字段在隐藏再显示后,之前输入的值、校验状态等会被保留 - 未设置
keepState的字段在隐藏再显示后,状态会被重置
使用 ArrayField
针对动态增删的数组类表单项,我们提供了 ArrayField 作用域来简化新增、删除行的操作
ArrayField 的默认作用域插槽提供 onAdd、onAddWithInitValue 用来新增行、新增带有初始值的行,每一行的 onRemove 用来删除本行
ArrayField 详细的 API请查阅下方 ArrayField 注意:ArrayField 的 initValue 类型必须是数组
代码
<script setup lang="ts">
import { IconMinusCircle, IconPlusCircle } from '@fibrous/semi-icons-vue';
import { ArrayField, Button, Form, FormInput, FormSelect, TextArea } from '@fibrous/semi-ui-vue';
const data = [
{ name: 'Semi D2C', role: 'Engineer' },
{ name: 'Semi C2D', role: 'Designer' },
];
const roleOptions = [
{ label: 'Engineer', value: 'Engineer' },
{ label: 'Designer', value: 'Designer' },
];
</script>
<template>
<Form allowEmpty labelPosition="left" labelWidth="100px" :style="{ width: '800px' }">
<template #default="{ formState }">
<ArrayField field="rules" :initValue="data">
<template #default="{ arrayFields, onAdd, onAddWithInitValue }">
<Button theme="light" @click="onAdd">
<template #icon>
<IconPlusCircle />
</template>
Add new line
</Button>
<Button :style="{ marginLeft: '8px' }" @click="onAddWithInitValue({ name: 'Semi DSM', role: 'Designer' })">
<template #icon>
<IconPlusCircle />
</template>
Add new line with init value
</Button>
<div v-for="item in arrayFields" :key="item.key" :style="{ width: '1000px', display: 'flex' }">
<FormInput :field="`${item.field}[name]`" :label="`${item.field}.name`" :style="{ width: '200px', marginRight: '16px' }" />
<FormSelect :field="`${item.field}[role]`" :label="`${item.field}.role`" :optionList="roleOptions" :style="{ width: '120px' }" />
<Button aria-label="Delete" :style="{ margin: '12px' }" theme="borderless" type="danger" @click="item.onRemove">
<template #icon>
<IconMinusCircle />
</template>
</Button>
</div>
</template>
</ArrayField>
<TextArea aria-label="Form state" :modelValue="JSON.stringify(formState)" :style="{ marginTop: '10px' }" />
</template>
</Form>
</template>嵌套 ArrayField
ArrayField 支持多级嵌套,如下是一个两级嵌套的例子
代码
<script setup lang="ts">
import { IconMinusCircle, IconPlusCircle } from '@fibrous/semi-icons-vue';
import { ArrayField, Button, Card, Form, FormInput, FormSelect, TypographyText } from '@fibrous/semi-ui-vue';
const selectOption = [
{ label: '发件人地址', value: 'address' },
{ label: '邮件主题', value: 'title' },
{ label: '发送时间', value: 'sendTime' },
{ label: '接收时间', value: 'receiveTime' },
{ label: '正文', value: 'main' },
{ label: '附件名称', value: 'attachmentName' },
];
const typeOption = [
{ label: '包含', value: 'include' },
{ label: '不包含', value: 'exclude' },
];
const initValue = {
group: [
{
name: '收信规则1',
rules: [
{ ruleType: 'address', type: 'include', text: 'bytedance.com' },
{ ruleType: 'title', type: 'exclude', text: '更新日志' },
],
},
{
name: '收信规则2',
rules: [{ ruleType: 'sendTime', type: 'include', text: '2019' }],
},
],
};
const newRule = {
name: '新规则名称',
rules: [
{ ruleType: 'main', type: 'include', text: '' },
{ ruleType: 'attachmentName', type: 'include', text: '' },
],
};
const rowStyle = { marginTop: '12px', marginLeft: '12px' };
</script>
<template>
<Form allowEmpty :initValues="initValue" labelPosition="left" :style="{ textAlign: 'left' }" @valueChange="(values) => console.log(values)">
<ArrayField field="group">
<template #default="{ arrayFields, onAddWithInitValue }">
<Button theme="solid" @click="onAddWithInitValue(newRule)">
<template #icon>
<IconPlusCircle />
</template>
新增收信规则
</Button>
<div v-for="item in arrayFields" :key="item.key" :style="{ width: '1000px', display: 'flex', flexWrap: 'wrap' }">
<FormInput :field="`${item.field}[name]`" label="规则名称" labelPosition="top" :style="{ width: '600px' }" />
<Button aria-label="删除规则" :style="{ margin: '36px 0 0 12px' }" type="danger" @click="item.onRemove">
<template #icon>
<IconMinusCircle />
</template>
</Button>
<TypographyText strong :style="{ flexBasis: '100%' }">当邮件到达,满足以下条件时:</TypographyText>
<Card shadows="hover" :style="{ width: '620px', margin: '12px 0 0 24px' }">
<ArrayField :field="`${item.field}.rules`">
<template #default="{ arrayFields: rules, onAddWithInitValue: addRule }">
<div v-for="(rule, i) in rules" :key="rule.key" :style="{ display: 'flex' }">
<FormSelect :field="`${rule.field}[ruleType]`" :label="`${rule.field}.ruleType`" noLabel :optionList="selectOption" :style="{ width: '120px', marginRight: '12px' }" />
<FormSelect :field="`${rule.field}[type]`" :label="`${rule.field}.type`" noLabel :optionList="typeOption" :style="{ width: '100px', marginRight: '12px' }" />
<FormInput aria-label="条件内容" :field="`${rule.field}[text]`" :label="`${rule.field}.text`" noLabel :style="{ width: '200px' }" />
<Button aria-label="删除条件" :style="rowStyle" theme="borderless" type="danger" @click="rule.onRemove">
<template #icon>
<IconMinusCircle />
</template>
</Button>
<Button aria-label="新增条件" :disabled="i !== rules.length - 1" :style="rowStyle" @click="addRule({ ruleType: `条件${rules.length + 1}`, type: 'include' })">
<template #icon>
<IconPlusCircle />
</template>
</Button>
</div>
</template>
</ArrayField>
</Card>
</div>
</template>
</ArrayField>
</Form>
</template>动态删减表单项 - 通过 formApi
如果不使用 ArrayField,也可以通过 Form 提供的 formApi,手动增删 formState 中的数据。
代码
<script setup lang="ts">
import { shallowRef } from 'vue';
import { Button, Form, FormInput, FormSelect, TextArea, type FormApi } from '@fibrous/semi-ui-vue';
interface Effect {
name: string;
type: string;
key: number;
}
interface FormValues {
effects: Effect[];
}
const initValues: FormValues = {
effects: [
{ name: '脸部贴纸', type: '2D', key: 1 },
{ name: '背景贴纸', type: '3D', key: 2 },
],
};
const formApi = shallowRef<FormApi<FormValues>>();
let nextKey = 3;
const onGetFormApi = (api: FormApi<FormValues>) => {
formApi.value = api;
};
const add = () => {
const effects = formApi.value?.getValue('effects') ?? [];
formApi.value?.setValue('effects', [...effects, { name: '', type: '', key: nextKey++ }]);
};
const remove = (key: number) => {
const effects = (formApi.value?.getValue('effects') ?? []).filter((effect) => key !== effect.key);
formApi.value?.setValue('effects', effects.length ? effects : undefined);
};
</script>
<template>
<Form :initValues="initValues" labelPosition="left" labelWidth="180px" :style="{ width: '500px' }" @getFormApi="onGetFormApi">
<template #default="{ values }">
<Button @click="add">添加</Button>
<div v-for="(effect, i) in values.effects" :key="effect.key" :style="{ width: '1000px', display: 'flex' }">
<FormInput :aria-label="`特效 ${i + 1} 名称`" :field="`effects[${i}].name`" :style="{ width: '200px', marginRight: '12px' }" />
<FormSelect :field="`effects[${i}].type`" :style="{ width: '90px' }">
<FormSelect.Option value="2D">2D</FormSelect.Option>
<FormSelect.Option value="3D">3D</FormSelect.Option>
</FormSelect>
<Button :style="{ margin: '12px' }" type="danger" @click="remove(effect.key)">删除</Button>
</div>
<TextArea aria-label="表单值" :modelValue="JSON.stringify(values)" :style="{ marginTop: '10px' }" />
</template>
</Form>
</template>Hooks 的使用
我们提供了四个组合式函数,使你在不需要通过 props 传递的情况下,也能在放置于 Form 结构内部的组件的 setup 中轻易访问到 Form 内部状态数据,以及调用 Form、Field 的相关 api。它们依赖 Form 提供的上下文,在 Form 外部调用会抛出错误
import { useFormApi, useFormState, useFieldApi, useFieldState } from '@fibrous/semi-ui-vue';
useFormApi
useFormApi 允许你在 Form 内部的组件中直接访问父级 Form 组件的 formApi
代码
<script setup lang="ts">
import { defineComponent, h } from 'vue';
import { Button, Form, FormInput, useFormApi } from '@fibrous/semi-ui-vue';
// useFormApi reads the Form from provide/inject, so it has to run in a component rendered inside the Form
const ComponentUsingFormApi = defineComponent(() => {
const formApi = useFormApi();
const change = () => {
formApi.setValue('name', Math.random());
};
return () => h(Button, { onClick: change }, () => 'ChangeName By【formApi】');
});
</script>
<template>
<Form>
<FormInput field="name" initValue="mike" />
<ComponentUsingFormApi />
</Form>
</template>useFormState
useFormState 允许你在 Form 内部的组件中直接访问父级 Form 组件的 formState,返回值是 ComputedRef,在脚本中通过 .value 读取
代码
<script setup lang="ts">
import { defineComponent, h } from 'vue';
import { Form, FormInput, useFormState } from '@fibrous/semi-ui-vue';
// useFormState reads the Form from provide/inject, so it has to run in a component rendered inside the Form
const ComponentUsingFormState = defineComponent(() => {
const formState = useFormState();
return () => h('pre', h('code', JSON.stringify(formState.value)));
});
</script>
<template>
<Form>
<FormInput field="name" initValue="mike" />
<h5>FormState read by 【useFormState】:</h5>
<ComponentUsingFormState />
</Form>
</template>useFieldApi
useFieldApi 允许你在 Form 内部的组件中直接调用指定 Field 的 api,参数可以是字段名、ref 或 getter
代码
<script setup lang="ts">
import { defineComponent, h } from 'vue';
import { Button, Form, FormInput, useFieldApi } from '@fibrous/semi-ui-vue';
// useFieldApi reads the Form from provide/inject, so it has to run in a component rendered inside the Form
const ComponentUsingFieldApi = defineComponent(() => {
const nameFieldApi = useFieldApi('name');
const change = () => {
nameFieldApi.setValue(Math.random());
};
return () => h(Button, { onClick: change }, () => 'Click Me!!! changeNameBy【fieldApi】');
});
</script>
<template>
<Form>
<FormInput field="name" initValue="mike" />
<ComponentUsingFieldApi />
</Form>
</template>useFieldState
useFieldState 允许你在 Form 内部的组件中直接访问指定 Field 的 State,参数可以是字段名、ref 或 getter,返回值是 ComputedRef<{ value, error, touched }>
代码
<script setup lang="ts">
import { defineComponent, h } from 'vue';
import { Form, FormInput, useFieldState } from '@fibrous/semi-ui-vue';
// useFieldState reads the Form from provide/inject, so it has to run in a component rendered inside the Form
const ComponentUsingFieldState = defineComponent(
(props: { field: string }) => {
const fieldState = useFieldState(() => props.field);
return () => h('div', [h('span', `【${props.field}】FieldState read by 【useFieldState】:`), h('code', JSON.stringify(fieldState.value))]);
},
{ props: ['field'] }
);
</script>
<template>
<Form layout="horizontal">
<div style="width: 400px">
<FormInput field="name" initValue="mike" />
<FormInput field="country" initValue="china" />
</div>
<div style="width: 500px; margin-top: 30px">
<ComponentUsingFieldState field="name" />
<ComponentUsingFieldState field="country" />
</div>
</Form>
</template>通过默认插槽访问 formApi、formState
Vue 不提供 withFormApi、withFormState。Form 的默认作用域插槽已经提供了 formApi、formState;需要在子组件内访问时使用上面的 useFormApi、useFormState
访问 formApi
代码
<script setup lang="ts">
import { Button, Form, FormInput } from '@fibrous/semi-ui-vue';
</script>
<template>
<Form>
<template #default="{ formApi }">
<FormInput field="name" initValue="semi" />
<FormInput field="familyName" initValue="design" />
<Button htmlType="submit" style="margin-right: 4px">submit</Button>
<Button @click="formApi.setValue('name', Math.random())">Click Me!!! ChangeName By【formApi】</Button>
</template>
</Form>
</template>访问 formState
代码
<script setup lang="ts">
import { Form, FormInput } from '@fibrous/semi-ui-vue';
</script>
<template>
<Form>
<template #default="{ formState }">
<FormInput field="name" initValue="semi" />
<FormInput field="familyName" initValue="design" />
<code>{{ JSON.stringify(formState) }}</code>
</template>
</Form>
</template>withField 封装自定义表单控件
通过 withField,你可以将其他自定义组件或原生标签(如 'input')扩展成为表单控件,由 Form 接管其行为
withField 主要做了以下事情
- 负责接管组件的
modelValue(或者valueKey指定的其他属性)、change事件(或onKeyChangeFnName指定的其他事件) - 负责在表单控件上方插入 Field 的
FormLabel - 负责在表单控件下方插入 Field 的 ErrorMessage
- 负责在表单控件下方插入 Field 的 extraText
withFieldOptions 具体配置可参考 withFieldOptions
你的自定义受控组件需要做以下事情:
- 声明
modelValue属性(或者valueKey指定的其他属性),响应它的变化,并更新你的组件UI渲染结果 - 值发生变化时,发出
change事件 (或onKeyChangeFnName指定的其他事件) 并且将最新的值作为参数。组件只发出update:modelValue时,把onKeyChangeFnName设为'update:modelValue'即可
withField(YourComponent, withFieldOptions);
返回组件的 Props 类型由 YourComponent 的 Props 推断:保留组件自身的属性,加上 Field Props,去掉 modelValue、onUpdate:modelValue、defaultValue、defaultChecked(值由 Form 接管,写了会被忽略并在开发模式下给出警告)。
代码
<script setup lang="ts">
import { Form, withField } from '@fibrous/semi-ui-vue';
const CustomInput = withField('input', {
valueKey: 'value',
onKeyChangeFnName: 'input',
valuePath: 'target.value',
});
</script>
<template>
<Form>
<template #default="{ formState }">
<CustomInput field="name" />
<pre><code>{{ JSON.stringify(formState) }}</code></pre>
</template>
</Form>
</template>代码
<script setup lang="ts">
import { defineComponent, h, type PropType } from 'vue';
import { Form, Input, Select, withField } from '@fibrous/semi-ui-vue';
interface BaseInfo {
name?: string;
role?: string;
}
const roles = [
{ value: 'rd', label: '开发' },
{ value: 'UED', label: '设计师' },
];
// A single-file demo cannot define a component inline, so a render function; use a separate SFC in real code
// aria-required is a prop for each control; id, aria-labelledby and the rest land on the root fieldset
const MyComponent = defineComponent({
props: { modelValue: Object as PropType<BaseInfo>, ariaRequired: Boolean },
emits: { change: (value: BaseInfo) => Boolean(value) },
setup(props, { emit }) {
const update = (patch: BaseInfo) => emit('change', { ...props.modelValue, ...patch });
return () =>
h('fieldset', { class: 'customField', style: 'border: 0; margin: 0; padding: 0' }, [
h(
Input,
{
'aria-label': '名称',
'aria-required': props.ariaRequired,
modelValue: props.modelValue?.name,
'onUpdate:modelValue': (name: string) => update({ name }),
style: 'width: 180px; margin-inline-end: 12px',
},
{ prefix: () => '名称' }
),
h(
Select,
{
'aria-label': '角色',
'aria-required': props.ariaRequired,
modelValue: props.modelValue?.role,
'onUpdate:modelValue': (role: unknown) => update({ role: role as string }),
style: 'width: 200px',
optionList: roles,
},
{ prefix: () => '角色' }
),
]);
},
});
const CustomField = withField(MyComponent);
</script>
<template>
<Form>
<template #default="{ formState }">
<CustomField field="baseInfo" :label="{ text: '基本信息', required: true }" />
<pre><code>{{ JSON.stringify(formState) }}</code></pre>
</template>
</Form>
</template>API 参考
Form
<script setup lang="ts">
import { Form } from '@fibrous/semi-ui-vue';
</script>
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| initValues | 表单的初始值,仅在 Form 创建时复制一次。 | Values | undefined |
| layout | 字段的排列方向,可选 vertical 或 horizontal。 | "horizontal" | "vertical" | semiDefault("Form", "layout", "vertical") |
| labelPosition | 统一应用在每个字段上的 label 位置,inset 仅部分字段支持,其余字段按 top 布局。字段上的同名属性优先。 | "top" | "left" | "inset" | semiDefault("Form", "labelPosition", "top") |
| labelWidth | 统一应用在每个字段上的 label 宽度,数字单位为 px。 | string | number | semiGlobalDefault("Form", "labelWidth") |
| labelAlign | 统一应用在每个字段上的 label 文本对齐方式,字段上的同名属性优先。 | "left" | "right" | semiGlobalDefault("Form", "labelAlign") |
| labelCol | 统一应用在每个字段上的 label 栅格布局,同 Col 的 span、offset 等属性,需与 wrapperCol 一起设置。 | ColProps | semiGlobalDefault("Form", "labelCol") |
| wrapperCol | 统一应用在每个字段上的控件栅格布局,同 Col 的 span、offset 等属性,需与 labelCol 一起设置。 | ColProps | semiGlobalDefault("Form", "wrapperCol") |
| extraTextPosition | 统一应用在每个字段上的 extraText 位置,middle 在 Label 与控件之间,bottom 在控件之后。字段上的同名属性优先。 | "middle" | "bottom" | semiGlobalDefault("Form", "extraTextPosition") |
| allowEmpty | 值变为空的字段是否在 values 中保留 key。字段上的同名属性优先。 | boolean | semiDefault("Form", "allowEmpty", false) |
| autoScrollToError | 提交或调用 formApi.validate 校验失败时,是否自动滚动到出错的字段;传对象时作为 scroll-into-view-if-needed 的选项。 | Typeboolean | Options<any> | semiDefault("Form", "autoScrollToError", false) |
| showValidateIcon | 校验信息前是否显示对应状态的图标。 | boolean | semiDefault("Form", "showValidateIcon", true) |
| disabled | 统一应用在每个字段上的 disabled,字段自身的 disabled 优先。 | boolean | semiGlobalDefault("Form", "disabled") |
| stopValidateWithError | 使用 rules 校验时,遇到第一条不通过的规则后是否不再校验后续规则。字段上的同名属性优先。 | boolean | semiGlobalDefault("Form", "stopValidateWithError") |
| trigger | 统一应用在每个字段上的校验时机,可选 change、blur、mount、custom 或其数组。字段上的同名属性优先。 | FieldValidateTriggerType | semiGlobalDefault("Form", "trigger") |
| stopPropagation | 是否阻止 submit、reset 事件冒泡,用于嵌套 Form 时避免触发外层 Form 的事件。 | { submit?: boolean | undefined; reset?: boolean | undefined; } | semiGlobalDefault("Form", "stopPropagation") |
| validator | Form 级别的校验函数,submit 与 formApi.validate 时调用,支持返回 Promise。设置后字段自身的 validator 与 rules 不再生效。返回空字符串或空对象表示通过,失败时返回以字段名为 key 的错误对象。 | Type((values: Values) => FormValidateResult<Values> | Promise<FormValidateResult<Values>>) | undefined |
| form | useForm 返回的 formApi,用于在 Form 外部控制表单。 | Type(FormApi<Values> & FormApiProxyInternals<Values>) | undefined |
| id | form 元素的 id,同时写入 x-form-id;不传时自动生成。 | string | undefined |
Events
| 事件 | 说明 | 回调参数 |
|---|---|---|
| submit | 点击提交按钮或调用 formApi.submitForm 且校验通过后触发,参数为 values 与提交事件。与原生 submit 事件同名,会遮住原生事件。 | [values: Values, e?: SubmitEvent | undefined] |
| submitFail | 点击提交按钮或调用 formApi.submitForm 且校验失败后触发,参数为 errors、values 与提交事件。 | Type[errors: FormValidateResult<Values>, values: Partial<Values>, e?: SubmitEvent | undefined] |
| reset | 点击重置按钮或调用 formApi.reset 时触发。与原生 reset 事件同名,会遮住原生事件。 | [] |
| change | 表单状态更新时触发,包括字段挂载、卸载、值变更、blur、校验状态与错误信息变更,参数为 formState。与原生 change 事件同名,会遮住原生事件。 | Type[formState: FormState<Values>] |
| valueChange | 字段的值变化时触发,参数为全部 values 与本次变化的字段值。 | Type[values: Values, changedValues: Partial<Values>] |
| errorChange | 校验状态更新时触发,参数为全部错误和本次变化的字段错误。validate、submit、reset 等批量操作只触发一次,静默校验不触发。 | Type[errors: AllErrors<Values>, changedError?: Partial<Record<FieldPath<Values, 5>, FieldError>> | undefined] |
| getFormApi | Form 挂载后触发,参数为 formApi。 | Type[formApi: FormApi<Values>] |
Slots
| 插槽 | 说明 | 作用域参数 |
|---|---|---|
| default | 表单内容,作用域参数为 formState、formApi 与 values,只用于读取,修改应通过 formApi。 | TypeFormDefaultSlotProps<Values> |
Exposes
| 名称 | 说明 | 类型 |
|---|---|---|
| 无 |
class、style 与原生属性经 attrs 落在 form 元素上;设置了 labelCol 与 wrapperCol 时 form 外层多一层 Row,attrs 仍落在 form 上。Form 的 change、submit、reset 事件与原生事件同名,会遮住原生事件。form 元素默认带 novalidate,校验只由 Semi 负责,传 :novalidate="false" 可以恢复原生校验。
通过模板 ref 取得 Form 实例时,可以访问其 formApi(类型 FormHandle)。
FormState
FormState 存储了所有 Form 内部的状态值,包括各表单控件的值,错误信息、touched 状态
进行表单提交时,实际提交的就是 formState.values
| Name | 说明 | 初始值 | 示例 |
|---|---|---|---|
| values | 表单的值 | {} | { fieldA: 'str', fieldB: true } |
| errors | 表单错误信息集合,你可以通过判断是否有错误信息来决定是否允许用户提交 | {} | { fieldA: 'length not valid'} |
| touched | 用户点击过的 field 集合 | {} | { fieldA: true } |
如何访问 formState
- 通过调用 formApi.getFormState(),在 Form 外部也可以获取 formState
- 通过 默认作用域插槽,formState 会作为参数注入
- 通过 useFormState,Form 内部的组件访问父级 Form 的 formState,无需繁琐的引用传递
- 通过 useForm 创建的 formState,在 Form 外部以响应式的方式读取
FormApi
我们提供了 FormApi。你在 Form 内部、外部都可以很方便地获取到 formApi,它允许你使用 getter 和 setter 来获取和操作 formState 的值。
下面的表格描述了 formApi 中可用的功能。
| Function | 说明 | example |
|---|---|---|
| getFormProps | 获取 Form 组件上当前所有props的值,例如可用于读取 disabled 等。 | formApi.getFormProps(propNames?: string[]) |
| getFormState | 获取 FormState | formApi.getFormState() |
| submitForm | 可手动触发 submit 提交操作 | formApi.submitForm() |
| reset | 可手动对 form 进行重置 | formApi.reset(fields?: Array <string>) |
| validate | 可手动触发对表单的校验,不传参时默认触发整全体Field的校验(配置Form级别校验器后,Field级别校验器在submit或formApi.validate()时不会再被触发),若想触发部分field的校验,将目标field数组传入即可 支持静默校验:传入 { silent: true } 可在不触发 UI 更新(不显示错误提示、不设置 touched 状态)的情况下获取校验结果,也可通过 { fields: ['fieldA'], silent: true } 对指定字段进行静默校验 | formApi.validate() .then(values=>{}) .catch(errors=>{}) 或 formApi.validate(['fieldA','fieldB']) 或 formApi.validate({ silent: true }) 或 formApi.validate({ fields: ['fieldA'], silent: true }) |
| setValues | 设置整个表单的值。第二个参数中的 isOverride 默认为 false isOverride 为 false 时:遍历 Form 中所有已注册的 field,从 newValues 中取对应字段的值更新到 formState.values 中。如果 newValues 中不包含某个已注册 field 的值,该字段会被设置为 undefined。isOverride 为 true 时:直接用 newValues 整体替换 formState.values,包括 newValues 中存在但表单中没有对应 field 的额外 key 也会写入。推荐用法:如果只需要更新部分字段,建议使用 formApi.setValue(field, value) 逐个更新,或者先获取当前值再合并:formApi.setValues({ ...formApi.getValues(), name: newValue }) | formApi.setValues(newValues: object, { isOverride: boolean }) |
| setValue | 提供直接修改 formState.values 方法,与 setValues 的区别是它仅修改单个 field | formApi.setValue(field: string, newFieldValue: any) |
| getValue | 获取 单个 Field 的值 | formApi.getValue() formApi.getValue(field: string) |
| getValues | 获取 所有 Field 的值 | formApi.getValues() |
| setTouched | 修改 formState.touched | formApi.setTouched(field: string, isTouched: boolean) |
| getTouched | 获取 Field 的 touched 状态 | formApi.getTouched(field: string) |
| setError | 修改 某个 field 的 error 信息 | formApi.setError(field: string, fieldErrorMessage: string) |
| getError | 获取 Field 的 error 状态 | formApi.getError(field: string) |
| getFieldExist | 获取 Form 中是否存在对应的 field | formApi.getFieldExist(field: string) |
| scrollToField | 滚动至指定的 field, 第二个入参将透传至scroll-into-view-if-needed | formApi.scrollToField(field: string, scrollOpts: ScrollIntoViewOptions) |
| scrollToError | 滚动至校验错误的field,可传指定 field 或者 index,传入 index 则滚动到第 index 个错误的 DOM,若不传参则滚动到DOM树中第一个校验出错的位置。 | formApi.scrollToError(ScrollToErrorOptions{field?: string; index?: number; scrollOpts?: ScrollIntoViewOptions }) |
如何获取 formApi
- 通过
useForm()在 Form 外部创建 formApi,然后通过form属性传入 Form 组件。这种方式允许你在 Form 渲染前就获取 formApi,适合需要在组件外部控制表单的场景。 - Form 组件挂载后,会发出
getFormApi事件,你可以在回调函数中保存 formApi 的引用,以便后续进行调用(示例如下代码)
除此之外,我们还提供了其他方式获取 formApi,你可以根据喜好选择不同的调用方式 - 通过模板 ref 获取 Form 组件实例,直接访问实例上的 formApi
- 通过 默认作用域插槽,formApi 会作为参数注入
- 通过 useFormApi,在 Form 内部的组件中获取
useForm
useForm() 用于在 Form 组件外部创建 formApi,适合需要在 Form 渲染前就控制表单的场景。它返回对象 { formApi, formState, values },其中 formState 是只读的 shallowRef,values 是 computed,通过 Form 的 form 属性绑定后随表单更新。
使用示例:
代码
<script setup lang="ts">
import { Button, Form, FormInput, useForm } from '@fibrous/semi-ui-vue';
const { formApi, formState, values } = useForm();
const handleClick = () => {
formApi.setValue('username', 'semi');
console.log('当前表单值:', values.value);
};
</script>
<template>
<Button @click="handleClick">外部设置值</Button>
<Button @click="formApi.reset()">重置表单</Button>
<Form :form="formApi">
<FormInput field="username" label="用户名" />
<FormInput field="email" label="邮箱" />
</Form>
<div style="margin-top: 16px">
<div>表单值:{{ JSON.stringify(values) }}</div>
<div>touched 状态:{{ JSON.stringify(formState.touched) }}</div>
</div>
</template><script setup lang="ts">
import { useTemplateRef } from 'vue';
import { Button, Form } from '@fibrous/semi-ui-vue';
import type { FormApi, FormHandle } from '@fibrous/semi-ui-vue';
let formApiA: FormApi | undefined;
const formB = useTemplateRef<FormHandle>('formB');
const changeValues = () => {
// 使用 FormA 的 formApi,获取到 formApi 后,你可以使用它来对表单进行任何你想做的修改 ~
formApiA?.setValues({ a: 1 });
// 使用 FormB 的 formApi
formB.value?.formApi.setValues({ b: 2 });
};
</script>
<template>
<!-- 通过 getFormApi 事件获取并保存 formApi -->
<Form @getFormApi="(formApi) => (formApiA = formApi)" />
<!-- 通过模板 ref 直接获取 Form 组件实例上的 formApi -->
<Form ref="formB" />
<Button @click="changeValues">Change</Button>
</template>
Field Props
Field 是 withField 生成的组件,不是单文件组件。字段通过模板 ref 取得的是 { control }(类型 FieldHandle),其中 control 是被包装组件自身的句柄,例如 FormInput 的 control 可以调用 Input 的 focus。
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| convert | field 值改变后,在存入 formState 与展示前,对 field 的值进行二次更新 | (fieldValue) => any | |
| field | 该表单控件的值在 formState.values 中的映射路径,Form 会使用该值来区分内部的表单控件 必填!!!(放在各自 Group 内的 FormCheckbox、FormRadio 除外)示例:Bindding Syntax | string | |
| label | 该表单控件的 label 标签文本,不传的时候默认与 field 同名;传入对象时可配置 text、extra、required、optional、align、width、disabled,对象中的 text、extra 只收字符串或数字。#fieldLabel、#labelExtra 插槽优先 | string | number | FieldLabel | |
| labelPosition | 该表单控件的 label 位置。在Form与Field上同时传入时,以Field props为准 | 'top' | 'left' | 'inset' | |
| labelAlign | 该表单控件的 label 文本的 text-align。在Form与Field上同时传入时,以Field props为准 | 'left' | 'right' | |
| labelWidth | 该表单控件的 label 文本的 width,数字单位为 px。在Form与Field上同时传入时,以Field props为准 | string | number | |
| labelCol | 该表单控件的 label 栅格布局,同Col 组件,覆盖 Form 上的同名属性。与 wrapperCol 一起设置,且 Form 没有同时设置二者时,Field 会在内部自行包一层 Row | ColProps | |
| wrapperCol | 该表单控件的控件栅格布局,同Col 组件,覆盖 Form 上的同名属性。使用方式同 labelCol | ColProps | |
| noLabel | 当你不需要自动添加 label 时,可以将该值置为 true | boolean | |
| noErrorMessage | 当你不需要自动添加 ErrorMessage 模块时,可以将该值置为 true,注意此时 helpText 也不会被展示 | boolean | |
| name | 控件名称,传入时会自动在对应 field wrapper div 追加对应的 className,如:abc => '.semi-form-field-abc',同时传给底层组件,例如可以用于配置 input 的 name 属性 | string | |
| id | 控件的 id,不传时自动生成。label 的 for,以及 label、helpText、extraText、errormessage 的 id 都由它派生 | string | |
| fieldClassName | 整个 fieldWrapper 的 className,作用与 name 参数一致,区别是不会自动追加前缀 | string | |
| fieldStyle | 整个 fieldWrapper 的内联样式 | StyleValue | |
| initValue | 该表单控件的初始值(仅在 Field mounted 时消费一次,后续更新无效),相比 Form 的 initValues 中的值,它的优先级更高 | unknown(类型取决于当前组件,详细见各组件的 api) | |
| validator | 该表单控件的自定义校验函数。支持同步、异步校验(通过返回 promise)。返回空字符串或 undefined 表示通过,其他结果为错误信息,只能是数据,自定义渲染错误信息使用 #error 插槽。设置了 validator 时,rules 不会生效 | (fieldValue, values) => string | string[] | boolean | undefined | Promise | |
| rules | 校验规则,校验库基于async-validator,message 只收字符串 | RuleItem[] | |
| validateStatus | 该表单控件的校验结果状态(仅影响样式),可选值:success/error/warning/default,不传时取 Field 自身的校验状态 | string | |
| trigger | 触发校验的时机,可选值:blur/change/custom/mount,或以上值的组合1、设置为 custom 时,仅会由 formApi/fieldApi 触发校验时被触发 2、mount(挂载时即触发一次校验) | FieldValidateTriggerType | 'change' |
| transform | 校验前转换字段值,转换后的值仅会在校验时被消费,对 formState 无影响 | (fieldValue) => any | |
| allowEmpty | 值为空时是否在 values 中保留该 field 的 key(值为 undefined)。Field 与 Form 任一设置即生效 | boolean | false |
| allowEmptyString | 是否允许值为空字符串。默认情况下值为''时,该 field 对应的 key 会从 values 中移除,如果你希望保留该 key,那么需要将 allowEmptyString 设为 true | boolean | false |
| emptyValue | 被视为空值的值。值变化为它时,该 field 的 key 会从 values 中移除(allowEmpty 为 true 时保留为 undefined) | unknown | '' |
| keepState | 是否在 Field 卸载后保留其状态(value、error、touched)。为 true 时,Field 卸载后其值、校验信息、交互状态不会丢失,再次挂载时会自动恢复 | boolean | false |
| stopValidateWithError | 为 true 时,使用 rules 校验,碰到第一个检验不通过的 rules 后,将不再触发后续 rules 的校验 | boolean | false |
| pure | 是否仅接管数据流,为 true 时不会自动插入 ErrorMessage、Label、extraText 等模块,样式、DOM 结构与原始的组件保持一致 | boolean | false |
| extraTextPosition | 控制extraText的显示位置,可选middle(垂直方向以Label、extraText、Field主体的顺序显示)、bottom (垂直方向以Label、Field主体、extraText的顺序显示);在Form与Field上同时传入时,以Field props为准 | 'middle' | 'bottom' | 'bottom' |
Field 不声明 disabled、helpText、extraText:disabled 取 Form 的值,写在 Field 上的 disabled 优先;提示信息使用下面的插槽。组件自身的其他属性(如 Input 的 size、placeholder)与 class、style、aria-* 一并写在 Field 上,Field 会将其透传至被包装的组件本身,而不是 fieldWrapper。
Events
| 事件 | 说明 | 回调参数 |
|---|---|---|
| change | 值变化时触发(onKeyChangeFnName 指定了其他事件时,为那个事件)。回调参数是被包装组件该事件的原始参数,最后追加最新的表单值 latestValues,用于避免读到闭包里的旧值;不传 field 的 FormCheckbox、FormRadio 只是普通控件,没有 latestValues。与原生 change 事件同名,会遮住原生事件;不支持 .once 修饰符,也不出现在 Devtools 的事件面板。被包装组件的其他事件(如 select、focus)原样传给该组件,可以直接监听 | 被包装组件该事件的参数,latestValues |
Slots
| 插槽 | 说明 | 作用域参数 |
|---|---|---|
| fieldLabel | label 内容,优先于 label 属性中的文本 | |
| labelExtra | 跟随在 label 文本后的内容,优先于 label 对象中的 extra | |
| helpText | 自定义提示信息,与校验信息公用同一区块展示,两者均有值时,优先展示校验信息 | |
| extraText | 额外的提示信息,当需要错误信息和提示文案同时出现时,可以使用这个,位于 helpText/errorMessage 后 | |
| error | 自定义错误信息的渲染 | { error } |
| 其他 | 其余插槽(含 #default、#label)原样传给被包装组件,如 FormSelect 的选项、FormCheckboxGroup 的 #label | 由被包装组件决定 |
Field Api
我们同样提供了 fieldApi,其功能与 formApi 大部分相似,区别在于 fieldApi 限定了修改的范围,它只能修改所绑定的 field。通过 useFieldApi 获取
| Function | 说明 | example |
|---|---|---|
| setValue | 修改当前 Field 的值 | fieldApi.setValue(newValue: any) |
| getValue | 获取当前 Field 的值 | fieldApi.getValue() |
| setTouched | 修改当前 Field 的 touched | fieldApi.setTouched(true) |
| getTouched | 获取当前 Field 的 touched | fieldApi.getTouched() |
| setError | 修改当前 Field 的错误信息 | fieldApi.setError(newErrorMessage: string) |
| getError | 获取当前 Field 的错误信息 | fieldApi.getError() |
ArrayField
针对动态增删的数组类表单项,我们提供了 ArrayField 作用域来简化新增、删除行的操作
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| field | 数组在 formState.values 中的路径,必填。例如各行字段为 a[0].name、a[1].name 时应传 a。 | string | undefined |
| initValue | 数组的初始值,必须是数组;与 Form 的 initValues 同时配置时优先。 | unknown[] | semiGlobalDefault("ArrayField", "initValue") |
Events
| 事件 | 说明 | 回调参数 |
|---|---|---|
| 无 |
Slots
| 插槽 | 说明 | 作用域参数 |
|---|---|---|
| default | 各行内容,作用域参数为 arrayFields、onAdd 与 onAddWithInitValue。 | ArrayFieldSlotProps |
Exposes
| 名称 | 说明 | 类型 |
|---|---|---|
| 无 |
ArrayField 没有自己的 DOM,未声明的属性会被丢弃。默认作用域插槽的参数类型如下:
interface ArrayFieldSlotProps {
arrayFields: ArrayFieldItem[]; // 当前数组表单,可以用来执行 v-for 渲染出每一行
onAdd: (index?: number | Event) => void; // 新增空白行;传入数字时插入到该位置,传入事件时被忽略,因此可直接写成 @click="onAdd"
onAddWithInitValue: (row: unknown, index?: number) => void; // 新增一个带初始值的行,index 为插入位置
}
interface ArrayFieldItem {
key: string; // 一个用于标识当前行的 key,应当绑定在当前行的 wrapper 上
field: string; // 本行 fieldPath,它等同于 ArrayField 的 field + [index]
onRemove: () => void; // 移除本行的操作函数,调用时将直接删除本行
}
FormSection
import { FormSection } from '@fibrous/semi-ui-vue';
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| 无 |
Events
| 事件 | 说明 | 回调参数 |
|---|---|---|
| 无 |
Slots
| 插槽 | 说明 | 作用域参数 |
|---|---|---|
| default | 分组内的字段。 | any |
| text | 分组标题,没有内容时不渲染标题元素。 | any |
Exposes
| 名称 | 说明 | 类型 |
|---|---|---|
| 无 |
class、style 与原生属性经 attrs 落在 section 元素上。
FormLabel
默认情况下,Label 会由 Form 自行插入到每个 Field 中。如果你需要在其他地方自行插入 Label,我们提供了 FormLabel 组件可以导入
import { FormLabel } from '@fibrous/semi-ui-vue';
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| text | Label 文本,默认插槽优先。 | string | number | undefined |
| extra | 跟随在 label 文本后的内容,extra 插槽优先。 | string | number | undefined |
| align | Label 文本的对齐方式。 | string | "left" |
| disabled | 是否为禁用样式。 | boolean | undefined |
| required | 是否显示必填的 * 号。 | boolean | false |
| optional | 是否在文本后追加语言包中的可选标识。为 true 时不再显示 required 的 *。 | boolean | false |
| name | 所标注控件的 id,写入 label 的 for 属性。 | string | undefined |
| width | Label 宽度,数字单位为 px。 | string | number | undefined |
Events
| 事件 | 说明 | 回调参数 |
|---|---|---|
| 无 |
Slots
| 插槽 | 说明 | 作用域参数 |
|---|---|---|
| default | Label 内容,优先于 text。 | any |
| extra | 跟随在 label 文本后的内容,优先于 extra。 | any |
Exposes
| 名称 | 说明 | 类型 |
|---|---|---|
| 无 |
class、style 与原生属性经 attrs 落在 label 元素上。
FormInputGroup
import { FormInputGroup } from '@fibrous/semi-ui-vue';
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| disabled | 组内字段统一的 disabled,不传时沿用 Form 的 disabled。 | boolean | undefined |
| label | 整个 Group 的 Label:字符串作为文本,对象作为 Label 配置,其中 id 用作 Label 的 id;label 插槽优先。 | string | number | FormInputGroupLabel | undefined |
| labelPosition | Label 的位置,默认取自 Form,可选 top 或 left。 | "top" | "left" | undefined |
| extraTextPosition | extraText 的位置,middle 在 Label 与 Group 之间,bottom 在 Group 之后。 | "bottom" | "middle" | "bottom" |
Events
| 事件 | 说明 | 回调参数 |
|---|---|---|
| 无 |
Slots
| 插槽 | 说明 | 作用域参数 |
|---|---|---|
| default | 组内的字段,只渲染设置了 field 的直接子项。 | any |
| label | Group 的 Label 内容,优先于 label 属性中的文本。 | any |
| labelExtra | 跟随在 Group Label 文本后的内容。 | any |
| extraText | 额外的提示信息,常显,由组内每个控件的 aria-describedby 引用。 | any |
Exposes
| 名称 | 说明 | 类型 |
|---|---|---|
| 无 |
class、style 与原生属性(如 size)落在内部的 InputGroup 上,并传给组内每个字段;aria-labelledby、aria-describedby 与组生成的 id 合并,使用方的在前。设置了 label 时外层带 role="group",没有 label 时没有。
当 extraTextPosition 为 middle,且 labelPosition 为 left时。由于 #extraText 的内容高度不定,Label将不再确保能与 Field / FormInputGroup 中的首行文本对齐。
FormSlot
import { FormSlot } from '@fibrous/semi-ui-vue';
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| label | Label 的文本,或 text、extra、align、width、required、optional 等 Label 配置;label 插槽优先。 | string | number | FieldLabel | undefined |
| labelPosition | Label 的位置,默认取自 Form,可选 top 或 left。 | "top" | "left" | undefined |
| noLabel | 是否不渲染 Label;Form 同时设置了 labelCol 与 wrapperCol 时,Label 列仍保留。 | boolean | undefined |
| error | 错误信息,渲染在内容之后;不传则不显示。 | FieldError | undefined |
Events
| 事件 | 说明 | 回调参数 |
|---|---|---|
| 无 |
Slots
| 插槽 | 说明 | 作用域参数 |
|---|---|---|
| default | 放置的自定义内容。 | any |
| label | Label 内容,优先于 label 属性中的文本。 | any |
| labelExtra | 跟随在 Label 文本后的内容。 | any |
| error | 自定义错误信息的渲染,作用域参数为 error。 | { error: FieldError; } |
Exposes
| 名称 | 说明 | 类型 |
|---|---|---|
| 无 |
class、style 与原生属性经 attrs 落在根元素上;传入 aria-label 或 aria-labelledby 时根元素带 role="group",使用方传入的 role 优先。label 的宽度、对齐取自 Form 的 labelWidth、labelAlign,label 对象中的 width、align 优先。
FormErrorMessage
import { FormErrorMessage } from '@fibrous/semi-ui-vue';
- 当 error 为字符串时,直接渲染;为 true 时只渲染错误态容器,不显示文本;为 false 时不渲染
- 当 error 为数组时,会自动执行 join 操作聚合数组内的错误信息;错误只能是数据,自定义渲染使用
#error插槽
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| error | 错误信息。字符串直接显示,数组会合并其中的错误;true 只显示错误态容器,没有文本;false 与空值不渲染。 | FieldError | undefined |
| showValidateIcon | 是否显示 validateStatus 对应的图标。 | boolean | undefined |
| validateStatus | 信息所属的校验状态,可选 default、error、warning、success。 | "error" | "default" | "warning" | "success" | undefined |
| isInInputGroup | 是否位于 InputGroup 中;为 true 且 showValidateIcon 为 true 时,不论 validateStatus 都显示错误图标。 | boolean | undefined |
| errorMessageId | 错误文本元素的 id,供 aria-errormessage 引用。 | string | undefined |
| helpTextId | 提示文本元素的 id,供 aria-describedby 引用。 | string | undefined |
Events
| 事件 | 说明 | 回调参数 |
|---|---|---|
| 无 |
Slots
| 插槽 | 说明 | 作用域参数 |
|---|---|---|
| helpText | 提示信息,没有错误时显示,与错误信息共用同一区块。 | any |
| error | 自定义错误信息的渲染,作用域参数为 error。 | { error: FieldError; } |
Exposes
| 名称 | 说明 | 类型 |
|---|---|---|
| 无 |
class、style 与原生属性经 attrs 落在根元素上。
withFieldOptions
| key | 描述 | 默认值 |
|---|---|---|
| valueKey | 组件表示值的属性,如自定义组件的 modelValue、原生 input 标签的 value | 'modelValue' |
| onKeyChangeFnName | 组件值变化时发出的事件名,如 change、input;Field 上该事件的监听器会在原参数后收到 latestValues | 'change' |
| valuePath | 值在事件第一个参数中的路径,如 Checkbox 的 target.checked、RadioGroup 的 target.value、原生 input 的 target.value;若第一个参数就是值本身,无需再往下取值,该项不需要设 | '' |
| shouldInject | 为 false 时,没有传 field 的组件原样渲染而不被 Form 接管,如 Group 内的 FormCheckbox、FormRadio;为 true 且没有 field 时开发模式下给出警告 | true |
Accessibility
ARIA
- aria-labelledby、for
- Field 组件,会自动添加 label DOM。控件的 id 取
props.id,未传入时自动生成;label 的for属性与该 id 相同;label 的 id 属性为${控件 id}-label; - 当 Form 或者 Field 的 props.labelPosition 设置为 inset时,此时不存在 label 标签,而是 div 标签。insetLabel 对应的 div 标签会被自动追加 id,值与上述 label 的 id 相同,对应 Field 组件的
aria-labelledby。不支持内嵌标签的字段按 top 布局,aria-labelledby指向实际渲染的 label - Field 组件会被自动追加
aria-labelledby,值与上述 label 的id 相同;使用方传入的aria-labelledby排在前面,两者合并而不是覆盖
- Field 组件,会自动添加 label DOM。控件的 id 取
- aria-required
- 当 Field 配置了必填时(即 props.rules中包含 require: true 或 props.label配置了required: true 且没有 optional: true),Field 组件会被自动追加 aria-required = true(CheckboxGroup、Slider、Upload 和单个 Radio 除外);字段上显式传入的 aria-required 优先
- aria-invalid 、aria-errormessage
- 当 Field 校验未通过,或 validateStatus 为 error 时,Field 组件会被自动添加
aria-invalid= true 属性。 - 当 Field 校验未通过时,Field 组件会被自动追加
aria-errormessage属性,值为 errorMessage 所对应DOM元素的 id (格式:${控件 id}-errormessage);CheckboxGroup 落在组内每个 checkbox 上。
- 当 Field 校验未通过,或 validateStatus 为 error 时,Field 组件会被自动添加
- aria-describedby
- 当 Field 配置了
helpText或extraText时,Field 组件会被自动添加aria-describedby属性,值为 helpText、extraText 所对应DOM元素的 id (格式:${控件 id}-helpText、${控件 id}-extraText);使用方传入的aria-describedby排在前面,两者合并而不是覆盖
- 当 Field 配置了
文案规范
- 表单标题
- 表单标题需要跟随标题的书写规范
- 表单标签
- 标签是输入框的简短描述。标签不是注释信息(help text),因此不应该是输入框的填写说明
- 标签必须要:
- 放在输入框的上方或下方
- 简短(1-3个词)
- 使用语句的大小写规范(首字母大写,其他小写)
- 帮助文本
- 帮助文本使用语句书写规范,首字母大写
- 表单按钮
- 关于表单按钮的文案规范,参考按钮Button组件的文案规范
| ✅ 推荐用法 | ❌ 不推荐用法 |
|---|---|
![]() | ![]() |
设计变量
| Sass 变量 | 值 | 用法 |
|---|---|---|
| $spacing-form_field_horizontal-paddingRight | $spacing-base | 水平布局表单项右侧内边距 |
| $spacing-form_field_group_horizontal-paddingRight | $spacing-base | 水平布局表单组标题右侧内边距 |
| $spacing-form_field_vertical-paddingTop | $spacing-base-tight | 表单项顶部内边距(垂直布局) |
| $spacing-form_field_vertical-paddingBottom | $spacing-base-tight | 表单项底部内边距(垂直布局) |
| $spacing-form_field_group_vertical-paddingTop | $spacing-base-tight | 垂直布局表单组顶部内边距 |
| $spacing-form_field_group_vertical-paddingBottom | $spacing-base-tight | 垂直布局表单组底部内边距 |
| $spacing-form_label-paddingRight | $spacing-base | 表单项标签右侧边距(水平布局) |
| $spacing-form_label-paddingTop | ($height-control-default - 20px) * 0.5 | 表单项标签顶部内边距(水平布局) |
| $spacing-form_label-marginBottom | $spacing-extra-tight | 表单项标签底部外边距 |
| $spacing-form_label-marginTop | 0px | 表单项标签顶部外边距 |
| $spacing-form_label_extra-marginLeft | $spacing-extra-tight | 表单项标签额外信息左侧边距 |
| $spacing-form_label_required-marginLeft | $spacing-extra-tight | 表单项标签必填标志左侧边距 |
| $spacing-form_label_posLeft-marginRight | 0 | 表单项左侧标签右侧外边距 |
| $spacing-form_label_posLeft-marginBottom | 0 | 表单项左侧标签底部外边距 |
| $spacing-form_label_posTop-paddingTop | $spacing-extra-tight | 表单项顶部标签顶部边距 |
| $spacing-form_label_posTop-paddingBottom | $spacing-extra-tight | 表单项顶部标签底部边距 |
| $spacing-form_extra_posMid-marginTop | $spacing-extra-tight | 表单项额外提示信息顶部外边距 - 额外信息处于中间时 |
| $spacing-form_extra_posMid-marginBottom | $spacing-extra-tight | 表单项额外提示信息底部外边距 - 额外信息处于中间时 |
| $spacing-form_extra_posBottom-marginTop | $spacing-extra-tight | 表单项额外提示信息顶部外边距 - 额外信息处于底部时 |
| $spacing-form_switch_rating_marginY | ($height-control-default - 24px) * 0.5 | Switch / Rating 表单项顶部内边距(水平布局) |
| $color-form_requiredMark_disabled-text-default | var(--semi-color-danger) | 禁用表单项必填标记颜色 |
| $color-form_label_disabled-text-default | var(--semi-color-disabled-text) | 禁用表单项标签文字颜色 |
| $font-form_label-fontWeight | $font-weight-bold | 表单项标签字重 |
| $color-form_label-text-default | var(--semi-color-text-0) | 表单项标签文字颜色 |
| $color-form_label_optional-text-default | var(--semi-color-tertiary) | 表单项标签可选标记颜色 |
| $color-form_label_extra-text-default | var(--semi-color-tertiary) | 表单项标签图标颜色 |
| $color-form_requiredMark-text-default | var(--semi-color-danger) | 必填标记颜色 |
| $font-form_requiredMark-fontWeight | $font-weight-bold | 表单必填标识字重 |
| $color-form_message_error-text-default | var(--semi-color-danger) | 错误提示颜色 |
| $color-form_alertIcon-icon-default | var(--semi-color-warning) | 警告图标颜色 |
| $spacing-form_statusIcon-marginRight | $spacing-extra-tight | 表单校验状态图标右侧外边距 |
| $spacing-form_message-marginTop | $spacing-extra-tight | 表单错误信息、辅助文字顶部外边距 |
| $color-form_section-text-default | var(--semi-color-text-0) | 表单分组标题文字颜色 |
| $color-form_section-border-default | var(--semi-color-border) | 表单分组标题底部描边颜色 |
| $width-form_section-border | $border-thickness-control | 表单分组标题底部描边宽度 |
| $spacing-form_section-marginTop | $spacing-super-loose - $spacing-base-tight | 表单分组顶部内边距 |
| $spacing-form_section_text-paddingBottom | $spacing-tight | 表单分组标题底部内边距 |
| $spacing-form_section_text-marginBottom | $spacing-extra-tight | 表单分组标题底部外边距 |
| $spacing-form_section_text-paddingTop | 0px | 表单分组标题顶部内边距 |
| $spacing-form_section_text-marginTop | 0px | 表单分组标题顶部外边距 |
FAQ
-
为什么我声明了表单,对值进行了修改,数据没有自动映射到 formState.values 中?
请检查是否正确传入了 field,Field 上的field属性是必填项!!! -
为什么传入了 defaultValue、defaultChecked 不生效?
请参考文档开头表单控件,Field 组件对默认值做了统一处理,你应该使用initValue或者initValues来传入默认值 -
为什么异步更新了 initValue、initValues 后,组件没有发生变化,值没有生效?
initValue、initValues只在 Field、Form mounted 时进行消费,后续做的异步更新并不会起效。
如果你的初始值需要从远程取,那么你可以在获取到值之后,使用formApi.setValue/setValues进行更新。
或者直接给 Form、Field 传入一个新的key强制它重新挂载 -
为什么调用了 formApi.setValues 更新 fields 的值,但是实际渲染并没有更新?
setValues 默认情况下对尚未存在的 field 进行赋值不会生效。如果你的 fields 是动态加载的话,请检查在 setValues 时,该 field 是否已 mounted。
如有需要,可以使用 override 模式formApi.setValues(newValue, { isOverride: true }) -
为什么 rules 中的 validator 校验失败,但是对应的错误信息没有被展示?
async-validator 的自定义 validator 返回值必须是 boolean 类型,否则它不执行任何回调,semi 后续的钩子也不会被调用。建议通过加 !! 或者 Boolean() 强制转换返回类型
-
为什么 getValues 拿不到某个 field?
field 没有初始值的话,
getValues获取不到这一项。可以设置initValues/initValue或者给 form 设置allowEmpty属性。 -
为什么在输入框(如 Input)上敲击 enter 回车键会触发了 Form 的 submit?
这个是标准 HTML 行为,我们不计划进行干预,会与原生保持一致。如果表单内确实只有一个 Input 元素,又不想回车时触发 submit 回调,建议对 Input 的 keydown 事件的 enter 采取 preventDefault 阻止默认行为。
点击 #767 查看相关背景和内容。
-
表单会自动保存历史输入项,不想要这个功能怎么办?
在 v2.3 之前,Form 并没有严格按照 A11y 无障碍标准为输入控件配置for、name、id等属性,因此在之前的版本并没有该功能。v2.3 后我们严格按照 W3C 标准进行了实现。如果你不希望浏览器自动保存历史输入项,也可以通过在 Form 级别或者 Field 级别设置autoComplete=off关掉

