UNPKG

@douyinfe/semi-ui

Version:

A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.

1,329 lines (1,182 loc) 137 kB
--- localeCode: zh-CN order: 40 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 下,使用时直接以<Form.Input\> 、<Form.Select\>声明即可 ```javascript import import { Form } from '@douyinfe/semi-ui'; // 具有数据同步功能的表单控件,在<Form></Form>内使用时,数据流会被Form自动接管 // 从Form中导出表单控件时,你还可以进行重命名(这里命名为FormInput仅仅是为了在以下示例中跟普通Input做区分) const FormInput = Form.Input; const FormSelect = Form.Select; const Option = FormSelect.Option; // 普通Input,在<Form></Form>内部使用时,Form不会对其做任何处理 import { Input } from '@douyinfe/semi-ui'; ``` Form 提供的 Field 级别组件,它的 value(或者 valueKey 指定的其他属性)、onChange(或 onKeyChangeFnName 指定的其他回调函数) 属性都会被 Form 劫持,所以 <Notice type="primary" title="注意事项"> 1. 你不需要也不应该用 onChange 来作同步,当然你可以继续监听 onChange 事件获取最新的值 2. 你不能再用控件的 `value`、`defaultValue`、`checked`、`defaultChecked` 等属性来设置表单控件的值,默认值可以通过 Field 的 `initValue` 或者 Form 的 `initValues` 设置 3. 你不应该直接修改 FormState 的值,所有对 Form 内数据的修改都应该通过提供的 formApi、fieldApi 来完成 </Notice> ## 代码演示 ### 声明表单的多种写法 Semi Form 同时支持多种写法 #### 基本写法 从 Form 中导出表单控件,给表单控件添加`field`属性,将其放置于 Form 内部即可 还可以给每个表单控件设置`label`属性,不传入时默认与 field 相同 `label`可以直接传入字符串,亦可以以 object 方式声明,配置 `extra`、`required`、`optional`等属性应对更复杂的场景 <Notice type='primary' title='注意事项'> 对于Field级别组件来说,field 属性是必填项! </Notice> ```jsx live=true dir="column" import React from 'react'; import { Form, Tooltip } from '@douyinfe/semi-ui'; import { IconHelpCircle } from '@douyinfe/semi-icons'; () => { const { Option } = Form.Select; return ( <Form layout='horizontal' onValueChange={values=>console.log(values)}> <Form.Input field='UserName' label='用户名' style={{ width: 80 }}/> <Form.Input field='Password' label={{ text: '密码', extra: <Tooltip content='详情'><IconHelpCircle style={{ color: 'var(--semi-color-text-2)' }}/></Tooltip> }} style={{ width: 176 }} /> <Form.Select field="Role" label={{ text: '角色', optional: true }} style={{ width: 176 }}> <Option value="admin">管理员</Option> <Option value="user">普通用户</Option> <Option value="guest">访客</Option> </Form.Select> </Form> ); }; ``` #### 支持的其他写法 当你需要在 Form 结构内部直接获取到 `formState`、`formApi`、`values` 等值时,你还可以使用以下的写法 <Notice type='primary' title='注意事项'> 注意,此处获取的 formState、values 等并没有经过 deepClone。你应该只做读操作,而不应该做写操作,否则会导致你可能意外修改了form内部的状态。所有对 Form 内部状态的更新都应该通过 formApi 去操作 </Notice> #### 通过 render 属性传入 即 render props ```jsx live=true dir="column" hideInDSM import React from 'react'; import { Form } from '@douyinfe/semi-ui'; () => { return ( <Form render={({ formState, formApi, values }) => ( <> <Form.Select field="Role" label='角色' style={{ width: 176 }}> <Form.Select.Option value="admin">管理员</Form.Select.Option> <Form.Select.Option value="user">普通用户</Form.Select.Option> <Form.Select.Option value="guest">访客</Form.Select.Option> </Form.Select> <Form.Input field='UserName' label='用户名' style={{ width: 80 }}/> <Form.Input field='Password' label='密码' style={{ width: 176 }}/> <code style={{ marginTop: 24 }}>{JSON.stringify(formState)}</code> </> )} layout='horizontal' onValueChange={values=>console.log(values)}> </Form> ); }; ``` #### 通过 child render function Form 的 children 是一个 function,return 出所有表单控件 ```jsx live=true dir="column" hideInDSM import React from 'react'; import { Form } from '@douyinfe/semi-ui'; () => { return ( <Form layout='horizontal' onValueChange={values=>console.log(values)}> { ({ formState, values, formApi }) => ( <> <Form.Select field="Role" label='角色' style={{ width: 176 }}> <Form.Select.Option value="admin">管理员</Form.Select.Option> <Form.Select.Option value="user">普通用户</Form.Select.Option> <Form.Select.Option value="guest">访客</Form.Select.Option> </Form.Select> <Form.Input field='UserName' label='用户名' style={{ width: 80 }} /> <Form.Input field='Password' label='密码' style={{ width: 176 }}/> <code style={{ marginTop: 24 }}>{JSON.stringify(formState)}</code> </> ) } </Form> ); }; ``` #### 通过 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 }) => ( <> <Form.Input field='Role' style={{ width: 176 }}/> <Form.Input field='UserName' style={{ width: 80 }}/> <Form.Input field='Password' style={{ width: 176 }}/> <code style={{ marginTop: 24 }}>{JSON.stringify(formState)}</code> </> ); return <Form component={fields} layout='horizontal' onValueChange={values=>console.log(values)}/>; }; ``` ### 已支持的表单控件 ```jsx live=true dir="column" import React from 'react'; import { Form, Col, Row, Button, Space } from '@douyinfe/semi-ui'; import { IconUpload } from '@douyinfe/semi-icons'; () => { const 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: 'resso.jpeg', status: 'validateFail', size: '222KB', percent: 50, preview: true, fileInstance: new File([new ArrayBuffer(2048)], 'resso.jpeg', { type: 'image/jpeg' }), url: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/Resso.png' }, { 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: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/dy.png' } ] }; const { Section, Input, InputNumber, AutoComplete, Select, TreeSelect, Cascader, DatePicker, TimePicker, TextArea, CheckboxGroup, Checkbox, RadioGroup, Radio, Slider, Rating, Switch, TagInput } = Form; 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 ( <Form initValues={initValues} style={{ padding: 10, width: '100%' }} onValueChange={(v)=>console.log(v)} > <Section text={'基本信息'}> <Row> <Col span={12}> <Input field="name" label="名称(Input)" initValue={'mikeya'} style={style} trigger='blur' /> </Col> <Col span={12}> <DatePicker field="date" label='日期(DatePicker)' style={style} initValue={new Date()} placeholder='请选择生效日期' /> </Col> </Row> <Row> <Col span={12}> <Select field="role" style={style} label='角色(Select)' placeholder='请选择你的角色'> <Select.Option value="operate">运营</Select.Option> <Select.Option value="rd">开发</Select.Option> <Select.Option value="pm">产品</Select.Option> <Select.Option value="ued">设计</Select.Option> </Select> </Col> <Col span={12}> <Select field="business" multiple style={style} placeholder='请选择业务线' label="业务线(多选Select)" extraText={ <div style={{ color: 'rgba(var(--semi-blue-5), 1)', fontSize: 14, userSelect: 'none', cursor: 'pointer' }}> 没有找到合适的业务线? </div> } > <Select.Option value="abc">Semi</Select.Option> <Select.Option value="ulikeCam">轻颜相机</Select.Option> <Select.Option value="toutiao">今日头条</Select.Option> </Select> </Col> </Row> <Row> <Col span={12}> <Form.Cascader placeholder="请选择所在地区" treeData={treeData} field='area' label='地区(Cascader)' style={style} > </Form.Cascader> </Col> <Col span={12}> <Form.TreeSelect field="tree" style={style} label='节点(TreeSelect)' placeholder='请选择服务节点' treeData={treeData} filterTreeNode > </Form.TreeSelect> </Col> </Row> <Row> <Col span={12}> <TagInput field="product" label='产品(TagInput)' initValue={['abc', 'ulikeCam']} placeholder='请输入产品' style={style} /> </Col> </Row> <Row> <Col span={24}> <Form.Upload field='files' label='证明文件(Upload)' action='//semi.design/api/upload' > <Button icon={<IconUpload />} theme="light"> 点击上传 </Button> </Form.Upload> </Col> </Row> </Section> <Section text='资源详情'> <Row> <Col span={12}> <TextArea style={{ ...style, height: 120 }} field='description' label='申请理由(TextArea)' placeholder='请填写申请资源理由' /> </Col> <Col span={12}> <CheckboxGroup field="type" direction='horizontal' label='申请类型(CheckboxGroup)' initValue={['user', 'admin']} rules={[ { required: true } ]} > <Checkbox value="admin">admin</Checkbox> <Checkbox value="user">user</Checkbox> <Checkbox value="guest">guest</Checkbox> <Checkbox value="root">root</Checkbox> </CheckboxGroup> <RadioGroup field="isMonopolize" label='是否独占资源(Radio)' rules={[ { type: 'boolean' }, { required: true, message: '必须选择是否独占 ' } ]}> <Radio value={1}>是</Radio> <Radio value={0}>否</Radio> </RadioGroup> </Col> </Row> <Row> <Col span={12}> <TimePicker field="time" label='截止时刻(TimePicker)' style={{ width: '90%' }}/> </Col> <Col span={12}> <InputNumber field='number' label='申请数量(InputNumber)' initValue={20} style={style}/> </Col> </Row> <Row> <Col span={12}> <Slider field="range" label='资源使用报警阈值(%)(Slider)' initValue={10} style={{ width: '90%' }}/> </Col> <Col span={12}> <Switch field='switch' label='开关(Switch)'/> </Col> </Row> <Row> <Col span={12}> <Rating field="rating" label='满意度(Rating)' initValue={2} style={{ width: '90%' }}/> </Col> </Row> </Section> <Checkbox value="false" field="agree" noLabel={true}> 我已阅读并清楚相关规定(Checkbox) </Checkbox> <Space> <Button type="primary" htmlType="submit" className="btn-margin-right">提交(submit)</Button> <Button htmlType="reset">重置(reset)</Button> </Space> </Form> ); }; ``` ### 表单控件值的绑定 每个表单控件都需要以`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 | ```jsx live=true dir="column" hideInDSM import React from 'react'; import { Form, Toast, Row, Col, TextArea } from '@douyinfe/semi-ui'; () => ( <Form onSubmit={values => Toast.info({ content: JSON.stringify(values) })} > { ({ formState, values, formApi }) => ( <Row> <Col span={12}> <Form.Input field='username' placeholder='请尝试输入值'/> <Form.Input field='user[0]' placeholder='请尝试输入值'/> <Form.Input field='siblings.1' placeholder='请尝试输入值'/> <Form.Input field="siblings['2']" placeholder='请尝试输入值'/> <Form.Input field='parents[0].name' placeholder='请尝试输入值'/> <Form.Input field="parents[1]['name']" placeholder='请尝试输入值'/> </Col> <Col span={10} offset={1} style={{ marginTop: 12 }}> <Form.Label text='FormState实时映射值:'></Form.Label> <TextArea value={JSON.stringify(formState.values)}></TextArea> </Col> </Row> ) } </Form> ); ``` ### 表单布局 - 垂直布局:表单控件之间上下垂直排列(默认) 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 ( <Form onSubmit={values => handleSubmit(values)} style={{ width: 400 }}> {({ formState, values, formApi }) => ( <> <Form.Input field='phone' label='PhoneNumber' style={{ width: '100%' }} placeholder='Enter your phone number'></Form.Input> <Form.Input field='password' label='Password' style={{ width: '100%' }} placeholder='Enter your password'></Form.Input> <Form.Checkbox field='agree' noLabel>I have read and agree to the terms of service</Form.Checkbox> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <p> <span>Or</span><Button theme='borderless' style={{ color: 'var(--semi-color-primary)', marginLeft: 10, cursor: 'pointer' }}>Sign up</Button> </p> <Button disabled={!values.agree} htmlType='submit' type="tertiary">Log in</Button> </div> </> )} </Form> ); }; ``` - 水平布局:表单控件之间水平排列 你可以通过设置 `layout='horizontal'`来使用水平布局 ```jsx live=true dir="column" import React from 'react'; import { Form } from '@douyinfe/semi-ui'; () => ( <Form layout='horizontal'> <Form.Input field='phone' label='PhoneNumber' placeholder='Enter your phone number'></Form.Input> <Form.Input field='password' label='Password' placeholder='Enter your password'></Form.Input> </Form> ); ``` - 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 ( <> <div style={{ borderBottom: '1px solid var(--semi-color-border)', paddingBottom: 12 }}> <Form.Label style={{ marginLeft: 10 }}>切换Label位置:</Form.Label> <Select onChange={this.changeLabelPos} value={labelPosition} style={{ width: 200 }} prefix='labelPosition'> <Select.Option value='top'>top</Select.Option> <Select.Option value='left'>left</Select.Option> </Select> <Form.Label style={{ marginLeft: 10 }}>切换Label文本对齐方向:</Form.Label> <Select onChange={this.changeLabelAlign} value={labelAlign} style={{ width: 200 }} prefix='labelAlign'> <Select.Option value='left'>left</Select.Option> <Select.Option value='right'>right</Select.Option> </Select> </div> <Form labelPosition={labelPosition} labelWidth={labelWidth} labelAlign={labelAlign} key={labelPosition + labelAlign} style={{ padding: '10px', width: 600 }}> <Form.Input field="input" label="手机号码" trigger='blur' style={{ width: 200 }} rules={[ { required: true, message: 'required error' }, { type: 'string', message: 'type error' }, { validator: (rule, value) => value === 'semi', message: 'should be semi' } ]} /> <Form.Switch label="是否同意" field='agree'/> <Form.InputNumber field='price' label='价格' style={{ width: 200 }}/> <Form.Select label="姓名" field='name' style={{ width: 200 }}> <Form.Select.Option value="mike">mike</Form.Select.Option> <Form.Select.Option value="jane">jane</Form.Select.Option> <Form.Select.Option value="kate">kate</Form.Select.Option> </Form.Select> <Form.CheckboxGroup label="角色" field='role' direction='horizontal'> <Form.Checkbox value="admin">admin</Form.Checkbox> <Form.Checkbox value="user">user</Form.Checkbox> <Form.Checkbox value="guest">guest</Form.Checkbox> <Form.Checkbox value="root">root</Form.Checkbox> </Form.CheckboxGroup> <Form.RadioGroup field="性别"> <Form.Radio value="1">man</Form.Radio> <Form.Radio value="2">woman</Form.Radio> </Form.RadioGroup> </Form> </> ); } } ``` - 更复杂的布局 你还可以结合 Grid 提供的 Row、Col,来对表单进行你想要的排列 ```jsx live=true dir="column" import React from 'react'; import { Form, Col, Row } from '@douyinfe/semi-ui'; () => ( <Form labelPosition='top' getFormApi={this.getFormApi} style={{ padding: '10px' }}> <Row> <Col span={8}> <Form.Input field="nickName1" label="用户名" style={{ width: '250px' }} trigger='blur' rules={[ { required: true, message: 'required error' }, { type: 'string', message: 'type error' }, { validator: (rule, value) => value === 'semi', message: 'should be semi' } ]} /> </Col> <Col span={8}> <Form.DatePicker field='date1' label='有效日期' style={{ width: '250px' }}/> </Col> <Col span={8}> <Form.Select label="业务线" field='business1' style={{ width: '250px' }}> <Form.Select.Option value="abc">Semi</Form.Select.Option> <Form.Select.Option value="ulikeCam">轻颜相机</Form.Select.Option> <Form.Select.Option value="toutiao">今日头条</Form.Select.Option> </Form.Select> </Col> </Row> <Row> <Col span={6}> <Form.Input field="nickName2" label="用户名" style={{ width: '200px' }} trigger='blur' rules={[ { required: true, message: 'required error' }, { type: 'string', message: 'type error' }, { validator: (rule, value) => value === 'semi', message: 'should be semi' } ]} /> </Col> <Col span={6}> <Form.DatePicker field='date2' label='有效日期' style={{ width: '200px' }}/> </Col> <Col span={6}> <Form.Select label="业务线" field='business2' style={{ width: '200px' }}> <Form.Select.Option value="abc">Semi</Form.Select.Option> <Form.Select.Option value="ulikeCam">轻颜相机</Form.Select.Option> <Form.Select.Option value="toutiao">今日头条</Form.Select.Option> </Form.Select> </Col> <Col span={6}> <Form.Select field="role" label='角色' style={{ width: '200px' }}> <Form.Select.Option value="operate">运营</Form.Select.Option> <Form.Select.Option value="rd">开发</Form.Select.Option> <Form.Select.Option value="pm">产品</Form.Select.Option> <Form.Select.Option value="ued">设计</Form.Select.Option> </Form.Select> </Col> </Row> </Form> ); ``` ### 表单分组 字段数量较多的表单应考虑对字段进行分组,可以使用`Form.Section`对 Fields 进行分组(仅影响布局,不会影响数据结构) ```jsx live=true dir="column" import React from 'react'; import { Form, Button, Space } from '@douyinfe/semi-ui'; () => { const { Section, Input, DatePicker, TimePicker, Select, Switch, InputNumber, Checkbox, CheckboxGroup, RadioGroup, Radio } = Form; return ( <Form style={{ width: 560 }}> <Section text={'基本信息'}> <Input field='name' label='考试名称' initValue='TCS任务平台使用' style={{ width: 560 }}/> </Section> <Section text={'合格标准'} > <div style={{ display: 'flex' }}> <InputNumber field='pass' initValue={60} style={{ width: 80 }} label={{ text: '及格正确率', required: true }}/> <InputNumber field='number' initValue={10} style={{ width: 80 }} label={{ text: '合格人数', required: true }}/> </div> </Section> <Section text={'考试时间'} > <DatePicker field='date' type='dateTime' initValue={new Date()} style={{ width: 272 }} label={{ text: '开始时间', required: true }}/> <div style={{ display: 'flex' }}> <Input field='time' label='考试时长' style={{ width: 176 }} initValue={'60'} addonAfter='分钟'/> <Checkbox initValue={true} noLabel field='auto' style={{ paddingTop: 30, marginLeft: 12 }}>到时间自动交卷</Checkbox> </div> <RadioGroup field="type" label='有效时间' direction='vertical' initValue={'always'} > <Radio value="always">永久有效</Radio> <Radio value="user">自定义有效期</Radio> </RadioGroup> <RadioGroup field="answerTime" label='答案放出时间' direction='vertical' initValue={'always'} rules={[ { required: true } ]} > <Radio value="always">自动放出</Radio> <Radio value="user"> <div style={{ display: 'inline-block' }}> 自定义放出时间 <Form.DatePicker type='dateTimeRange' noLabel field='customTime' style={{ width: 464, display: 'inline-block' }}/> </div> </Radio> </RadioGroup> </Section> <Section text={'考试人员'}> <div style={{ display: 'flex' }}> <Switch field='open' label={{ text: '对外开放', required: true }} checkedText='开' uncheckedText='关'></Switch> </div> <Select field='users' label={{ text: '考生', required: true }} style={{ width: 560 }} multiple initValue={['1', '2', '3', '4']} > <Select.Option value='1'>曲晨一</Select.Option> <Select.Option value='2'>夏可曼</Select.Option> <Select.Option value='3'>曲晨三</Select.Option> <Select.Option value='4'>蔡妍</Select.Option> </Select> </Section> <Space> <Button type='primary' theme='solid' style={{ width: 120, marginTop: 12, marginRight: 4 }}>创建考试</Button> <Button style={{ marginTop: 12 }}>预览</Button> </Space> </Form> ); }; ``` ### 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'; () => ( <Form wrapperCol={{ span: 20 }} labelCol={{ span: 2 }} labelPosition='left' labelAlign='right' > <Form.Input field='name' style={{ width: 250 }} label='姓名' trigger='blur' placeholder='请输入姓名'/> <Form.Select field="role" label='角色' placeholder='请选择角色' style={{ width: 250 }}> <Form.Select.Option value="operate">运营</Form.Select.Option> <Form.Select.Option value="rd">开发</Form.Select.Option> <Form.Select.Option value="pm">产品</Form.Select.Option> <Form.Select.Option value="ued">设计</Form.Select.Option> </Form.Select> </Form> ); ``` ### 隐藏Label Form 会自动为 Field 控件插入 Label。如果你不需要自动插入 Label 模块, 可以通过在 Field 中设置`noLabel=true`将自动插入 Label 功能关闭(此时 Field 仍然具备自动展示 ErrorMessage 的能力,因此 DOM 结构与原始控件依然会有区别) 如果你希望与原始控件保持 DOM 结构一致,可以使用 pure=true,此时除了数据流被接管外,DOM结构不会有任何变化(你需要自行负责 ErrorMessage的渲染,同时它也无法被 formProps.wrapperCol 属性影响) ```jsx live=true dir="column" import React from 'react'; import { Form } from '@douyinfe/semi-ui'; () => ( <Form onSubmit={(values) => console.log(values)} style={{ width: 400 }}> <Form.Input field='name' label='姓名' trigger='blur' noLabel={true} style={{ width: 250 }} placeholder='请输入姓名'/> <Form.Input field='purename' pure placeholder='DOM结构与普通 Input 组件完全一致'/> </Form> ); ``` ### 内嵌 Label 通过将 labelPosition 设为`inset`,可以将 Label 内嵌在表单控件中。目前支持这项功能的组件有`Input`、`InputNumber`、`DatePicker`、`TimePicker`、`Select`、`TreeSelect`、`Cascader`、`TagInput` ```jsx live=true dir="column" import React from 'react'; import { Form } from '@douyinfe/semi-ui'; () => ( <Form labelPosition='inset' layout='horizontal'> <Form.Input field='name' label='姓名' trigger='blur' style={{ width: 250 }} placeholder='请输入姓名' initValue='semi'/> <Form.Select field="role" label='角色' style={{ width: '250px' }} initValue='rd'> <Form.Select.Option value="operate">运营</Form.Select.Option> <Form.Select.Option value="rd">开发</Form.Select.Option> <Form.Select.Option value="pm">产品</Form.Select.Option> <Form.Select.Option value="ued">设计</Form.Select.Option> </Form.Select> <Form.DatePicker field="date" label='开始日期' style={{ width: '250px' }} initValue={new Date()}> </Form.DatePicker> </Form> ); ``` ### 导出 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 { render() { return ( <Form onChange={v=>console.log(v)} onSubmit={v=>console.log(v)} style={{ width: 600 }} labelPosition='left' labelWidth={100} > <Form.Input field='特效名称' style={{ width: 250 }}/> <Form.Slot label={{ text: 'SlotA' }} error='我是SlotA的ErrorMessage'> <div style={{ display: 'flex', alignItems: 'center', height: 32, marginTop: 8 }}> 我是Semi Form SlotA, 我是自定义的ReactNode </div> </Form.Slot> <Form.Slot label={{ text: 'SlotB', width: 160, align: 'right' }}> <div style={{ display: 'flex', alignItems: 'center', height: '100%' }}> 我是Semi Form SlotB, 我的Label Align、Width与众不同 </div> </Form.Slot> </Form> ); } } ``` ### 使用 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'; () => { const [helpText, setHelpText] = useState(''); const [validateStatus, setValidateStatus] = useState('default'); const formRef = useRef(); const validator = (val, values) => { if (!val) { setValidateStatus('error'); return <span>密码不能为空</span>; } else if (val && val.length <= 3) { setValidateStatus('warning'); setHelpText(<span style={{ color: 'var(--semi-color-warning)' }}>密码强度:弱</span>); // show helpText return ''; // validate pass } else { setHelpText(''); setValidateStatus('success'); return ''; } }; const random = () => { let pw = (Math.random() * 100000).toString().slice(0, 5); formRef.current.formApi.setValue('Password', pw); formRef.current.formApi.setError('Password', ''); setHelpText(''); setValidateStatus('success'); }; return ( <Form showValidateIcon={true} ref={formRef} onSubmit={(value) => console.log('submit success')} onSubmitFail={(errors) => console.log(errors)} > <Form.Input validator={validator} field="Password" validateStatus={validateStatus} helpText={helpText} extraText={ <div style={{ color: 'var(--semi-color-link)', fontSize: 14, userSelect: 'none', cursor: 'pointer' }} onClick={random} > 没有想到合适的密码?点击随机生成一个 </div> } ></Form.Input> </Form> ); }; ``` 通过配置 `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 ( <Form extraTextPosition='middle'> <Form.CheckboxGroup direction='horizontal' field='notify' label='通知方式' extraText={notifyText} options={options} /> <Form.InputNumber field='force' label='强制读取(可选)' placeholder='秒' extraText={forceText} extraTextPosition='bottom'/> </Form> ); }; ``` ### 使用 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'; () => ( <Form onSubmit={(values) => console.log(values)} labelPosition='top' style={{ width: 400 }}> <Form.InputGroup label={{ text: (<span>手机号码</span>), required: true }} labelPosition='top'> <Form.Select style={{ width: 150 }} field='phonePrefix' initValue='+86' rules={[{ required: true }]} showClear> <Form.Select.Option value='+1'>美国+1</Form.Select.Option> <Form.Select.Option value='+852'>香港+852</Form.Select.Option> <Form.Select.Option value='+86'>中国+86</Form.Select.Option> <Form.Select.Option value='+81'>日本+81</Form.Select.Option> </Form.Select> <Form.Input initValue='18912345678' style={{ width: 250 }} field='phoneNumber' rules={[{ required: true }]} showClear /> </Form.InputGroup> <Form.Input field='姓名' trigger='blur' initValue='Semi'></Form.Input> <Button htmlType='submit'>提交</Button> </Form> ); ``` ### 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 ( <> <Button onClick={this.showDialog}>打开弹窗</Button> <Modal title="新建" visible={visible} onOk={this.handleOk} style={{ width: 600 }} onCancel={this.handleCancel} > <Form getFormApi={this.getFormApi} > <Row> <Col span={5}> <Form.Select field='region' label="国家/地区" placeholder='请选择' style={{ width: '100%' }} rules={[ { required: true, message }, ]} > <Form.Select.Option value="China">中国</Form.Select.Option> <Form.Select.Option value="US">美国</Form.Select.Option> <Form.Select.Option value="Europe">欧洲</Form.Select.Option> <Form.Select.Option value="Japan">日本</Form.Select.Option> </Form.Select> </Col> <Col span={15} offset={2}> <Form.Input field='owner' label="业务执行人" trigger='blur' rules={[ { required: true, message }, ]} /> </Col> </Row> <Row> <Col span={5}> <Form.Select field='area' label="投放区域" placeholder='请选择' style={{ width: '100%' }} rules={[ { required: true, message }, ]} > <Form.Select.Option value="China">中国</Form.Select.Option> <Form.Select.Option value="US">美国</Form.Select.Option> <Form.Select.Option value="Europe">欧洲</Form.Select.Option> <Form.Select.Option value="Japan">日本</Form.Select.Option> </Form.Select> </Col> <Col span={15} offset={2}> <Form.Input field='department' label="业务执行部门" trigger='blur' rules={[ { required: true, message }, ]} /> </Col> </Row> </Form> </Modal> </> ); } } ``` ### 配置初始值与校验规则 - 你可以通过`rules`为每个 Field 表单控件配置校验规则 Form 内部的校验库基于 async-validator,更多配置规则可查阅其[官方文档](https://github.com/yiminghe/async-validator) - 你可以通过 form 的`initValues`为整个表单统一设置初始值,也可以在每个 field 中通过`initValue`设置初始值(后者优先级更高) - 可以通过 trigger 为每个 Field 配置不同的校验触发时机,默认为 change(即onChange触发时,自动进行校验)。还支持 change、blur、mount、custom 或以上的组合。v2.42 后支持通过 FormProps 统一配置, 若都配置时,以 FieldProps 为准 - 可以通过 stopValidateWithError 开关,决定使用 rules 校验时,当碰到第一个检验不通过的 rules 后,是否继续触发后续 rules 的校验。v2.42 后支持通过 FormProps 统一配置,若都配置时,以 FieldProps 为准 ```jsx live=true dir="column" hideInDSM import React from 'react'; import { Form, Button } from '@douyinfe/semi-ui'; () => { const initValues = { name: 'semi', shortcut: 'se' }; const style = { width: '100%' }; const { Select, Input } = Form; return ( <Form initValues={initValues}> <Input field="name" style={style} trigger='blur' rules={[ { 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' } ]} /> <Input field="shortcut" style={style} stopValidateWithError rules={[ { 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' } ]} /> <Button htmlType='submit'>提交</Button> </Form> ); }; ``` ### 自定义校验(Form 级别) 你可以给`Form`整体设置自定义校验函数。推荐使用 `validator`(`validateFields` 为旧写法,仍保持兼容)。submit 或调用 formApi.validate() 时会进行调用 <Notice title='注意'> 当配置了 Form 级别校验器(validator / validateFields)后,Field 级别的校验器(fieldProps.validator / fieldProps.validate、fieldProps.rules 将不再生效) </Notice> #### 同步校验 校验通过时,你应该返回一个空字符串; 校验失败时,你应该返回错误信息(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 ( <Form validator={this.syncValidate} layout='horizontal'> <Form.Input field='name' trigger='blur'></Form.Input> <Form.Input field='familyName[0].before' trigger='blur'></Form.Input> <Form.Input field='familyName[0].after' trigger='blur'></Form.Input> <Form.Input field='familyName[1]' trigger='blur'></Form.Input> <div style={{ display: 'flex', alignItems: 'flex-end' }}> <Button type="primary" htmlType="submit" className="btn-margin-right"> Submit </Button> <Button htmlType="reset">reset</Button> </div> </Form > ); } } ``` #### 异步校验 异步校验时,你应当返回一个 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 ( <Form validator={this.asyncValidate} layout='horizontal'> <Form.Input field='name' trigger='blur'></Form.Input> <Form.Input field='familyName[0].before' trigger='blur'></Form.Input> <Form.Input field='familyName[1]' trigger='blur'></Form.Input> <Form.Input field='sex' trigger='blur'></Form.Input> <div style={{ display: 'flex', alignItems: 'flex-end' }}> <Button type="primary" htmlType="submit" className="btn-margin-right"> Submit </Button> <Button htmlType="reset">reset</Button> </div> </Form > ); } } ``` ### 自定义校验(Field 级别) 你可以指定单个表单控件的自定义校验函数,推荐使用 `validator`(`validate` 为旧写法,仍保持兼容)。支持同步、异步校验(通过返回 promise) <Notice title='关于 validator 与 rules[].validator'> Field 上的 `validator`(本次新增)和 `rules[]` 数组里的 `validator`(基于 async-validator)是两个不同的 API: - Field `validator`:签名为 `(fieldValue, values) => string | Promise<string>`,整字段级别的自定义校验,返回错误信息字符串。 - `rules[].validator`:签名为 `(rule, value, callback) => void | Promise<void>`,基于 [async-validator](https://github.com/yiminghe/async-validator) 的单条规则级校验,由 callback 或 reject 上报错误。 两者**互斥**:只要设置了 Field 的 `validator`(或旧的 `validate`),`rules` 就不会再被触发。如果只需要简单的整字段自定义判断,建议使用 Field `validator`;如果需要多条规则、复用 async-validator 内置规则(如 required / type / pattern),请使用 `rules`。 </Notice> ```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'; } ret