1
0
Эх сурвалжийг харах

feat: form add stopValidateWithError, trigger, close #640 (#1778)

* feat: form add stopValidateWithError, trigger, close #640
pointhalo 2 жил өмнө
parent
commit
ae2f5b1770

+ 43 - 41
content/input/form/index-en-US.md

@@ -982,7 +982,7 @@ class ModalFormDemo extends React.Component {
         this.formApi = formApi;
     }
 
-    render(){
+    render() {
         const { visible } = this.state;
         let message = 'Required';
         return (
@@ -1066,51 +1066,53 @@ class ModalFormDemo extends React.Component {
 -   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](https://github.com/yiminghe/async-validator)
 -   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, when onChange is triggered, the verification is performed automatically). Also supports `change`, `blur`, `mount`, `custom` or a combination of the above. After v2.42, it 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. After v2.42, unified configuration through FormProps is supported. If both are configured, FieldProps shall prevail
+
 
 ```jsx live=true dir="column"
 import React from 'react';
-import { Form } from '@douyinfe/semi-ui';
-
-class BasicDemoWithInit extends React.Component {
-    constructor() {
-        super();
-        this.state = {
-            initValues: {
-                name: 'semi',
-                role: 'rd'
-            }
-        };
-        this.getFormApi = this.getFormApi.bind(this);
-    }
+import { Form, Button } from '@douyinfe/semi-ui';
 
-    getFormApi(formApi) { this.formApi = formApi; }
+() => {
+    
+    const initValues = {
+        name: 'semi',
+        shortcut: 'se'
+    };
+    
+    const style = { width: '100%' };
+    
+    const { Select, Input } = Form;
 
-    render() {
-        const { Select, Input } = Form;
-        const style = { width: '100%' };
-        return (
-            <Form initValues={this.state.initValues}>
-                <Input
-                    field="name"
-                    label="Name(Input)"
-                    style={style}
-                    trigger='blur'
-                    rules={[
-                        { required: true, message: 'required error' },
-                        { type: 'string', message: 'type error' },
-                        { validator: (rule, value) => value === 'muji', message: 'not muji' }
-                    ]}
-                />
-                <Select field="role" style={style} label='Role' placeholder='Choose Role' initValue={'pm'}>
-                    <Select.Option value="qa">Quality Assurance</Select.Option>
-                    <Select.Option value="rd">Software Engineer</Select.Option>
-                    <Select.Option value="pm">Product Manager</Select.Option>
-                    <Select.Option value="ued">Designer</Select.Option>
-                </Select>
-            </Form>
-        );
-    }
-}
+    return (
+        <Form initValues={initValues}>
+            <Input
+                field="name"
+                style={style}
+                trigger='blur'
+                rules={[
+                    { required: true, message: 'required error' },
+                    { type: 'string', message: 'type error' },
+                    { validator: (rule, value) => value === 'semi', message: 'should be semi' },
+                    { validator: (rule, value) => value && value.startsWith('se'), message: 'should startsWith se' }
+                ]}
+            />
+            <Input
+                field="shortcut"
+                style={style}
+                stopValidateWithError
+                rules={[
+                    { required: true, message: 'required error' },
+                    { type: 'string', message: 'type error' },
+                    { validator: (rule, value) => value === 'semi', message: 'should be semi' },
+                    { validator: (rule, value) => value && value.startsWith('se'), message: 'should startsWith se' }
+                ]}
+            />
+            <Button htmlType='submit'>提交</Button>
+        </Form>
+    );
+};
 ```
 
 ### Custom Validate (Form Level)

+ 48 - 46
content/input/form/index.md

@@ -1062,7 +1062,7 @@ class ModalFormDemo extends React.Component {
         this.formApi = formApi;
     }
 
-    render(){
+    render() {
         const { visible } = this.state;
         let message = '该项为必填项';
         return (
@@ -1147,52 +1147,52 @@ class ModalFormDemo extends React.Component {
 -   你可以通过`rules`为每个 Field 表单控件配置校验规则  
     Form 内部的校验库基于 async-validator,更多配置规则可查阅其[官方文档](https://github.com/yiminghe/async-validator)
 -   你可以通过 form 的`initValues`为整个表单统一设置初始值,也可以在每个 field 中通过`initValue`设置初始值(后者优先级更高)
+-   可以通过 trigger 为每个 Field 配置不同的校验触发时机,默认为 change(即onChange触发时,自动进行校验)。还支持 change、blur、mount、custom 或以上的组合。v2.42 后支持通过 FormProps 统一配置, 若都配置时,以 FieldProps 为准  
+-   可以通过 stopValidateWithError 开关,决定使用 rules 校验时,当碰到第一个检验不通过的 rules 后,是否继续触发后续 rules 的校验。v2.42 后支持通过 FormProps 统一配置,若都配置时,以 FieldProps 为准  
 
 ```jsx live=true dir="column" hideInDSM
 import React from 'react';
 import { Form, Button } from '@douyinfe/semi-ui';
 
-class BasicDemoWithInit extends React.Component {
-    constructor() {
-        super();
-        this.state = {
-            initValues: {
-                name: 'semi',
-                role: 'rd'
-            }
-        };
-        this.getFormApi = this.getFormApi.bind(this);
-    }
-
-    getFormApi(formApi) { this.formApi = formApi; }
+() => {
+    
+    const initValues = {
+        name: 'semi',
+        shortcut: 'se'
+    };
+    
+    const style = { width: '100%' };
+    
+    const { Select, Input } = Form;
 
-    render() {
-        const { Select, Input } = Form;
-        const style = { width: '100%' };
-        return (
-            <Form initValues={this.state.initValues}>
-                <Input
-                    field="name"
-                    label="名称(Input)"
-                    style={style}
-                    trigger='blur'
-                    rules={[
-                        { required: true, message: 'required error' },
-                        { type: 'string', message: 'type error' },
-                        { validator: (rule, value) => value === 'semi', message: 'should be semi' }
-                    ]}
-                />
-                <Select field="role" style={style} label='角色' placeholder='请选择你的角色' initValue={'pm'}>
-                    <Select.Option value="operate">运营</Select.Option>
-                    <Select.Option value="rd">开发</Select.Option>
-                    <Select.Option value="pm">产品</Select.Option>
-                    <Select.Option value="ued">设计</Select.Option>
-                </Select>
-                <Button htmlType='submit'>提交</Button>
-            </Form>
-        );
-    }
-}
+    return (
+        <Form initValues={initValues}>
+            <Input
+                field="name"
+                style={style}
+                trigger='blur'
+                rules={[
+                    { required: true, message: 'required error' },
+                    { type: 'string', message: 'type error' },
+                    { validator: (rule, value) => value === 'semi', message: 'should be semi' },
+                    { validator: (rule, value) => value && value.startsWith('se'), message: 'should startsWith se' }
+                ]}
+            />
+            <Input
+                field="shortcut"
+                style={style}
+                stopValidateWithError
+                rules={[
+                    { required: true, message: 'required error' },
+                    { type: 'string', message: 'type error' },
+                    { validator: (rule, value) => value === 'semi', message: 'should be semi' },
+                    { validator: (rule, value) => value && value.startsWith('se'), message: 'should startsWith se' }
+                ]}
+            />
+            <Button htmlType='submit'>提交</Button>
+        </Form>
+    );
+};
 ```
 
 ### 自定义校验(Form 级别)
@@ -1918,8 +1918,10 @@ render(WithFieldDemo2);
 | onSubmit          | 点击 submit 按钮或调用 `formApi.submitForm()`,数据验证成功后的回调函数                                                                                                      | function(values:object, e: event)                       |            |
 | onSubmitFail      | 点击 submit 按钮或调用 `formApi.submitForm()`,数据验证失败后的回调函数                                                                                                      | function(errors:object, values:object, e: event)        |            |
 | render            | 用于声明表单控件,不可与 component、props.children 同时使用                                                                                                                  | function                                      |
-| showValidateIcon  | Field 内的校验信息区块否自动添加对应状态的 icon 展示 <br/>**在 v1.0.0 开始提供**                                                                                                                         | boolean                                       | true       |
+| showValidateIcon  | Field 内的校验信息区块否自动添加对应状态的 icon 展示                                                                                                                         | boolean                                       | true       |
 | style             | 可将内联样式传入 form 标签                                                                                                                                                   | object                                        |
+| stopValidateWithError | 统一应用在每个 Field 的 stopValidateWithError,使用说明见 Field props中同名 API (v2.42后提供)                                                                            | boolean                             | false     |
+| trigger    |  统一应用在每个 Field 的 trigger,使用说明详见 Field props中同名 API(v2.42后提供)                                                        | string\|array                            |  'change'  |
 | validateFields    | Form 级别的自定义校验函数,submit 时或 formApi.validate 时会被调用(配置Form级别校验器后,Field级别校验器在submit或formApi.validate()时不会再被触发)。支持同步校验、异步校验                                                                                   | function(values)                              |            |
 | wrapperCol        | 统一应用在每个 Field 上的布局,同[Col 组件](/zh-CN/basic/grid#Col),设置`span`、`offset`值,如{span: 20, offset: 4}                                 | object                                        |
 
@@ -2061,10 +2063,10 @@ import { Form, Button } from '@douyinfe/semi-ui';
 | transform             | 校验前转换字段值,转换后的值仅会在校验时被消费,对 formState 无影响<br/> 使用示例: (value) => Number                                                                                                                 | function(fieldValue)                                                                          |           |
 | allowEmptyString      | 是否允许值为空字符串。默认情况下值为''时,该 field 对应的 key 会从 values 中移除,如果你希望保留该 key,那么需要将 allowEmptyString 设为 true                                                                       | boolean                                                                                       | false     |
 | stopValidateWithError | 为 true 时,使用 rules 校验,碰到第一个检验不通过的 rules 后,将不再触发后续 rules 的校验                                                                                                  | boolean                                                                                       | false     |
-| helpText              | 自定义提示信息,与校验信息公用同一区块展示,两者均有值时,优先展示校验信息<br/>**v1.0.0 开始提供**                                                                                                                  | ReactNode                                                                                     |           |
-| extraText             | 额外的提示信息,当需要错误信息和提示文案同时出现时,可以使用这个,位于 helpText/errorMessage 后<br/>**v1.0.0 开始提供**                                                                                             | ReactNode                                                                                     |           |
-| pure                  | 是否仅接管数据流,为 true 时不会自动插入 ErrorMessage、Label、extraText 等模块,样式、DOM 结构与原始的组件保持一致<br/>**v1.1.0 开始提供**                                                                          | boolean                                                                                       | false     |
-| extraTextPosition     | 控制extraText的显示位置,可选`middle`(垂直方向以Label、extraText、Field主体的顺序显示)、`bottom` (垂直方向以Label、Field主体、extraText的顺序显示);在Form与Field上同时传入时,以Field props为准<br/>**v1.9.0 开始提供**                                                                          | string                                                                                       | 'bottom'     |
+| helpText              | 自定义提示信息,与校验信息公用同一区块展示,两者均有值时,优先展示校验信息                                                                                                                | ReactNode                                                                                     |           |
+| extraText             | 额外的提示信息,当需要错误信息和提示文案同时出现时,可以使用这个,位于 helpText/errorMessage 后                                                                                           | ReactNode                                                                                     |           |
+| pure                  | 是否仅接管数据流,为 true 时不会自动插入 ErrorMessage、Label、extraText 等模块,样式、DOM 结构与原始的组件保持一致                                                                         | boolean                                                                                       | false     |
+| extraTextPosition     | 控制extraText的显示位置,可选`middle`(垂直方向以Label、extraText、Field主体的顺序显示)、`bottom` (垂直方向以Label、Field主体、extraText的顺序显示);在Form与Field上同时传入时,以Field props为准                                                                          | string                                                                                       | 'bottom'     |
 | ...other              | 组件的其他可配置属性,与上面的属性平级一并传入即可,例如 Input 的 size/placeholder,**Field 会将其透传至组件本身**                                                                                                  |                                                                                               |
 
 

+ 2 - 1
packages/semi-foundation/form/constants.ts

@@ -8,7 +8,8 @@ const strings = {
     LAYOUT: ['horizontal', 'vertical'],
     LABEL_POS: ['left', 'top', 'inset'],
     LABEL_ALIGN: ['left', 'right'],
-    EXTRA_POS: ['middle', 'bottom']
+    EXTRA_POS: ['middle', 'bottom'],
+    DEFAULT_TRIGGER: 'change',
 };
 
 const numbers = {};

+ 37 - 12
packages/semi-foundation/form/utils.ts

@@ -1,6 +1,7 @@
 import AsyncValidator from 'async-validator';
-import { cloneDeep, toPath } from 'lodash';
+import { cloneDeep, toPath, isUndefined } from 'lodash';
 import { FieldValidateTriggerType, BasicTriggerType, ComponentProps, WithFieldOption } from './interface';
+import { strings } from './constants';
 
 /**
  * 
@@ -50,17 +51,43 @@ export function isValid(errors: any): boolean {
     return valid;
 }
 
-// Compatible with String and Array
-function transformTrigger(trigger: FieldValidateTriggerType): Array<BasicTriggerType> {
-    let result: BasicTriggerType[] = [];
-    if (Array.isArray(trigger)) {
-        result = trigger;
+/**
+ * trigger transform rule
+    1. If the user has set fieldProps, follow the user's fieldProps
+    2. If the user does not set fieldProps, follow formProps
+    3. If there is no formProps, follow the change
+    4. If it is an array, follow the array, if it is not an array (pure string), convert it to a string array
+ */
+
+export function transformTrigger(fieldTrigger: FieldValidateTriggerType, formTrigger: FieldValidateTriggerType): Array<BasicTriggerType> {
+    let result: BasicTriggerType[] | FieldValidateTriggerType = [];
+    let finalResult = [];
+    if (!isUndefined(fieldTrigger)) {
+        result = fieldTrigger;
+    } else if (!isUndefined(formTrigger)) {
+        result = formTrigger;
+    } else {
+        result = strings.DEFAULT_TRIGGER as BasicTriggerType;
+    }
+
+    if (Array.isArray(result)) {
+        finalResult = result;
     }
 
-    if (typeof trigger === 'string') {
-        result[0] = trigger;
+    if (typeof result === 'string') {
+        finalResult[0] = result;
+    }
+    return finalResult;
+}
+
+export function transformDefaultBooleanAPI(fieldProp: boolean, formProp: boolean, defaultVal = false) {
+    if (!isUndefined(fieldProp)) {
+        return fieldProp;
+    } else if (!isUndefined(formProp)) {
+        return formProp;
+    } else {
+        return defaultVal;
     }
-    return result;
 }
 
 export function mergeOptions(opts: WithFieldOption, props: ComponentProps) {
@@ -85,7 +112,6 @@ export function mergeOptions(opts: WithFieldOption, props: ComponentProps) {
 
 export function mergeProps(props: any) {
     const defaultProps = {
-        trigger: 'change',
         // validateStatus: 'default',
         allowEmptyString: false,
         allowEmpty: false,
@@ -93,7 +119,6 @@ export function mergeProps(props: any) {
         noLabel: false,
         noErrorMessage: false,
         isInInputGroup: false,
-        stopValidateWithError: false,
     };
     let {
         field,
@@ -151,7 +176,7 @@ export function mergeProps(props: any) {
     }
 
     const required = isRequired(rules);
-    trigger = transformTrigger(trigger);
+
     emptyValue = typeof emptyValue !== 'undefined' ? emptyValue : '';
     return {
         field,

+ 245 - 0
packages/semi-ui/form/_story/Validate/TriggerAndStopValidateWithError.jsx

@@ -0,0 +1,245 @@
+import React, { useState, useLayoutEffect, Component } from 'react';
+import { storiesOf } from '@storybook/react';
+import { Button, Modal, TreeSelect, Row, Col, Avatar, Toast, Select as BasicSelect,
+    Form,
+    useFormState,
+    useFormApi,
+    useFieldApi,
+    useFieldState,
+    withFormState,
+    withFormApi,
+    withField,
+    ArrayField,
+    AutoComplete,
+    Collapse,
+    Icon } from '../../../index';
+
+import { ComponentUsingFormState } from '../Hook/hookDemo';
+const { Input, Select, DatePicker, Switch, Slider, CheckboxGroup, Checkbox, RadioGroup, Radio, TimePicker, InputNumber, InputGroup } = Form;
+
+const FieldLevelTriggerDemo = () => {
+    const initValues = {
+        name: 'semi',
+        role: 'rd'
+    };
+    
+    const style = { width: '100%' };
+    
+    const { Select, Input } = Form;
+
+    return (
+        <Form initValues={initValues}>
+            <Form.Section text='FieldLevelTrigger'>
+                <Input
+                    field="system"
+                    label='trigger=change(default)'
+                    style={style}
+                    rules={[
+                        { required: true, message: 'required error' },
+                        { type: 'string', message: 'type error' },
+                        { validator: (rule, value) => value === 'semi', message: 'should be semi' }
+                    ]}
+                />
+                <Input
+                    field="name"
+                    label='trigger=blur'
+                    style={style}
+                    trigger='blur'
+                    rules={[
+                        { required: true, message: 'required error' },
+                        { type: 'string', message: 'type error' },
+                        { validator: (rule, value) => value === 'semi', message: 'should be semi' }
+                    ]}
+                />
+                <Input
+                    field="both"
+                    label='trigger=blur & change'
+                    style={style}
+                    trigger={['blur', 'change']}
+                    rules={[
+                        { required: true, message: 'required error' },
+                        { type: 'string', message: 'type error' },
+                        { validator: (rule, value) => value === 'semi', message: 'should be semi' }
+                    ]}
+                />
+                <Input
+                    field="role"
+                    label='trigger=mount'
+                    style={style}
+                    trigger='mount'
+                    rules={[
+                        { required: true, message: 'required error' },
+                        { type: 'string', message: 'type error' },
+                        { validator: (rule, value) => value === 'semi', message: 'should be semi' }
+                    ]}
+                />
+                <Button htmlType='submit'>提交</Button>
+                <Button htmlType='reset'>reset</Button>
+            </Form.Section>
+        </Form>
+    );
+};
+
+const FormLevelTriggerDemo = () => {
+    const initValues = {
+        name: 'semi',
+        role: 'rd'
+    };
+    
+    const style = { width: '100%' };
+    
+    const { Select, Input } = Form;
+
+    return (
+        <Form initValues={initValues} trigger='blur'>
+            <Form.Section text='FormLevelTrigger blur'>
+                <Input
+                    field="name"
+                    style={style}
+                    label='fieldTrigger=change'
+                    trigger='change'
+                    rules={[
+                        { required: true, message: 'required error' },
+                        { type: 'string', message: 'type error' },
+                        { validator: (rule, value) => value === 'semi', message: 'should be semi' }
+                    ]}
+                />
+                <Input
+                    field="role"
+                    label="fieldTrigger unset (default)"
+                    style={style}
+                    rules={[
+                        { required: true, message: 'required error' },
+                        { type: 'string', message: 'type error' },
+                        { validator: (rule, value) => value === 'semi', message: 'should be semi' }
+                    ]}
+                />
+                <Input
+                    field="custom"
+                    label="fieldTrigger=custom"
+                    trigger='custom'
+                    style={style}
+                    rules={[
+                        { required: true, message: 'required error' },
+                        { type: 'string', message: 'type error' },
+                        { validator: (rule, value) => value === 'semi', message: 'should be semi' }
+                    ]}
+                />
+                <Input
+                    field="both"
+                    label="fieldTrigger=mount & custom"
+                    trigger={['custom', 'mount']}
+                    style={style}
+                    rules={[
+                        { required: true, message: 'required error' },
+                        { type: 'string', message: 'type error' },
+                        { validator: (rule, value) => value === 'semi', message: 'should be semi' }
+                    ]}
+                />
+                <Button htmlType='submit'>提交</Button>
+                <Button htmlType='reset'>reset</Button>
+            </Form.Section>
+        </Form>
+    );
+};
+
+const FieldStopDemo = () => {
+    const initValues = {
+        name: 'semi',
+        role: 'rd'
+    };
+    
+    const style = { width: '100%' };
+    
+    const { Select, Input } = Form;
+
+    return (
+        <Form initValues={initValues}>
+            <Form.Section text='Field Stop=true'>
+                <Input
+                    field="name"
+                    style={style}
+                    label='field stop=true'
+                    stopValidateWithError
+                    rules={[
+                        { required: true, message: 'required error' },
+                        { validator: (rule, value) => value === 'semi', message: 'should be semi' },
+                        { validator: (rule, value) => value.startsWith('s'), message: 'should startwith s' },
+                    ]}
+                />
+                <Input
+                    field="role"
+                    style={style}
+                    label='field stop default (false)'
+                    rules={[
+                        { required: true, message: 'required error' },
+                        { validator: (rule, value) => value === 'semi', message: 'should be semi' },
+                        { validator: (rule, value) => value && value.startsWith('s'), message: 'should startwith s' },
+                    ]}
+                />
+                <Button htmlType='submit'>提交</Button>
+                <Button htmlType='reset'>reset</Button>
+            </Form.Section>
+        </Form>
+    );
+};
+
+const FormStopDemo = () => {
+
+    const style = { width: '100%' };
+    
+    const { Select, Input } = Form;
+
+    return (
+        <Form stopValidateWithError>
+            <Form.Section text='Form Stop=true'>
+                <Input
+                    field="name"
+                    style={style}
+                    label='field stop default (false)'
+                    rules={[
+                        { required: true, message: 'required error' },
+                        { validator: (rule, value) => value === 'semi', message: 'should be semi' },
+                        { validator: (rule, value) => value.startsWith('s'), message: 'should startwith s' },
+                    ]}
+                />
+                <Input
+                    field="role"
+                    style={style}
+                    label='field stop default (false)'
+                    rules={[
+                        { required: true, message: 'required error' },
+                        { validator: (rule, value) => value === 'semi', message: 'should be semi' },
+                        { validator: (rule, value) => value && value.startsWith('s'), message: 'should startwith s' },
+                    ]}
+                />
+                <Input
+                    field="role"
+                    style={style}
+                    stopValidateWithError={false}
+                    label='field stop false'
+                    rules={[
+                        { required: true, message: 'required error' },
+                        { validator: (rule, value) => value === 'semi', message: 'should be semi' },
+                        { validator: (rule, value) => value && value.startsWith('s'), message: 'should startwith s' },
+                    ]}
+                />
+                <Button htmlType='submit'>提交</Button>
+                <Button htmlType='reset'>reset</Button>
+            </Form.Section>
+        </Form>
+    );
+};
+
+const TriggerDemo = () => {
+    return (
+        <>
+            <FieldLevelTriggerDemo></FieldLevelTriggerDemo>
+            <FormLevelTriggerDemo></FormLevelTriggerDemo>
+            <FieldStopDemo></FieldStopDemo>
+            <FormStopDemo></FormStopDemo>
+        </>
+    );
+};
+
+export { TriggerDemo };

+ 3 - 0
packages/semi-ui/form/_story/form.stories.jsx

@@ -46,6 +46,7 @@ import {
   RulesExample,
   RaceAsyncDemo,
 } from './Validate/validateDemo';
+import { TriggerDemo } from './Validate/TriggerAndStopValidateWithError';
 
 // field props
 import { ConvertDemo } from './FieldProps/convert';
@@ -243,6 +244,8 @@ RaceAsyncDemo.story = {
   name: 'Validate - race async'
 }
 
+export const Trigger = () => <TriggerDemo></TriggerDemo>;
+
 export const HooksUseFormApi = () => <UseFormApiDemo />;
 
 HooksUseFormApi.story = {

+ 17 - 10
packages/semi-ui/form/baseForm.tsx

@@ -57,25 +57,30 @@ class Form<Values extends Record<string, any> = any> extends BaseComponent<BaseF
         onReset: PropTypes.func,
         // Triggered when the value of the form is updated, only when the value of the subfield changes. The entry parameter is formState.values
         onValueChange: PropTypes.func,
-        initValues: PropTypes.object,
-        getFormApi: PropTypes.func,
+        autoScrollToError: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
+        allowEmpty: PropTypes.bool,
+        className: PropTypes.string,
         component: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
-        render: PropTypes.func,
+        disabled: PropTypes.bool,
+        extraTextPosition: PropTypes.oneOf(strings.EXTRA_POS),
+        getFormApi: PropTypes.func,
+        initValues: PropTypes.object,
         validateFields: PropTypes.func,
-        style: PropTypes.object,
-        className: PropTypes.string,
         layout: PropTypes.oneOf(strings.LAYOUT),
         labelPosition: PropTypes.oneOf(strings.LABEL_POS),
         labelWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
         labelAlign: PropTypes.oneOf(strings.LABEL_ALIGN),
         labelCol: PropTypes.object, // Control labelCol {span: number, offset: number} for all field child nodes
-        wrapperCol: PropTypes.object, // Control wrapperCol {span: number, offset: number} for all field child nodes
-        allowEmpty: PropTypes.bool,
-        autoScrollToError: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
-        disabled: PropTypes.bool,
+        render: PropTypes.func,
+        style: PropTypes.object,
         showValidateIcon: PropTypes.bool,
-        extraTextPosition: PropTypes.oneOf(strings.EXTRA_POS),
+        stopValidateWithError: PropTypes.bool,
         id: PropTypes.string,
+        wrapperCol: PropTypes.object, // Control wrapperCol {span: number, offset: number} for all field child nodes
+        trigger: PropTypes.oneOfType([
+            PropTypes.oneOf(['blur', 'change', 'custom', 'mount']),
+            PropTypes.arrayOf(PropTypes.oneOf(['blur', 'change', 'custom', 'mount'])),
+        ])
     };
 
     static defaultProps = {
@@ -263,8 +268,10 @@ class Form<Values extends Record<string, any> = any> extends BaseComponent<BaseF
             allowEmpty,
             autoScrollToError,
             showValidateIcon,
+            stopValidateWithError,
             extraTextPosition,
             id,
+            trigger,
             ...rest
         } = this.props;
 

+ 26 - 22
packages/semi-ui/form/hoc/withField.tsx

@@ -2,7 +2,7 @@
 import React, { useState, useLayoutEffect, useEffect, useMemo, useRef, forwardRef } from 'react';
 import classNames from 'classnames';
 import { cssClasses } from '@douyinfe/semi-foundation/form/constants';
-import { isValid, generateValidatesFromRules, mergeOptions, mergeProps, getDisplayName } from '@douyinfe/semi-foundation/form/utils';
+import { isValid, generateValidatesFromRules, mergeOptions, mergeProps, getDisplayName, transformTrigger, transformDefaultBooleanAPI } from '@douyinfe/semi-foundation/form/utils';
 import * as ObjectUtil from '@douyinfe/semi-foundation/utils/object';
 import isPromise from '@douyinfe/semi-foundation/utils/isPromise';
 import warning from '@douyinfe/semi-foundation/utils/warning';
@@ -98,6 +98,27 @@ function withField<
             return null;
         }
 
+        let formProps = updater.getFormProps([
+            'labelPosition',
+            'labelWidth',
+            'labelAlign',
+            'labelCol',
+            'wrapperCol',
+            'disabled',
+            'showValidateIcon',
+            'extraTextPosition',
+            'stopValidateWithError',
+            'trigger'
+        ]);
+        let mergeLabelPos = labelPosition || formProps.labelPosition;
+        let mergeLabelWidth = labelWidth || formProps.labelWidth;
+        let mergeLabelAlign = labelAlign || formProps.labelAlign;
+        let mergeLabelCol = labelCol || formProps.labelCol;
+        let mergeWrapperCol = wrapperCol || formProps.wrapperCol;
+        let mergeExtraPos = extraTextPosition || formProps.extraTextPosition || 'bottom';
+        let mergeStopValidateWithError = transformDefaultBooleanAPI(stopValidateWithError, formProps.stopValidateWithError, false);
+        let mergeTrigger = transformTrigger(trigger, formProps.trigger);
+
         // To prevent user forgetting to pass the field, use undefined as the key, and updater.getValue will get the wrong value.
         let initValueInFormOpts = typeof field !== 'undefined' ? updater.getValue(field) : undefined; // Get the init value of form from formP rops.init Values Get the initial value set in the initValues of Form
         let initVal = typeof initValue !== 'undefined' ? initValue : initValueInFormOpts;
@@ -116,7 +137,7 @@ function withField<
 
         // FIXME typeof initVal
         const [value, setValue, getVal] = useStateWithGetter(typeof initVal !== undefined ? initVal : null);
-        const validateOnMount = trigger.includes('mount');
+        const validateOnMount = mergeTrigger.includes('mount');
 
         allowEmpty = allowEmpty || updater.getFormProps().allowEmpty;
 
@@ -188,7 +209,7 @@ function withField<
                     .validate(
                         model,
                         {
-                            first: stopValidateWithError,
+                            first: mergeStopValidateWithError,
                         },
                         (errors, fields) => {}
                     )
@@ -346,7 +367,7 @@ function withField<
             updateTouched(true, { notNotify: true, notUpdate: true });
             updateValue(val);
             // only validate when trigger includes change
-            if (trigger.includes('change')) {
+            if (mergeTrigger.includes('change')) {
                 fieldValidate(val);
             }
         };
@@ -358,7 +379,7 @@ function withField<
             if (!touched) {
                 updateTouched(true);
             }
-            if (trigger.includes('blur')) {
+            if (mergeTrigger.includes('blur')) {
                 let val = getVal();
                 fieldValidate(val);
             }
@@ -429,23 +450,6 @@ function withField<
             // eslint-disable-next-line react-hooks/exhaustive-deps
         }, [field]);
 
-        let formProps = updater.getFormProps([
-            'labelPosition',
-            'labelWidth',
-            'labelAlign',
-            'labelCol',
-            'wrapperCol',
-            'disabled',
-            'showValidateIcon',
-            'extraTextPosition',
-        ]);
-        let mergeLabelPos = labelPosition || formProps.labelPosition;
-        let mergeLabelWidth = labelWidth || formProps.labelWidth;
-        let mergeLabelAlign = labelAlign || formProps.labelAlign;
-        let mergeLabelCol = labelCol || formProps.labelCol;
-        let mergeWrapperCol = wrapperCol || formProps.wrapperCol;
-        let mergeExtraPos = extraTextPosition || formProps.extraTextPosition || 'bottom';
-
         // id attribute to improve a11y
         const a11yId = id ? id : field;
         const labelId = `${a11yId}-label`;

+ 5 - 3
packages/semi-ui/form/interface.ts

@@ -3,7 +3,7 @@ import { Subtract } from 'utility-types';
 import type { RuleItem } from 'async-validator';
 import type { Options as ScrollIntoViewOptions } from 'scroll-into-view-if-needed';
 
-import type { BaseFormApi as FormApi, FormState, WithFieldOption, AllErrors } from '@douyinfe/semi-foundation/form/interface';
+import type { BaseFormApi as FormApi, FormState, WithFieldOption, AllErrors, FieldValidateTriggerType } from '@douyinfe/semi-foundation/form/interface';
 import type { SelectProps } from '../select/index';
 import Option from '../select/option';
 import OptGroup from '../select/optionGroup';
@@ -105,6 +105,7 @@ export interface BaseFormProps <Values extends Record<string, any> = any> extend
     onReset?: () => void;
     onValueChange?: (values: Values, changedValue: Partial<Values>) => void;
     onChange?: (formState: FormState<Values>) => void;
+    allowEmpty?: boolean;
     validateFields?: (values: Values) => string | Partial<AllErrors<Values>>;
     /** Use this if you want to populate the form with initial values. */
     initValues?: Values;
@@ -113,18 +114,19 @@ export interface BaseFormProps <Values extends Record<string, any> = any> extend
     getFormApi?: (formApi: FormApi<Values>) => void;
     style?: React.CSSProperties;
     className?: string;
+    extraTextPosition?: 'middle' | 'bottom';
     layout?: 'horizontal' | 'vertical';
     labelPosition?: 'top' | 'left' | 'inset';
     labelWidth?: number | string;
     labelAlign?: 'left' | 'right';
     labelCol?: Record<string, any>;
     wrapperCol?: Record<string, any>;
-    allowEmpty?: boolean;
     render?: (internalProps: FormFCChild) => React.ReactNode;
     component?: React.FC<any> | React.ComponentClass<any>;
     children?: React.ReactNode | ((internalProps: FormFCChild) => React.ReactNode);
     autoScrollToError?: boolean | ScrollIntoViewOptions;
     disabled?: boolean;
     showValidateIcon?: boolean;
-    extraTextPosition?: 'middle' | 'bottom'
+    stopValidateWithError?: boolean;
+    trigger?: FieldValidateTriggerType
 }