Form
- Rerender on demand, avoids unnecessary full-volume rendering, higher performance
- Easy to use, simple structure, avoids unnecessary hierarchical nesting
- Perfect accessibility support
- FormState / FieldState can also be easily obtained from outside the Form Provides an external method to operate inside the form: formApi / fieldApi
- Support for encapsulating custom components into form controls, and you can quickly access your team's components through the extension mechanism provided by Form (through
withField) - Support Form level / Field level assignment, verification (synchronous / asynchronous)
Field
Semi encapsulates all form field component (Input、Select、Checkbox、DatePicker etc.) with withField once.
Taking over their data flow (props.value & props.onChange)
When in use, you need to import from the Form (note: only the control imported from the Form has data synchronization)
Supported Field Component
Input,InputNumber,TextArea,Select,Checkbox,CheckboxGroup,Radio,RadioGroup,Switch,DatePicker,TimePicker,Slider,InputGroup,TreeSelect,Cascader,Rating,AutoComplete,Upload,PinCode,Label,ErrorMessage,Section,Slot,TagInputAll mounted under Form and declared directly in<Form.Input />and<Form.Select />when used.
import { Form } from '@fibrous/semi-ui-react';
const FormInput = Form.Input;
const FormSelect = Form.Select;
const Option = FormSelect.Option;
The Field level component provided by Form, its value (or other properties specified by valueKey), onChange (or other callback functions specified by onKeyChangeFnName) Properties are hijacked by Form, so
Demos
Various ways to declare form
Semi Form supports multiple writing at the same time.
Basic Usage
Add field property to each field component. You can also set label properties for each field, by default is the same as field
label can be passed in a string directly, or declared in the form of an object, configure extra, required, optional and other attributes to deal with more complex scenarios
Code
import { IconHelpCircle } from '@fibrous/semi-icons-react';
import { Form, Tooltip } from '@fibrous/semi-ui-react';
export default function FormDemo() {
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 aria-label="帮助" 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>
);
}Other declaration methods
When you need to get formState, formApi, values, etc. directly inside the Form structure, you can use the following writing
Via render props
Code
import { Form } from '@fibrous/semi-ui-react';
export default function FormDemo() {
return (
<Form
render={({ formState }) => (
<>
<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)}
/>
);
}Via children function
declare children as a function that returns all field components
Code
import { Form } from '@fibrous/semi-ui-react';
export default function FormDemo() {
return (
<Form layout="horizontal" onValueChange={(values) => console.log(values)}>
{({ formState }) => (
<>
<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>
);
}Via props.component
Pass the entire internal structure directly in the form through component attribute.
Code
import { Form, type FormFCChild } from '@fibrous/semi-ui-react';
export default function FormDemo() {
const fields = ({ formState }: FormFCChild) => (
<>
<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)} />;
}All supported field components
Code
import { IconUpload } from '@fibrous/semi-icons-react';
import { Form, Col, Row, Button, Space } from '@fibrous/semi-ui-react';
const startDate = new Date(2019, 6, 23, 12);
export default function FormDemo() {
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 { Section, Input, InputNumber, Select, DatePicker, TimePicker, TextArea, CheckboxGroup, Checkbox, RadioGroup, Radio, Slider, Rating, Switch, TagInput } = Form;
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={startDate} 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: 'var(--semi-blue-5)',
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} />
</Col>
<Col span={12}>
<Form.TreeSelect field="tree" style={style} label="节点(TreeSelect)" placeholder="请选择服务节点" treeData={treeData} filterTreeNode />
</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>
我已阅读并清楚相关规定(Checkbox)
</Checkbox>
<Space>
<Button type="primary" htmlType="submit" className="btn-margin-right">
提交(submit)
</Button>
<Button htmlType="reset">重置(reset)</Button>
</Space>
</Form>
);
}Field binding syntax
Every Field component must have a field property. This is how the form manages the state of this field. See the field syntax section below for additional details on what you can pass in for field.
The field can be a simple string, can be contained.Or[]String that supports multi-level nesting
Below is an example of the field name and their mapping path in 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 |
Code
import { Form, Toast, Row, Col, TextArea } from '@fibrous/semi-ui-react';
export default function FormDemo() {
return (
<Form onSubmit={(values) => Toast.info({ content: JSON.stringify(values) })}>
{({ formState }) => (
<Row>
<Col span={12}>
<Form.Input field="username" aria-label="username" placeholder="请尝试输入值" />
<Form.Input field="user[0]" aria-label="user[0]" placeholder="请尝试输入值" />
<Form.Input field="siblings.1" aria-label="siblings.1" placeholder="请尝试输入值" />
<Form.Input field="siblings['2']" aria-label="siblings['2']" placeholder="请尝试输入值" />
<Form.Input field="parents[0].name" aria-label="parents[0].name" placeholder="请尝试输入值" />
<Form.Input field="parents[1]['name']" aria-label="parents[1]['name']" placeholder="请尝试输入值" />
</Col>
<Col span={10} offset={1} style={{ marginTop: 12 }}>
<Form.Label text="FormState实时映射值:" />
<TextArea aria-label="FormState实时映射值" value={JSON.stringify(formState.values)} />
</Col>
</Row>
)}
</Form>
);
}Form layout
- Vertical Layout: Arrange each field vertically (By default)
Semi Design Next recommends a vertical layout.
Code
import { Form, Toast, Button } from '@fibrous/semi-ui-react';
export default function FormDemo() {
const handleSubmit = (values: Record<string, unknown>) => {
console.log(values);
Toast.info('表单已提交');
};
return (
<Form onSubmit={(values) => handleSubmit(values)} style={{ width: 400 }}>
{({ values }) => (
<>
<Form.Input field="phone" label="PhoneNumber" style={{ width: '100%' }} placeholder="Enter your phone number" />
<Form.Input field="password" label="Password" style={{ width: '100%' }} placeholder="Enter your password" />
<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>
);
}- Horizontal Layout: Arrange each field horizontally You can use the horizontal layout by setting
layout='horizontal'
Code
import { Form } from '@fibrous/semi-ui-react';
export default function FormDemo() {
return (
<Form layout="horizontal">
<Form.Input field="phone" label="PhoneNumber" placeholder="Enter your phone number" />
<Form.Input field="password" label="Password" placeholder="Enter your password" />
</Form>
);
}- Label Position, Label Align
You can control the position of the label in the Field and the direction of text alignment by settinglabelPosition,labelAlign
Code
import { useState } from 'react';
import { Form, Select, type FormProps } from '@fibrous/semi-ui-react';
export default function FormDemo() {
const [labelPosition, setLabelPosition] = useState<FormProps['labelPosition']>('left');
const [labelAlign, setLabelAlign] = useState<FormProps['labelAlign']>('left');
const labelWidth = labelPosition === 'left' ? '180px' : 'auto';
return (
<>
<div style={{ borderBottom: '1px solid var(--semi-color-border)', paddingBottom: 12 }}>
<Form.Label style={{ marginLeft: 10 }}>切换Label位置:</Form.Label>
<Select onChange={setLabelPosition} 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={setLabelAlign} 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>
</>
);
}- A more complex layout.
You can also combine theRowandColprovided by theGridto arrange the form structure as you want.
Code
import { Form, Col, Row } from '@fibrous/semi-ui-react';
export default function FormDemo() {
return (
<Form labelPosition="top" 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 grouping
Forms with many fields should group them. Use Form.Section to group Fields (it only affects layout and does not change the data structure).
Code
import { Form, Button, Space } from '@fibrous/semi-ui-react';
const startDate = new Date(2019, 6, 23, 12);
export default function FormDemo() {
const { Section, Input, DatePicker, Select, Switch, InputNumber, Checkbox, 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={startDate} style={{ width: 272 }} label={{ text: '开始时间', required: true }} />
<div style={{ display: 'flex' }}>
<Input field="time" label="考试时长" style={{ width: 176 }} initValue="60" addonAfter="分钟" />
<Checkbox initValue noLabel field="auto" style={{ paddingTop: 30, marginInlineStart: 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="关" />
</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, marginInlineEnd: 4 }}>
创建考试
</Button>
<Button style={{ marginTop: 12 }}>预览</Button>
</Space>
</Form>
);
}wrapper Col / label Col
When you need to set a uniform layout for all Fields in a Form, you can set wrapperCol and labelCol on the Form to quickly generate the layout. No need to manually use Row, Col manual layout.
wrapperCol,labelColProperty Configuration Reference Col components
Code
import { Form } from '@fibrous/semi-ui-react';
export default function FormDemo() {
return (
<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>
);
}Remove automatically added Label
Form will automatically insert Label for Field control. If you don't need to automatically insert the Label module, you can turn off the automatic label insertion function by setting noLabel=true in the Field (at this time, the Field still has the ability to automatically display ErrorMessage, so the DOM structure is still different from the original component)
If you want to keep the DOM structure consistent with the original component, you can use pure=true. At this time, the DOM structure will not change except that the data flow is taken over (you need to be responsible for the rendering of ErrorMessage, and it cannot be used by formProps.wrapperCol property impact)
Code
import { Form } from '@fibrous/semi-ui-react';
export default function FormDemo() {
return (
<Form onSubmit={(values) => console.log(values)} style={{ width: 400 }}>
<Form.Input field="name" label="姓名" trigger="blur" noLabel style={{ width: 250 }} placeholder="请输入姓名" />
<Form.Input field="purename" pure placeholder="DOM结构与普通 Input 组件完全一致" />
</Form>
);
}Embedded Label
A Label can be inlined in a field control by setting labelPosition to inset. Components currently supporting this feature include Input, InputNumber, DatePicker, TimePicker, Select, TreeSelect, Cascader, TagInput, AutoComplete. With labelPosition inset, the other fields are laid out as top and warn in development mode.
Code
import { Form } from '@fibrous/semi-ui-react';
const startDate = new Date(2019, 6, 23, 12);
export default function FormDemo() {
return (
<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={startDate} />
</Form>
);
}Export Label, ErrorMessage use
When the built-in Label and ErrorMessage layout does not meet the business requirements, you need to combine the positions yourself, but you want to use the default styles of Label and ErrorMessage directly.
you can import them from the Form module, and combine Form.Label / Form.ErrorMessage by yourself.
For details of their API, refer to Label / ErrorMessage
import { Form } from '@fibrous/semi-ui-react';
const { Label, ErrorMessage } = Form;
Use Form.Slot
When your custom component needs to maintain the same layout style as the Field component, you can place your custom component in Form.Slot
labelWidth, labelAlign, wrapperCol, labelCol set on the Form component automatically acts on Form.Slot
For the Slot property configuration, refer to Form.Slot
Code
import { Form } from '@fibrous/semi-ui-react';
export default function FormDemo() {
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, 我是自定义的节点</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>
);
}Use helpText、extraText set prompt information
You can place custom prompt information through helpText, and display it in the same block as the verification information (error). When both have values, the verification information will be displayed first.
Additional prompt information can be placed through extraText. When the error message and prompt text need to appear at the same time, this configuration can be used. It is always displayed and located after helpText/error
When validateStatus is passed in, the UI style corresponding to the value of validateStatus will be displayed first. If not passed in, the internal verification status of the field shall prevail.
Code
import { useState, useRef, type ReactNode } from 'react';
import { Form, type CommonFieldProps, type FormHandle, type InputValidateStatus as ValidateStatus } from '@fibrous/semi-ui-react';
export default function FormDemo() {
const [helpText, setHelpText] = useState<ReactNode>('');
const [validateStatus, setValidateStatus] = useState<ValidateStatus>('default');
const formRef = useRef<FormHandle>(null);
const validator: CommonFieldProps['validator'] = (val) => {
if (!val) {
setValidateStatus('error');
return <span>密码不能为空</span>;
} else if (val.length <= 3) {
setValidateStatus('warning');
setHelpText(<span style={{ color: 'var(--semi-color-warning)' }}>密码强度:弱</span>); // 展示 helpText
return ''; // 校验通过
} 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 ref={formRef} onSubmit={() => console.log('submit success')} onSubmitFail={(errors) => console.log(errors)}>
<Form.Input
validator={validator}
field="Password"
validateStatus={validateStatus}
helpText={helpText}
extraText={
<button
type="button"
style={{
padding: 0,
border: 'none',
background: 'none',
font: 'inherit',
color: 'var(--semi-color-link)',
fontSize: 14,
userSelect: 'none',
cursor: 'pointer',
}}
onClick={random}
>
没有想到合适的密码?点击随机生成一个
</button>
}
/>
</Form>
);
}By configuring extraTextPosition, you can control the display position of extraText. Optional values bottom, middle
For example, when you want to display the extraText prompt information between the Label and Field component.
This attribute can be configured uniformly on the Form or individually on each Field. When passing in at the same time, the configuration of the Field shall prevail.
Code
import { Form } from '@fibrous/semi-ui-react';
export default function FormDemo() {
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>
);
}Using Input Group
When you need to combine some fields to use, you can use Form.InputGroup to wrap them.
In Semi Form, when you using field components like Form.Input、Form.Select, Form will insert Label module automatically for them.
But usually, inInputGroup you only need a Label belonging to the entire Group. You can set the label property in the InputGroup to insert a Label belonging to the Group
label configurable properties, see Label
Code
import { Form, Button } from '@fibrous/semi-ui-react';
export default function FormDemo() {
return (
<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" />
<Button htmlType="submit">提交</Button>
</Form>
);
}Form in the Modal pop-up layer
You can place the Form in Modal and load it as a popup.
When submitting, use formApi.validate() to centrally verify the Field
Code
import { useState, useRef } from 'react';
import { Form, Modal, Button, Row, Col, type FormApi } from '@fibrous/semi-ui-react';
const message = '该项为必填项';
export default function FormDemo() {
const [visible, setVisible] = useState(false);
const formApiRef = useRef<FormApi | null>(null);
const handleOk = () => {
formApiRef.current
?.validate()
.then((values) => {
console.log(values);
})
.catch((errors) => {
console.log(errors);
});
};
return (
<>
<Button onClick={() => setVisible(true)}>打开弹窗</Button>
<Modal title="新建" visible={visible} onOk={handleOk} style={{ width: 600 }} onCancel={() => setVisible(false)}>
<Form
getFormApi={(api) => {
formApiRef.current = api;
}}
>
<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>
</>
);
}Configure initial values and verification rules
- You can configure check rules for each Field through
rules
The verification library inside the Form is based onasync-validator, and more configuration rules can be found in its official documentation - You can uniformly set the initial value for the entire form through the
initValuesof form, or you can set the initial value throughinitValuein each field (the latter has a higher priority) - You can configure different verification trigger timings for each Field through
trigger, and the default ischange(that is, the verification is performed automatically when the value changes). Also supportschange,blur,mount,customor a combination of the above. It also supports unified configuration through FormProps. If both are configured, FieldProps shall prevail - You can use the
stopValidateWithErrorswitch to decide whether to continue to trigger the validation of subsequent rules when the first rule that fails the validation is encountered. Unified configuration through FormProps is also supported. If both are configured, FieldProps shall prevail
Code
import { Form, Button } from '@fibrous/semi-ui-react';
export default function FormDemo() {
const initValues = {
name: 'semi',
shortcut: 'se',
};
const style = { width: '100%' };
const { 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>
);
}Custom Validate (Form Level)
You can set a custom validation function for the Form as a whole with validator. It will be called on submit or when calling formApi.validate().
Synchronous Validate
When validate success, you should return an empty string.
When validate fails, you should return the error message (Object, key is fieldName, value is the corresponding error message)
Code
import { Form, Button } from '@fibrous/semi-ui-react';
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;
};
export default function FormDemo() {
return (
<Form validator={syncValidate} layout="horizontal">
<Form.Input field="name" trigger="blur" />
<Form.Input field="familyName[0].before" trigger="blur" />
<Form.Input field="familyName[0].after" trigger="blur" />
<Form.Input field="familyName[1]" trigger="blur" />
<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>
);
}Asynchronous Validate
For asynchronous validation, you should return a promise. In promise.then() you need to return the corresponding error message.
Code
import { Form, Button } from '@fibrous/semi-ui-react';
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;
});
export default function FormDemo() {
return (
<Form validator={asyncValidate} layout="horizontal">
<Form.Input field="name" trigger="blur" />
<Form.Input field="familyName[0].before" trigger="blur" />
<Form.Input field="familyName[1]" trigger="blur" />
<Form.Input field="sex" trigger="blur" />
<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>
);
}Custom Validate (Field Level)
You can specify a custom validation function for a single Field with validator. Supports synchronous and asynchronous validation (by returning promises)
Code
import { Form, Button } from '@fibrous/semi-ui-react';
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 '';
}
});
export default function FormDemo() {
return (
<Form>
<Form.Input field="name" label="【name】asyncValidate after 2s" validator={asyncValidate} trigger="blur" />
<Form.Input field="familyName" label="【familyName】syncValidate" validator={validateName} trigger="blur" />
<Button htmlType="reset">reset</Button>
</Form>
);
}Manually Trigger specified validation
When you want to manually trigger the validation of some specific Field, you can do it through formApi.validate.
When no parameters are passed in, all Fields are checked by default. When parameters are passed in, the parameters specified shall prevail
Silent Validation
When you need to get the validation result without triggering UI updates (no error messages shown, no touched state set), you can use formApi.validate({ silent: true }). This is useful in scenarios where you want to decide whether to make a backend request based on the validation result.
Code
import { useRef } from 'react';
import { Form, Button, Toast, type FormApi } from '@fibrous/semi-ui-react';
export default function FormDemo() {
const formApiRef = useRef<FormApi | null>(null);
const handleSilentValidate = () => {
formApiRef.current
?.validate({ silent: true })
.then((values) => {
Toast.success('表单校验通过,准备发起请求');
console.log('发起后端请求:', values);
})
.catch((errors) => {
Toast.error('表单校验未通过,但不显示错误提示');
console.log('校验错误:', errors);
});
};
const handleNormalValidate = () => {
formApiRef.current
?.validate()
.then(() => {
Toast.success('表单校验通过');
})
.catch(() => {
Toast.error('表单校验未通过,显示错误提示');
});
};
return (
<Form
getFormApi={(api) => {
formApiRef.current = api;
}}
layout="horizontal"
>
<Form.Input
field="username"
label="用户名"
rules={[
{ required: true, message: '用户名不能为空' },
{ min: 3, message: '用户名至少 3 个字符' },
]}
/>
<Form.Input
field="email"
label="邮箱"
rules={[
{ required: true, message: '邮箱不能为空' },
{ type: 'email', message: '邮箱格式不正确' },
]}
/>
<div style={{ display: 'flex', gap: 12, marginTop: 12 }}>
<Button onClick={handleSilentValidate} type="primary">
静默校验(不显示错误)
</Button>
<Button onClick={handleNormalValidate}>普通校验(显示错误)</Button>
</div>
</Form>
);
}Code
import { useRef } from 'react';
import { Form, Button, Space, Toast, type FormApi } from '@fibrous/semi-ui-react';
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 <span>我是传入的节点</span>;
}
return;
};
export default function FormDemo() {
const formApiRef = useRef<FormApi | null>(null);
const validatePartial = (type?: 'all') => {
const scope = type === 'all' ? allFields : (formApiRef.current?.getValue('validateScope') ?? []);
formApiRef.current
?.validate(scope)
.then((values) => {
console.log(values);
Toast.success('pass');
})
.catch((error) => {
Toast.error('error');
console.log(error);
});
};
const resetPartial = () => {
formApiRef.current?.reset(formApiRef.current.getValue('resetScope'));
};
return (
<Form
getFormApi={(api) => {
formApiRef.current = api;
}}
autoScrollToError
layout="horizontal"
>
{({ values }) => (
<>
<div>
<Form.Input field="a[1]" validator={validate} trigger="blur" />
<Form.Input field="a[0]" validator={validate} trigger="blur" />
<Form.Input field="b.name[0]" validator={validate} trigger="blur" />
<Form.Input field="b.name[1]" validator={validate} trigger="blur" />
<Form.Input field="b.type" validator={validate} trigger="blur" />
<Form.Input field="c" validator={validate} trigger="blur" />
<Form.Input field="d" validator={validate} trigger="blur" />
</div>
<div>
<Form.CheckboxGroup options={options} field="validateScope" label="当前希望校验的Field" initValue={['a', 'b']} direction="horizontal" />
<Form.CheckboxGroup options={options} field="resetScope" label="当前需要Reset的Field" direction="horizontal" />
<Space>
<Button htmlType="reset">reset</Button>
<Button onClick={() => validatePartial('all')}>all validate</Button>
<Button onClick={() => validatePartial()}>partial validate {JSON.stringify(values.validateScope)}</Button>
<Button onClick={resetPartial}>partial reset</Button>
</Space>
</div>
</>
)}
</Form>
);
}Linkage Fields
You can achieve the linkage between Fields by listening to the onChange of Field and then using formApi to make modifications.
Code
import { useRef } from 'react';
import { Form, Button, Row, type FormHandle } from '@fibrous/semi-ui-react';
export default function FormDemo() {
const formRef = useRef<FormHandle>(null);
const handleSelectChange = (value: unknown) => {
let text = value === 'male' ? 'Hi male' : 'Hi female!';
formRef.current?.formApi.setValue('Note', text);
};
return (
<Form ref={formRef} onValueChange={(values) => console.log(values)} style={{ width: 250 }}>
<span>Note will change after Sex select</span>
<Form.Input field="Note" style={{ width: 250 }} />
<Form.Select field="Sex" onChange={handleSelectChange} style={{ width: 250 }}>
<Form.Select.Option value="female">female</Form.Select.Option>
<Form.Select.Option value="male">male</Form.Select.Option>
</Form.Select>
<Row>
<Button type="primary" htmlType="submit" className="btn-margin-right">
Submit
</Button>
<Button htmlType="reset">reset</Button>
</Row>
</Form>
);
}Dynamic form
Dynamically add and delete fields
Code
import React from 'react';
import { Form, Button } from '@fibrous/semi-ui-react';
export default function FormDemo() {
return (
<Form style={{ width: 450 }}>
{({ formState }) => (
<React.Fragment>
<Form.Input field="name" label="用户名称:" />
<Form.RadioGroup field="isAnchor" label="是否已注册主播">
<Form.Radio value="yes">yes</Form.Radio>
<Form.Radio value="no">no</Form.Radio>
</Form.RadioGroup>
{formState.values.isAnchor === 'yes' && <Form.Input field="liveRoom" label="直播间名称" />}
<Button htmlType="submit">提交</Button>
</React.Fragment>
)}
</Form>
);
}Keep empty values (allowEmpty)
By default a field whose value becomes empty is removed from values. Set allowEmpty on a Field to keep its key with an undefined value, or on Form to apply it to every field. Clear both selects below and compare the keys of values.
Code
import { Form } from '@fibrous/semi-ui-react';
const roleOptions = [
{ label: '管理员', value: 'admin' },
{ label: '访客', value: 'guest' },
];
export default function FormDemo() {
return (
<Form initValues={{ keep: 'admin', drop: 'admin' }}>
{({ values }) => (
<>
<Form.Select field="keep" label="设置了 allowEmpty" allowEmpty showClear optionList={roleOptions} style={{ width: 176 }} />
<Form.Select field="drop" label="未设置" showClear optionList={roleOptions} style={{ width: 176 }} />
<code>{JSON.stringify(Object.keys(values))}</code>
</>
)}
</Form>
);
}Keep field state on unmount (keepState)
By default, when a Field component is unmounted, its value, error, and touched state are all reset. If you want to preserve these states after the field is unmounted (for example, in conditional rendering scenarios), you can use the keepState property.
Code
import React from 'react';
import { Form, Switch, Button } from '@fibrous/semi-ui-react';
export default function FormDemo() {
const [show, setShow] = React.useState(true);
return (
<Form style={{ width: 450 }}>
<Form.Switch field="alwaysShow" label="始终显示的字段" />
<Switch aria-labelledby="form-keep-state-toggle" checked={show} onChange={setShow} style={{ marginBottom: 12 }} />
<span id="form-keep-state-toggle" style={{ marginLeft: 8, color: 'var(--semi-color-text-2)' }}>
显示条件字段
</span>
{show && <Form.Input field="conditionalField" label="条件字段(keepState)" keepState />}
{show && <Form.Input field="noKeepState" label="条件字段(无 keepState)" />}
<Button htmlType="submit">提交</Button>
</Form>
);
}In the example above:
- The field with
keepStatewill retain its previously entered value, validation state, etc. after being hidden and then shown again - The field without
keepStatewill have its state reset after being hidden and then shown again
ArrayField Usage
For array items that are dynamically added or deleted, we provide the ArrayField component to simplify the operation of add / remove
For the detailed API of ArrayField, please refer to ArrayField Props below
Note: The initValue type of ArrayField must be an array
Code
import { IconPlusCircle, IconMinusCircle } from '@fibrous/semi-icons-react';
import { ArrayField, TextArea, Form, Button, useFormState } from '@fibrous/semi-ui-react';
const data = [
{ name: 'Semi D2C', role: 'Engineer' },
{ name: 'Semi C2D', role: 'Designer' },
];
const ComponentUsingFormState = () => {
const formState = useFormState();
return <TextArea aria-label="Form state" style={{ marginTop: 10 }} value={JSON.stringify(formState)} />;
};
export default function FormDemo() {
return (
<Form style={{ width: 800 }} labelPosition="left" labelWidth="100px" allowEmpty>
<ArrayField field="rules" initValue={data}>
{({ add, arrayFields, addWithInitValue }) => (
<>
<Button onClick={add} icon={<IconPlusCircle />} theme="light">
Add new line
</Button>
<Button
icon={<IconPlusCircle />}
onClick={() => {
addWithInitValue({ name: 'Semi DSM', role: 'Designer' });
}}
style={{ marginLeft: 8 }}
>
Add new line with init value
</Button>
{arrayFields.map(({ field, key, remove }) => (
<div key={key} style={{ width: 1000, display: 'flex' }}>
<Form.Input field={`${field}[name]`} label={`${field}.name`} style={{ width: 200, marginRight: 16 }} />
<Form.Select
field={`${field}[role]`}
label={`${field}.role`}
style={{ width: 120 }}
optionList={[
{ label: 'Engineer', value: 'Engineer' },
{ label: 'Designer', value: 'Designer' },
]}
/>
<Button type="danger" theme="borderless" aria-label="Delete" icon={<IconMinusCircle />} onClick={remove} style={{ margin: 12 }} />
</div>
))}
</>
)}
</ArrayField>
<ComponentUsingFormState />
</Form>
);
}Nesting ArrayField
ArrayField supports multi-level nesting. The following is an example of two-level nesting.
Code
import React from 'react';
import { IconPlusCircle, IconMinusCircle } from '@fibrous/semi-icons-react';
import { Form, ArrayField, Button, Card, Typography } from '@fibrous/semi-ui-react';
const selectOption = [
{ label: '发件人地址', value: 'address' },
{ label: '邮件主题', value: 'title' },
{ label: '发送时间', value: 'sendTime' },
{ label: '接收时间', value: 'receiveTime' },
{ label: '正文', value: 'main' },
{ label: '附件名称', value: 'attachmentName' },
];
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 NestedField = (props: { field: string }) => {
const rowStyle = {
marginTop: 12,
marginLeft: 12,
};
return (
<ArrayField field={`${props.field}.rules`}>
{({ arrayFields, addWithInitValue }) => (
<React.Fragment>
{arrayFields.map(({ field, key, remove }, i) => (
<div style={{ display: 'flex' }} key={key}>
<Form.Select field={`${field}[ruleType]`} label={`${field}.ruleType`} noLabel optionList={selectOption} style={{ width: 120, marginRight: 12 }} />
<Form.Select
field={`${field}[type]`}
label={`${field}.type`}
noLabel
style={{ width: 100, marginRight: 12 }}
optionList={[
{ label: '包含', value: 'include' },
{ label: '不包含', value: 'exclude' },
]}
/>
<Form.Input field={`${field}[text]`} label={`${field}.text`} aria-label="条件内容" noLabel style={{ width: 200 }} />
<Button type="danger" theme="borderless" style={rowStyle} aria-label="删除条件" icon={<IconMinusCircle />} onClick={remove} />
<Button
aria-label="新增条件"
icon={<IconPlusCircle />}
style={rowStyle}
disabled={i !== arrayFields.length - 1}
onClick={() => {
addWithInitValue({
ruleType: `条件${arrayFields.length + 1}`,
type: 'include',
});
}}
/>
</div>
))}
</React.Fragment>
)}
</ArrayField>
);
};
export default function FormDemo() {
return (
<Form onValueChange={(values) => console.log(values)} initValues={initValue} labelPosition="left" style={{ textAlign: 'left' }} allowEmpty>
<ArrayField field="group">
{({ arrayFields, addWithInitValue }) => (
<React.Fragment>
<Button
icon={<IconPlusCircle />}
theme="solid"
onClick={() => {
addWithInitValue({
name: '新规则名称',
rules: [
{ ruleType: 'main', type: 'include', text: '' },
{ ruleType: 'attachmentName', type: 'include', text: '' },
],
});
}}
>
新增收信规则
</Button>
{arrayFields.map(({ field, key, remove }) => (
<div key={key} style={{ width: 1000, display: 'flex', flexWrap: 'wrap' }}>
<Form.Input field={`${field}[name]`} labelPosition="top" label="规则名称" style={{ width: '600px' }} />
<Button type="danger" style={{ margin: '36px 0 0 12px' }} aria-label="删除规则" icon={<IconMinusCircle />} onClick={remove} />
<Typography.Text strong style={{ flexBasis: '100%' }}>
当邮件到达,满足以下条件时:
</Typography.Text>
<Card
shadows="hover"
style={{
width: 620,
margin: '12px 0 0 24px',
}}
>
<NestedField field={field} />
</Card>
</div>
))}
</React.Fragment>
)}
</ArrayField>
</Form>
);
}Add or delete form items dynamically - by use formApi
If you don't use ArrayField, you can use the provided formApi to manually add or delete formState.
Code
import { useRef } from 'react';
import { Form, Button, TextArea, type FormApi } from '@fibrous/semi-ui-react';
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 },
],
};
export default function FormDemo() {
const formApiRef = useRef<FormApi<FormValues> | null>(null);
const idRef = useRef(3);
const add = () => {
const effects = formApiRef.current?.getValue('effects') ?? [];
formApiRef.current?.setValue('effects', [...effects, { name: '', type: '', key: idRef.current++ }]);
};
const remove = (key: number) => {
const effects = (formApiRef.current?.getValue('effects') ?? []).filter((effect) => key !== effect.key);
formApiRef.current?.setValue('effects', effects.length ? effects : undefined);
};
return (
<Form
getFormApi={(api) => {
formApiRef.current = api;
}}
initValues={initValues}
style={{ width: 500 }}
labelPosition="left"
labelWidth="180px"
>
{({ formState, values }) => (
<>
<Button onClick={add}>添加</Button>
{values.effects &&
values.effects.map((effect, i) => (
<div key={effect.key} style={{ width: 1000, display: 'flex' }}>
<Form.Input field={`effects[${i}].name`} aria-label={`特效 ${i + 1} 名称`} style={{ width: 200, marginRight: 12 }} />
<Form.Select field={`effects[${i}].type`} style={{ width: 90 }}>
<Form.Select.Option value="2D">2D</Form.Select.Option>
<Form.Select.Option value="3D">3D</Form.Select.Option>
</Form.Select>
<Button type="danger" onClick={() => remove(effect.key)} style={{ margin: 12 }}>
删除
</Button>
</div>
))}
<TextArea aria-label="表单值" style={{ marginTop: 10 }} value={JSON.stringify(formState.values)} />
</>
)}
</Form>
);
}Use of Hook
We provide four Hooks so that you can easily access Form internal state and call Form and Field related api in Functional Component which placed inside the Form structure without passing through props.
import { useFormApi, useFormState, useFieldApi, useFieldState } from '@fibrous/semi-ui-react';
useFormApi
useFormApi allows you to directly access the formApi of the parent Form component within Functional Component via hook
Code
import { useFormApi, Form, Button } from '@fibrous/semi-ui-react';
const ComponentUsingFormApi = () => {
const formApi = useFormApi();
const change = () => {
formApi.setValue('name', Math.random());
};
return <Button onClick={change}>ChangeName By【formApi】</Button>;
};
export default function FormDemo() {
return (
<Form>
<Form.Input field="name" initValue="mike" />
<ComponentUsingFormApi />
</Form>
);
}useFormState
useFormState allows you to directly access the form State of the parent Form component within Functional Component via hook
Code
import { useFormState, Form } from '@fibrous/semi-ui-react';
const ComponentUsingFormState = () => {
const formState = useFormState();
return (
<pre>
<code>{JSON.stringify(formState)}</code>
</pre>
);
};
export default function FormDemo() {
return (
<Form>
<Form.Input field="name" initValue="mike" />
<h5>FormState read by 【useFormState】:</h5>
<ComponentUsingFormState />
</Form>
);
}useFieldApi
useFieldApi allows you to call the api of the specified Field directly within Functional Component via hook
Code
import { useFieldApi, Form, Button } from '@fibrous/semi-ui-react';
const ComponentUsingFieldApi = () => {
const nameFieldApi = useFieldApi('name');
const change = () => {
nameFieldApi.setValue(Math.random());
};
return <Button onClick={change}>Click Me!!! changeNameBy【fieldApi】</Button>;
};
export default function FormDemo() {
return (
<Form>
<Form.Input field="name" initValue="mike" />
<ComponentUsingFieldApi />
</Form>
);
}useFieldState
useFieldState allows you to directly access the State of the specified Field within Functional Component via hook
Code
import { useFieldState, Form } from '@fibrous/semi-ui-react';
const ComponentUsingFieldState = ({ field }: { field: string }) => {
const fieldState = useFieldState(field);
return (
<div>
<span>【{field}】FieldState read by 【useFieldState】:</span>
<code>{JSON.stringify(fieldState)}</code>
</div>
);
};
export default function FormDemo() {
return (
<Form layout="horizontal">
<div style={{ width: 400 }}>
<Form.Input field="name" initValue="mike" />
<Form.Input field="country" initValue="china" />
</div>
<div style={{ width: 500, marginTop: 30 }}>
<ComponentUsingFieldState field="name" />
<ComponentUsingFieldState field="country" />
</div>
</Form>
);
}Use of HOC
We provided two HOC: withFormApi、withFormState, you can access the API of the Form and the internal state within other components
Provided HOC: withField, to encapsulating custom components as Field that conform the Semi Form data flow.
import { withFormApi, withFormState, withField } from '@fibrous/semi-ui-react';
HOC - withFormApi
You can encapsulate the component via withFormApi HOC so that the formApi of the parent Form component can be called directly inside the component
Note that the encapsulated components must be placed inside the Form structure.
Code
import { withFormApi, Form, Button, type FormApi } from '@fibrous/semi-ui-react';
const SomeComponetInsideForm = (props: { formApi: FormApi }) => (
<Button
onClick={() => {
props.formApi.setValue('name', Math.random());
}}
>
Click Me!!! ChangeName By【formApi】
</Button>
);
const ComponentWithFormApi = withFormApi(SomeComponetInsideForm);
export default function FormDemo() {
return (
<Form>
<Form.Input field="name" initValue="semi" />
<Form.Input field="familyName" initValue="design" />
<Button htmlType="submit" style={{ marginRight: 4 }}>
submit
</Button>
<ComponentWithFormApi />
</Form>
);
}HOC - withFormState
You can encapsulate the component via withFormState HOC so that the component has direct access to the Form State of the parent Form component.
Note that the encapsulated components must be placed inside the Form structure.
Code
import { withFormState, Form, type FormState } from '@fibrous/semi-ui-react';
const SomeComponentInsideForm = (props: { formState: FormState }) => <code>{JSON.stringify(props.formState)}</code>;
const ComponentWithFormState = withFormState(SomeComponentInsideForm);
export default function FormDemo() {
return (
<Form>
<Form.Input field="name" initValue="semi" />
<Form.Input field="familyName" initValue="design" />
<ComponentWithFormState />
</Form>
);
}Take over custom components
Via withField, you can extend other custom components into Field. Form will taking over its behavior.
With Field did the following things.
- Take over the
valueof the component (or other properties specified by valueKey),onChange(or other callback functions specified by onKeyChangeFnName) - Insert Field's
<Form.Label>above the field - Insert Field's
<ErrorMessage>under the field - Insert Field's extraText under the field
With Field Options specific configuration can be consulted withFieldOption
Your custom controlled component needs to do the following:
- When the value changes, call
props.onChangeand use the latest value as an input parameter - Respond to changes in
props.valueand update your component UI rendering results
withField(YourComponent, withFieldOption);
The props type of the returned component is inferred from the props of YourComponent: the component's own props plus Field Props, without value, defaultValue, checked and defaultChecked (the Form owns the value).
Code
import type { ComponentProps } from 'react';
import { withField, Form, useFormState } from '@fibrous/semi-ui-react';
const htmlInput = (props: ComponentProps<'input'> & { validateStatus?: string }) => {
const value = props.value || '';
const { validateStatus, ...rest } = props; // keep these props off the DOM
return <input {...rest} value={value} />;
};
const CustomInput = withField(htmlInput, {
valueKey: 'value',
onKeyChangeFnName: 'onChange',
valuePath: 'target.value',
});
const ComponentUsingFormState = () => {
const formState = useFormState();
return (
<pre>
<code>{JSON.stringify(formState)}</code>
</pre>
);
};
export default function FormDemo() {
return (
<Form>
<CustomInput field="name" />
<ComponentUsingFormState />
</Form>
);
}Code
import { withField, Input, Select, Form, useFormState } from '@fibrous/semi-ui-react';
interface BaseInfo {
name?: string;
role?: string;
}
interface MyComponentProps {
id?: string;
value?: BaseInfo;
onChange?: (value: BaseInfo) => void;
'aria-labelledby'?: string;
'aria-required'?: boolean;
}
// id and aria-labelledby go on the fieldset (named by the field label); aria-required goes to each control
const MyComponent = ({ id, onChange, value, 'aria-labelledby': labelledBy, 'aria-required': required }: MyComponentProps) => {
const { name, role } = value || {};
const handleChange = (v: string | undefined, type: 'name' | 'role') => {
const newValue = { ...value, [type === 'name' ? 'name' : 'role']: v };
onChange?.(newValue);
};
return (
<fieldset className="customField" id={id} aria-labelledby={labelledBy} style={{ border: 0, margin: 0, padding: 0 }}>
<Input aria-label="名称" aria-required={required} prefix="名称" value={name} onChange={(v) => handleChange(v, 'name')} style={{ width: 180, marginRight: 12 }} />
<Select
aria-label="角色"
aria-required={required}
prefix="角色"
value={role}
onChange={(v) => handleChange(v, 'role')}
style={{ width: 200 }}
optionList={[
{ value: 'rd', label: '开发' },
{ value: 'UED', label: '设计师' },
]}
/>
</fieldset>
);
};
const CustomField = withField(MyComponent, { valueKey: 'value', onKeyChangeFnName: 'onChange' });
const ComponentUsingFormState = () => {
const formState = useFormState();
return (
<pre>
<code>{JSON.stringify(formState)}</code>
</pre>
);
};
export default function FormDemo() {
return (
<Form>
<CustomField field="baseInfo" label={{ text: '基本信息', required: true }} />
<ComponentUsingFormState />
</Form>
);
}API reference
Form
Props
| Property | Description | Type | Default |
|---|---|---|---|
| allowEmpty | Whether a field whose value becomes empty keeps its key in values. Setting it to true on either the Form or the field takes effect. | boolean | false |
| aria-label | Accessible name of the form, set on the form element. | React.AriaAttributes['aria-label'] | - |
| autoScrollToError | Whether to scroll to the first invalid field when submit or formApi.validate() fails. An object is passed to scroll-into-view-if-needed as its options. | boolean | ScrollIntoViewOptions | false |
| children | The content of the form. A function receives { formState, values, formApi } and declares the fields. | TypeReact.ReactNode | ((internalProps: FormFCChild<Values>) => React.ReactNode) | - |
| className | Class name of the form element. | string | - |
| component | A component that declares the fields, rendered with formState, values and formApi as its props. Do not use it together with render or children. | TypeReact.ComponentType<any> | - |
| disabled | The disabled state applied to every field. A field's own disabled takes precedence. | boolean | - |
| extraTextPosition | Position of the extraText of every field: middle puts it between the label and the control, bottom after the control. Defaults to bottom. The same property on a field takes precedence. | 'middle' | 'bottom' | - |
| form | The formApi returned by Form.useForm(), used to control the form from outside. | TypeReturnType<typeof useForm<Values>>[0] | - |
| getFormApi | Called once the Form has mounted, with the formApi. | Type(formApi: FormApi<Values>) => void | - |
| id | The id of the form element, also written to x-form-id. Generated when omitted. | string | - |
| initValues | Initial values of the form, copied once when the Form is created. | Values | - |
| labelAlign | Text alignment of the label of every field: left or right. Defaults to left. The same property on a field takes precedence. | 'left' | 'right' | - |
| labelCol | Grid layout of the label of every field, with the properties of Col such as span and offset. Set it together with wrapperCol. | TypeRecord<string, any> | - |
| labelPosition | Label position of every field: top, left or inset. inset is supported by some fields only; the others are laid out as top. The same property on a field takes precedence. | 'top' | 'left' | 'inset' | "top" |
| labelWidth | Label width of every field. A number is in px. | number | string | - |
| layout | Direction in which the fields are arranged: vertical or horizontal. | 'horizontal' | 'vertical' | "vertical" |
| onChange | Called when the form state changes: a field mounts, unmounts, changes its value, blurs, or changes its validation state or error. Receives formState. | Type(formState: FormState<Values>) => void | - |
| onErrorChange | Called when validation state changes, with all errors and the errors that changed. Batch operations such as validate, submit and reset call it once; silent validation does not. | Type(errors: AllErrors<Values>, changedError?: Partial<Record<FieldPath<Values>, FieldError>>) => void | - |
| onReset | Called when a reset button is clicked or formApi.reset() is called. | Type() => void | - |
| onSubmit | Called when a submit button is clicked or formApi.submitForm() is called and validation passes, with values and the submit event. | Type(values: Values, e?: React.SubmitEvent<HTMLFormElement>) => void | - |
| onSubmitFail | Called when a submit button is clicked or formApi.submitForm() is called and validation fails, with errors, values and the submit event. | Type(errors: BatchValidateResult<Values>, values: Partial<Values>, e?: React.SubmitEvent<HTMLFormElement>) => void | - |
| onValueChange | Called when a field value changes, with all values and the values that changed. | Type(values: Values, changedValue: Partial<Values>) => void | - |
| ref | Reference to the handle { formApi }, whose formApi controls the form. | TypeReact.Ref<FormHandle<Values>> | - |
| render | A function that declares the fields, called with { formState, values, formApi }. Do not use it together with component or children. | Type(internalProps: FormFCChild<Values>) => React.ReactNode | - |
| showValidateIcon | Whether the validation message is preceded by an icon of its status. | boolean | true |
| stopPropagation | Whether to stop the submit and reset events from bubbling, as { submit, reset }, so that a nested Form does not trigger the events of the outer one. Events bubble when it is not set. | { submit?: boolean; reset?: boolean; } | - |
| stopValidateWithError | Whether to skip the remaining rules after the first rule fails. The same property on a field takes precedence. | boolean | - |
| style | Inline style of the form element. | React.CSSProperties | - |
| trigger | When every field validates: change, blur, mount, custom, or an array of them. The same property on a field takes precedence. | FieldValidateTriggerType | - |
| validator | Form-level validation, called on submit and formApi.validate(), and allowed to return a Promise. When set, the validator and rules of the fields no longer apply. An empty string or object means valid; on failure return an error object shaped like the values. | Type(values: Values) => BatchValidateResult<Values> | Promise<BatchValidateResult<Values>> | - |
| wrapperCol | Grid layout of the control of every field, with the properties of Col such as span and offset. Set it together with labelCol. | TypeRecord<string, any> | - |
Methods
| Method | Description | Type |
|---|---|---|
| formApi | The formApi of the form, used to read and change its state, validate and submit. | TypeFormApi<Values> |
Native attributes and events such as aria-* land on the form element. When both labelCol and wrapperCol are set, the form is wrapped in an extra Row and the other attributes still land on the form. The form element has noValidate by default, so only Semi validates; pass noValidate={false} to restore native validation.
FormState
FormState stores all the state values within the Form, including the values of each field, error information, touched status
| Name | Instructions | Initial value | Example |
|---|---|---|---|
| values | Value Collection of the form | {} | {fieldA: 'str', fieldB: true} |
| errors | Form error information collection, you can decide whether to allow users to submit by judging whether there is error information | {} | {fieldA: 'length not valid'} |
| touched | The collection of fields the user has clicked on | {} | {fieldA: true} |
How to access the form state
- By calling
formApi.getFormState() - By declaring fields through child render function, formState will injected as a parameter
- By declaring fields through render props, formState will injected as a parameter
- Via useFormState hook
- Via withFormState HOC
FormApi
We provide FormApi. You have easy access to FormApi both inside and outside the Form, which allows you to use getter and setter to get and manipulate the values of FormState.
The table below describes the features available in the formApi.
| Function | Description | example |
|---|---|---|
| getFormProps | Get Form Component Props | formApi.getFormProps(propNames?: string[]) |
| getFormState | Get FormState | formApi.getFormState() |
| submitForm | Manually submit form operation | formApi.submitForm() |
| reset | Reset the form manually | formApi.reset(fields?: Array <string>) |
| validate | Manually trigger validation of the entire form. the verification of the entire Field will be triggered by default when no parameters are passed , if you want to trigger the verification of some fields, pass in the target field array After the Form level validator is configured, the Field level validator will not be triggered again when submit or formApi.validate() Silent validation: Pass { silent: true } to get validation results without triggering UI updates (no error messages shown, no touched state set). You can also use { fields: ['fieldA'], silent: true } to perform silent validation on specific fields. | formApi.validate() .then(values => {}) .catch(errors => {}) OR formApi.validate(['fieldA','fieldB']) OR formApi.validate({ silent: true }) OR formApi.validate({ fields: ['fieldA'], silent: true }) |
| setValues | Set the values of the entire form. The isOverride in the second parameter is false by default. When isOverride is false: Iterate through all registered fields in the Form, and update the values from newValues to formState.values. If a registered field is not included in newValues, that field will be set to undefined. When isOverride is true: Directly replace formState.values with newValues, including extra keys in newValues that don't have corresponding fields in the form. Recommended usage: If you only need to update some fields, it's recommended to use formApi.setValue(field, value) for individual updates, or get current values first then merge: formApi.setValues({ ...formApi.getValues(), name: newValue }) | formApi.setValues(newValues: object, {isOverride: boolean}) |
| getValues | Get the values of all Field | formApi.getValues() |
| setValue | provides direct modification of formState.values method. The difference from setValues is that it only modifies a single field. | formApi.setValue(field: string, newFieldValue: any) |
| getValue | Get the value of all / single Field | formApi.getValue() formApi.getValue(field: string) |
| setTouched | Modify formState.touched | formApi.setTouched(field: string, isTouched: boolean) |
| getTouched | Get the touched state of the Field | formApi.getTouched(field: string) |
| setError | Modify the error information of a field | formApi.setError(field: string, fieldErrorMessage: string) |
| getError | Get Error Status of Field | formApi.getError(field: string) |
| getFieldExist | Get whether the field exists in the Form | formApi.getFieldExist(field: string) |
| scrollToField | Scroll to the specified field, the second input parameter will be passed to scroll-into-view-if-needed | formApi.scrollToField(field: string, scrollOpts: ScrollIntoViewOptions) |
| scrollToError | Scroll to the field with validation error. You can pass a specified field or index. If you pass index, scroll to the index-th error DOM. If you do not pass any parameters, scroll to the first validation error position in the DOM tree. | formApi.scrollToError(ScrollToErrorOptions{field?: string; index?: number; scrollOpts?: ScrollIntoViewOptions }) |
How to access formApi
- Use
Form.useForm()to create formApi outside Form, then pass it via props.form to the Form component. This allows you to get formApi before Form renders, suitable for scenarios where you need to control the form from outside the component. - The Form component in the
ComponentDidMountphase will execute thegetFormApicallback passed in by props. You can save a reference to formApi in the callback function for subsequent calls (example code below) In addition, we provide other ways to get formApi, and you can choose different ways of calling according to your preference. - Use reference to get form instance,you can access form instance & its formApi
- By declaring fields through "child render function", formApi will injected as a parameter
- By declaring fields through "render props", formApi will injected as a parameter
- Via useFormApi hook for children component of Form
- Via withFormApi HOC for children component of Form
Using Form.useForm()
Form.useForm() is used to create a formApi instance outside the Form component, suitable for scenarios where you need to control the form before it renders. It returns an array [formApi, formState, formValues].
Usage example:
Code
import { Form, Button } from '@fibrous/semi-ui-react';
export default function FormDemo() {
const [formApi, formState, formValues] = Form.useForm();
const handleClick = () => {
formApi.setValue('username', 'semi');
console.log('当前表单值:', formValues);
};
return (
<>
<Button onClick={handleClick}>外部设置值</Button>
<Button onClick={() => formApi.reset()}>重置表单</Button>
<Form form={formApi}>
<Form.Input field="username" label="用户名" />
<Form.Input field="email" label="邮箱" />
</Form>
<div style={{ marginTop: 16 }}>
<div>表单值:{JSON.stringify(formValues)}</div>
<div>touched 状态:{JSON.stringify(formState.touched)}</div>
</div>
</>
);
}import React from 'react';
import { Form, Button } from '@fibrous/semi-ui-react';
class FormApiDemo extends React.Component {
constructor() {
super();
this.getFormApi = this.getFormApi.bind(this);
this.formBRef = React.createRef();
}
getFormApi(formApi) {
this.formApi = formApi;
// After getting the formApi object, you can use it to make any changes you want to the form
}
changeValues() {
// use formApi to update formA
this.formApi.setValues({ a: 1 });
// use formApi to update formB
this.formBRef.current.formApi.setValues({ b: 2 });
}
render() {
return (
<>
<Form getFormApi={this.getFormApi} />
<Form ref={this.formBRef} />
<Button onClick={() => this.changeValues()}>Change</Button>
</>
);
}
}
import React from 'react';
import { Form, Button } from '@fibrous/semi-ui-react';
() => {
// functional compoentn usage
const api = useRef();
return (
<>
<Form getFormApi={(formApi) => (api.current = formApi)}>
<Form.Input field="a" />
<Button
onClick={() => {
console.log(api);
}}
>
log
</Button>
</Form>
</>
);
};
Field Props
| Properties | Description | Type | Default | Examples |
|---|---|---|---|---|
| field | The mapping path of the field's value in formState.values. Form will use this value to distinguish the internal form control. Required!!! | string | ||
| label | The label text for this field. When not passed, it defaults to the same name as field | string | ||
| labelPosition | Label position of this field, optional 'top' / 'left' / 'inset' | string | ||
| labelAlign | Text-align of the label text of this field | string | ||
| labelWidth | The width of the label text of this field | string|number | ||
| labelCol | Grid layout of the label of this field, with Col component. It overrides the same property of Form. When it is set together with wrapperCol while the Form does not set both, the Field wraps itself in a Row | object | ||
| wrapperCol | Grid layout of the control of this field, with Col component. It overrides the same property of Form. Used the same way as labelCol | object | ||
| noLabel | When you don't need to add label automatically, you can set this value to true | boolean | ||
| noErrorMessage | When you don't need to add the ErrorMessage module automatically, you can set this value to true. Note that helpText is not shown either | boolean | ||
| name | Field name. When passed in, the corresponding className will be automatically added to the field wrapper div, such as: money => '.semi-form-field-money'. The name will also be transparently transmitted to the underlying component for consumption. For example, you can configure the name attribute of input | string | ||
| fieldClassName | The className of the entire fieldWrapper is the same as the name parameter, except that the prefix is not automatically appended | string | ||
| fieldStyle | The inline style of the entire fieldWrapper | object | ||
| initValue | The initial value of the field (consumed only once when Field mounted, subsequent updates are invalid), it has higher priority than the values in Form's initValues | any(type depends on current component) | ||
| validator | The custom validation function for this form control. Supports synchronous and asynchronous validation. Rules does not take effect when validator is set | function(fieldValue, values) | (fieldValue) => fieldValue.length>5? 'error balabala': '' | |
| rules | validation rules, validation library based on async-validator | array | const rules = [{type:' string ', message:' invalidate string'} ] | |
| validateStatus | The validation result status of this form control, optional: success / error / warning / default | string | 'default' | |
| trigger | The timing of triggering the verification, optional: blur / change / custom / mount 1. When set to custom, only formApi will trigger the verification 2.mount (triggered once when mounting) | 'blur' | 'change' | 'custom' | 'mount', or an array of them | 'change' | |
| onChange | Callback invoked when this field value changes. The last parameter of the callback is the latest form values latestValues, which can be used to solve the React closure trap problem.Usage example: onChange={(value, e, ...other) => { const latestValues = other[other.length - 1]; }} | function(fieldValue: any | ev: { target: { value: any }}, ...args: any[], latestValues?: object) (see each component's onChange method) | ||
| onBlur | Callback invoked when the control loses focus | function() (see each component's onBlur method) | ||
| transform | transform field values before validation | function(fieldValue) | (value) => Number(value) | |
| allowEmpty | Whether to keep the key of this field in values (as undefined) when its value is empty. Setting it on either the Field or the Form takes effect | boolean | false | |
| allowEmptyString | Whether to allow values to be empty strings. When the value is '' by default, the key corresponding to this field will be removed from values. If you want to keep the key, you need to set allowEmptyString to true | boolean | false | |
| emptyValue | The value that counts as empty. When the value changes to it, the key of this field is removed from values (kept as undefined when allowEmpty is true) | any | '' | |
| keepState | Whether to retain the field state (value, error, touched) after the field is unmounted. When true, the field's value, validation message, and interaction state are preserved after unmounting, and will be automatically restored upon remounting | boolean | false | |
| convert | After the field value changes, before rerender, update the value of filed | function(fieldValue) | (value) => newValue | |
| stopValidateWithError | When it is true, the rules check is used. After encountering the first rule that fails the check, it will no longer trigger the check of subsequent rules | boolean | false | |
| helpText | Custom prompt information, which is displayed in the same block as the verification information. When both have values, the verification information is displayed first | ReactNode | ||
| extraText | Additional prompt information, you can use this when both error information and prompt copy are required, after helpText/errorMessage | ReactNode | ||
| pure | Whether to only take over the data stream, when true, it will not automatically insert modules such as ErrorMessage, Label, extraText, etc. The style and DOM structure are consistent with the original components | boolean | false | |
| extraTextPosition | controls the display position of extraText. Middle (the vertical direction is displayed in the order of Label, extraText, and Field), bottom (the vertical direction is displayed in the order of Label, Field, and extraText) | string | 'bottom' | |
| ...other | The other configurable properties of the component can be passed in together with the above properties, such as the size / placeholder of Input,Field passes it to the component itself |
Field Api
We also provide fieldApi, most of which is similar to formApi, with the difference that fieldApi limits the scope of modification, and it can only modify the bound field. Get it through useFieldApi
| Function | Instructions | example |
|---|---|---|
| setValue | Modify the value of the current Field | fieldApi.setValue(newValue: any) |
| getValue | Gets the value of the current Field | fieldApi.getValue() |
| setTouched | Modify the touched state of the current Field | fieldApi.setTouched(true) |
| getTouched | Get the touched state of the current Field | fieldApi.getTouched() |
| setError | Modify the error information of the current Field | fieldApi.setError(newErrorMessage: string) |
| getError | Gets field's error status | fieldApi.getError() |
ArrayField
For dynamically added and deleted array form items, we provide the ArrayField scope to simplify add/remove operations
Props
| Property | Description | Type | Default |
|---|---|---|---|
| children | Required. A function that renders the rows. It receives { arrayFields, add, addWithInitValue } and returns a ReactNode. | Type(props: ArrayFieldChildrenProps) => React.ReactNode | - |
| field | Required. Path of the array in formState.values. For rows with fields a[0].name and a[1].name, pass a. | string | - |
| initValue | Initial value of the array, which must be an array. It takes precedence over the initValues of the Form. | any[] | - |
ArrayField has no DOM of its own, so undeclared attributes are dropped.
interface ArrayFieldChildrenProps {
arrayFields: ArrayFieldItem<>; // The current array form, which can be used to perform map operations to render each row
add: (index?: number | React.MouseEvent) => void; // Add a blank line; a number inserts it at that position, an event is ignored so it can be used as onClick
addWithInitValue: (lineObject: Record<string, any>, index?: number) => void; // Add a new row with an initial value, index is the insert position
}
interface ArrayFieldItem {
key: string; // A key used to identify the current row, which should be bound to the wrapper of the current row
field: string; // This row fieldPath, which is equivalent to ArrayFieldProps.field + [index]
remove: () => void; // Remove operation function of this line, when called, this line will be deleted directly
}
Form.Section
import { Form } from '@fibrous/semi-ui-react';
const { Section } = Form;
Props
| Property | Description | Type | Default |
|---|---|---|---|
| children | Content of the section. | React.ReactNode | - |
| className | Class name of the section element. | string | - |
| ref | Reference to the section element. | TypeReact.Ref<HTMLElement> | - |
| style | Inline style of the section element. | React.CSSProperties | - |
| text | The title of the section. No title is rendered when it is empty. | React.ReactNode | - |
Native attributes such as aria-* land on the section element.
Form.Label
By default, Label is self-inserted into each Field by Form.
If you need to self-insert Label elsewhere, we have provided the Label component for you.
import { Form } from '@fibrous/semi-ui-react';
const { Label } = Form;
Props
| Property | Description | Type | Default |
|---|---|---|---|
| align | Text alignment of the label: left or right. | string | "left" |
| children | Content of the label, used when text is not set. | React.ReactNode | - |
| className | Class name of the label element. | string | "" |
| disabled | Whether the label looks disabled. | boolean | - |
| extra | Content after the label text. | React.ReactNode | - |
| id | The id of the label element. | string | - |
| name | The id of the labeled control, written to the for attribute. The native htmlFor is used when it is not set. | string | - |
| optional | Whether to append the optional text of the locale. When true, the * of required is hidden. | boolean | false |
| ref | Reference to the label element. | TypeReact.Ref<HTMLLabelElement> | - |
| required | Whether to display the required * symbol. It is hidden when optional is true. | boolean | false |
| style | Inline style of the label element. | React.CSSProperties | - |
| text | Text of the label. children is used when it is not set. | React.ReactNode | - |
| width | Width of the label. A number is in px. | number | string | - |
Native attributes such as htmlFor and aria-* land on the label element. When both name and htmlFor are passed, name wins.
Form.InputGroup
Props
| Property | Description | Type | Default |
|---|---|---|---|
| children | The fields in the group. | React.ReactNode | - |
| className | Class name of the InputGroup inside. | string | - |
| disabled | The disabled state applied to the fields in the group. Falls back to the Form's disabled when not set. | boolean | - |
| extraText | Extra text of the group, positioned by extraTextPosition. | React.ReactNode | - |
| extraTextPosition | Position of the extra text: middle puts it between the label and the group, bottom after the group. Defaults to bottom. | 'bottom' | 'middle' | - |
| label | Label of the whole group: an object is the label configuration, whose id becomes the id of the label, and any other content is the label text. | LabelProps | React.ReactNode | - |
| labelPosition | Label position. Defaults to the Form's, and can be top or left. | 'left' | 'top' | - |
| onBlur | Called as focus leaves any control in the group, also for a move to another control in it. | Type(e: React.FocusEvent<HTMLSpanElement>) => void | - |
| onFocus | Called as any control in the group takes focus, also for a move from another control in it. | Type(e: React.FocusEvent<HTMLSpanElement>) => void | - |
| ref | Reference to the root element of the InputGroup inside (role="group"). | TypeReact.Ref<HTMLSpanElement> | - |
| size | Size of every control in the group: small, default or large. Defaults to default. | InputSize | - |
| style | Inline style of the InputGroup inside. | TypeRecord<string, any> | - |
className, style and ref land on the role="group" element of the InputGroup inside. aria-labelledby and aria-describedby are merged with the ids generated by the group and passed to every field in it, the caller's first. The outer element has role="group" when a label is set. Form.InputGroup works only inside a Form; outside one it renders nothing and logs a warning.
When extraTextPositon is middle and labelPosition is left. Since extraText is allowed to be ReactNode, the height of the content is variable, and the Label will no longer ensure that it can be aligned with the first line of text in the Field / InputGroup.
Form.Slot
import { Form } from '@fibrous/semi-ui-react';
const { Slot } = Form;
Props
| Property | Description | Type | Default |
|---|---|---|---|
| children | The content of the slot. | React.ReactNode | - |
| className | Class name of the slot. | string | - |
| error | The error, rendered after the content: an object is the props of ErrorMessage and any other content is the error. Nothing shows when omitted. | ErrorMessageProps | React.ReactNode | - |
| label | Label configuration: an object is the props of Form.Label, with text, extra, align, width, required, optional and so on; any other content is the label text. | LabelProps | React.ReactNode | - |
| labelPosition | Label position of the slot. Defaults to the Form's, and can be top or left. | 'top' | 'left' | - |
| noLabel | Whether to render no label. When the Form sets both labelCol and wrapperCol, the label column is still kept. | boolean | - |
| ref | Reference to the root div element. | TypeReact.Ref<HTMLDivElement> | - |
| style | Inline style of the slot. | React.CSSProperties | - |
Native attributes such as aria-* land on the root div. The root has role="group" when there is a label or aria-label or aria-labelledby is passed, and a role passed by the caller takes precedence. The width and alignment of the label come from the Form's labelWidth and labelAlign; width and align in the label object take precedence.
Form.ErrorMessage
import { Form } from '@fibrous/semi-ui-react';
const { ErrorMessage } = Form;
- When the error is React Node or String, render directly; when it is true, only the error container is rendered, without text; when it is false, nothing is rendered
- When the error is an array, the join operation is automatically performed to aggregate the error information in the array
Props
| Property | Description | Type | Default |
|---|---|---|---|
| className | Class name of the error message element. | string | - |
| error | The error. A string or ReactNode is shown as is and an array is joined with , . true shows the error container without text; false and empty values render nothing. | ReactFieldError | - |
| errorMessageId | The id of the error text element, referenced by aria-errormessage. | string | - |
| helpText | The help text. It shares the block with the error, and the error is shown when both are set. | React.ReactNode | - |
| helpTextId | The id of the help text element, referenced by aria-describedby. | string | - |
| isInInputGroup | Whether it is inside an InputGroup. When it and showValidateIcon are true, the error icon shows whatever validateStatus is. | boolean | - |
| ref | Reference to the error message element. | TypeReact.Ref<HTMLDivElement> | - |
| showValidateIcon | Whether to show the icon of validateStatus. | boolean | - |
| style | Inline style of the error message element. | React.CSSProperties | - |
| validateStatus | Validation status of the message: default, error, warning or success. | string | - |
Native attributes such as aria-* land on the root div.
withFieldOption
| key | Description | Default |
|---|---|---|
| valueKey | The component represents the property of the value, such as Switch, Radio is' checked 'and Input is' value ' | 'value' |
| onKeyChangeFnName | The callback function when the component value changes, generally 'onChange' | 'onChange' |
| valuePath | The path of the value attribute to the first parameter in the callback function, such as Radio's onChange (e.target. checked), then the value needs to be set to target .checked; Radio Group's onChange (e.target. value), which is' target .value '; if the first parameter is the value itself, there is no need to take the value down, the item does not need to be set |
Accessibility
ARIA
- aria-labelledby、for
- Field component will automatically add label DOM. The id of the control is
props.id, generated when omitted; the for attribute of label is the same as this id; theidattribute of label is${control id}-label; - When the props.labelPosition of the Form or Field is set to
inset, there is no label tag at this time, but a div tag. The div tag corresponding to insetLabel will be automatically appended withid, the value is the same as the id of the above label, corresponding to thearia-labelledbyof the Field component. A field that does not support the inset label is laid out astop, and itsaria-labelledbypoints to the label actually rendered - The Field component will be automatically appended with
aria-labelledby, the value is the same as the id of the above label; thearia-labelledbypassed by the caller comes first and the two are merged instead of overwritten
- Field component will automatically add label DOM. The id of the control is
- aria-required
- When the Field is configured with required fields (that is, props.rules contains require: true or props.label is configured with required: true without optional: true), the Field component will be automatically appended with
aria-required = true(except CheckboxGroup, Slider, Upload and a single Radio); an aria-required passed on the Field takes precedence
- When the Field is configured with required fields (that is, props.rules contains require: true or props.label is configured with required: true without optional: true), the Field component will be automatically appended with
- aria-invalid 、aria-errormessage
- When the Field check fails, or validateStatus is error, the Field component will be automatically added with the
aria-invalid= true attribute. - When the Field check fails, the Field component will be automatically appended with the
aria-errormessageattribute, the value of which is the id of the DOM element corresponding to the errorMessage (format like:${control id}-errormessage); for CheckboxGroup it is set on every checkbox in the group.
- When the Field check fails, or validateStatus is error, the Field component will be automatically added with the
- aria-describedby
- When the Field is configured with
helpTextorextraText, the Field component will be automatically added with thearia-describedbyattribute, whose value is the id of the DOM element corresponding to helpText and extraText (format like:${control id}-helpText,${control id}-extraText); thearia-describedbypassed by the caller comes first and the two are merged instead of overwritten
- When the Field is configured with
Content Guidelines
- Form title
- The title of the form needs to follow the writing specification of the title
- Form label
- The label is a short description of the input box. The label is not a help text, so it should not be a description of the input
- Labels must:
- Place it above or below the input box
- Short (1-3 words)
- Use case conventions for statements (first letter uppercase, others lowercase)
- Help text
- Help text use statement writing conventions, capitalized
- Form button
- For the content Guidelines of the form button, refer to Button component
Design Tokens
| Sass variable | Value | Usage |
|---|---|---|
| $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
-
Why did I declare the form, modify the value, and the data is not automatically mapped to formState.values?
Check that the field has been passed correctly, and thefieldattribute on the Field component is a must-fill property ! -
Why doesn't the passed
defaultValueordefaultCheckedtake effect?
Refer to the beginning of the document Field. The Field component unifies the default value. You should pass the default value usinginitValueorinitValues -
Why did the component not change and the value not take effect after
initValueandinitValueswere updated asynchronously?
initValue,initValuesare only consumed once when Field and Form mount, and subsequent asynchronous updates will not take effect.
If your initial value needs to be taken remotely, you can update it usingformApi.setValue / setValuesafter you get the value
Or send a newkeydirectly to Form or Field to force it to remount. -
Why can't getValues get a certain field?
If the field has no initial value,
getValuescannot get this item. You can setinitValues/initValueor set theallowEmptyattribute to the form. -
Why does hitting enter on the input box trigger the form's submit?
This is standard HTML behavior. We do not plan to intervene and we will remain the same as the HTML. If there is really only one input element in the form, and you don't want to trigger the submit callback when you press Enter, it is recommended to use preventDefault for the enter of the keydown event of input to prevent the default behavior.
Click #767 for background and content.
-
The form will automatically save the historical input items, what should I do if I don't want this function?
Before v2.3, Form did not configurefor,name,idand other attributes for input controls strictly according to the A11y accessibility standard, so this function was not available in previous versions. After v2.3, we implemented it strictly according to the W3C standard. If you don't want the browser to automatically save history input items, you can also turn it off by settingautoComplete=offat the Form level or Field level