--- localeCode: zh-CN order: 21 category: 输入类 title: Form 表单 icon: doc-form dir: column --- ## 表单(Form) - **按需重绘**,避免了不必要的全量渲染, 性能更高 - 简单易用,**结构极简**,避免了不必要的层级嵌套 - 在 Form 外部可方便地获取 formState / fieldState 提供在外部对表单内部进行操作的方法:formApi / fieldApi - 支持将自定义组件封装成表单控件,你可以通过 Form 提供的扩展机制(withField HOC)快捷接入自己团队的组件 - 支持 Form level/Field level 级别的赋值、校验(同步/异步) ## 表单控件(Field) Semi 将所有自带的输入控件(文本输入框、下拉选择、复选框、单选框等)都使用 withField 封装了一次。 接管了他们的数据流(value & onChange) 使用的时候,需要从 Form 中导出(注意:从 Form 导出的控件才具有数据同步功能) #### 目前 Form 提供了如下表单控件 - `Input`、`InputNumber`、`TextArea`、`Select`、`Checkbox`、`Radio`、`RadioGroup`、`Switch`、`DatePicker`、`TimePicker`、`Slider`、`InputGroup`、`TreeSelect`、`Cascader`、`Rating`、`AutoComplete`、`Upload`、`Label`、`ErrorMessage`、`Section`、`TagInput` 都挂载在 Form 下,使用时直接以声明即可 ```javascript import import { Form } from '@douyinfe/semi-ui'; // 具有数据同步功能的表单控件,在
内使用时,数据流会被Form自动接管 // 从Form中导出表单控件时,你还可以进行重命名(这里命名为FormInput仅仅是为了在以下示例中跟普通Input做区分) const FormInput = Form.Input; const FormSelect = Form.Select; const Option = FormSelect.Option; // 普通Input,在
内部使用时,Form不会对其做任何处理 import { Input } from '@douyinfe/semi-ui'; ``` Form 提供的 Field 级别组件,它的 value(或者 valueKey 指定的其他属性)、onChange(或 onKeyChangeFnName 指定的其他回调函数) 属性都会被 Form 劫持,所以
1. 你不需要也不应该用 onChange 来作同步,当然你可以继续监听 onChange 事件获取最新的值
2. 你不能再用控件的`value`、`defaultValue`、`checked`、`defaultChecked`等属性来设置表单控件的值,默认值可以通过 Field 的`initValue`或者 Form 的`initValues`设置
3. 你不应该直接修改 FormState 的值,所有对 Form 内数据的修改都应该通过提供的formApi、fieldApi来完成
## 代码演示 ### 声明表单的多种写法 Semi Form 同时支持多种写法 #### 基本写法 给表单控件添加`field`属性即可 还可以给每个表单控件设置`label`属性,不传入时默认与 field 相同 对于Field级别组件来说,field 属性是必填项! ```jsx live=true dir="column" import React from 'react'; import { Form } from '@douyinfe/semi-ui'; import { IconHelpCircle } from '@douyinfe/semi-icons'; () => { const { Option } = Form.Select; return (
console.log(values)}> }} style={{width:176}}/> ); }; ``` #### 支持的其他写法 当你需要在 Form 结构内部直接获取到 `formState`、`formApi`、`values` 等值时,你还可以使用以下的写法 #### 通过 render 属性传入 即 render props ```jsx live=true dir="column" hideInDSM import React from 'react'; import { Form } from '@douyinfe/semi-ui'; () => { return (
( <> 管理员 普通用户 访客 {JSON.stringify(formState)} )} layout='horizontal' onValueChange={values=>console.log(values)}> ); }; ``` #### 通过 child render function Form 的 children 是一个 function,return 出所有表单控件 ```jsx live=true dir="column" hideInDSM import React from 'react'; import { Form } from '@douyinfe/semi-ui'; () => { return (
console.log(values)}> { ({ formState, values, formApi }) => ( <> 管理员 普通用户 访客 {JSON.stringify(formState)} ) } ); }; ``` #### 通过 props.component 通过 component 属性直接将整个内部结构以 ReactNode 形式传入 ```jsx live=true dir="column" hideInDSM import React from 'react'; import { Form } from '@douyinfe/semi-ui'; () => { const fields = ({ formState, formApi, values }) => ( <> {JSON.stringify(formState)} ); return
console.log(values)}/>; }; ``` ### 已支持的表单控件 > Form.TreeSelect、Form.Cascader、Form.Rating 在 v0.22.0 及之后的版本开始提供; > Form.AutoComplete 在 v0.28.0 及之后的版本开始提供 > Form.Upload 在 v1.0.0 及之后的版本开始提供 > Form.TagInput 在 v1.21.0 及之后的版本开始提供 ```jsx live=true dir="column" import React from 'react'; import { Form, Col, Row, Button } from '@douyinfe/semi-ui'; import { IconUpload } from '@douyinfe/semi-icons'; class BasicDemoWithInit extends React.Component { constructor() { super(); this.state = { initValues: { name: 'semi', business: ['ulikeCam'], role: 'ued', switch: true, files: [ { uid: '1', name: 'vigo.png', status: 'success', size: '130KB', preview: true, url: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/vigo.png' }, { uid: '2', name: 'jiafang1.jpeg', status: 'validateFail', size: '222KB', percent: 50, preview: true, fileInstance: new File([new ArrayBuffer(2048)], 'jiafang1.jpeg', { type: 'image/jpeg' }), url: 'https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/bf8647bffab13c38772c9ff94bf91a9d.jpg' }, { uid: '3', name: 'jiafang2.jpeg', status: 'uploading', size: '222KB', percent: 50, preview: true, fileInstance: new File([new ArrayBuffer(2048)], 'jiafang2.jpeg', { type: 'image/jpeg' }), url: 'https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/bf8647bffab13c38772c9ff94bf91a9d.jpg' } ] } }; this.getFormApi = this.getFormApi.bind(this); } getFormApi(formApi) { this.formApi = formApi; } render() { const { Section, Input, InputNumber, AutoComplete, Select, TreeSelect, Cascader, DatePicker, TimePicker, TextArea, CheckboxGroup, Checkbox, RadioGroup, Radio, Slider, Rating, Switch, TagInput } = Form; const { initValues } = this.state; const plainOptions = ['A', 'B', 'C']; const style = { width: '90%' }; 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', } ]; return ( console.log(v)} >
) } ); ``` ### 表单布局 - 垂直布局:表单控件之间上下垂直排列(默认) Semi Design 更推荐表单采用垂直布局 ```jsx live=true dir="column" import React from 'react'; import { Form, Toast, Button } from '@douyinfe/semi-ui'; () => { const handleSubmit = (values) => { console.log(values); Toast.info('表单已提交'); }; return (
handleSubmit(values)} style={{width: 400}}> {({formState, values, formApi}) => ( <> I have read and agree to the terms of service

Or

)}
); }; ``` - 水平布局:表单控件之间水平排列 你可以通过设置 layout='horizontal'来使用水平布局 ```jsx live=true dir="column" import React from 'react'; import { Form } from '@douyinfe/semi-ui'; () => (
); ``` - labelPosition、labelAlign 你可以通过设置 labelPosition、labelAlign 控制 label 在 Field 中出现的位置,文本对齐的方向 ```jsx live=true dir="column" import React from 'react'; import { Form, Select } from '@douyinfe/semi-ui'; class BasicDemo extends React.Component { constructor() { super(); this.state = { labelPosition: 'left', labelAlign: 'left', labelWidth: '180px' }; this.changeLabelPos = this.changeLabelPos.bind(this); this.changeLabelAlign = this.changeLabelAlign.bind(this); } changeLabelPos(labelPosition) { let labelWidth; labelPosition === 'left' ? labelWidth = '180px' : labelWidth = 'auto'; this.setState({ labelPosition, labelWidth }); } changeLabelAlign(labelAlign) { this.setState({ labelAlign }); } render() { const { labelPosition, labelAlign, labelWidth } = this.state; return ( <>
切换Label位置: 切换Label文本对齐方向:
value === 'muji', message: 'not muji' } ]} /> mike jane kate admin user guest root man woman ); } } ``` - 更复杂的布局 你还可以结合 Grid 提供的 Row、Col,来对表单进行你想要的排列 ```jsx live=true dir="column" import React from 'react'; import { Form, Col, Row } from '@douyinfe/semi-ui'; () => (
value === 'muji', message: 'not muji' } ]} /> Semi 轻颜相机 今日头条 value === 'muji', message: 'not muji' } ]} /> Semi 轻颜相机 今日头条 运营 开发 产品 设计
); ``` ### 表单分组 字段数量较多的表单应考虑对字段进行分组,可以使用`Form.Section`对 Fields 进行分组(仅影响布局,不会影响数据结构) ```jsx live=true dir="column" import React from 'react'; import { Form, Button, Radio } from '@douyinfe/semi-ui'; () => { const { Section, Input, DatePicker, TimePicker, Select, Switch, InputNumber, Checkbox, CheckboxGroup, RadioGroup } = Form; return (
到时间自动交卷
永久有效 自定义有效期 自动放出
自定义放出时间
); }; ``` ### wrapperCol / labelCol 需要为 Form 内的所有 Field 设置统一的布局时,可以在 Form 上设置 wrapperCol 、labelCol 快速生成布局,无需手动使用 Row、Col 手动布局 `wrapperCol`、`labelCol`属性配置参考[Col 组件](/zh-CN/basic/grid#Col) ```jsx live=true dir="column" import React from 'react'; import { Form } from '@douyinfe/semi-ui'; () => (
运营 开发 产品 设计 ); ``` ### 隐藏Label Form 会自动为 Field 控件插入 Label。如果你不需要自动插入 Label 模块, 可以通过在 Field 中设置`noLabel=true`将自动插入 Label 功能关闭 ```jsx live=true dir="column" import React from 'react'; import { Form } from '@douyinfe/semi-ui'; () => (
console.log(values)} style={{ width: 400 }}> 运营 开发 产品 设计 ); ``` ### 导出 Label、ErrorMessage 使用 如果你需要 Form.Label、Form.ErrorMessage 模块自行组合使用,可以从 Form 中导出 - Label 的 API 详见[Label](#Form.Label) - ErrorMessage 的 API 详见[ErrorMessage](#Form.ErrorMessage) 例如:当自带的 Label、ErrorMessage 布局不满足业务需求,需要自行组合位置,但又希望能直接使用 Label、ErrorMessage 的默认样式时 ``` import { Form } from '@douyinfe/semi-ui'; const { Label, ErrorMessage } = Form; ``` ### 使用 Form.Slot 放置自定义组件 当你的自定义组件,需要与 Field 组件保持同样的布局样式时,你可以通过 Form.Slot 放置你的自定义组件 在 Form 组件上设置的 labelWidth、labelAlign、wrapperCol、labelCol 会自动作用在 Form.Slot 上 Slot 属性配置详见[Form.Slot](#Form.Slot) ```jsx live=true dir="column" import React from 'react'; import { Form } from '@douyinfe/semi-ui'; class AssistComponent extends React.Component { constructor() { super(); } render() { return (
console.log(v)} onSubmit={v=>console.log(v)} style={{width: 600}} labelPosition='left' labelWidth={100} > 脸部贴纸 前景贴纸
我是Semi Form SlotA, 我是自定义的ReactNode
我是Semi Form SlotB, 我的Label Align、Width与众不同
);} } ``` ### 内嵌 Label 通过将 labelPosition 设为`inset`,可以将 Label 内嵌在表单控件中。目前支持这项功能的组件有`Input`、`InputNumber`、`DatePicker`、`TimePicker`、`Select`、`TreeSelect`、`Cascader` ```jsx live=true dir="column" import React from 'react'; import { Form } from '@douyinfe/semi-ui'; () => (
运营 开发 产品 设计 ); ``` ### 使用 helpText、extraText 放置提示信息 可以通过`helpText`放置自定义提示信息,与校验信息(error)公用同一区块展示,两者均有值时,优先展示校验信息。 可以通过`extraText`放置额外的提示信息,当需要错误信息和提示文案同时出现时,可以使用这个配置,常显,位于 helpText/error 后 当传入 validateStatus 时,优先展示 validateStatus 值对应的 UI 样式。不传入时,以 field 内部校验状态为准。 ```jsx live=true dir="column" import React from 'react'; import { Form } from '@douyinfe/semi-ui'; class HelpAndExtra extends React.Component { constructor(props) { super(props); this.state = { helpText: '', validateStatus: 'default' }; this.formApi = null; this.getFormApi = this.getFormApi.bind(this); this.validate = this.validate.bind(this); this.random = this.random.bind(this); } getFormApi(formApi) { this.formApi = formApi; } validate(val, values) { if (!val) { this.setState({ validateStatus: 'error' }); return 密码不能为空; } else if (val && val.length <= 3) { this.setState({ helpText: 密码强度:弱, validateStatus: 'warning' }); // show helpText return ''; // validate pass } else { this.setState({ helpText: '', validateStatus: 'success' }); return ''; } } random() { let pw = (Math.random() * 100000).toString().slice(0, 5); this.formApi.setValue('Password', pw); this.formApi.setError('Password', ''); this.setState({ helpText: '', validateStatus: 'success' }); } render() { let { helpText, validateStatus } = this.state; return (
console.log('submit success')} onSubmitFail={(errors) => console.log(errors)} > 没有想到合适的密码?点击随机生成一个 } >
); } } ``` 通过配置`extraTextPosition`,你可以控制extraText的显示位置。可选值 `bottom`、`middle` 例如如当你希望将extraText 提示信息显示在Label与Field控件中间时 该属性可在Form上统一配置,亦可在每个Field上单独配置,同时传入时,以Field的配置为准。 ```jsx live=true dir="column" import React from 'react'; import { Form } from '@douyinfe/semi-ui'; () => { const options = [ { label: '飞书通知', value: 'lark' }, { label: '邮件通知', value: 'email' }, { label: '顶部横幅通知', value: 'notification' } ]; const notifyText = '未勾选时,默认为红点提醒,消息默认进入收件人消息列表。对于重要通知,可同时勾选相应的通知方式。'; const forceText = '对于对话框通知,可指定该消息必须在指定时长后才可置为已读。'; return (
); }; ``` ### 使用 InputGroup 组合多个 Field 当你需要将一些表单控件组合起来使用时,你可以用`Form.InputGroup`将其包裹起来 当你给`Select`、`Input`等表单控件加上 field 属性时,`Form`会默认给每个 Field 控件自动插入`Label` 而在`InputGroup`中一般仅需要一个属于整个 Group 的 Label,你可以在 InputGroup 中设置 label 属性,插入一个属于 Group 的`Label` `label`可配置属性详见[Label](#Form.Label) ```jsx live=true dir="column" import React from 'react'; import { Form, Button } from '@douyinfe/semi-ui'; () => (
console.log(values)} labelPosition='top' style={{ width: 400 }}> 手机号码), required: true }} labelPosition='top'> 美国+1 香港+852 中国+86 日本+81
); ``` ### Modal 弹出层中的表单 你可以将 Form 放置于 Modal 中,以弹窗形式承载 在提交时,通过 formApi.validate()对 Field 进行集中校验 ```jsx live=true dir="column" import React from 'react'; import { Form, Modal, Button, Row, Col } from '@douyinfe/semi-ui'; class ModalFormDemo extends React.Component { constructor(props) { super(props); this.state = { visible: false, }; this.showDialog = this.showDialog.bind(this); this.handleOk = this.handleOk.bind(this); this.handleCancel = this.handleCancel.bind(this); this.getFormApi = this.getFormApi.bind(this); } showDialog() { this.setState({ visible: true }); } handleOk() { this.formApi.validate() .then((values) => { console.log(values); }) .catch((errors) => { console.log(errors); }); } handleCancel() { this.setState({ visible: false }); } getFormApi(formApi) { this.formApi = formApi; } render(){ const {visible} = this.state; let message = '该项为必填项'; return ( <>
中国 美国 欧洲 日本 中国 美国 欧洲 日本
); } } ``` ### 配置初始值与校验规则 - 你可以通过`rules`为每个 Field 表单控件配置校验规则 Form 内部的校验库基于 async-validator,更多配置规则可查阅其[官方文档](https://github.com/yiminghe/async-validator) - 你可以通过 form 的`initValues`为整个表单统一设置初始值,也可以在每个 field 中通过`initValue`设置初始值(后者优先级更高) ```jsx live=true dir="column" hideInDSM import React from 'react'; import { Form, Button } from '@douyinfe/semi-ui'; class BasicDemoWithInit extends React.Component { constructor() { super(); this.state = { initValues: { name: 'semi', role: 'rd' } }; this.getFormApi = this.getFormApi.bind(this); } getFormApi(formApi) { this.formApi = formApi; } render() { const { Select, Input } = Form; const style = { width: '100%' }; return (
value === 'muji', message: 'not muji' } ]} />
); } } ``` ### 自定义校验(Form 级别) 你可以给`Form`整体设置自定义校验函数 validateFields,submit 或调用formApi.validate()时会进行调用 #### 同步校验 校验通过时,你应该返回一个空字符串; 校验失败时,你应该返回错误信息(Object,key 为 fieldName,value 为对应的错误信息) ```jsx live=true dir="column" hideInDSM import React from 'react'; import { Form, Button } from '@douyinfe/semi-ui'; class FormLevelValidateSync extends React.Component { constructor() { super(); this.syncValidate = this.syncValidate.bind(this); } syncValidate(values) { const errors = {}; if (values.name !== 'mike') { errors.name = 'you must name mike'; } if (values.sex !== 'female') { errors.sex = 'must be woman'; } errors.familyName = [ { before: 'before errror balabala ', after: 'after error balabala' }, 'familyName[1] error balabala' ]; return errors; } render() { return (
); } } ``` #### 异步校验 异步校验时,你应当返回一个 promise,在 promise.then()中 你需要 return 对应的错误信息 ```jsx live=true dir="column" hideInDSM import React from 'react'; import { Form, Button } from '@douyinfe/semi-ui'; class FormLevelValidateAsync extends React.Component { constructor() { super(); this.asyncValidate = this.asyncValidate.bind(this); } asyncValidate(values) { const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); return sleep(2000).then(() => { let errors = {}; if (values.name !== 'mike') { errors.name = 'you must name mike'; } if (values.sex !== 'female') { errors.sex = 'sex not valid'; } return errors; }); } render() { return (
); } } ``` ### 自定义校验(Field 级别) 你可以指定单个表单控件的自定义校验函数,支持同步、异步校验(通过返回 promise) ```jsx live=true dir="column" hideInDSM import React from 'react'; import { Form, Button } from '@douyinfe/semi-ui'; class FieldLevelValidateDemo extends React.Component { constructor() { super(); this.validateName = this.validateName.bind(this); this.asyncValidate = this.asyncValidate.bind(this); } validateName(val) { if (!val) { return '【sync】can\'t be empty'; } else if (val.length <= 5) { return '【sync】must more than 5'; } return ''; } asyncValidate(val, values) { const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); return sleep(2000).then(() => { if (!val) { return '【async】can\'t be empty'; } else if (val.length <= 5) { return '【async】must more than 5'; } else { return ''; } }); } render() { return (
); } } ``` ### 表单联动 你可以通过监听 Field 的 onChange 事件,然后使用 formApi 进行相关修改,来使 Field 之间达到联动 ```jsx live=true dir="column" hideInDSM import React from 'react'; import { Form, Button, Row } from '@douyinfe/semi-ui'; class LinkFieldForm extends React.Component { constructor() { super(); this.getFormApi = this.getFormApi.bind(this); this.handleSelectChange = this.handleSelectChange.bind(this); } handleSelectChange(value) { let text = value === 'male' ? 'Hi male' : 'Hi female!'; this.formApi.setValue('Note', text); } getFormApi(formApi) { this.formApi = formApi; } render() { return (
console.log(values) } style={{ width: 250 }}> Note will change after Sex select female male ); } } ``` ### 动态表单 #### 动态删减表单项 ```jsx live=true dir="column" import React from 'react'; import { Form, Button } from '@douyinfe/semi-ui'; () => (
{({ formState }) => ( yes no {formState.values.isAnchor === 'yes' ? ( ) : null} )}
); ``` #### 数组类动态增删表单项-使用 ArrayField 针对动态增删的数组类表单项,我们提供了 ArrayField 作用域来简化 add/remove 的操作 ArrayField 自带了 add、remove、addWithInitValue 等 api 用来执行新增行,删除行,新增带有初始值的行等操作 注意:ArrayField 的 initValue 类型必须是数组 ```jsx live=true dir="column" hideInDSM import React from 'react'; import { ArrayField, TextArea, Form, Button, useFormState } from '@douyinfe/semi-ui'; import { IconPlusCircle, IconMinusCircle } from '@douyinfe/semi-icons'; class ArrayFieldDemo extends React.Component { constructor() { super(); this.state = { menu: [ { name: '脸部贴纸', type: '2D' }, { name: '前景贴纸', type: '3D' }, ] }; } render() { let { menu } = this.state; const ComponentUsingFormState = () => { const formState = useFormState(); return (