输入类

表单

表单(Form)

  • 按需重绘,避免了不必要的全量渲染, 性能更高
  • 简单易用,结构极简,避免了不必要的层级嵌套
  • 完善的无障碍支持
  • 在 Form 外部可方便地获取 formState / fieldState
    提供在外部对表单内部进行操作的方法:formApi / fieldApi
  • 支持将自定义组件封装成表单控件,你可以通过 Form 提供的扩展机制(withField)快捷接入自己团队的组件
  • 支持 Form level / Field level 级别的赋值、校验(同步/异步)

表单控件(Field)

Semi 将所有自带的输入控件(文本输入框、下拉选择、复选框、单选框等)都使用 withField 封装了一次。接管了他们的数据流(value & onChange)
使用的时候,需要从 Form 中导出(注意:从 Form 导出的控件才具有数据同步功能)

目前 Form 提供了如下表单控件

  • Input、InputNumber、TextArea、Select、Checkbox、CheckboxGroup、Radio、RadioGroup、Switch、DatePicker、TimePicker、Slider、InputGroup、TreeSelect、Cascader、Rating、AutoComplete、Upload、PinCode、Label、ErrorMessage、Section、Slot、TagInput 都挂载在 Form 下,使用时直接以<Form.Input> 、<Form.Select>声明即可
import { Form } from '@fibrous/semi-ui-react';
// 具有数据同步功能的表单控件,在<Form></Form>内使用时,数据流会被Form自动接管
// 从Form中导出表单控件时,你还可以进行重命名(这里命名为FormInput仅仅是为了在以下示例中跟普通Input做区分)
const FormInput = Form.Input;
const FormSelect = Form.Select;
const Option = FormSelect.Option;
// 普通Input,在<Form></Form>内部使用时,Form不会对其做任何处理
import { Input } from '@fibrous/semi-ui-react';

Form 提供的 Field 级别组件,它的 value(或者 valueKey 指定的其他属性)、onChange(或 onKeyChangeFnName 指定的其他回调函数)属性都会被 Form 劫持,所以

代码演示

声明表单的多种写法

Semi Form 同时支持多种写法

基本写法

从 Form 中导出表单控件,给表单控件添加field属性,将其放置于 Form 内部即可
还可以给每个表单控件设置label属性,不传入时默认与 field 相同
label可以直接传入字符串,亦可以以 object 方式声明,配置 extra、required、optional等属性应对更复杂的场景

代码
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>
    );
}

支持的其他写法

当你需要在 Form 结构内部直接获取到 formState、formApi、values 等值时,你还可以使用以下的写法

通过 render 属性传入

即 render props

{"values":{},"errors":{},"touched":{}}
代码
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)}
        />
    );
}

通过 child render function

Form 的 children 是一个 function,return 出所有表单控件

{"values":{},"errors":{},"touched":{}}
代码
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>
    );
}

通过 props.component

通过 component 属性直接将整个内部结构以 ReactNode 形式传入

{"values":{},"errors":{},"touched":{}}
代码
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)} />;
}

已支持的表单控件

基本信息
没有找到合适的业务线?

abc

ulikeCam

已选择文件
vigo.png130KB
resso.jpeg222KB
douyin.jpeg222KB
资源详情
admin
user
guest
root
0100
我已阅读并清楚相关规定(Checkbox)
代码
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属性绑定一个字段名称,用于将表单项的值正确映射到FormState values / errors / touched 中
字段可以是简单的字符串,可以是包含.或者[]的字符串, 支持多级嵌套
下面是字段名称以及他们在 FormState 中的映射路径的示例

FieldResolution
usernameformState.values.username
user[0]formState.values.user[0]
siblings.1formState.values.siblings[1]
siblings['2']formState.values.siblings[2]
parents[0].nameformState.values.parents[0].name
parents[1]['name']formState.values.parents[1].name
代码
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>
    );
}

表单布局

  • 垂直布局:表单控件之间上下垂直排列(默认)
    Semi Design Next 更推荐表单采用垂直布局
I have read and agree to the terms of service

Or

代码
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>
    );
}
  • 水平布局:表单控件之间水平排列你可以通过设置 layout='horizontal'来使用水平布局
代码
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>
    );
}
  • labelPosition、labelAlign
    你可以通过设置 labelPosition、labelAlign 控制 label 在 Field 中出现的位置,文本对齐的方向
admin
user
guest
root
代码
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>
        </>
    );
}
  • 更复杂的布局你还可以结合 Grid 提供的 Row、Col,来对表单进行你想要的排列
代码
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.Section对 Fields 进行分组(仅影响布局,不会影响数据结构)

基本信息
合格标准
考试时间
分钟
到时间自动交卷
考试人员
关
代码
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>
    );
}

wrapperCol / labelCol

需要为 Form 内的所有 Field 设置统一的布局时,可以在 Form 上设置 wrapperCol 、labelCol 快速生成布局,无需手动使用 Row、Col 手动布局
wrapperCol、labelCol属性配置参考Col 组件

代码
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>
    );
}

隐藏Label

Form 会自动为 Field 控件插入 Label。如果你不需要自动插入 Label 模块, 可以通过在 Field 中设置noLabel=true将自动插入 Label 功能关闭(此时 Field 仍然具备自动展示 ErrorMessage 的能力,因此 DOM 结构与原始控件依然会有区别)
如果你希望与原始控件保持 DOM 结构一致,可以使用 pure=true,此时除了数据流被接管外,DOM结构不会有任何变化(你需要自行负责 ErrorMessage的渲染,同时它也无法被 formProps.wrapperCol 属性影响)

代码
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>
    );
}

内嵌 Label

通过将 labelPosition 设为inset,可以将 Label 内嵌在表单控件中。目前支持这项功能的组件有Input、InputNumber、DatePicker、TimePicker、Select、TreeSelect、Cascader、TagInput、AutoComplete。其余字段在 labelPosition 为inset时按top布局,并在开发模式下给出警告。

姓名
开始日期
代码
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>
    );
}

导出 Label、ErrorMessage 使用

如果你需要 Form.Label、Form.ErrorMessage 模块自行组合使用,可以从 Form 中导出

例如:当自带的 Label、ErrorMessage 布局不满足业务需求,需要自行组合位置,但又希望能直接使用 Label、ErrorMessage 的默认样式时

import { Form } from '@fibrous/semi-ui-react';
const { Label, ErrorMessage } = Form;

使用 Form.Slot 放置自定义组件

当你的自定义组件,需要与 Field 组件保持同样的布局样式时,你可以通过 Form.Slot 放置你的自定义组件
在 Form 组件上设置的 labelWidth、labelAlign、wrapperCol、labelCol 会自动作用在 Form.Slot 上
Slot 属性配置详见Form.Slot

我是Semi Form SlotA, 我是自定义的节点
我是SlotA的ErrorMessage
我是Semi Form SlotB, 我的Label Align、Width与众不同
代码
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>
    );
}

使用 helpText、extraText 放置提示信息

可以通过helpText放置自定义提示信息,与校验信息(error)公用同一区块展示,两者均有值时,优先展示校验信息。
可以通过extraText放置额外的提示信息,当需要错误信息和提示文案同时出现时,可以使用这个配置,常显,位于 helpText/error 后
当传入 validateStatus 时,优先展示 validateStatus 值对应的 UI 样式。不传入时,以 field 内部校验状态为准。

代码
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>
    );
}

通过配置 extraTextPosition,你可以控制 extraText 的显示位置。可选值 bottom、middle
例如如当你希望将 extraText 提示信息显示在 Label 与 Field 控件中间时
该属性可在 Form 上统一配置,亦可在每个 Field 上单独配置,同时传入时,以 Field 的配置为准。

未勾选时,默认为红点提醒,消息默认进入收件人消息列表。对于重要通知,可同时勾选相应的通知方式。
飞书通知
邮件通知
顶部横幅通知
对于对话框通知,可指定该消息必须在指定时长后才可置为已读。
代码
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>
    );
}

使用 InputGroup 组合多个 Field

当你需要将一些表单控件组合起来使用时,你可以用Form.InputGroup将其包裹起来
当你给Select、Input等表单控件加上 field 属性时,Form会默认给每个 Field 控件自动插入Label
而在InputGroup中一般仅需要一个属于整个 Group 的 Label,你可以在 InputGroup 中设置 label 属性,插入一个属于 Group 的Label
label可配置属性详见Label

代码
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 放置于 Modal 中,以弹窗形式承载
在提交时,通过 formApi.validate()对 Field 进行集中校验

代码
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>
        </>
    );
}

配置初始值与校验规则

  • 你可以通过rules为每个 Field 表单控件配置校验规则
    Form 内部的校验库基于 async-validator,更多配置规则可查阅其官方文档
  • 你可以通过 form 的initValues为整个表单统一设置初始值,也可以在每个 field 中通过initValue设置初始值(后者优先级更高)
  • 可以通过 trigger 为每个 Field 配置不同的校验触发时机,默认为 change(即值变化时,自动进行校验)。还支持 change、blur、mount、custom 或以上的组合。也支持通过 FormProps 统一配置,若都配置时,以 FieldProps 为准
  • 可以通过 stopValidateWithError 开关,决定使用 rules 校验时,当碰到第一个检验不通过的 rules 后,是否继续触发后续 rules 的校验。也支持通过 FormProps 统一配置,若都配置时,以 FieldProps 为准
代码
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>
    );
}

自定义校验(Form 级别)

你可以通过 validator 给Form整体设置自定义校验函数。submit 或调用 formApi.validate() 时会进行调用

同步校验

校验通过时,你应该返回一个空字符串;
校验失败时,你应该返回错误信息(Object,key 为 fieldName,value 为对应的错误信息)

代码
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>
    );
}

异步校验

异步校验时,你应当返回一个 promise,在 promise.then()中 你需要 return 对应的错误信息

代码
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>
    );
}

自定义校验(Field 级别)

你可以通过 validator 指定单个表单控件的自定义校验函数,支持同步、异步校验(通过返回 promise)

代码
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>
    );
}

手动触发指定 Field 校验

当你希望手动触发某些特定 Field 的校验操作时,可以通过 formApi.validate 完成。不传入参数时,默认对全部 Field 进行校验,传入参数时,以参数指定为准

静默校验

当你需要获取校验结果但不想触发 UI 更新时(如不显示错误提示、不设置 touched 状态),可以通过 formApi.validate({ silent: true }) 实现。这在根据校验结果决定是否发起后端请求等场景中非常有用。

代码
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>
    );
}
a
b
c
d
b.name
a
b
c
d
b.name
代码
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>
    );
}

表单联动

你可以通过监听 Field 的 onChange 事件,然后使用 formApi 进行相关修改,来使 Field 之间达到联动

Note will change after Sex select
代码
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>
    );
}

动态表单

动态删减表单项

代码
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>
    );
}

保留空值(allowEmpty)

默认情况下,值变为空的 field 会从 values 中移除。在 Field 上设置 allowEmpty 可以保留它的 key(值为 undefined),在 Form 上设置则对所有 field 生效。清空下面两个选择框,对比 values 中的 key。

["keep","drop"]
代码
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>
    );
}

动态表单保留状态(keepState)

默认情况下,当 Field 组件卸载后,其对应的值(value)、校验信息(error)、交互状态(touched)都会被重置。如果你希望 Field 卸载后保留这些状态(例如在条件渲染的场景中),可以使用 keepState 属性。

显示条件字段
代码
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>
    );
}

在上面的示例中:

  • 设置了 keepState 的字段在隐藏再显示后,之前输入的值、校验状态等会被保留
  • 未设置 keepState 的字段在隐藏再显示后,状态会被重置

使用 ArrayField

针对动态增删的数组类表单项,我们提供了 ArrayField 作用域来简化 add/remove 的操作
ArrayField 自带了 add、remove、addWithInitValue 等 api 用来执行新增行,删除行,新增带有初始值的行等操作
ArrayField 详细的 API请查阅下方 ArrayField Props 注意:ArrayField 的 initValue 类型必须是数组

代码
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>
    );
}

嵌套 ArrayField

ArrayField 支持多级嵌套,如下是一个两级嵌套的例子

当邮件到达,满足以下条件时:
当邮件到达,满足以下条件时:
代码
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>
    );
}

动态删减表单项 - 通过 formApi

如果不使用 ArrayField,也可以通过 Form 提供的 formApi,手动增删 formState 中的数据。

代码
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>
    );
}

Hooks 的使用

我们提供了四个 Hooks,使你在不需要通过 props 传递的情况下,也能在放置于 Form 结构内部的 Functional Component 中也能轻易访问到 Form 内部状态数据,以及调用 Form、Field 的相关 api

import { useFormApi, useFormState, useFieldApi, useFieldState } from '@fibrous/semi-ui-react';

useFormApi

useFormApi 允许你通过 hook,在 Functional Component 内直接访问父级 Form 组件的 formApi

代码
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 允许你通过 hook,在 Functional Component 内直接访问父级 Form 组件的 formState

FormState read by 【useFormState】:
{"values":{},"errors":{},"touched":{}}
代码
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 允许你通过 hook,在 Functional Component 内直接调用指定 Field 的 api

代码
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 允许你通过 hook,在 Functional Component 内直接访问指定 Field 的 State

【name】FieldState read by 【useFieldState】:{}
【country】FieldState read by 【useFieldState】:{}
代码
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>
    );
}

HOC 的使用

我们提供了两个 HOC: withFormApi、withFormState,可以在其他组件内部访问到 Form 的 formApi 以及内部状态 formState 提供了 HOC: withField,用于将自定义组件封装成符合 Semi Form 数据流的表单控件

import { withFormApi, withFormState, withField } from '@fibrous/semi-ui-react';

HOC-withFormApi

你可以通过 withFormApi HOC 来封装组件,使得该组件内部可以直接调用父级 Form 组件的 formApi
注意封装后的组件必须放置于 Form 结构内部

代码
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

你可以通过 withFormState HOC 来封装组件,使得该组件内部可直接访问到父级 Form 组件的 FormState
注意封装后的组件必须放置于 Form 结构内部 (因其强依赖 Context 机制进行消费)

{"values":{},"errors":{},"touched":{}}
代码
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>
    );
}

withField 封装自定义表单控件

通过 withField,你可以将其他自定义组件扩展成为表单控件,由 Form 接管其行为

withField 主要做了以下事情

  • 负责接管组件的 value(或者 valueKey 指定的其他属性)、onChange(或 onKeyChangeFnName 指定的其他回调函数)
  • 负责在表单控件上方插入 Field 的<Form.Label>
  • 负责在表单控件下方插入 Field 的<ErrorMessage>
  • 负责在表单控件下方插入 Field 的 extraText

withFieldOption 具体配置可参考 withField Option

你的自定义受控组件需要做以下事情:

  • 值发生变化时,调用props.onChange (或 onKeyChangeFnName 指定的其他回调函数) 并且将最新的值作为入参
  • 响应props.value(或者 valueKey 指定的其他属性)的变化,并更新你的组件UI渲染结果
withField(YourComponent, withFieldOption);

返回组件的 Props 类型由 YourComponent 的 Props 推断:保留组件自身的属性,加上 Field Props,去掉 value、defaultValue、checked、defaultChecked(值由 Form 接管)。

{"values":{},"errors":{},"touched":{}}
代码
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>
    );
}
名称
{"values":{},"errors":{},"touched":{}}
代码
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 参考

Form

Props

属性说明类型默认值
allowEmpty值变为空时是否在 values 中保留该字段的 key。Form 与字段任一设置为 true 即生效。booleanfalse
aria-label表单的可访问名称,写在 form 元素上。React.AriaAttributes['aria-label']-
autoScrollToError提交或调用 formApi.validate() 校验失败时,是否自动滚动到出错的字段;传对象时作为 scroll-into-view-if-needed 的选项。boolean | ScrollIntoViewOptionsfalse
children表单内容。传入函数时,参数为 { formState, values, formApi },用于声明表单控件。TypeReact.ReactNode | ((internalProps: FormFCChild<Values>) => React.ReactNode)-
classNameform 元素的类名。string-
component用于声明表单控件的组件,以 formState、values、formApi 作为 props 渲染;不可与 render、children 同时使用。TypeReact.ComponentType<any>-
disabled统一应用在每个字段上的 disabled,字段自身的 disabled 优先。boolean-
extraTextPosition统一应用在每个字段上的 extraText 位置,middle 在 Label 与控件之间,bottom 在控件之后。未设置时为 bottom,字段上的同名属性优先。'middle' | 'bottom'-
formForm.useForm() 返回的 formApi,用于在 Form 外部控制表单。TypeReturnType<typeof useForm<Values>>[0]-
getFormApiForm 挂载后调用,参数为 formApi。Type(formApi: FormApi<Values>) => void-
idform 元素的 id,同时写入 x-form-id;不传时自动生成。string-
initValues表单的初始值,仅在 Form 创建时复制一次。Values-
labelAlign统一应用在每个字段上的 label 文本对齐方式,可选 left 或 right,未设置时为 left。字段上的同名属性优先。'left' | 'right'-
labelCol统一应用在每个字段上的 label 栅格布局,同 Col 的 span、offset 等属性,需与 wrapperCol 一起设置。TypeRecord<string, any>-
labelPosition统一应用在每个字段上的 label 位置,可选 top、left 或 inset,inset 仅部分字段支持,其余字段按 top 布局。字段上的同名属性优先。'top' | 'left' | 'inset'"top"
labelWidth统一应用在每个字段上的 label 宽度,数字单位为 px。number | string-
layout字段的排列方向,可选 vertical 或 horizontal。'horizontal' | 'vertical'"vertical"
onChange表单状态更新时调用,包括字段挂载、卸载、值变更、blur、校验状态与错误信息变更,参数为 formState。Type(formState: FormState<Values>) => void-
onErrorChange校验状态更新时调用,参数为全部错误和本次变化的字段错误。validate、submit、reset 等批量操作只调用一次,静默校验不调用。Type(errors: AllErrors<Values>, changedError?: Partial<Record<FieldPath<Values>, FieldError>>) => void-
onReset点击重置按钮或调用 formApi.reset() 时调用。Type() => void-
onSubmit点击提交按钮或调用 formApi.submitForm() 且校验通过后调用,参数为 values 与提交事件。Type(values: Values, e?: React.SubmitEvent<HTMLFormElement>) => void-
onSubmitFail点击提交按钮或调用 formApi.submitForm() 且校验失败后调用,参数为 errors、values 与提交事件。Type(errors: BatchValidateResult<Values>, values: Partial<Values>, e?: React.SubmitEvent<HTMLFormElement>) => void-
onValueChange字段的值变化时调用,参数为全部 values 与本次变化的字段值。Type(values: Values, changedValue: Partial<Values>) => void-
ref指向句柄 { formApi } 的引用,可通过句柄上的 formApi 控制表单。TypeReact.Ref<FormHandle<Values>>-
render用于声明表单控件的函数,参数为 { formState, values, formApi };不可与 component、children 同时使用。Type(internalProps: FormFCChild<Values>) => React.ReactNode-
showValidateIcon校验信息前是否显示对应状态的图标。booleantrue
stopPropagation是否阻止 submit、reset 事件冒泡,可传 { submit, reset } 分别设置,用于嵌套 Form 时避免触发外层 Form 的事件。未设置时不阻止冒泡。{ submit?: boolean; reset?: boolean; }-
stopValidateWithError使用 rules 校验时,遇到第一条不通过的规则后是否不再校验后续规则。字段上的同名属性优先。boolean-
styleform 元素的内联样式。React.CSSProperties-
trigger统一应用在每个字段上的校验时机,可选 change、blur、mount、custom 或其数组。字段上的同名属性优先。FieldValidateTriggerType-
validatorForm 级别的校验函数,submit 与 formApi.validate() 时调用,可返回 Promise。设置后字段自身的 validator 与 rules 不再生效。返回空字符串或空对象表示通过,失败时返回与 values 结构一致的错误对象。Type(values: Values) => BatchValidateResult<Values> | Promise<BatchValidateResult<Values>>-
wrapperCol统一应用在每个字段上的控件栅格布局,同 Col 的 span、offset 等属性,需与 labelCol 一起设置。TypeRecord<string, any>-

Methods

方法说明类型
formApi表单的 formApi,用于读取与修改表单状态、校验与提交。TypeFormApi<Values>

aria-* 等原生属性与事件落在 form 元素上;设置了 labelCol 与 wrapperCol 时 form 外层多一层 Row,其余属性仍落在 form 上。form 元素默认带 noValidate,校验只由 Semi 负责,传 noValidate={false} 可以恢复原生校验。

FormState

FormState 存储了所有 Form 内部的状态值,包括各表单控件的值,错误信息、touched 状态
进行表单提交时,实际提交的就是 formState.values

Name说明初始值示例
values表单的值{}{ fieldA: 'str', fieldB: true }
errors表单错误信息集合,你可以通过判断是否有错误信息来决定是否允许用户提交{}{ fieldA: 'length not valid'}
touched用户点击过的 field 集合{}{ fieldA: true }

如何访问 formState

FormApi

我们提供了 FormApi。你在 Form 内部、外部都可以很方便地获取到 formApi,它允许你使用 getter 和 setter 来获取和操作 formState 的值。
下面的表格描述了 formApi 中可用的功能。

Function说明example
getFormProps获取 Form 组件上当前所有props的值,例如可用于读取 disabled 等。formApi.getFormProps(propNames?: string[])
getFormState获取 FormStateformApi.getFormState()
submitForm可手动触发 submit 提交操作formApi.submitForm()
reset可手动对 form 进行重置formApi.reset(fields?: Array <string>)
validate可手动触发对表单的校验,不传参时默认触发整全体Field的校验(配置Form级别校验器后,Field级别校验器在submit或formApi.validate()时不会再被触发),若想触发部分field的校验,将目标field数组传入即可

支持静默校验:传入 { silent: true } 可在不触发 UI 更新(不显示错误提示、不设置 touched 状态)的情况下获取校验结果,也可通过 { fields: ['fieldA'], silent: true } 对指定字段进行静默校验
formApi.validate()
.then(values=>{})
.catch(errors=>{})
或 formApi.validate(['fieldA','fieldB'])
或 formApi.validate({ silent: true })
或 formApi.validate({ fields: ['fieldA'], silent: true })
setValues设置整个表单的值。第二个参数中的 isOverride 默认为 false
isOverride 为 false 时:遍历 Form 中所有已注册的 field,从 newValues 中取对应字段的值更新到 formState.values 中。如果 newValues 中不包含某个已注册 field 的值,该字段会被设置为 undefined。
isOverride 为 true 时:直接用 newValues 整体替换 formState.values,包括 newValues 中存在但表单中没有对应 field 的额外 key 也会写入。

推荐用法:如果只需要更新部分字段,建议使用 formApi.setValue(field, value) 逐个更新,或者先获取当前值再合并:formApi.setValues({ ...formApi.getValues(), name: newValue })
formApi.setValues(newValues: object, { isOverride: boolean })
setValue提供直接修改 formState.values 方法,与 setValues 的区别是它仅修改单个 fieldformApi.setValue(field: string, newFieldValue: any)
getValue获取 单个 Field 的值formApi.getValue()
formApi.getValue(field: string)
getValues获取 所有 Field 的值formApi.getValues()
setTouched修改 formState.touchedformApi.setTouched(field: string, isTouched: boolean)
getTouched获取 Field 的 touched 状态formApi.getTouched(field: string)
setError修改 某个 field 的 error 信息formApi.setError(field: string, fieldErrorMessage: string)
getError获取 Field 的 error 状态formApi.getError(field: string)
getFieldExist获取 Form 中是否存在对应的 fieldformApi.getFieldExist(field: string)
scrollToField滚动至指定的 field, 第二个入参将透传至scroll-into-view-if-neededformApi.scrollToField(field: string, scrollOpts: ScrollIntoViewOptions)
scrollToError滚动至校验错误的field,可传指定 field 或者 index,传入 index 则滚动到第 index 个错误的 DOM,若不传参则滚动到DOM树中第一个校验出错的位置。formApi.scrollToError(ScrollToErrorOptions{field?: string; index?: number; scrollOpts?: ScrollIntoViewOptions })

如何获取 formApi

  • 通过 Form.useForm() 在 Form 外部创建 formApi,然后通过 props.form 传入 Form 组件。这种方式允许你在 Form 渲染前就获取 formApi,适合需要在组件外部控制表单的场景。
  • Form 组件在 ComponentDidMount 阶段,会执行 props 传入的 getFormApi 回调,你可以在回调函数中保存 formApi 的引用,以便后续进行调用(示例如下代码)
    除此之外,我们还提供了其他方式获取 formApi,你可以根据喜好选择不同的调用方式
  • 通过 ref 的方式获取 Form 组件实例,直接访问实例上的 formApi
  • 通过 child render function 方式声明表单,formApi 会作为参数注入
  • 通过 render props 方式声明表单,formApi 会作为参数注入
  • 通过 useFormApi hook
  • 通过 withFormApi HOC

使用 Form.useForm()

Form.useForm() 用于在 Form 组件外部创建 formApi 实例,适合需要在 Form 渲染前就控制表单的场景。它返回一个数组 [formApi, formState, formValues]。

使用示例:

表单值:{}
touched 状态:{}
代码
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';

() => {
    // 函数式组件通过useRef存储formApi
    const api = useRef();

    return (
        <Form getFormApi={(formApi) => (api.current = formApi)}>
            <Form.Input field="a" />
            <Button
                onClick={() => {
                    console.log(api);
                }}
            >
                log
            </Button>
        </Form>
    );
};
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;
        // 获取到formApi对象后,你可以使用它来对表单进行任何你想做的修改 ~
    }

    changeValues() {
        // 使用 FormA的 formApi
        this.formApi.setValues({ a: 1 });
        // 使用 FormB的 formApi
        this.formBRef.current.formApi.setValues({ b: 2 });
    }

    render() {
        return (
            <>
                {/* 通过getFormApi回调获取并保存formApi */}
                <Form getFormApi={this.getFormApi} />
                {/* 通过ref直接获取Form组件实例上的formApi */}
                <Form ref={this.formBRef} />
                <Button onClick={() => this.changeValues()}>Change</Button>
            </>
        );
    }
}

Field Props

属性说明类型默认值
convertfield 值改变后,在 rerender 前,对 filed 的值进行二次更新
使用示例: (value) => newValue
function(fieldValue)
field该表单控件的值在 formState.values 中的映射路径,Form 会使用该值来区分内部的表单控件
必填!!! 示例:Bindding Syntax
string
label该表单控件的 label 标签文本,不传的时候默认与 field 同名, 传入 object 时会将其透传给 Form.Label,具体配置请参考Labelstring|object
labelPosition该表单控件的 label 位置,可选'top'/'left'/'inset'。在Form与Field上同时传入时,以Field props为准string
labelAlign该表单控件的 label 文本的 text-align。在Form与Field上同时传入时,以Field props为准string
labelWidth该表单控件的 label 文本的 width。在Form与Field上同时传入时,以Field props为准string|number
labelCol该表单控件的 label 栅格布局,同Col 组件,覆盖 Form 上的同名属性。与 wrapperCol 一起设置,且 Form 没有同时设置二者时,Field 会在内部自行包一层 Rowobject
wrapperCol该表单控件的控件栅格布局,同Col 组件,覆盖 Form 上的同名属性。使用方式同 labelColobject
noLabel当你不需要自动添加 label 时,可以将该值置为 trueboolean
noErrorMessage当你不需要自动添加 ErrorMessage 模块时,可以将该值置为 true,注意此时 helpText 也不会被展示boolean
name控件名称,传入时会自动在对应 field wrapper div 追加对应的 className,如:abc => '.semi-form-field-abc'。 还会将 name 透传至底层组件消费,例如你可以用于配置 input的name属性string
fieldClassName整个 fieldWrapper 的 className,作用与 name 参数一致,区别是不会自动追加前缀string
fieldStyle整个 fieldWrapper 的 内联样式object
initValue该表单控件的初始值(仅在 Field mounted 时消费一次,后续更新无效),相比 Form 的 initValues 中的值,它的优先级更高any(类型取决于当前组件,详细见各组件的 api)
validator该表单控件的自定义校验函数。支持同步、异步校验(通过返回 promise)。
设置了 validator 时,rules 不会生效
使用示例:(fieldValue, values) => fieldValue >= 5 ? 'value not valid' : ''
function(fieldValue, values)
rules校验规则,校验库基于async-validator
使用示例:const rules=[{ required: true, message: 'can't be null ' },
{ max: 10, message: 'can't more than 10 word' }]
array
validateStatus该表单控件的校验结果状态(仅影响样式),可选值:success/error/warning/defaultstring'default'
trigger触发校验的时机,可选值:blur/change/custom/mount,或以上值的组合['blur','change']
1、设置为 custom 时,仅会由 formApi/fieldApi 触发校验时被触发
2、mount(挂载时即触发一次校验)
'blur' | 'change' | 'custom' | 'mount',或其数组'change'
onChange值变化时触发的回调。回调函数的最后一个参数为最新的表单值 latestValues,可用于解决 React 闭包陷阱问题。
使用示例:onChange={(value, e, ...other) => { const latestValues = other[other.length - 1]; }}
function(fieldValue: any | ev: { target: { value: any }}, ...args: any[], latestValues?: object)
(具体参见各组件的 onChange 方法)
onBlur失去焦点时触发的回调function() (具体参见各组件的 onBlur 方法)
transform校验前转换字段值,转换后的值仅会在校验时被消费,对 formState 无影响
使用示例: (value) => Number
function(fieldValue)
allowEmpty值为空时是否在 values 中保留该 field 的 key(值为 undefined)。Field 与 Form 任一设置即生效booleanfalse
allowEmptyString是否允许值为空字符串。默认情况下值为''时,该 field 对应的 key 会从 values 中移除,如果你希望保留该 key,那么需要将 allowEmptyString 设为 truebooleanfalse
emptyValue被视为空值的值。值变化为它时,该 field 的 key 会从 values 中移除(allowEmpty 为 true 时保留为 undefined)any''
keepState是否在 Field 卸载后保留其状态(value、error、touched)。为 true 时,Field 卸载后其值、校验信息、交互状态不会丢失,再次挂载时会自动恢复booleanfalse
stopValidateWithError为 true 时,使用 rules 校验,碰到第一个检验不通过的 rules 后,将不再触发后续 rules 的校验booleanfalse
helpText自定义提示信息,与校验信息公用同一区块展示,两者均有值时,优先展示校验信息ReactNode
extraText额外的提示信息,当需要错误信息和提示文案同时出现时,可以使用这个,位于 helpText/errorMessage 后ReactNode
pure是否仅接管数据流,为 true 时不会自动插入 ErrorMessage、Label、extraText 等模块,样式、DOM 结构与原始的组件保持一致booleanfalse
extraTextPosition控制extraText的显示位置,可选middle(垂直方向以Label、extraText、Field主体的顺序显示)、bottom (垂直方向以Label、Field主体、extraText的顺序显示);在Form与Field上同时传入时,以Field props为准string'bottom'
...other组件的其他可配置属性,与上面的属性平级一并传入即可,例如 Input 的 size/placeholder,Field 会将其透传至组件本身

Field Api

我们同样提供了 fieldApi,其功能与 formApi 大部分相似,区别在于 fieldApi 限定了修改的范围,它只能修改所绑定的 field。通过 useFieldApi 获取

Function说明example
setValue修改当前 Field 的值fieldApi.setValue(newValue: any)
getValue获取当前 Field 的值fieldApi.getValue()
setTouched修改当前 Field 的 touchedfieldApi.setTouched(true)
getTouched获取当前 Field 的 touchedfieldApi.getTouched()
setError修改当前 Field 的错误信息fieldApi.setError(newErrorMessage: string)
getError获取当前 Field 的错误信息fieldApi.getError()

ArrayField

针对动态增删的数组类表单项,我们提供了 ArrayField 作用域来简化 add/remove 的操作

Props

属性说明类型默认值
children每一行的渲染函数,必填,参数为 { arrayFields, add, addWithInitValue },返回 ReactNode。Type(props: ArrayFieldChildrenProps) => React.ReactNode-
field数组在 formState.values 中的路径,必填。例如各行字段为 a[0].name、a[1].name 时应传 a。string-
initValue数组的初始值,必须是数组;与 Form 的 initValues 同时配置时优先。any[]-

ArrayField 没有自己的 DOM,未声明的属性会被丢弃。

interface ArrayFieldChildrenProps {
    arrayFields: ArrayFieldItem<>;                               // 当前数组表单,可以用来执行map操作渲染出每一行
    add: (index?: number | React.MouseEvent) => void;                         // 新增空白行;传入数字时插入到该位置,传入事件时被忽略,因此可直接用作 onClick
    addWithInitValue: (lineObject: Record<string, any>, index?: number) => void; // 新增一个带初始值的行,index 为插入位置
}

interface ArrayFieldItem {
    key: string;        // 一个用于标识当前行的key,应当绑定在当前行的 wrapper 上
    field: string;      // 本行 fieldPath, 它等同于 ArrayFieldProps.field + [index]
    remove: () => void; // 移除本行的操作函数,调用时将直接删除本行
}

Form.Section

import { Form } from '@fibrous/semi-ui-react';
const { Section } = Form;

Props

属性说明类型默认值
children分组的内容。React.ReactNode-
classNamesection 元素的类名。string-
refsection 元素的引用。TypeReact.Ref<HTMLElement>-
stylesection 元素的内联样式。React.CSSProperties-
text分组标题,为空时不渲染标题。React.ReactNode-

aria-* 等原生属性落在 section 元素上。

Form.Label

默认情况下,Label 会由 Form 自行插入到每个 Field 中。如果你需要在其他地方自行插入 Label,我们提供了 Label 组件可以导出

import { Form } from '@fibrous/semi-ui-react';
const { Label } = Form;

Props

属性说明类型默认值
alignLabel 文本的对齐方式,可选 left 或 right。string"left"
childrenLabel 的内容,text 未设置时使用。React.ReactNode-
classNamelabel 元素的类名。string""
disabled是否为禁用样式。boolean-
extra跟随在 label 文本后的内容。React.ReactNode-
idlabel 元素的 id。string-
name所标注控件的 id,写入 label 的 for 属性;不传时使用原生的 htmlFor。string-
optional是否在文本后追加语言包中的可选标识。为 true 时不再显示 required 的 *。booleanfalse
reflabel 元素的引用。TypeReact.Ref<HTMLLabelElement>-
required是否显示必填的 * 号,optional 为 true 时不显示。booleanfalse
stylelabel 元素的内联样式。React.CSSProperties-
textLabel 文本;不传时使用 children。React.ReactNode-
widthLabel 宽度,数字单位为 px。number | string-

htmlFor、aria-* 等原生属性落在 label 元素上;name 与 htmlFor 同时传入时以 name 为准。

Form.InputGroup

Props

属性说明类型默认值
children组内的字段。React.ReactNode-
className组内 InputGroup 的类名。string-
disabled组内字段统一的 disabled,不传时沿用 Form 的 disabled。boolean-
extraText额外的提示信息,位置由 extraTextPosition 决定。React.ReactNode-
extraTextPositionextraText 的位置,middle 在 Label 与 Group 之间,bottom 在 Group 之后。未设置时为 bottom。'bottom' | 'middle'-
label整个 Group 的 Label:对象作为 Label 配置,其中 id 用作 Label 的 id,其他内容作为 Label 文本。LabelProps | React.ReactNode-
labelPositionLabel 的位置,默认取自 Form,可选 top 或 left。'left' | 'top'-
onBlur焦点离开组内任一控件时调用,在组内控件之间移动焦点时也会调用。Type(e: React.FocusEvent<HTMLSpanElement>) => void-
onFocus组内任一控件获得焦点时调用,从组内另一控件移入时也会调用。Type(e: React.FocusEvent<HTMLSpanElement>) => void-
ref组内 InputGroup 的根元素(role="group")的引用。TypeReact.Ref<HTMLSpanElement>-
size组内所有控件的尺寸,可选 small、default 或 large,未设置时为 default。InputSize-
style组内 InputGroup 的内联样式。TypeRecord<string, any>-

className、style、ref 落在内部 InputGroup 的 role="group" 元素上;aria-labelledby、aria-describedby 与组生成的 id 合并后传给组内每个字段,使用方的在前。设置了 label 时外层带 role="group"。Form.InputGroup 只能在 Form 内使用,在 Form 外不渲染任何内容并给出警告。

当 extraTextPositon 为 middle,且 labelPosition 为 left时。由于 extraText允许为 ReactNode,内容高度不定,Label将不再确保能与 Field / InputGroup 中的首行文本对齐。

Form.Slot

import { Form } from '@fibrous/semi-ui-react';
const { Slot } = Form;

Props

属性说明类型默认值
childrenslot 的主体内容。React.ReactNode-
classNameslot 的类名。string-
error错误信息,渲染在内容之后:对象作为 ErrorMessage 的属性,其他内容作为错误内容;不传则不显示。ErrorMessageProps | React.ReactNode-
labelLabel 配置:对象作为 Form.Label 的属性,可配置 text、extra、align、width、required、optional 等;其他内容作为 Label 文本。LabelProps | React.ReactNode-
labelPositionslot 的 Label 位置,默认取自 Form,可选 top 或 left。'top' | 'left'-
noLabel是否不渲染 Label;Form 同时设置了 labelCol 与 wrapperCol 时,Label 列仍保留。boolean-
ref根 div 元素的引用。TypeReact.Ref<HTMLDivElement>-
styleslot 的内联样式。React.CSSProperties-

aria-* 等原生属性落在根 div 上;有 label,或传入 aria-label、aria-labelledby 时根元素带 role="group",使用方传入的 role 优先。label 的宽度、对齐取自 Form 的 labelWidth、labelAlign,label 对象中的 width、align 优先。

Form.ErrorMessage

import { Form } from '@fibrous/semi-ui-react';
const { ErrorMessage } = Form;
  • 当 error 为 ReactNode、String 时,直接渲染;为 true 时只渲染错误态容器,不显示文本;为 false 时不渲染
  • 当 error 为数组时,会自动执行 join 操作聚合数组内的错误信息

Props

属性说明类型默认值
className错误信息元素的类名。string-
error错误信息。字符串与 ReactNode 直接显示,数组会用 , 合并其中的错误;true 只显示错误态容器,没有文本;false 与空值不渲染。ReactFieldError-
errorMessageId错误文本元素的 id,供 aria-errormessage 引用。string-
helpText提示信息,与错误信息共用同一区块,两者都有时显示错误信息。React.ReactNode-
helpTextId提示文本元素的 id,供 aria-describedby 引用。string-
isInInputGroup是否位于 InputGroup 中;为 true 且 showValidateIcon 为 true 时,不论 validateStatus 都显示错误图标。boolean-
ref错误信息元素的引用。TypeReact.Ref<HTMLDivElement>-
showValidateIcon是否显示 validateStatus 对应的图标。boolean-
style错误信息元素的内联样式。React.CSSProperties-
validateStatus信息所属的校验状态,可选 default、error、warning、success。string-

aria-* 等原生属性落在根 div 上。

withFieldOption

key描述默认值
valueKey组件表示值的属性,如 Switch、Radio 的是'checked',Input 的是'value''value'
onKeyChangeFnName组件值变化时的回调函数,一般为'onChange''onChange'
valuePath值属性在回调函数中第一个参数的路径,如 Radio 的 onChange(e.target.checked),那么该值需要设为 target.checked;RadioGroup 的 onChange(e.target.value),该值为'target.value';若第一个参数就是值本身,无需再往下取值,该项不需要设

Accessibility

ARIA

  • aria-labelledby、for
    • Field 组件,会自动添加 label DOM。控件的 id 取 props.id,未传入时自动生成;label 的 for 属性与该 id 相同;label 的 id 属性为 ${控件 id}-label;
    • 当 Form 或者 Field 的 props.labelPosition 设置为 inset时,此时不存在 label 标签,而是 div 标签。insetLabel 对应的 div 标签会被自动追加 id,值与上述 label 的 id 相同,对应 Field 组件的 aria-labelledby。不支持内嵌标签的字段按 top 布局,aria-labelledby 指向实际渲染的 label
    • Field 组件会被自动追加 aria-labelledby,值与上述 label 的id 相同;使用方传入的 aria-labelledby 排在前面,两者合并而不是覆盖
  • aria-required
    • 当 Field 配置了必填时(即 props.rules中包含 require: true 或 props.label配置了required: true 且没有 optional: true),Field 组件会被自动追加 aria-required = true(CheckboxGroup、Slider、Upload 和单个 Radio 除外);字段上显式传入的 aria-required 优先
  • aria-invalid 、aria-errormessage
    • 当 Field 校验未通过,或 validateStatus 为 error 时,Field 组件会被自动添加 aria-invalid = true 属性。
    • 当 Field 校验未通过时,Field 组件会被自动追加 aria-errormessage 属性,值为 errorMessage 所对应DOM元素的 id (格式: ${控件 id}-errormessage);CheckboxGroup 落在组内每个 checkbox 上。
  • aria-describedby
    • 当 Field 配置了 helpText 或 extraText 时,Field 组件会被自动添加 aria-describedby 属性,值为 helpText、extraText 所对应DOM元素的 id (格式:${控件 id}-helpText 、${控件 id}-extraText);使用方传入的 aria-describedby 排在前面,两者合并而不是覆盖

文案规范

  • 表单标题
    • 表单标题需要跟随标题的书写规范
  • 表单标签
    • 标签是输入框的简短描述。标签不是注释信息(help text),因此不应该是输入框的填写说明
    • 标签必须要:
      • 放在输入框的上方或下方
      • 简短(1-3个词)
      • 使用语句的大小写规范(首字母大写,其他小写)
  • 帮助文本
    • 帮助文本使用语句书写规范,首字母大写
  • 表单按钮
✅ 推荐用法❌ 不推荐用法

设计变量

下表是组件内部的 Sass 变量,不能覆写。定制样式请覆写 --semi-* CSS 变量(见 Design Token 页),或写不分层的 CSS 规则。
Sass 变量值用法
$spacing-form_field_horizontal-paddingRight$spacing-base水平布局表单项右侧内边距
$spacing-form_field_group_horizontal-paddingRight$spacing-base水平布局表单组标题右侧内边距
$spacing-form_field_vertical-paddingTop$spacing-base-tight表单项顶部内边距(垂直布局)
$spacing-form_field_vertical-paddingBottom$spacing-base-tight表单项底部内边距(垂直布局)
$spacing-form_field_group_vertical-paddingTop$spacing-base-tight垂直布局表单组顶部内边距
$spacing-form_field_group_vertical-paddingBottom$spacing-base-tight垂直布局表单组底部内边距
$spacing-form_label-paddingRight$spacing-base表单项标签右侧边距(水平布局)
$spacing-form_label-paddingTop($height-control-default - 20px) * 0.5表单项标签顶部内边距(水平布局)
$spacing-form_label-marginBottom$spacing-extra-tight表单项标签底部外边距
$spacing-form_label-marginTop0px表单项标签顶部外边距
$spacing-form_label_extra-marginLeft$spacing-extra-tight表单项标签额外信息左侧边距
$spacing-form_label_required-marginLeft$spacing-extra-tight表单项标签必填标志左侧边距
$spacing-form_label_posLeft-marginRight0表单项左侧标签右侧外边距
$spacing-form_label_posLeft-marginBottom0表单项左侧标签底部外边距
$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.5Switch / Rating 表单项顶部内边距(水平布局)
$color-form_requiredMark_disabled-text-defaultvar(--semi-color-danger)禁用表单项必填标记颜色
$color-form_label_disabled-text-defaultvar(--semi-color-disabled-text)禁用表单项标签文字颜色
$font-form_label-fontWeight$font-weight-bold表单项标签字重
$color-form_label-text-defaultvar(--semi-color-text-0)表单项标签文字颜色
$color-form_label_optional-text-defaultvar(--semi-color-tertiary)表单项标签可选标记颜色
$color-form_label_extra-text-defaultvar(--semi-color-tertiary)表单项标签图标颜色
$color-form_requiredMark-text-defaultvar(--semi-color-danger)必填标记颜色
$font-form_requiredMark-fontWeight$font-weight-bold表单必填标识字重
$color-form_message_error-text-defaultvar(--semi-color-danger)错误提示颜色
$color-form_alertIcon-icon-defaultvar(--semi-color-warning)警告图标颜色
$spacing-form_statusIcon-marginRight$spacing-extra-tight表单校验状态图标右侧外边距
$spacing-form_message-marginTop$spacing-extra-tight表单错误信息、辅助文字顶部外边距
$color-form_section-text-defaultvar(--semi-color-text-0)表单分组标题文字颜色
$color-form_section-border-defaultvar(--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-paddingTop0px表单分组标题顶部内边距
$spacing-form_section_text-marginTop0px表单分组标题顶部外边距

FAQ

  • 为什么我声明了表单,对值进行了修改,数据没有自动映射到 formState.values 中?
    请检查是否正确传入了 field,Field 上的field属性是必填项!!!

  • 为什么传入了 defaultValue、defaultChecked 不生效?
    请参考文档开头表单控件,Field 组件对默认值做了统一处理,你应该使用initValue或者initValues来传入默认值

  • 为什么异步更新了 initValue、initValues 后,组件没有发生变化,值没有生效?
    initValue、initValues只在 Field、Form mounted 时进行消费,后续做的异步更新并不会起效。
    如果你的初始值需要从远程取,那么你可以在获取到值之后,使用formApi.setValue/setValues进行更新。
    或者直接给 Form、Field 传入一个新的key强制它重新挂载

  • 为什么调用了 formApi.setValues 更新 fields 的值,但是实际渲染并没有更新?

    setValues 默认情况下对尚未存在的 field 进行赋值不会生效。如果你的 fields 是动态加载的话,请检查在 setValues 时,该 field 是否已 mounted。
    如有需要,可以使用 override 模式 formApi.setValues(newValue, { isOverride: true })

  • 为什么 rules 中的 validator 校验失败,但是对应的错误信息没有被展示?

    async-validator 的自定义 validator 返回值必须是 boolean 类型,否则它不执行任何回调,semi 后续的钩子也不会被调用。建议通过加 !! 或者 Boolean() 强制转换返回类型

  • 为什么 getValues 拿不到某个 field?

    field 没有初始值的话,getValues 获取不到这一项。可以设置 initValues/initValue 或者给 form 设置 allowEmpty 属性。

  • 为什么在输入框(如 Input)上敲击 enter 回车键会触发了 Form 的 submit?

    这个是标准 HTML 行为,我们不计划进行干预,会与原生保持一致。如果表单内确实只有一个 Input 元素,又不想回车时触发 submit 回调,建议对 Input 的 keydown 事件的 enter 采取 preventDefault 阻止默认行为。

    点击 #767 查看相关背景和内容。

  • 表单会自动保存历史输入项,不想要这个功能怎么办?
    在 v2.3 之前,Form 并没有严格按照 A11y 无障碍标准为输入控件配置for、name、id 等属性,因此在之前的版本并没有该功能。v2.3 后我们严格按照 W3C 标准进行了实现。如果你不希望浏览器自动保存历史输入项,也可以通过在 Form 级别或者 Field 级别设置 autoComplete=off 关掉