Input · Form

Form

Form

  • Rerender on demand, avoids unnecessary full-volume rendering, higher performance
  • Easy to use, simple structure, avoids unnecessary hierarchical nesting
  • Perfect accessibility support
  • FormState / FieldState can also be easily obtained from outside the Form Provides an external method to operate inside the form: formApi / fieldApi
  • Support for encapsulating custom components into form controls, and you can quickly access your team's components through the extension mechanism provided by Form (through withField)
  • Support Form level / Field level assignment, verification (synchronous / asynchronous)

Field

Semi encapsulates all form field component (Input、Select、Checkbox、DatePicker etc.) with withField once.
Taking over their data flow (modelValue & change)
When in use, you need to use the components whose names start with Form (note: only these components have data synchronization). They are named exports and are not mounted under Form.

Supported Field Component

  • Fields: FormInput, FormInputNumber, FormTextArea, FormSelect, FormCheckbox, FormCheckboxGroup, FormRadio, FormRadioGroup, FormSwitch, FormDatePicker, FormTimePicker, FormSlider, FormTreeSelect, FormCascader, FormRating, FormAutoComplete, FormUpload, FormPinCode, FormTagInput
  • Other parts: ArrayField, FormInputGroup, FormLabel, FormErrorMessage, FormSection, FormSlot
<script setup lang="ts">
    // A field takes over its data flow when used inside <Form>
    import { Form, FormInput, FormSelect } from '@fibrous/semi-ui-vue';
    // A plain Input is left untouched by the Form
    import { Input } from '@fibrous/semi-ui-vue';
</script>

<template>
    <Form>
        <FormInput field="name" />
        <FormSelect field="role">
            <FormSelect.Option value="rd">Engineer</FormSelect.Option>
        </FormSelect>
    </Form>
</template>

The Field level component provided by Form, its modelValue (or other properties specified by valueKey) and change event (or other events specified by onKeyChangeFnName) are taken over by Form, so

Demos

Various ways to declare form

Semi Form supports multiple writing at the same time.

Basic Usage

Use the fields whose names start with Form, such as FormInput and FormSelect, add the field property to each of them, and place them inside the Form. You can also set label properties for each field, by default is the same as field

label can be passed in a string directly, or declared in the form of an object, configure required, optional, align, width and other attributes to deal with more complex scenarios. Use slots for nodes: #fieldLabel replaces the label content and #labelExtra places content after it (the help icon in the example below). Slots take precedence over the label property

Code
<script setup lang="ts">
    import { IconHelpCircle } from '@fibrous/semi-icons-vue';
    import { Form, FormInput, FormSelect, Tooltip } from '@fibrous/semi-ui-vue';
</script>

<template>
    <Form layout="horizontal" @valueChange="(values) => console.log(values)">
        <FormInput field="UserName" label="用户名" :style="{ width: '80px' }" />
        <FormInput field="Password" label="密码" :style="{ width: '176px' }">
            <template #labelExtra>
                <Tooltip content="详情">
                    <IconHelpCircle aria-label="帮助" :style="{ color: 'var(--semi-color-text-2)' }" />
                </Tooltip>
            </template>
        </FormInput>
        <FormSelect field="Role" :label="{ text: '角色', optional: true }" :style="{ width: '176px' }">
            <FormSelect.Option value="admin">管理员</FormSelect.Option>
            <FormSelect.Option value="user">普通用户</FormSelect.Option>
            <FormSelect.Option value="guest">访客</FormSelect.Option>
        </FormSelect>
    </Form>
</template>

Other declaration methods

When you need to get formState, formApi, values, etc. directly inside the Form structure, you can use the following writing

Via the default scoped slot

The default slot of Form is a scoped slot with the parameters { formState, formApi, values }. Destructure what you need with #default="{ formState }". The render props, children function and props.component of React are all covered by this one in Vue.

{"values":{},"errors":{},"touched":{}}
Code
<script setup lang="ts">
    import { Form, FormInput, FormSelect } from '@fibrous/semi-ui-vue';
</script>

<template>
    <Form layout="horizontal" @valueChange="(values) => console.log(values)">
        <template #default="{ formState }">
            <FormSelect field="Role" label="角色" :style="{ width: '176px' }">
                <FormSelect.Option value="admin">管理员</FormSelect.Option>
                <FormSelect.Option value="user">普通用户</FormSelect.Option>
                <FormSelect.Option value="guest">访客</FormSelect.Option>
            </FormSelect>
            <FormInput field="UserName" label="用户名" :style="{ width: '80px' }" />
            <FormInput field="Password" label="密码" :style="{ width: '176px' }" />
            <code :style="{ marginTop: '24px' }">{{ JSON.stringify(formState) }}</code>
        </template>
    </Form>
</template>

All supported field components

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

abc

ulikeCam

已选择文件
vigo.png130KB
resso.jpeg222KB
douyin.jpeg222KB
资源详情
admin
user
guest
root
0100
我已阅读并清楚相关规定(Checkbox)
Code
<script setup lang="ts">
    import type { StyleValue } from 'vue';
    import { IconUpload } from '@fibrous/semi-icons-vue';
    import {
        Button,
        Col,
        Form,
        FormCascader,
        FormCheckbox,
        FormCheckboxGroup,
        FormDatePicker,
        FormInput,
        FormInputNumber,
        FormRadio,
        FormRadioGroup,
        FormRating,
        FormSection,
        FormSelect,
        FormSlider,
        FormSwitch,
        FormTagInput,
        FormTextArea,
        FormTimePicker,
        FormTreeSelect,
        FormUpload,
        Row,
        Space,
    } from '@fibrous/semi-ui-vue';

    const today = new Date(2024, 4, 6, 10, 30, 0);

    const initValues = {
        name: 'semi',
        business: ['ulikeCam'],
        role: 'ued',
        switch: true,
        files: [
            {
                uid: '1',
                name: 'vigo.png',
                status: 'success',
                size: '130KB',
                preview: true,
                url: '/assets/placeholder/avatar-d.svg',
            },
            {
                uid: '2',
                name: 'resso.jpeg',
                status: 'validateFail',
                size: '222KB',
                percent: 50,
                preview: true,
                fileInstance: new File([new ArrayBuffer(2048)], 'resso.jpeg', { type: 'image/jpeg' }),
                url: '/assets/placeholder/avatar-b.svg',
            },
            {
                uid: '3',
                name: 'douyin.jpeg',
                status: 'uploading',
                size: '222KB',
                percent: 50,
                preview: true,
                fileInstance: new File([new ArrayBuffer(2048)], 'dy.jpeg', { type: 'image/jpeg' }),
                url: '/assets/placeholder/avatar-a.svg',
            },
        ],
    };

    const treeData = [
        {
            label: '亚洲',
            value: 'Asia',
            key: '0',
            children: [
                {
                    label: '中国',
                    value: 'China',
                    key: '0-0',
                    children: [
                        {
                            label: '北京',
                            value: 'Beijing',
                            key: '0-0-0',
                        },
                        {
                            label: '上海',
                            value: 'Shanghai',
                            key: '0-0-1',
                        },
                    ],
                },
            ],
        },
        {
            label: '北美洲',
            value: 'North America',
            key: '1',
        },
    ];

    const style = { width: '90%' };
    const extraTextStyle: StyleValue = {
        color: 'var(--semi-blue-5)',
        fontSize: '14px',
        userSelect: 'none',
        cursor: 'pointer',
    };
</script>

<template>
    <Form :initValues="initValues" :style="{ padding: '10px', width: '100%' }" @valueChange="(values) => console.log(values)">
        <FormSection>
            <template #text>基本信息</template>
            <Row>
                <Col :span="12">
                    <FormInput field="name" initValue="mikeya" label="名称(Input)" :style="style" trigger="blur" />
                </Col>
                <Col :span="12">
                    <FormDatePicker field="date" :initValue="today" label="日期(DatePicker)" placeholder="请选择生效日期" :style="style" />
                </Col>
            </Row>
            <Row>
                <Col :span="12">
                    <FormSelect field="role" label="角色(Select)" placeholder="请选择你的角色" :style="style">
                        <FormSelect.Option value="operate">运营</FormSelect.Option>
                        <FormSelect.Option value="rd">开发</FormSelect.Option>
                        <FormSelect.Option value="pm">产品</FormSelect.Option>
                        <FormSelect.Option value="ued">设计</FormSelect.Option>
                    </FormSelect>
                </Col>
                <Col :span="12">
                    <FormSelect field="business" label="业务线(多选Select)" multiple placeholder="请选择业务线" :style="style">
                        <template #extraText>
                            <div :style="extraTextStyle">没有找到合适的业务线?</div>
                        </template>
                        <FormSelect.Option value="abc">Semi</FormSelect.Option>
                        <FormSelect.Option value="ulikeCam">轻颜相机</FormSelect.Option>
                        <FormSelect.Option value="toutiao">今日头条</FormSelect.Option>
                    </FormSelect>
                </Col>
            </Row>
            <Row>
                <Col :span="12">
                    <FormCascader field="area" label="地区(Cascader)" placeholder="请选择所在地区" :style="style" :treeData="treeData" />
                </Col>
                <Col :span="12">
                    <FormTreeSelect field="tree" filterTreeNode label="节点(TreeSelect)" placeholder="请选择服务节点" :style="style" :treeData="treeData" />
                </Col>
            </Row>
            <Row>
                <Col :span="12">
                    <FormTagInput field="product" :initValue="['abc', 'ulikeCam']" label="产品(TagInput)" placeholder="请输入产品" :style="style" />
                </Col>
            </Row>
            <Row>
                <Col :span="24">
                    <FormUpload action="//semi.design/api/upload" field="files" label="证明文件(Upload)">
                        <Button theme="light">
                            <template #icon>
                                <IconUpload />
                            </template>
                            点击上传
                        </Button>
                    </FormUpload>
                </Col>
            </Row>
        </FormSection>
        <FormSection>
            <template #text>资源详情</template>
            <Row>
                <Col :span="12">
                    <FormTextArea field="description" label="申请理由(TextArea)" placeholder="请填写申请资源理由" :style="{ ...style, height: '120px' }" />
                </Col>
                <Col :span="12">
                    <FormCheckboxGroup direction="horizontal" field="type" :initValue="['user', 'admin']" label="申请类型(CheckboxGroup)" :rules="[{ required: true }]">
                        <FormCheckbox value="admin">admin</FormCheckbox>
                        <FormCheckbox value="user">user</FormCheckbox>
                        <FormCheckbox value="guest">guest</FormCheckbox>
                        <FormCheckbox value="root">root</FormCheckbox>
                    </FormCheckboxGroup>
                    <FormRadioGroup field="isMonopolize" label="是否独占资源(Radio)" :rules="[{ type: 'boolean' }, { required: true, message: '必须选择是否独占 ' }]">
                        <FormRadio :value="1">是</FormRadio>
                        <FormRadio :value="0">否</FormRadio>
                    </FormRadioGroup>
                </Col>
            </Row>
            <Row>
                <Col :span="12">
                    <FormTimePicker field="time" label="截止时刻(TimePicker)" :style="style" />
                </Col>
                <Col :span="12">
                    <FormInputNumber field="number" :initValue="20" label="申请数量(InputNumber)" :style="style" />
                </Col>
            </Row>
            <Row>
                <Col :span="12">
                    <FormSlider field="range" :initValue="10" label="资源使用报警阈值(%)(Slider)" :style="style" />
                </Col>
                <Col :span="12">
                    <FormSwitch field="switch" label="开关(Switch)" />
                </Col>
            </Row>
            <Row>
                <Col :span="12">
                    <FormRating field="rating" :initValue="2" label="满意度(Rating)" :style="style" />
                </Col>
            </Row>
        </FormSection>
        <FormCheckbox field="agree" noLabel value="false">我已阅读并清楚相关规定(Checkbox)</FormCheckbox>
        <Space>
            <Button class="btn-margin-right" htmlType="submit" type="primary">提交(submit)</Button>
            <Button htmlType="reset">重置(reset)</Button>
        </Space>
    </Form>
</template>

Field binding syntax

Every Field component must have a field property. This is how the form manages the state of this field. See the field syntax section below for additional details on what you can pass in for field.

The field can be a simple string, can be contained.Or[]String that supports multi-level nesting
Below is an example of the field name and their mapping path in FormState

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
Code
<script setup lang="ts">
    import { Col, Form, FormInput, FormLabel, Row, TextArea, Toast } from '@fibrous/semi-ui-vue';

    const onSubmit = (values: Record<string, unknown>) => Toast.info({ content: JSON.stringify(values) });
</script>

<template>
    <Form @submit="onSubmit">
        <template #default="{ formState }">
            <Row>
                <Col :span="12">
                    <FormInput aria-label="username" field="username" placeholder="请尝试输入值" />
                    <FormInput aria-label="user[0]" field="user[0]" placeholder="请尝试输入值" />
                    <FormInput aria-label="siblings.1" field="siblings.1" placeholder="请尝试输入值" />
                    <FormInput aria-label="siblings['2']" field="siblings['2']" placeholder="请尝试输入值" />
                    <FormInput aria-label="parents[0].name" field="parents[0].name" placeholder="请尝试输入值" />
                    <FormInput aria-label="parents[1]['name']" field="parents[1]['name']" placeholder="请尝试输入值" />
                </Col>
                <Col :offset="1" :span="10" :style="{ marginTop: '12px' }">
                    <FormLabel text="FormState实时映射值:" />
                    <TextArea aria-label="FormState实时映射值" :modelValue="JSON.stringify(formState.values)" />
                </Col>
            </Row>
        </template>
    </Form>
</template>

Form layout

  • Vertical Layout: Arrange each field vertically (By default)
    Semi Design Next recommends a vertical layout.
I have read and agree to the terms of service

Or

Code
<script setup lang="ts">
    import { Button, Form, FormCheckbox, FormInput, Toast } from '@fibrous/semi-ui-vue';

    const handleSubmit = (values: Record<string, unknown>) => {
        console.log(values);
        Toast.info('表单已提交');
    };
</script>

<template>
    <Form :style="{ width: '400px' }" @submit="handleSubmit">
        <template #default="{ values }">
            <FormInput field="phone" label="PhoneNumber" placeholder="Enter your phone number" :style="{ width: '100%' }" />
            <FormInput field="password" label="Password" placeholder="Enter your password" :style="{ width: '100%' }" />
            <FormCheckbox field="agree" noLabel>I have read and agree to the terms of service</FormCheckbox>
            <div :style="{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }">
                <p>
                    <span>Or</span>
                    <Button :style="{ color: 'var(--semi-color-primary)', marginLeft: '10px', cursor: 'pointer' }" theme="borderless"> Sign up </Button>
                </p>
                <Button :disabled="!values.agree" htmlType="submit" type="tertiary">Log in</Button>
            </div>
        </template>
    </Form>
</template>
  • Horizontal Layout: Arrange each field horizontally You can use the horizontal layout by setting layout='horizontal'
Code
<script setup lang="ts">
    import { Form, FormInput } from '@fibrous/semi-ui-vue';
</script>

<template>
    <Form layout="horizontal">
        <FormInput field="phone" label="PhoneNumber" placeholder="Enter your phone number" />
        <FormInput field="password" label="Password" placeholder="Enter your password" />
    </Form>
</template>
  • Label Position, Label Align
    You can control the position of the label in the Field and the direction of text alignment by setting labelPosition, labelAlign
admin
user
guest
root
Code
<script setup lang="ts">
    import { computed, ref } from 'vue';
    import { Form, FormCheckbox, FormCheckboxGroup, FormInput, FormInputNumber, FormLabel, FormRadio, FormRadioGroup, FormSelect, FormSwitch, Select, type FormProps, type RuleItem } from '@fibrous/semi-ui-vue';

    const labelPosition = ref<FormProps['labelPosition']>('left');
    const labelAlign = ref<FormProps['labelAlign']>('left');
    const labelWidth = computed(() => (labelPosition.value === 'left' ? '180px' : 'auto'));

    const rules: RuleItem[] = [
        { required: true, message: 'required error' },
        { type: 'string', message: 'type error' },
        { validator: (_rule, value) => value === 'semi', message: 'should be semi' },
    ];
</script>

<template>
    <div :style="{ borderBottom: '1px solid var(--semi-color-border)', paddingBottom: '12px' }">
        <FormLabel :style="{ marginLeft: '10px' }">切换Label位置:</FormLabel>
        <Select v-model="labelPosition" aria-label="labelPosition" :style="{ width: '200px' }">
            <template #prefix>labelPosition</template>
            <Select.Option value="top">top</Select.Option>
            <Select.Option value="left">left</Select.Option>
        </Select>
        <FormLabel :style="{ marginLeft: '10px' }">切换Label文本对齐方向:</FormLabel>
        <Select v-model="labelAlign" aria-label="labelAlign" :style="{ width: '200px' }">
            <template #prefix>labelAlign</template>
            <Select.Option value="left">left</Select.Option>
            <Select.Option value="right">right</Select.Option>
        </Select>
    </div>
    <Form :key="`${labelPosition}${labelAlign}`" :labelAlign="labelAlign" :labelPosition="labelPosition" :labelWidth="labelWidth" :style="{ padding: '10px', width: '600px' }">
        <FormInput field="input" label="手机号码" :rules="rules" :style="{ width: '200px' }" trigger="blur" />
        <FormSwitch field="agree" label="是否同意" />
        <FormInputNumber field="price" label="价格" :style="{ width: '200px' }" />
        <FormSelect field="name" label="姓名" :style="{ width: '200px' }">
            <FormSelect.Option value="mike">mike</FormSelect.Option>
            <FormSelect.Option value="jane">jane</FormSelect.Option>
            <FormSelect.Option value="kate">kate</FormSelect.Option>
        </FormSelect>
        <FormCheckboxGroup direction="horizontal" field="role" label="角色">
            <FormCheckbox value="admin">admin</FormCheckbox>
            <FormCheckbox value="user">user</FormCheckbox>
            <FormCheckbox value="guest">guest</FormCheckbox>
            <FormCheckbox value="root">root</FormCheckbox>
        </FormCheckboxGroup>
        <FormRadioGroup field="性别">
            <FormRadio value="1">man</FormRadio>
            <FormRadio value="2">woman</FormRadio>
        </FormRadioGroup>
    </Form>
</template>
  • A more complex layout.
    You can also combine the Row and Col provided by the Grid to arrange the form structure as you want.
Code
<script setup lang="ts">
    import { Col, Form, FormDatePicker, FormInput, FormSelect, Row, type RuleItem } from '@fibrous/semi-ui-vue';

    const nickNameRules: RuleItem[] = [
        { required: true, message: 'required error' },
        { type: 'string', message: 'type error' },
        { validator: (_rule, value) => value === 'semi', message: 'should be semi' },
    ];
</script>

<template>
    <Form labelPosition="top" :style="{ padding: '10px' }">
        <Row>
            <Col :span="8">
                <FormInput field="nickName1" label="用户名" :rules="nickNameRules" :style="{ width: '250px' }" trigger="blur" />
            </Col>
            <Col :span="8">
                <FormDatePicker field="date1" label="有效日期" :style="{ width: '250px' }" />
            </Col>
            <Col :span="8">
                <FormSelect field="business1" label="业务线" :style="{ width: '250px' }">
                    <FormSelect.Option value="abc">Semi</FormSelect.Option>
                    <FormSelect.Option value="ulikeCam">轻颜相机</FormSelect.Option>
                    <FormSelect.Option value="toutiao">今日头条</FormSelect.Option>
                </FormSelect>
            </Col>
        </Row>
        <Row>
            <Col :span="6">
                <FormInput field="nickName2" label="用户名" :rules="nickNameRules" :style="{ width: '200px' }" trigger="blur" />
            </Col>
            <Col :span="6">
                <FormDatePicker field="date2" label="有效日期" :style="{ width: '200px' }" />
            </Col>
            <Col :span="6">
                <FormSelect field="business2" label="业务线" :style="{ width: '200px' }">
                    <FormSelect.Option value="abc">Semi</FormSelect.Option>
                    <FormSelect.Option value="ulikeCam">轻颜相机</FormSelect.Option>
                    <FormSelect.Option value="toutiao">今日头条</FormSelect.Option>
                </FormSelect>
            </Col>
            <Col :span="6">
                <FormSelect field="role" label="角色" :style="{ width: '200px' }">
                    <FormSelect.Option value="operate">运营</FormSelect.Option>
                    <FormSelect.Option value="rd">开发</FormSelect.Option>
                    <FormSelect.Option value="pm">产品</FormSelect.Option>
                    <FormSelect.Option value="ued">设计</FormSelect.Option>
                </FormSelect>
            </Col>
        </Row>
    </Form>
</template>

Form grouping

Forms with many fields should group them. Use FormSection to group Fields and put the title in the #text slot (it only affects layout and does not change the data structure).

基本信息
合格标准
考试时间
分钟
到时间自动交卷
考试人员
关
Code
<script setup lang="ts">
    import { Button, Form, FormCheckbox, FormDatePicker, FormInput, FormInputNumber, FormRadio, FormRadioGroup, FormSection, FormSelect, FormSwitch, Space } from '@fibrous/semi-ui-vue';

    const today = new Date(2024, 4, 6, 10, 30, 0);
    // initValue is not a declared Boolean prop, so a bare attribute would pass an empty string
    const autoSubmit = true;
</script>

<template>
    <Form style="width: 560px">
        <FormSection>
            <template #text>基本信息</template>
            <FormInput field="name" initValue="TCS任务平台使用" label="考试名称" style="width: 560px" />
        </FormSection>
        <FormSection>
            <template #text>合格标准</template>
            <div style="display: flex">
                <FormInputNumber field="pass" :initValue="60" :label="{ text: '及格正确率', required: true }" style="width: 80px" />
                <FormInputNumber field="number" :initValue="10" :label="{ text: '合格人数', required: true }" style="width: 80px" />
            </div>
        </FormSection>
        <FormSection>
            <template #text>考试时间</template>
            <FormDatePicker field="date" :initValue="today" :label="{ text: '开始时间', required: true }" style="width: 272px" type="dateTime" />
            <div style="display: flex">
                <FormInput field="time" initValue="60" label="考试时长" style="width: 176px">
                    <template #addonAfter>分钟</template>
                </FormInput>
                <FormCheckbox field="auto" :initValue="autoSubmit" noLabel style="padding-top: 30px; margin-inline-start: 12px"> 到时间自动交卷 </FormCheckbox>
            </div>
            <FormRadioGroup direction="vertical" field="type" initValue="always" label="有效时间">
                <FormRadio value="always">永久有效</FormRadio>
                <FormRadio value="user">自定义有效期</FormRadio>
            </FormRadioGroup>
            <FormRadioGroup direction="vertical" field="answerTime" initValue="always" label="答案放出时间" :rules="[{ required: true }]">
                <FormRadio value="always">自动放出</FormRadio>
                <FormRadio value="user">
                    <div style="display: inline-block">
                        自定义放出时间
                        <FormDatePicker field="customTime" noLabel style="width: 464px; display: inline-block" type="dateTimeRange" />
                    </div>
                </FormRadio>
            </FormRadioGroup>
        </FormSection>
        <FormSection>
            <template #text>考试人员</template>
            <div style="display: flex">
                <FormSwitch field="open" :label="{ text: '对外开放', required: true }">
                    <template #checkedText>开</template>
                    <template #uncheckedText>关</template>
                </FormSwitch>
            </div>
            <FormSelect field="users" :initValue="['1', '2', '3', '4']" :label="{ text: '考生', required: true }" multiple style="width: 560px">
                <FormSelect.Option value="1">曲晨一</FormSelect.Option>
                <FormSelect.Option value="2">夏可曼</FormSelect.Option>
                <FormSelect.Option value="3">曲晨三</FormSelect.Option>
                <FormSelect.Option value="4">蔡妍</FormSelect.Option>
            </FormSelect>
        </FormSection>
        <Space>
            <Button style="width: 120px; margin-top: 12px; margin-inline-end: 4px" theme="solid" type="primary"> 创建考试 </Button>
            <Button style="margin-top: 12px">预览</Button>
        </Space>
    </Form>
</template>

wrapper Col / label Col

When you need to set a uniform layout for all Fields in a Form, you can set wrapperCol and labelCol on the Form to quickly generate the layout. No need to manually use Row, Col manual layout.
wrapperCol,labelColProperty Configuration Reference Col components

Code
<script setup lang="ts">
    import { Form, FormInput, FormSelect } from '@fibrous/semi-ui-vue';
</script>

<template>
    <Form :labelCol="{ span: 2 }" labelAlign="right" labelPosition="left" :wrapperCol="{ span: 20 }">
        <FormInput field="name" label="姓名" placeholder="请输入姓名" style="width: 250px" trigger="blur" />
        <FormSelect field="role" label="角色" placeholder="请选择角色" style="width: 250px">
            <FormSelect.Option value="operate">运营</FormSelect.Option>
            <FormSelect.Option value="rd">开发</FormSelect.Option>
            <FormSelect.Option value="pm">产品</FormSelect.Option>
            <FormSelect.Option value="ued">设计</FormSelect.Option>
        </FormSelect>
    </Form>
</template>

Remove automatically added Label

Form will automatically insert Label for Field control. If you don't need to automatically insert the Label module, you can turn off the automatic label insertion function by setting noLabel=true in the Field (at this time, the Field still has the ability to automatically display ErrorMessage, so the DOM structure is still different from the original component)
If you want to keep the DOM structure consistent with the original component, you can use pure=true. At this time, the DOM structure will not change except that the data flow is taken over (you need to be responsible for the rendering of ErrorMessage, and it cannot be used by formProps.wrapperCol property impact)

Code
<script setup lang="ts">
    import { Form, FormInput } from '@fibrous/semi-ui-vue';
</script>

<template>
    <Form :style="{ width: '400px' }" @submit="(values) => console.log(values)">
        <FormInput field="name" label="姓名" noLabel placeholder="请输入姓名" :style="{ width: '250px' }" trigger="blur" />
        <FormInput field="purename" placeholder="DOM结构与普通 Input 组件完全一致" pure />
    </Form>
</template>

Embedded Label

A Label can be inlined in a field control by setting labelPosition to inset. Components currently supporting this feature include Input, InputNumber, DatePicker, TimePicker, Select, TreeSelect, Cascader, TagInput, AutoComplete. With labelPosition inset, the other fields are laid out as top and warn in development mode.

A custom component wrapped with withField is laid out with an inset label only when it declares the insetLabelId property: it then receives the label content through the #insetLabel slot and writes insetLabelId to the element that holds the label. A component that does not declare it is laid out as top too.

姓名
开始日期
Code
<script setup lang="ts">
    import { Form, FormDatePicker, FormInput, FormSelect } from '@fibrous/semi-ui-vue';

    const today = new Date(2024, 4, 6, 10, 30, 0);
</script>

<template>
    <Form labelPosition="inset" layout="horizontal">
        <FormInput field="name" initValue="semi" label="姓名" placeholder="请输入姓名" :style="{ width: '250px' }" trigger="blur" />
        <FormSelect field="role" initValue="rd" label="角色" :style="{ width: '250px' }">
            <FormSelect.Option value="operate">运营</FormSelect.Option>
            <FormSelect.Option value="rd">开发</FormSelect.Option>
            <FormSelect.Option value="pm">产品</FormSelect.Option>
            <FormSelect.Option value="ued">设计</FormSelect.Option>
        </FormSelect>
        <FormDatePicker field="date" :initValue="today" label="开始日期" :style="{ width: '250px' }" />
    </Form>
</template>

Export Label, ErrorMessage use

When the built-in Label and ErrorMessage layout does not meet the business requirements, you need to combine the positions yourself, but you want to use the default styles of Label and ErrorMessage directly.
you can import FormLabel / FormErrorMessage and combine them by yourself.
For details of their API, refer to FormLabel / FormErrorMessage

import { FormErrorMessage, FormLabel } from '@fibrous/semi-ui-vue';

Use FormSlot

When your custom component needs to maintain the same layout style as the Field component, you can place your custom component in FormSlot
labelWidth, labelAlign, wrapperCol, labelCol set on the Form component automatically acts on FormSlot
For the FormSlot property configuration, refer to FormSlot

我是Semi Form SlotA, 我是自定义的节点
我是SlotA的ErrorMessage
我是Semi Form SlotB, 我的Label Align、Width与众不同
Code
<script setup lang="ts">
    import { Form, FormInput, FormSlot } from '@fibrous/semi-ui-vue';

    const log = (value: unknown) => console.log(value);
</script>

<template>
    <Form :labelWidth="100" labelPosition="left" style="width: 600px" @change="log" @submit="log">
        <FormInput field="特效名称" style="width: 250px" />
        <FormSlot error="我是SlotA的ErrorMessage" :label="{ text: 'SlotA' }">
            <div style="display: flex; align-items: center; height: 32px; margin-top: 8px">我是Semi Form SlotA, 我是自定义的节点</div>
        </FormSlot>
        <FormSlot :label="{ text: 'SlotB', width: 160, align: 'right' }">
            <div style="display: flex; align-items: center; height: 100%">我是Semi Form SlotB, 我的Label Align、Width与众不同</div>
        </FormSlot>
    </Form>
</template>

Use helpText、extraText set prompt information

You can place custom prompt information through the #helpText slot, and display it in the same block as the verification information (error). When both have values, the verification information will be displayed first.
Additional prompt information can be placed through the #extraText slot. When the error message and prompt text need to appear at the same time, this slot can be used. It is always displayed and located after helpText/error
When validateStatus is passed in, the UI style corresponding to the value of validateStatus will be displayed first. If not passed in, the internal verification status of the field shall prevail.

Code
<script setup lang="ts">
    import { ref, useTemplateRef } from 'vue';
    import { Form, FormInput, type FormHandle, type InputValidateStatus as ValidateStatus } from '@fibrous/semi-ui-vue';

    const helpText = ref('');
    const validateStatus = ref<ValidateStatus>('default');
    const form = useTemplateRef<FormHandle>('form');

    const validator = (val?: string) => {
        if (!val) {
            validateStatus.value = 'error';
            return '密码不能为空';
        } else if (val.length <= 3) {
            validateStatus.value = 'warning';
            helpText.value = '密码强度:弱'; // 展示 helpText
            return ''; // 校验通过
        }
        helpText.value = '';
        validateStatus.value = 'success';
        return '';
    };

    const onSubmit = () => console.log('submit success');
    const onSubmitFail = (errors: unknown) => console.log(errors);

    const random = () => {
        const pw = (Math.random() * 100000).toString().slice(0, 5);
        form.value?.formApi.setValue('Password', pw);
        form.value?.formApi.setError('Password', '');
        helpText.value = '';
        validateStatus.value = 'success';
    };
</script>

<template>
    <Form ref="form" showValidateIcon @submit="onSubmit" @submitFail="onSubmitFail">
        <FormInput field="Password" :validateStatus="validateStatus" :validator="validator">
            <template v-if="helpText" #helpText>
                <span :style="{ color: 'var(--semi-color-warning)' }">{{ helpText }}</span>
            </template>
            <template #extraText>
                <button
                    :style="{
                        padding: 0,
                        border: 'none',
                        background: 'none',
                        font: 'inherit',
                        color: 'var(--semi-color-link)',
                        fontSize: '14px',
                        userSelect: 'none',
                        cursor: 'pointer',
                    }"
                    type="button"
                    @click="random"
                >
                    没有想到合适的密码?点击随机生成一个
                </button>
            </template>
        </FormInput>
    </Form>
</template>

By configuring extraTextPosition, you can control the display position of extraText. Optional values bottom, middle
For example, when you want to display the extraText prompt information between the Label and Field component.
This attribute can be configured uniformly on the Form or individually on each Field. When passing in at the same time, the configuration of the Field shall prevail.

未勾选时,默认为红点提醒,消息默认进入收件人消息列表。对于重要通知,可同时勾选相应的通知方式。
飞书通知
邮件通知
顶部横幅通知
对于对话框通知,可指定该消息必须在指定时长后才可置为已读。
Code
<script setup lang="ts">
    import { Form, FormCheckboxGroup, FormInputNumber } from '@fibrous/semi-ui-vue';

    const options = [
        { label: '飞书通知', value: 'lark' },
        { label: '邮件通知', value: 'email' },
        { label: '顶部横幅通知', value: 'notification' },
    ];
    const notifyText = '未勾选时,默认为红点提醒,消息默认进入收件人消息列表。对于重要通知,可同时勾选相应的通知方式。';
    const forceText = '对于对话框通知,可指定该消息必须在指定时长后才可置为已读。';
</script>

<template>
    <Form extraTextPosition="middle">
        <FormCheckboxGroup direction="horizontal" field="notify" label="通知方式" :options="options">
            <template #extraText>{{ notifyText }}</template>
        </FormCheckboxGroup>
        <FormInputNumber extraTextPosition="bottom" field="force" label="强制读取(可选)" placeholder="秒">
            <template #extraText>{{ forceText }}</template>
        </FormInputNumber>
    </Form>
</template>

Using Input Group

When you need to combine some fields to use, you can use FormInputGroup to wrap them. Only the direct children that set field are rendered.
In Semi Form, when you using field components like FormInput、FormSelect, Form will insert Label module automatically for them.
But usually, inFormInputGroup you only need a Label belonging to the entire Group. You can set the label property in the FormInputGroup to insert a Label belonging to the Group
label configurable properties, see FormLabel

Code
<script setup lang="ts">
    import { Button, Form, FormInput, FormInputGroup, FormSelect } from '@fibrous/semi-ui-vue';

    const onSubmit = (values: unknown) => console.log(values);
</script>

<template>
    <Form labelPosition="top" :style="{ width: '400px' }" @submit="onSubmit">
        <FormInputGroup :label="{ text: '手机号码', required: true }" labelPosition="top">
            <FormSelect field="phonePrefix" initValue="+86" :rules="[{ required: true }]" showClear :style="{ width: '150px' }">
                <FormSelect.Option value="+1">美国+1</FormSelect.Option>
                <FormSelect.Option value="+852">香港+852</FormSelect.Option>
                <FormSelect.Option value="+86">中国+86</FormSelect.Option>
                <FormSelect.Option value="+81">日本+81</FormSelect.Option>
            </FormSelect>
            <FormInput field="phoneNumber" initValue="18912345678" :rules="[{ required: true }]" showClear :style="{ width: '250px' }" />
        </FormInputGroup>
        <FormInput field="姓名" initValue="Semi" trigger="blur" />
        <Button htmlType="submit">提交</Button>
    </Form>
</template>

Form in the Modal pop-up layer

You can place the Form in Modal and load it as a popup.
When submitting, use formApi.validate() to centrally verify the Field

Code
<script setup lang="ts">
    import { ref } from 'vue';
    import { Button, Col, Form, FormInput, FormSelect, Modal, Row, type FormApi } from '@fibrous/semi-ui-vue';

    const message = '该项为必填项';
    const rules = [{ required: true, message }];
    const regions = [
        { value: 'China', label: '中国' },
        { value: 'US', label: '美国' },
        { value: 'Europe', label: '欧洲' },
        { value: 'Japan', label: '日本' },
    ];

    const visible = ref(false);
    let formApi: FormApi | undefined;

    const handleOk = () => {
        formApi
            ?.validate()
            .then((values) => {
                console.log(values);
            })
            .catch((errors) => {
                console.log(errors);
            });
    };
</script>

<template>
    <Button @click="visible = true">打开弹窗</Button>
    <Modal v-model:visible="visible" :style="{ width: '600px' }" title="新建" @ok="handleOk">
        <Form @getFormApi="(api) => (formApi = api)">
            <Row>
                <Col :span="5">
                    <FormSelect field="region" label="国家/地区" placeholder="请选择" :rules="rules" :style="{ width: '100%' }">
                        <FormSelect.Option v-for="item in regions" :key="item.value" :value="item.value">
                            {{ item.label }}
                        </FormSelect.Option>
                    </FormSelect>
                </Col>
                <Col :offset="2" :span="15">
                    <FormInput field="owner" label="业务执行人" :rules="rules" trigger="blur" />
                </Col>
            </Row>
            <Row>
                <Col :span="5">
                    <FormSelect field="area" label="投放区域" placeholder="请选择" :rules="rules" :style="{ width: '100%' }">
                        <FormSelect.Option v-for="item in regions" :key="item.value" :value="item.value">
                            {{ item.label }}
                        </FormSelect.Option>
                    </FormSelect>
                </Col>
                <Col :offset="2" :span="15">
                    <FormInput field="department" label="业务执行部门" :rules="rules" trigger="blur" />
                </Col>
            </Row>
        </Form>
    </Modal>
</template>

Configure initial values and verification rules

  • You can configure check rules for each Field through rules
    The verification library inside the Form is based on async-validator, and more configuration rules can be found in its official documentation
  • You can uniformly set the initial value for the entire form through the initValues of form, or you can set the initial value through initValue in each field (the latter has a higher priority)
  • You can configure different verification trigger timings for each Field through trigger, and the default is change (that is, the verification is performed automatically when the value changes). Also supports change, blur, mount, custom or a combination of the above. It also supports unified configuration through FormProps. If both are configured, FieldProps shall prevail
  • You can use the stopValidateWithError switch to decide whether to continue to trigger the validation of subsequent rules when the first rule that fails the validation is encountered. Unified configuration through FormProps is also supported. If both are configured, FieldProps shall prevail
Code
<script setup lang="ts">
    import { Button, Form, FormInput, type RuleItem } from '@fibrous/semi-ui-vue';

    const initValues = {
        name: 'semi',
        shortcut: 'se',
    };

    const style = { width: '100%' };

    const rules: RuleItem[] = [
        { required: true, message: 'required error' },
        { type: 'string', message: 'type error' },
        { validator: (_rule, value) => value === 'semi', message: 'should be semi' },
        {
            validator: (_rule, value) => Boolean(value && value.startsWith('se')),
            message: 'should startsWith se',
        },
    ];
</script>

<template>
    <Form :initValues="initValues">
        <FormInput field="name" :rules="rules" :style="style" trigger="blur" />
        <FormInput field="shortcut" :rules="rules" stopValidateWithError :style="style" />
        <Button htmlType="submit">提交</Button>
    </Form>
</template>

Custom Validate (Form Level)

You can set a custom validation function for the Form as a whole with validator. It will be called on submit or when calling formApi.validate().

Synchronous Validate

When validate success, you should return an empty string.
When validate fails, you should return the error message (Object, key is fieldName, value is the corresponding error message)

Code
<script setup lang="ts">
    import { Button, Form, FormInput } from '@fibrous/semi-ui-vue';

    interface FormValues {
        name?: string;
        sex?: string;
    }

    const syncValidate = (values: FormValues) => {
        const errors: Record<string, unknown> = {};
        if (values.name !== 'mike') {
            errors.name = 'you must name mike';
        }
        if (values.sex !== 'female') {
            errors.sex = 'must be woman';
        }
        errors.familyName = [{ before: 'before error balabala ', after: 'after error balabala' }, 'familyName[1] error balabala'];
        return errors;
    };
</script>

<template>
    <Form layout="horizontal" :validator="syncValidate">
        <FormInput field="name" trigger="blur" />
        <FormInput field="familyName[0].before" trigger="blur" />
        <FormInput field="familyName[0].after" trigger="blur" />
        <FormInput field="familyName[1]" trigger="blur" />
        <div :style="{ display: 'flex', alignItems: 'flex-end' }">
            <Button class="btn-margin-right" htmlType="submit" type="primary">Submit</Button>
            <Button htmlType="reset">reset</Button>
        </div>
    </Form>
</template>

Asynchronous Validate

For asynchronous validation, you should return a promise. In promise.then() you need to return the corresponding error message.

Code
<script setup lang="ts">
    import { Button, Form, FormInput } from '@fibrous/semi-ui-vue';

    const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

    interface FormValues {
        name?: string;
        sex?: string;
    }

    const asyncValidate = (values: FormValues) =>
        sleep(2000).then(() => {
            const errors: FormValues = {};
            if (values.name !== 'mike') {
                errors.name = 'you must name mike';
            }
            if (values.sex !== 'female') {
                errors.sex = 'sex not valid';
            }
            return errors;
        });
</script>

<template>
    <Form layout="horizontal" :validator="asyncValidate">
        <FormInput field="name" trigger="blur" />
        <FormInput field="familyName[0].before" trigger="blur" />
        <FormInput field="familyName[1]" trigger="blur" />
        <FormInput field="sex" trigger="blur" />
        <div :style="{ display: 'flex', alignItems: 'flex-end' }">
            <Button class="btn-margin-right" htmlType="submit" type="primary">Submit</Button>
            <Button htmlType="reset">reset</Button>
        </div>
    </Form>
</template>

Custom Validate (Field Level)

You can specify a custom validation function for a single Field with validator. Supports synchronous and asynchronous validation (by returning promises)

Code
<script setup lang="ts">
    import { Button, Form, FormInput } from '@fibrous/semi-ui-vue';

    const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

    const validateName = (val?: string) => {
        if (!val) {
            return "【sync】can't be empty";
        } else if (val.length <= 5) {
            return '【sync】must more than 5';
        }
        return '';
    };

    const asyncValidate = (val?: string) =>
        sleep(2000).then(() => {
            if (!val) {
                return "【async】can't be empty";
            } else if (val.length <= 5) {
                return '【async】must more than 5';
            } else {
                return '';
            }
        });
</script>

<template>
    <Form>
        <FormInput field="name" label="【name】asyncValidate after 2s" trigger="blur" :validator="asyncValidate" />
        <FormInput field="familyName" label="【familyName】syncValidate" trigger="blur" :validator="validateName" />
        <Button htmlType="reset">reset</Button>
    </Form>
</template>

Manually Trigger specified validation

When you want to manually trigger the validation of some specific Field, you can do it through formApi.validate.
When no parameters are passed in, all Fields are checked by default. When parameters are passed in, the parameters specified shall prevail

Silent Validation

When you need to get the validation result without triggering UI updates (no error messages shown, no touched state set), you can use formApi.validate({ silent: true }). This is useful in scenarios where you want to decide whether to make a backend request based on the validation result.

Code
<script setup lang="ts">
    import { shallowRef } from 'vue';
    import { Button, Form, FormInput, Toast, type FormApi } from '@fibrous/semi-ui-vue';

    const formApi = shallowRef<FormApi>();

    const usernameRules = [
        { required: true, message: '用户名不能为空' },
        { min: 3, message: '用户名至少 3 个字符' },
    ];
    const emailRules = [
        { required: true, message: '邮箱不能为空' },
        { type: 'email' as const, message: '邮箱格式不正确' },
    ];

    const handleSilentValidate = () => {
        formApi.value
            ?.validate({ silent: true })
            .then((values) => {
                Toast.success('表单校验通过,准备发起请求');
                console.log('发起后端请求:', values);
            })
            .catch((errors) => {
                Toast.error('表单校验未通过,但不显示错误提示');
                console.log('校验错误:', errors);
            });
    };

    const handleNormalValidate = () => {
        formApi.value
            ?.validate()
            .then(() => {
                Toast.success('表单校验通过');
            })
            .catch(() => {
                Toast.error('表单校验未通过,显示错误提示');
            });
    };
</script>

<template>
    <Form layout="horizontal" @getFormApi="formApi = $event">
        <FormInput field="username" label="用户名" :rules="usernameRules" />
        <FormInput field="email" label="邮箱" :rules="emailRules" />
        <div style="display: flex; gap: 12px; margin-top: 12px">
            <Button type="primary" @click="handleSilentValidate">静默校验(不显示错误)</Button>
            <Button @click="handleNormalValidate">普通校验(显示错误)</Button>
        </div>
    </Form>
</template>
a
b
c
d
b.name
a
b
c
d
b.name
Code
<script setup lang="ts">
    import { shallowRef } from 'vue';
    import { Button, Form, FormCheckboxGroup, FormInput, Space, Toast, type FormApi } from '@fibrous/semi-ui-vue';

    const allFields = ['a', 'b', 'c', 'd', 'b.name'];
    const options = allFields.map((item) => ({ label: item, value: item }));

    const validate = (val?: string) => {
        if (!val) {
            return "can't be empty";
        } else if (val.length <= 5) {
            return '我是传入的节点';
        }
        return undefined;
    };

    const formApi = shallowRef<FormApi>();

    const validatePartial = (type?: 'all') => {
        const scope = type === 'all' ? allFields : (formApi.value?.getValue('validateScope') ?? []);
        formApi.value
            ?.validate(scope)
            .then((values) => {
                console.log(values);
                Toast.success('pass');
            })
            .catch((error) => {
                Toast.error('error');
                console.log(error);
            });
    };

    const resetPartial = () => {
        formApi.value?.reset(formApi.value.getValue('resetScope'));
    };
</script>

<template>
    <Form autoScrollToError layout="horizontal" @getFormApi="(api) => (formApi = api)">
        <template #default="{ values }">
            <div>
                <FormInput field="a[1]" trigger="blur" :validator="validate" />
                <FormInput field="a[0]" trigger="blur" :validator="validate" />
                <FormInput field="b.name[0]" trigger="blur" :validator="validate" />
                <FormInput field="b.name[1]" trigger="blur" :validator="validate" />
                <FormInput field="b.type" trigger="blur" :validator="validate" />
                <FormInput field="c" trigger="blur" :validator="validate" />
                <FormInput field="d" trigger="blur" :validator="validate" />
            </div>
            <div>
                <FormCheckboxGroup direction="horizontal" field="validateScope" :initValue="['a', 'b']" label="当前希望校验的Field" :options="options" />
                <FormCheckboxGroup direction="horizontal" field="resetScope" label="当前需要Reset的Field" :options="options" />
                <Space>
                    <Button htmlType="reset">reset</Button>
                    <Button @click="validatePartial('all')">all validate</Button>
                    <Button @click="validatePartial()">partial validate {{ JSON.stringify(values.validateScope) }}</Button>
                    <Button @click="resetPartial">partial reset</Button>
                </Space>
            </div>
        </template>
    </Form>
</template>

Linkage Fields

You can achieve the linkage between Fields by listening to the @change of Field and then using formApi to make modifications.

Note will change after Sex select
Code
<script setup lang="ts">
    import { useTemplateRef } from 'vue';
    import { Button, Form, FormInput, FormSelect, Row } from '@fibrous/semi-ui-vue';

    const form = useTemplateRef('form');

    const handleSelectChange = (value: unknown) => {
        const text = value === 'male' ? 'Hi male' : 'Hi female!';
        form.value?.formApi.setValue('Note', text);
    };
</script>

<template>
    <Form ref="form" :style="{ width: '250px' }" @valueChange="(values) => console.log(values)">
        <span>Note will change after Sex select</span>
        <FormInput field="Note" :style="{ width: '250px' }" />
        <FormSelect field="Sex" :style="{ width: '250px' }" @change="handleSelectChange">
            <FormSelect.Option value="female">female</FormSelect.Option>
            <FormSelect.Option value="male">male</FormSelect.Option>
        </FormSelect>
        <Row>
            <Button class="btn-margin-right" htmlType="submit" type="primary">Submit</Button>
            <Button htmlType="reset">reset</Button>
        </Row>
    </Form>
</template>

Dynamic form

Dynamically add and delete fields

Code
<script setup lang="ts">
    import { Button, Form, FormInput, FormRadio, FormRadioGroup } from '@fibrous/semi-ui-vue';
</script>

<template>
    <Form :style="{ width: '450px' }">
        <template #default="{ formState }">
            <FormInput field="name" label="用户名称:" />
            <FormRadioGroup field="isAnchor" label="是否已注册主播">
                <FormRadio value="yes">yes</FormRadio>
                <FormRadio value="no">no</FormRadio>
            </FormRadioGroup>
            <FormInput v-if="formState.values?.isAnchor === 'yes'" field="liveRoom" label="直播间名称" />
            <Button htmlType="submit">提交</Button>
        </template>
    </Form>
</template>

Keep empty values (allowEmpty)

By default a field whose value becomes empty is removed from values. Set allowEmpty on a Field to keep its key with an undefined value, or on Form to apply it to every field. Clear both selects below and compare the keys of values.

["keep","drop"]
Code
<script setup lang="ts">
    import { Form, FormSelect } from '@fibrous/semi-ui-vue';

    const roleOptions = [
        { label: '管理员', value: 'admin' },
        { label: '访客', value: 'guest' },
    ];
</script>

<template>
    <Form :initValues="{ keep: 'admin', drop: 'admin' }">
        <template #default="{ values }">
            <FormSelect allowEmpty field="keep" label="设置了 allowEmpty" :optionList="roleOptions" showClear :style="{ width: '176px' }" />
            <FormSelect field="drop" label="未设置" :optionList="roleOptions" showClear :style="{ width: '176px' }" />
            <code>{{ JSON.stringify(Object.keys(values)) }}</code>
        </template>
    </Form>
</template>

Keep field state on unmount (keepState)

By default, when a Field component is unmounted, its value, error, and touched state are all reset. If you want to preserve these states after the field is unmounted (for example, in conditional rendering scenarios), you can use the keepState property.

显示条件字段
Code
<script setup lang="ts">
    import { ref } from 'vue';
    import { Button, Form, FormInput, FormSwitch, Switch } from '@fibrous/semi-ui-vue';

    const show = ref(true);
</script>

<template>
    <Form :style="{ width: '450px' }">
        <FormSwitch field="alwaysShow" label="始终显示的字段" />
        <Switch v-model="show" aria-labelledby="form-keep-state-toggle" :style="{ marginBottom: '12px' }" />
        <span id="form-keep-state-toggle" :style="{ marginLeft: '8px', color: 'var(--semi-color-text-2)' }"> 显示条件字段 </span>
        <FormInput v-if="show" field="conditionalField" keepState label="条件字段(keepState)" />
        <FormInput v-if="show" field="noKeepState" label="条件字段(无 keepState)" />
        <Button htmlType="submit">提交</Button>
    </Form>
</template>

In the example above:

  • The field with keepState will retain its previously entered value, validation state, etc. after being hidden and then shown again
  • The field without keepState will have its state reset after being hidden and then shown again

ArrayField Usage

For array items that are dynamically added or deleted, we provide the ArrayField component to simplify the operation of adding and removing rows. The default scoped slot of ArrayField provides onAdd and onAddWithInitValue to add a blank row or a row with an initial value, and each row has an onRemove to delete itself

For the detailed API of ArrayField, please refer to ArrayField below

Note: The initValue type of ArrayField must be an array

Code
<script setup lang="ts">
    import { IconMinusCircle, IconPlusCircle } from '@fibrous/semi-icons-vue';
    import { ArrayField, Button, Form, FormInput, FormSelect, TextArea } from '@fibrous/semi-ui-vue';

    const data = [
        { name: 'Semi D2C', role: 'Engineer' },
        { name: 'Semi C2D', role: 'Designer' },
    ];
    const roleOptions = [
        { label: 'Engineer', value: 'Engineer' },
        { label: 'Designer', value: 'Designer' },
    ];
</script>

<template>
    <Form allowEmpty labelPosition="left" labelWidth="100px" :style="{ width: '800px' }">
        <template #default="{ formState }">
            <ArrayField field="rules" :initValue="data">
                <template #default="{ arrayFields, onAdd, onAddWithInitValue }">
                    <Button theme="light" @click="onAdd">
                        <template #icon>
                            <IconPlusCircle />
                        </template>
                        Add new line
                    </Button>
                    <Button :style="{ marginLeft: '8px' }" @click="onAddWithInitValue({ name: 'Semi DSM', role: 'Designer' })">
                        <template #icon>
                            <IconPlusCircle />
                        </template>
                        Add new line with init value
                    </Button>
                    <div v-for="item in arrayFields" :key="item.key" :style="{ width: '1000px', display: 'flex' }">
                        <FormInput :field="`${item.field}[name]`" :label="`${item.field}.name`" :style="{ width: '200px', marginRight: '16px' }" />
                        <FormSelect :field="`${item.field}[role]`" :label="`${item.field}.role`" :optionList="roleOptions" :style="{ width: '120px' }" />
                        <Button aria-label="Delete" :style="{ margin: '12px' }" theme="borderless" type="danger" @click="item.onRemove">
                            <template #icon>
                                <IconMinusCircle />
                            </template>
                        </Button>
                    </div>
                </template>
            </ArrayField>
            <TextArea aria-label="Form state" :modelValue="JSON.stringify(formState)" :style="{ marginTop: '10px' }" />
        </template>
    </Form>
</template>

Nesting ArrayField

ArrayField supports multi-level nesting. The following is an example of two-level nesting.

当邮件到达,满足以下条件时:
当邮件到达,满足以下条件时:
Code
<script setup lang="ts">
    import { IconMinusCircle, IconPlusCircle } from '@fibrous/semi-icons-vue';
    import { ArrayField, Button, Card, Form, FormInput, FormSelect, TypographyText } from '@fibrous/semi-ui-vue';

    const selectOption = [
        { label: '发件人地址', value: 'address' },
        { label: '邮件主题', value: 'title' },
        { label: '发送时间', value: 'sendTime' },
        { label: '接收时间', value: 'receiveTime' },
        { label: '正文', value: 'main' },
        { label: '附件名称', value: 'attachmentName' },
    ];
    const typeOption = [
        { label: '包含', value: 'include' },
        { label: '不包含', value: 'exclude' },
    ];

    const initValue = {
        group: [
            {
                name: '收信规则1',
                rules: [
                    { ruleType: 'address', type: 'include', text: 'bytedance.com' },
                    { ruleType: 'title', type: 'exclude', text: '更新日志' },
                ],
            },
            {
                name: '收信规则2',
                rules: [{ ruleType: 'sendTime', type: 'include', text: '2019' }],
            },
        ],
    };

    const newRule = {
        name: '新规则名称',
        rules: [
            { ruleType: 'main', type: 'include', text: '' },
            { ruleType: 'attachmentName', type: 'include', text: '' },
        ],
    };

    const rowStyle = { marginTop: '12px', marginLeft: '12px' };
</script>

<template>
    <Form allowEmpty :initValues="initValue" labelPosition="left" :style="{ textAlign: 'left' }" @valueChange="(values) => console.log(values)">
        <ArrayField field="group">
            <template #default="{ arrayFields, onAddWithInitValue }">
                <Button theme="solid" @click="onAddWithInitValue(newRule)">
                    <template #icon>
                        <IconPlusCircle />
                    </template>
                    新增收信规则
                </Button>
                <div v-for="item in arrayFields" :key="item.key" :style="{ width: '1000px', display: 'flex', flexWrap: 'wrap' }">
                    <FormInput :field="`${item.field}[name]`" label="规则名称" labelPosition="top" :style="{ width: '600px' }" />
                    <Button aria-label="删除规则" :style="{ margin: '36px 0 0 12px' }" type="danger" @click="item.onRemove">
                        <template #icon>
                            <IconMinusCircle />
                        </template>
                    </Button>
                    <TypographyText strong :style="{ flexBasis: '100%' }">当邮件到达,满足以下条件时:</TypographyText>
                    <Card shadows="hover" :style="{ width: '620px', margin: '12px 0 0 24px' }">
                        <ArrayField :field="`${item.field}.rules`">
                            <template #default="{ arrayFields: rules, onAddWithInitValue: addRule }">
                                <div v-for="(rule, i) in rules" :key="rule.key" :style="{ display: 'flex' }">
                                    <FormSelect :field="`${rule.field}[ruleType]`" :label="`${rule.field}.ruleType`" noLabel :optionList="selectOption" :style="{ width: '120px', marginRight: '12px' }" />
                                    <FormSelect :field="`${rule.field}[type]`" :label="`${rule.field}.type`" noLabel :optionList="typeOption" :style="{ width: '100px', marginRight: '12px' }" />
                                    <FormInput aria-label="条件内容" :field="`${rule.field}[text]`" :label="`${rule.field}.text`" noLabel :style="{ width: '200px' }" />
                                    <Button aria-label="删除条件" :style="rowStyle" theme="borderless" type="danger" @click="rule.onRemove">
                                        <template #icon>
                                            <IconMinusCircle />
                                        </template>
                                    </Button>
                                    <Button aria-label="新增条件" :disabled="i !== rules.length - 1" :style="rowStyle" @click="addRule({ ruleType: `条件${rules.length + 1}`, type: 'include' })">
                                        <template #icon>
                                            <IconPlusCircle />
                                        </template>
                                    </Button>
                                </div>
                            </template>
                        </ArrayField>
                    </Card>
                </div>
            </template>
        </ArrayField>
    </Form>
</template>

Add or delete form items dynamically - by use formApi

If you don't use ArrayField, you can use the provided formApi to manually add or delete formState.

Code
<script setup lang="ts">
    import { shallowRef } from 'vue';
    import { Button, Form, FormInput, FormSelect, TextArea, type FormApi } from '@fibrous/semi-ui-vue';

    interface Effect {
        name: string;
        type: string;
        key: number;
    }

    interface FormValues {
        effects: Effect[];
    }

    const initValues: FormValues = {
        effects: [
            { name: '脸部贴纸', type: '2D', key: 1 },
            { name: '背景贴纸', type: '3D', key: 2 },
        ],
    };

    const formApi = shallowRef<FormApi<FormValues>>();
    let nextKey = 3;

    const onGetFormApi = (api: FormApi<FormValues>) => {
        formApi.value = api;
    };

    const add = () => {
        const effects = formApi.value?.getValue('effects') ?? [];
        formApi.value?.setValue('effects', [...effects, { name: '', type: '', key: nextKey++ }]);
    };

    const remove = (key: number) => {
        const effects = (formApi.value?.getValue('effects') ?? []).filter((effect) => key !== effect.key);
        formApi.value?.setValue('effects', effects.length ? effects : undefined);
    };
</script>

<template>
    <Form :initValues="initValues" labelPosition="left" labelWidth="180px" :style="{ width: '500px' }" @getFormApi="onGetFormApi">
        <template #default="{ values }">
            <Button @click="add">添加</Button>
            <div v-for="(effect, i) in values.effects" :key="effect.key" :style="{ width: '1000px', display: 'flex' }">
                <FormInput :aria-label="`特效 ${i + 1} 名称`" :field="`effects[${i}].name`" :style="{ width: '200px', marginRight: '12px' }" />
                <FormSelect :field="`effects[${i}].type`" :style="{ width: '90px' }">
                    <FormSelect.Option value="2D">2D</FormSelect.Option>
                    <FormSelect.Option value="3D">3D</FormSelect.Option>
                </FormSelect>
                <Button :style="{ margin: '12px' }" type="danger" @click="remove(effect.key)">删除</Button>
            </div>
            <TextArea aria-label="表单值" :modelValue="JSON.stringify(values)" :style="{ marginTop: '10px' }" />
        </template>
    </Form>
</template>

Use of Hook

We provide four composables so that you can easily access Form internal state and call Form and Field related api in the setup of a component placed inside the Form structure without passing through props. They depend on the context provided by Form and throw an error when called outside a Form.

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

useFormApi

useFormApi allows you to directly access the formApi of the parent Form component within a component inside the Form

Code
<script setup lang="ts">
    import { defineComponent, h } from 'vue';
    import { Button, Form, FormInput, useFormApi } from '@fibrous/semi-ui-vue';

    // useFormApi reads the Form from provide/inject, so it has to run in a component rendered inside the Form
    const ComponentUsingFormApi = defineComponent(() => {
        const formApi = useFormApi();
        const change = () => {
            formApi.setValue('name', Math.random());
        };
        return () => h(Button, { onClick: change }, () => 'ChangeName By【formApi】');
    });
</script>

<template>
    <Form>
        <FormInput field="name" initValue="mike" />
        <ComponentUsingFormApi />
    </Form>
</template>

useFormState

useFormState allows you to directly access the form State of the parent Form component within a component inside the Form. It returns a ComputedRef, read through .value in scripts

FormState read by 【useFormState】:
{"values":{},"errors":{},"touched":{}}
Code
<script setup lang="ts">
    import { defineComponent, h } from 'vue';
    import { Form, FormInput, useFormState } from '@fibrous/semi-ui-vue';

    // useFormState reads the Form from provide/inject, so it has to run in a component rendered inside the Form
    const ComponentUsingFormState = defineComponent(() => {
        const formState = useFormState();
        return () => h('pre', h('code', JSON.stringify(formState.value)));
    });
</script>

<template>
    <Form>
        <FormInput field="name" initValue="mike" />
        <h5>FormState read by 【useFormState】:</h5>
        <ComponentUsingFormState />
    </Form>
</template>

useFieldApi

useFieldApi allows you to call the api of the specified Field directly within a component inside the Form. The parameter can be a field name, a ref or a getter

Code
<script setup lang="ts">
    import { defineComponent, h } from 'vue';
    import { Button, Form, FormInput, useFieldApi } from '@fibrous/semi-ui-vue';

    // useFieldApi reads the Form from provide/inject, so it has to run in a component rendered inside the Form
    const ComponentUsingFieldApi = defineComponent(() => {
        const nameFieldApi = useFieldApi('name');
        const change = () => {
            nameFieldApi.setValue(Math.random());
        };
        return () => h(Button, { onClick: change }, () => 'Click Me!!! changeNameBy【fieldApi】');
    });
</script>

<template>
    <Form>
        <FormInput field="name" initValue="mike" />
        <ComponentUsingFieldApi />
    </Form>
</template>

useFieldState

useFieldState allows you to directly access the State of the specified Field within a component inside the Form. The parameter can be a field name, a ref or a getter, and it returns a ComputedRef<{ value, error, touched }>

【name】FieldState read by 【useFieldState】:{}
【country】FieldState read by 【useFieldState】:{}
Code
<script setup lang="ts">
    import { defineComponent, h } from 'vue';
    import { Form, FormInput, useFieldState } from '@fibrous/semi-ui-vue';

    // useFieldState reads the Form from provide/inject, so it has to run in a component rendered inside the Form
    const ComponentUsingFieldState = defineComponent(
        (props: { field: string }) => {
            const fieldState = useFieldState(() => props.field);
            return () => h('div', [h('span', `【${props.field}】FieldState read by 【useFieldState】:`), h('code', JSON.stringify(fieldState.value))]);
        },
        { props: ['field'] }
    );
</script>

<template>
    <Form layout="horizontal">
        <div style="width: 400px">
            <FormInput field="name" initValue="mike" />
            <FormInput field="country" initValue="china" />
        </div>
        <div style="width: 500px; margin-top: 30px">
            <ComponentUsingFieldState field="name" />
            <ComponentUsingFieldState field="country" />
        </div>
    </Form>
</template>

Access formApi and formState through the default slot

Vue has no withFormApi or withFormState. The default scoped slot of Form already provides formApi and formState; use useFormApi and useFormState above inside a child component

Access formApi

Code
<script setup lang="ts">
    import { Button, Form, FormInput } from '@fibrous/semi-ui-vue';
</script>

<template>
    <Form>
        <template #default="{ formApi }">
            <FormInput field="name" initValue="semi" />
            <FormInput field="familyName" initValue="design" />
            <Button htmlType="submit" style="margin-right: 4px">submit</Button>
            <Button @click="formApi.setValue('name', Math.random())">Click Me!!! ChangeName By【formApi】</Button>
        </template>
    </Form>
</template>

Access formState

{"values":{},"errors":{},"touched":{}}
Code
<script setup lang="ts">
    import { Form, FormInput } from '@fibrous/semi-ui-vue';
</script>

<template>
    <Form>
        <template #default="{ formState }">
            <FormInput field="name" initValue="semi" />
            <FormInput field="familyName" initValue="design" />
            <code>{{ JSON.stringify(formState) }}</code>
        </template>
    </Form>
</template>

Take over custom components

Via withField, you can extend other custom components or native tags (such as 'input') into Field. Form will taking over its behavior.

With Field did the following things.

  • Take over the modelValue of the component (or other properties specified by valueKey), the change event (or other events specified by onKeyChangeFnName)
  • Insert Field's FormLabel above the field
  • Insert Field's ErrorMessage under the field
  • Insert Field's extraText under the field

With Field Options specific configuration can be consulted withFieldOptions

Your custom controlled component needs to do the following:

  • Declare the modelValue property (or the one specified by valueKey), respond to its changes and update your component UI rendering results
  • When the value changes, emit change (or the event specified by onKeyChangeFnName) with the latest value as the argument. A component that only emits update:modelValue can set onKeyChangeFnName to 'update:modelValue'
withField(YourComponent, withFieldOptions);

The props type of the returned component is inferred from the props of YourComponent: the component's own props plus Field Props, without modelValue, onUpdate:modelValue, defaultValue and defaultChecked (the Form owns the value; passing them is ignored and a warning is logged in development mode).

{"values":{},"errors":{},"touched":{}}
Code
<script setup lang="ts">
    import { Form, withField } from '@fibrous/semi-ui-vue';

    const CustomInput = withField('input', {
        valueKey: 'value',
        onKeyChangeFnName: 'input',
        valuePath: 'target.value',
    });
</script>

<template>
    <Form>
        <template #default="{ formState }">
            <CustomInput field="name" />
            <pre><code>{{ JSON.stringify(formState) }}</code></pre>
        </template>
    </Form>
</template>
名称
{"values":{},"errors":{},"touched":{}}
Code
<script setup lang="ts">
    import { defineComponent, h, type PropType } from 'vue';
    import { Form, Input, Select, withField } from '@fibrous/semi-ui-vue';

    interface BaseInfo {
        name?: string;
        role?: string;
    }

    const roles = [
        { value: 'rd', label: '开发' },
        { value: 'UED', label: '设计师' },
    ];

    // A single-file demo cannot define a component inline, so a render function; use a separate SFC in real code
    // aria-required is a prop for each control; id, aria-labelledby and the rest land on the root fieldset
    const MyComponent = defineComponent({
        props: { modelValue: Object as PropType<BaseInfo>, ariaRequired: Boolean },
        emits: { change: (value: BaseInfo) => Boolean(value) },
        setup(props, { emit }) {
            const update = (patch: BaseInfo) => emit('change', { ...props.modelValue, ...patch });
            return () =>
                h('fieldset', { class: 'customField', style: 'border: 0; margin: 0; padding: 0' }, [
                    h(
                        Input,
                        {
                            'aria-label': '名称',
                            'aria-required': props.ariaRequired,
                            modelValue: props.modelValue?.name,
                            'onUpdate:modelValue': (name: string) => update({ name }),
                            style: 'width: 180px; margin-inline-end: 12px',
                        },
                        { prefix: () => '名称' }
                    ),
                    h(
                        Select,
                        {
                            'aria-label': '角色',
                            'aria-required': props.ariaRequired,
                            modelValue: props.modelValue?.role,
                            'onUpdate:modelValue': (role: unknown) => update({ role: role as string }),
                            style: 'width: 200px',
                            optionList: roles,
                        },
                        { prefix: () => '角色' }
                    ),
                ]);
        },
    });
    const CustomField = withField(MyComponent);
</script>

<template>
    <Form>
        <template #default="{ formState }">
            <CustomField field="baseInfo" :label="{ text: '基本信息', required: true }" />
            <pre><code>{{ JSON.stringify(formState) }}</code></pre>
        </template>
    </Form>
</template>

API reference

Form

<script setup lang="ts">
    import { Form } from '@fibrous/semi-ui-vue';
</script>

Props

PropertyDescriptionTypeDefault
initValuesInitial values of the form, copied once when the Form is created.Valuesundefined
layoutDirection in which the fields are arranged: vertical or horizontal."horizontal" | "vertical"semiDefault("Form", "layout", "vertical")
labelPositionLabel position of every field. inset is supported by some fields only; the others are laid out as top. The same property on a field takes precedence."top" | "left" | "inset"semiDefault("Form", "labelPosition", "top")
labelWidthLabel width of every field. A number is in px.string | numbersemiGlobalDefault("Form", "labelWidth")
labelAlignText alignment of the label of every field. The same property on a field takes precedence."left" | "right"semiGlobalDefault("Form", "labelAlign")
labelColGrid layout of the label of every field, with the properties of Col such as span and offset. Set it together with wrapperCol.ColPropssemiGlobalDefault("Form", "labelCol")
wrapperColGrid layout of the control of every field, with the properties of Col such as span and offset. Set it together with labelCol.ColPropssemiGlobalDefault("Form", "wrapperCol")
extraTextPositionPosition of the extra text of every field: middle puts it between the label and the control, bottom after the control. The same property on a field takes precedence."middle" | "bottom"semiGlobalDefault("Form", "extraTextPosition")
allowEmptyWhether a field whose value becomes empty keeps its key in values. The same property on a field takes precedence.booleansemiDefault("Form", "allowEmpty", false)
autoScrollToErrorWhether to scroll to the first invalid field when submit or formApi.validate fails. An object is passed to scroll-into-view-if-needed as its options.Typeboolean | Options<any>semiDefault("Form", "autoScrollToError", false)
showValidateIconWhether the validation message is preceded by an icon of its status.booleansemiDefault("Form", "showValidateIcon", true)
disabledThe disabled state applied to every field. A field's own disabled takes precedence.booleansemiGlobalDefault("Form", "disabled")
stopValidateWithErrorWhether validation by rules stops at the first failing rule. The same property on a field takes precedence.booleansemiGlobalDefault("Form", "stopValidateWithError")
triggerWhen every field validates: change, blur, mount, custom, or an array of them. The same property on a field takes precedence.FieldValidateTriggerTypesemiGlobalDefault("Form", "trigger")
stopPropagationWhether to stop the submit and reset events from bubbling, so that a nested Form does not trigger the events of the outer one.{ submit?: boolean | undefined; reset?: boolean | undefined; }semiGlobalDefault("Form", "stopPropagation")
validatorForm-level validation, called on submit and formApi.validate and allowed to return a Promise. When set, the validator and rules of the fields no longer apply. An empty string or object means valid; on failure return an object keyed by field name.Type((values: Values) => FormValidateResult<Values> | Promise<FormValidateResult<Values>>)undefined
formThe formApi returned by useForm, used to control the form from outside.Type(FormApi<Values> & FormApiProxyInternals<Values>)undefined
idThe id of the form element, also written to x-form-id. Generated when omitted.stringundefined

Events

EventDescriptionCallback parameters
submitEmitted when a submit button is clicked or formApi.submitForm is called and validation passes. Receives values and the submit event. It has the name of the native submit event and hides it.[values: Values, e?: SubmitEvent | undefined]
submitFailEmitted when a submit button is clicked or formApi.submitForm is called and validation fails. Receives errors, values and the submit event.Type[errors: FormValidateResult<Values>, values: Partial<Values>, e?: SubmitEvent | undefined]
resetEmitted when a reset button is clicked or formApi.reset is called. It has the name of the native reset event and hides it.[]
changeEmitted when the form state changes: a field mounts, unmounts, changes its value, blurs, or changes its validation state or error. Receives formState. It has the name of the native change event and hides it.Type[formState: FormState<Values>]
valueChangeEmitted when a field value changes. Receives all values and the values that changed.Type[values: Values, changedValues: Partial<Values>]
errorChangeEmitted when validation state changes. Receives all errors and the errors that changed. Batch operations such as validate, submit and reset emit once; silent validation does not emit.Type[errors: AllErrors<Values>, changedError?: Partial<Record<FieldPath<Values, 5>, FieldError>> | undefined]
getFormApiEmitted after the Form mounts. Receives formApi.Type[formApi: FormApi<Values>]

Slots

SlotDescriptionScope parameters
defaultForm content. The scope provides formState, formApi and values; read them only and change the form through formApi.TypeFormDefaultSlotProps<Values>

Exposes

NameDescriptionType
None

class, style and native attributes fall through attrs to the form element. When labelCol and wrapperCol are set, the form is wrapped in a Row, and the attributes still land on the form. The change, submit and reset events of Form have the names of native events and hide them. The form element has novalidate by default, so that validation is done by Semi only; pass :novalidate="false" to bring the native validation back.

A template ref to the Form instance gives access to its formApi (type FormHandle).

FormState

FormState stores all the state values within the Form, including the values of each field, error information, touched status

NameInstructionsInitial valueExample
valuesValue Collection of the form{}{fieldA: 'str', fieldB: true}
errorsForm error information collection, you can decide whether to allow users to submit by judging whether there is error information{}{fieldA: 'length not valid'}
touchedThe collection of fields the user has clicked on{}{fieldA: true}

How to access the form state

  • By calling formApi.getFormState()
  • Via the default scoped slot, formState will injected as a parameter
  • Via useFormState in a component inside the Form
  • Via the formState created by useForm, read reactively outside the Form

FormApi

We provide FormApi. You have easy access to FormApi both inside and outside the Form, which allows you to use getter and setter to get and manipulate the values of FormState.
The table below describes the features available in the formApi.

FunctionDescriptionexample
getFormPropsGet Form Component PropsformApi.getFormProps(propNames?: string[])
getFormStateGet FormStateformApi.getFormState()
submitFormManually submit form operationformApi.submitForm()
resetReset the form manuallyformApi.reset(fields?: Array <string>)
validateManually trigger validation of the entire form. the verification of the entire Field will be triggered by default when no parameters are passed , if you want to trigger the verification of some fields, pass in the target field array

After the Form level validator is configured, the Field level validator will not be triggered again when submit or formApi.validate()

Silent validation: Pass { silent: true } to get validation results without triggering UI updates (no error messages shown, no touched state set). You can also use { fields: ['fieldA'], silent: true } to perform silent validation on specific fields.
formApi.validate()
.then(values => {})
.catch(errors => {})
OR formApi.validate(['fieldA','fieldB'])
OR formApi.validate({ silent: true })
OR formApi.validate({ fields: ['fieldA'], silent: true })
setValuesSet the values of the entire form. The isOverride in the second parameter is false by default.
When isOverride is false: Iterate through all registered fields in the Form, and update the values from newValues to formState.values. If a registered field is not included in newValues, that field will be set to undefined.
When isOverride is true: Directly replace formState.values with newValues, including extra keys in newValues that don't have corresponding fields in the form.

Recommended usage: If you only need to update some fields, it's recommended to use formApi.setValue(field, value) for individual updates, or get current values first then merge: formApi.setValues({ ...formApi.getValues(), name: newValue })
formApi.setValues(newValues: object, {isOverride: boolean})
getValuesGet the values of all FieldformApi.getValues()
setValueprovides direct modification of formState.values method.
The difference from setValues is that it only modifies a single field.
formApi.setValue(field: string, newFieldValue: any)
getValueGet the value of all / single FieldformApi.getValue()
formApi.getValue(field: string)
setTouchedModify formState.touchedformApi.setTouched(field: string, isTouched: boolean)
getTouchedGet the touched state of the FieldformApi.getTouched(field: string)
setErrorModify the error information of a fieldformApi.setError(field: string, fieldErrorMessage: string)
getErrorGet Error Status of FieldformApi.getError(field: string)
getFieldExistGet whether the field exists in the FormformApi.getFieldExist(field: string)
scrollToFieldScroll to the specified field, the second input parameter will be passed to scroll-into-view-if-neededformApi.scrollToField(field: string, scrollOpts: ScrollIntoViewOptions)
scrollToErrorScroll to the field with validation error. You can pass a specified field or index. If you pass index, scroll to the index-th error DOM. If you do not pass any parameters, scroll to the first validation error position in the DOM tree.formApi.scrollToError(ScrollToErrorOptions{field?: string; index?: number; scrollOpts?: ScrollIntoViewOptions })

How to access formApi

  • Use useForm() to create formApi outside Form, then pass it via the form property to the Form component. This allows you to get formApi before Form renders, suitable for scenarios where you need to control the form from outside the component.
  • The Form component emits getFormApi after it mounts. You can save a reference to formApi in the callback function for subsequent calls (example code below) In addition, we provide other ways to get formApi, and you can choose different ways of calling according to your preference.
  • Use a template ref to get the form instance,you can access its formApi
  • Via the default scoped slot, formApi will injected as a parameter
  • Via useFormApi for children component of Form

useForm

useForm() is used to create a formApi outside the Form component, suitable for scenarios where you need to control the form before it renders. It returns an object { formApi, formState, values }: formState is a read-only shallowRef and values is a computed, both updated once the Form binds the formApi through its form property.

Usage example:

表单值:{}
touched 状态:{}
Code
<script setup lang="ts">
    import { Button, Form, FormInput, useForm } from '@fibrous/semi-ui-vue';

    const { formApi, formState, values } = useForm();

    const handleClick = () => {
        formApi.setValue('username', 'semi');
        console.log('当前表单值:', values.value);
    };
</script>

<template>
    <Button @click="handleClick">外部设置值</Button>
    <Button @click="formApi.reset()">重置表单</Button>

    <Form :form="formApi">
        <FormInput field="username" label="用户名" />
        <FormInput field="email" label="邮箱" />
    </Form>

    <div style="margin-top: 16px">
        <div>表单值:{{ JSON.stringify(values) }}</div>
        <div>touched 状态:{{ JSON.stringify(formState.touched) }}</div>
    </div>
</template>
<script setup lang="ts">
    import { useTemplateRef } from 'vue';
    import { Button, Form } from '@fibrous/semi-ui-vue';
    import type { FormApi, FormHandle } from '@fibrous/semi-ui-vue';

    let formApiA: FormApi | undefined;
    const formB = useTemplateRef<FormHandle>('formB');

    const changeValues = () => {
        // use formApi to update formA. After getting the formApi object, you can use it to make any changes you want to the form
        formApiA?.setValues({ a: 1 });
        // use formApi to update formB
        formB.value?.formApi.setValues({ b: 2 });
    };
</script>

<template>
    <!-- save the formApi from the getFormApi event -->
    <Form @getFormApi="(formApi) => (formApiA = formApi)" />
    <!-- read the formApi of the Form instance through a template ref -->
    <Form ref="formB" />
    <Button @click="changeValues">Change</Button>
</template>

Field Props

A Field is a component made by withField, not a single-file component. The template ref of a field is { control } (type FieldHandle), where control is the handle of the wrapped component itself. For example, the control of FormInput can call the focus of Input.

Props

PropertiesDescriptionTypeDefault
convertAfter the field value changes, before it is stored in formState and shown, update the value of the field(fieldValue) => any
fieldThe mapping path of the field's value in formState.values. Form will use this value to distinguish the internal form control.
Required!!! (except a FormCheckbox or FormRadio inside its group)
string
labelThe label text for this field. When not passed, it defaults to the same name as field. An object can set text, extra, required, optional, align, width and disabled; its text and extra take strings or numbers only. The #fieldLabel and #labelExtra slots take precedencestring | number | FieldLabel
labelPositionLabel position of this field. When passed on both Form and Field, the Field props prevail'top' | 'left' | 'inset'
labelAlignText-align of the label text of this field. When passed on both Form and Field, the Field props prevail'left' | 'right'
labelWidthThe width of the label text of this field, a number is in px. When passed on both Form and Field, the Field props prevailstring | number
labelColGrid layout of the label of this field, with Col component. It overrides the same property of Form. When it is set together with wrapperCol while the Form does not set both, the Field wraps itself in a RowColProps
wrapperColGrid layout of the control of this field, with Col component. It overrides the same property of Form. Used the same way as labelColColProps
noLabelWhen you don't need to add label automatically, you can set this value to trueboolean
noErrorMessageWhen you don't need to add the ErrorMessage module automatically, you can set this value to true. Note that helpText is not shown eitherboolean
nameField name. When passed in, the corresponding className will be automatically added to the field wrapper div, such as: money => '.semi-form-field-money'. It is also passed to the underlying component, for example to configure the name attribute of inputstring
idThe id of the control, generated when omitted. The for of the label and the ids of the label, helpText, extraText and errormessage are derived from itstring
fieldClassNameThe className of the entire fieldWrapper is the same as the name parameter, except that the prefix is not automatically appendedstring
fieldStyleThe inline style of the entire fieldWrapperStyleValue
initValueThe initial value of the field (consumed only once when Field mounted, subsequent updates are invalid), it has higher priority than the values in Form's initValuesunknown (type depends on current component)
validatorThe custom validation function for this form control. Supports synchronous and asynchronous validation (by returning a promise). An empty string or undefined means valid; any other result is the error, which is data only. Use the #error slot to render an error differently. Rules does not take effect when validator is set(fieldValue, values) => string | string[] | boolean | undefined | Promise
rulesvalidation rules, validation library based on async-validator; message takes strings onlyRuleItem[]
validateStatusThe validation result status of this form control, which only affects the style, optional: success / error / warning / default. When omitted, the Field's own validation status is usedstring
triggerThe timing of triggering the verification, optional: blur / change / custom / mount, or an array of them
1. When set to custom, only formApi will trigger the verification
2.mount (triggered once when mounting)
FieldValidateTriggerType'change'
transformtransform field values before validation. The transformed value is only used for validation and does not affect formState(fieldValue) => any
allowEmptyWhether to keep the key of this field in values (as undefined) when its value is empty. Setting it on either the Field or the Form takes effectbooleanfalse
allowEmptyStringWhether to allow values to be empty strings.
When the value is '' by default, the key corresponding to this field will be removed from values.
If you want to keep the key, you need to set allowEmptyString to true
booleanfalse
emptyValueThe value that counts as empty. When the value changes to it, the key of this field is removed from values (kept as undefined when allowEmpty is true)unknown''
keepStateWhether to retain the field state (value, error, touched) after the field is unmounted. When true, the field's value, validation message, and interaction state are preserved after unmounting, and will be automatically restored upon remountingbooleanfalse
stopValidateWithErrorWhen it is true, the rules check is used. After encountering the first rule that fails the check, it will no longer trigger the check of subsequent rulesbooleanfalse
pureWhether to only take over the data stream, when true, it will not automatically insert modules such as ErrorMessage, Label, extraText, etc. The style and DOM structure are consistent with the original componentsbooleanfalse
extraTextPositioncontrols the display position of extraText. Middle (the vertical direction is displayed in the order of Label, extraText, and Field), bottom (the vertical direction is displayed in the order of Label, Field, and extraText); when passed on both Form and Field, the Field props prevail'middle' | 'bottom''bottom'

A Field does not declare disabled, helpText or extraText: disabled takes the value of the Form and a disabled written on the Field wins; use the slots below for prompt information. The other properties of the component (such as size and placeholder of Input), together with class, style and aria-*, are written on the Field as well, and Field passes them to the wrapped component itself, not to the fieldWrapper.

Events

EventDescriptionCallback parameters
changeEmitted when the value changes (the event specified by onKeyChangeFnName, when it is another one). The parameters are those of the event of the wrapped component, followed by the latest form values latestValues, which avoid reading a stale value from a closure; a FormCheckbox or FormRadio without field is a plain control and has no latestValues. It has the name of the native change event and hides it; the .once modifier is not supported and the event does not show in the Devtools event panel. The other events of the wrapped component (such as select and focus) are passed to it as they are and can be listened to directlyThe parameters of the event of the wrapped component, latestValues

Slots

SlotDescriptionScope parameters
fieldLabelContent of the label. It takes precedence over the text in the label property
labelExtraContent after the label text. It takes precedence over the extra in the label object
helpTextCustom prompt information, which is displayed in the same block as the verification information. When both have values, the verification information is displayed first
extraTextAdditional prompt information, you can use this when both error information and prompt copy are required, after helpText/errorMessage
errorCustom rendering of the error{ error }
OthersThe other slots (including #default and #label) are passed to the wrapped component as they are, such as the options of FormSelect and the #label of FormCheckboxGroupDecided by the wrapped component

Field Api

We also provide fieldApi, most of which is similar to formApi, with the difference that fieldApi limits the scope of modification, and it can only modify the bound field. Get it through useFieldApi

FunctionInstructionsexample
setValueModify the value of the current FieldfieldApi.setValue(newValue: any)
getValueGets the value of the current FieldfieldApi.getValue()
setTouchedModify the touched state of the current FieldfieldApi.setTouched(true)
getTouchedGet the touched state of the current FieldfieldApi.getTouched()
setErrorModify the error information of the current FieldfieldApi.setError(newErrorMessage: string)
getErrorGets field's error statusfieldApi.getError()

ArrayField

For dynamically added and deleted array form items, we provide the ArrayField scope to simplify adding and removing rows

Props

PropertyDescriptionTypeDefault
fieldRequired. Path of the array in formState.values. For rows with fields a[0].name and a[1].name pass a.stringundefined
initValueInitial value of the array, which must be an array. It takes precedence over the initValues of the Form.unknown[]semiGlobalDefault("ArrayField", "initValue")

Events

EventDescriptionCallback parameters
None

Slots

SlotDescriptionScope parameters
defaultThe rows. The scope provides arrayFields, onAdd and onAddWithInitValue.ArrayFieldSlotProps

Exposes

NameDescriptionType
None

ArrayField has no DOM of its own, so undeclared attributes are dropped. The scope of the default slot has this type:

interface ArrayFieldSlotProps {
    arrayFields: ArrayFieldItem[]; // The current array form, which can be used to render each row with v-for
    onAdd: (index?: number | Event) => void; // Add a blank line; a number inserts it at that position, an event is ignored so it can be written as @click="onAdd"
    onAddWithInitValue: (row: unknown, index?: number) => void; // Add a new row with an initial value, index is the insert position
}

interface ArrayFieldItem {
    key: string; // A key used to identify the current row, which should be bound to the wrapper of the current row
    field: string; // This row fieldPath, which is equivalent to the field of ArrayField + [index]
    onRemove: () => void; // Remove operation function of this line, when called, this line will be deleted directly
}

FormSection

import { FormSection } from '@fibrous/semi-ui-vue';

Props

PropertyDescriptionTypeDefault
None

Events

EventDescriptionCallback parameters
None

Slots

SlotDescriptionScope parameters
defaultThe fields in the group.any
textTitle of the group. No heading element is rendered without content.any

Exposes

NameDescriptionType
None

class, style and native attributes fall through attrs to the section element.

FormLabel

By default, Label is self-inserted into each Field by Form.
If you need to self-insert Label elsewhere, we have provided the FormLabel component for you.

import { FormLabel } from '@fibrous/semi-ui-vue';

Props

PropertyDescriptionTypeDefault
textText of the label. The default slot takes precedence.string | numberundefined
extraContent after the label text. The extra slot takes precedence.string | numberundefined
alignText alignment of the label.string"left"
disabledWhether the label looks disabled.booleanundefined
requiredWhether to display the required * symbol.booleanfalse
optionalWhether to append the optional text of the locale. When true, the * of required is hidden.booleanfalse
nameThe id of the labeled control, written to the for attribute.stringundefined
widthWidth of the label. A number is in px.string | numberundefined

Events

EventDescriptionCallback parameters
None

Slots

SlotDescriptionScope parameters
defaultContent of the label. It takes precedence over text.any
extraContent after the label text. It takes precedence over extra.any

Exposes

NameDescriptionType
None

class, style and native attributes fall through attrs to the label element.

FormInputGroup

import { FormInputGroup } from '@fibrous/semi-ui-vue';

Props

PropertyDescriptionTypeDefault
disabledThe disabled state applied to the fields in the group. Falls back to the Form's disabled when not set.booleanundefined
labelLabel of the whole group: a string is the text and an object is the label configuration, whose id becomes the id of the label. The label slot takes precedence.string | number | FormInputGroupLabelundefined
labelPositionLabel position. Defaults to the Form's, and can be top or left."top" | "left"undefined
extraTextPositionPosition of the extra text: middle puts it between the label and the group, bottom after the group."bottom" | "middle""bottom"

Events

EventDescriptionCallback parameters
None

Slots

SlotDescriptionScope parameters
defaultThe fields in the group. Only direct children that set field are rendered.any
labelContent of the group label. It takes precedence over the text in the label property.any
labelExtraContent after the group label text.any
extraTextExtra text that is always shown and referenced by the aria-describedby of every control in the group.any

Exposes

NameDescriptionType
None

class, style and native attributes (such as size) fall through attrs to the inner InputGroup and are passed to every field in the group. aria-labelledby and aria-describedby are merged with the ids generated by the group, the caller's first. The outer element has role="group" when a label is set, and has none without a label.

When extraTextPosition is middle and labelPosition is left. Since the content of #extraText has a variable height, the Label will no longer ensure that it can be aligned with the first line of text in the Field / FormInputGroup.

FormSlot

import { FormSlot } from '@fibrous/semi-ui-vue';

Props

PropertyDescriptionTypeDefault
labelThe text of the label, or a label configuration such as text, extra, align, width, required and optional. The label slot takes precedence.string | number | FieldLabelundefined
labelPositionLabel position. Defaults to the Form's, and can be top or left."top" | "left"undefined
noLabelWhether to render no label. When the Form sets both labelCol and wrapperCol, the label column is still kept.booleanundefined
errorThe error, rendered after the content. Nothing shows when omitted.FieldErrorundefined

Events

EventDescriptionCallback parameters
None

Slots

SlotDescriptionScope parameters
defaultThe custom content.any
labelContent of the label. It takes precedence over the text in the label property.any
labelExtraContent after the label text.any
errorCustom rendering of the error. The scope provides error.{ error: FieldError; }

Exposes

NameDescriptionType
None

class, style and native attributes fall through attrs to the root element. The root element has role="group" when aria-label or aria-labelledby is passed, and a role passed by the caller wins. The label width and alignment come from labelWidth and labelAlign of the Form, and width and align in the label object win.

FormErrorMessage

import { FormErrorMessage } from '@fibrous/semi-ui-vue';
  • When the error is a string, render directly; when it is true, only the error container is rendered, without text; when it is false, nothing is rendered
  • When the error is an array, the join operation is automatically performed to aggregate the error information in the array. An error is data only; use the #error slot to render it differently

Props

PropertyDescriptionTypeDefault
errorThe error. A string is shown as is and an array is joined. true shows the error container without text; false and empty values render nothing.FieldErrorundefined
showValidateIconWhether to show the icon of validateStatus.booleanundefined
validateStatusValidation status of the message: default, error, warning or success."error" | "default" | "warning" | "success"undefined
isInInputGroupWhether it is inside an InputGroup. When it and showValidateIcon are true, the error icon shows whatever validateStatus is.booleanundefined
errorMessageIdThe id of the error text element, referenced by aria-errormessage.stringundefined
helpTextIdThe id of the help text element, referenced by aria-describedby.stringundefined

Events

EventDescriptionCallback parameters
None

Slots

SlotDescriptionScope parameters
helpTextHelp text, shown while there is no error and sharing the block with the error.any
errorCustom rendering of the error. The scope provides error.{ error: FieldError; }

Exposes

NameDescriptionType
None

class, style and native attributes fall through attrs to the root element.

withFieldOptions

keyDescriptionDefault
valueKeyThe property of the component that represents the value, such as modelValue of a custom component and value of the native input tag'modelValue'
onKeyChangeFnNameThe name of the event the component emits when its value changes, such as change and input. A listener of this event on the Field receives latestValues after the original arguments'change'
valuePathThe path of the value in the first argument of the event, such as target.checked of Checkbox, target.value of RadioGroup and of the native input; if the first argument is the value itself, there is no need to take the value down, the item does not need to be set''
shouldInjectWhen false, a component without field is rendered as it is and not taken over by the Form, such as a FormCheckbox or FormRadio inside its group; when true and there is no field, a warning is logged in development modetrue

Accessibility

ARIA

  • aria-labelledby、for
    • Field component will automatically add label DOM. The id of the control is props.id, generated when omitted; the for attribute of label is the same as this id; the id attribute of label is ${control id}-label;
    • When the props.labelPosition of the Form or Field is set to inset, there is no label tag at this time, but a div tag. The div tag corresponding to insetLabel will be automatically appended with id, the value is the same as the id of the above label, corresponding to the aria-labelledby of the Field component. A field that does not support the inset label is laid out as top, and its aria-labelledby points to the label actually rendered
    • The Field component will be automatically appended with aria-labelledby, the value is the same as the id of the above label; the aria-labelledby passed by the caller comes first and the two are merged instead of overwritten
  • aria-required
    • When the Field is configured with required fields (that is, props.rules contains require: true or props.label is configured with required: true without optional: true), the Field component will be automatically appended with aria-required = true (except CheckboxGroup, Slider, Upload and a single Radio); an aria-required passed on the Field takes precedence
  • aria-invalid 、aria-errormessage
    • When the Field check fails, or validateStatus is error, the Field component will be automatically added with the aria-invalid = true attribute.
    • When the Field check fails, the Field component will be automatically appended with the aria-errormessage attribute, the value of which is the id of the DOM element corresponding to the errorMessage (format like: ${control id}-errormessage); for CheckboxGroup it is set on every checkbox in the group.
  • aria-describedby
    • When the Field is configured with helpText or extraText, the Field component will be automatically added with the aria-describedby attribute, whose value is the id of the DOM element corresponding to helpText and extraText (format like: ${control id}-helpText , ${control id}-extraText); the aria-describedby passed by the caller comes first and the two are merged instead of overwritten

Content Guidelines

  • Form title
    • The title of the form needs to follow the writing specification of the title
  • Form label
    • The label is a short description of the input box. The label is not a help text, so it should not be a description of the input
    • Labels must:
      • Place it above or below the input box
      • Short (1-3 words)
      • Use case conventions for statements (first letter uppercase, others lowercase)
  • Help text
    • Help text use statement writing conventions, capitalized
  • Form button

Design Tokens

These are the component's internal Sass variables and cannot be overridden. To customize styles, override the --semi-* CSS variables (see the Design Token page) or write unlayered CSS rules.
Sass variableValueUsage
$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

  • Why did I declare the form, modify the value, and the data is not automatically mapped to formState.values?
    Check that the field has been passed correctly, and the field attribute on the Field component is a must-fill property !

  • Why doesn't the passed defaultValue or defaultChecked take effect?
    Refer to the beginning of the document Field. The Field component unifies the default value. You should pass the default value using initValue or initValues

  • Why did the component not change and the value not take effect after initValue and initValues were updated asynchronously?
    initValue, initValues are only consumed once when Field and Form mount, and subsequent asynchronous updates will not take effect.
    If your initial value needs to be taken remotely, you can update it using formApi.setValue / setValues after you get the value
    Or send a new key directly to Form or Field to force it to remount.

  • Why can't getValues get a certain field?

    If the field has no initial value, getValues cannot get this item. You can set initValues/initValue or set the allowEmpty attribute to the form.

  • Why does hitting enter on the input box trigger the form's submit?

    This is standard HTML behavior. We do not plan to intervene and we will remain the same as the HTML. If there is really only one input element in the form, and you don't want to trigger the submit callback when you press Enter, it is recommended to use preventDefault for the enter of the keydown event of input to prevent the default behavior.

    Click #767 for background and content.

  • The form will automatically save the historical input items, what should I do if I don't want this function?
    Before v2.3, Form did not configure for, name, id and other attributes for input controls strictly according to the A11y accessibility standard, so this function was not available in previous versions. After v2.3, we implemented it strictly according to the W3C standard. If you don't want the browser to automatically save history input items, you can also turn it off by setting autoComplete=off at the Form level or Field level