Browse Source

feat: formApi add scrollToError (#2294)

pointhalo 1 năm trước cách đây
mục cha
commit
a0a3687d00

+ 2 - 1
content/input/form/index-en-US.md

@@ -2123,7 +2123,8 @@ The table below describes the features available in the formApi.
 | setError      | Modify the error information of a field                                                                                                                                                                                                                                                                                            | formApi.setError(field: string, fieldErrorMessage: string)                                                                    |
 | getError      | Get Error Status of Field                                                                                                                                                                                                                                                                                                          | formApi.getError(field: string)                                                                                               |
 | getFieldExist | Get whether the field exists in the Form                                                                                                                                                                                                                                                                                           | formApi.getFieldExist(field: string)                                                                                          |
-| scrollToField | Scroll to field                                                                                                                                                                                                                                                                                                                    | formApi.scrollToField(field: string, scrollOpts: [object](<(https://github.com/stipsan/scroll-into-view-if-needed#options)>)) |
+| scrollToField | Scroll to the specified field, the second input parameter will be passed to scroll-into-view-if-needed | formApi.scrollToField(field: string, scrollOpts: [ScrollIntoViewOptions](https://github.com/stipsan/scroll-into-view-if-needed#options))                                                            |
+| scrollToError | Scroll to the field with validation error. You can pass a specified field or index. If you pass index, scroll to the index-th error DOM. If you do not pass any parameters, scroll to the first validation error position in the DOM tree. Available after v2.61.0  | formApi.scrollToError(<ApiType detail='{field?: string; index?: number; scrollOpts?: ScrollIntoViewOptions }'>ScrollToErrorOptions</ApiType>) 
 
 ### How to access formApi
 

+ 2 - 1
content/input/form/index.md

@@ -2121,7 +2121,8 @@ FormState 存储了所有 Form 内部的状态值,包括各表单控件的值
 | setError      | 修改 某个 field 的 error 信息                                                                                                                                                                                                    | formApi.setError(field: string, fieldErrorMessage: string)                                                          |
 | getError      | 获取 Field 的 error 状态                                                                                                                                                                                                         | formApi.getError(field: string)                                                                                     |
 | getFieldExist | 获取 Form 中是否存在对应的 field                                                                                                                                                                                                 | formApi.getFieldExist(field: string)                                                                                |
-| scrollToField | 滚动至指定的 field                                                                                                                                                                                                                   | formApi.scrollToField(field: string, scrollOpts: object)                                                            |
+| scrollToField | 滚动至指定的 field, 第二个入参将透传至scroll-into-view-if-needed | formApi.scrollToField(field: string, scrollOpts: [ScrollIntoViewOptions](https://github.com/stipsan/scroll-into-view-if-needed#options))                                                            |
+| scrollToError | 滚动至校验错误的field,可传指定 field 或者 index,传入 index 则滚动到第 index 个错误的 DOM,若不传参则滚动到DOM树中第一个校验出错的位置。 v2.61.0后提供  | formApi.scrollToError(<ApiType detail='{field?: string; index?: number; scrollOpts?: ScrollIntoViewOptions }'>ScrollToErrorOptions</ApiType>)                                                            |
 ### 如何获取 formApi
 
 -   Form 组件在 ComponentDidMount 阶段,会执行 props 传入的 getFormApi 回调,你可以在回调函数中保存 formApi 的引用,以便后续进行调用(**示例如下代码**)  

+ 1 - 1
lerna.json

@@ -1,5 +1,5 @@
 {
     "useWorkspaces": true,
     "npmClient": "yarn",
-    "version": "2.60.0"
+    "version": "2.61.0-alpha.0"
 }

+ 40 - 3
packages/semi-foundation/form/foundation.ts

@@ -9,6 +9,11 @@ import { BaseFormAdapter, FormState, CallOpts, FieldState, FieldStaff, Component
 
 export type { BaseFormAdapter };
 
+type ScrollToErrorOpts = {
+    field?: string;
+    index?: number;
+    scrollOpts?: ScrollIntoViewOptions
+}
 export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
 
     data: FormState;
@@ -72,6 +77,7 @@ export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
         this.getFormProps = this.getFormProps.bind(this);
         this.getFieldExist = this.getFieldExist.bind(this);
         this.scrollToField = this.scrollToField.bind(this);
+        this.scrollToError = this.scrollToError.bind(this);
     }
 
     init() {
@@ -130,7 +136,7 @@ export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
         this._adapter.forceUpdate();
     }
 
-    // in order to slove byted-issue-289
+    // in order to solve bytedance internal issue-289
     registerArrayField(arrayFieldPath: string, val: any): void {
         this.updateArrayField(arrayFieldPath, {
             updateKey: new Date().valueOf(),
@@ -622,6 +628,7 @@ export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
             submitForm: () => this.submit(),
             getFieldExist: (field: string) => this.getFieldExist(field),
             scrollToField: (field: string, scrollOpts?: ScrollIntoViewOptions) => this.scrollToField(field, scrollOpts),
+            scrollToError: (opts?: ScrollToErrorOpts) => this.scrollToError(opts),
         };
     }
 
@@ -701,8 +708,8 @@ export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
         const errorDOM = this._adapter.getAllErrorDOM();
         if (errorDOM && errorDOM.length) {
             try {
-                const fieldDom = errorDOM[0].parentNode.parentNode;
-                scrollIntoView(fieldDom as Element, scrollOpts);
+                const fieldDOM = errorDOM[0].parentNode.parentNode;
+                scrollIntoView(fieldDOM as Element, scrollOpts);
             } catch (error) {}
         }
     }
@@ -713,4 +720,34 @@ export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
             scrollIntoView(fieldDOM as Element, scrollOpts);
         }
     }
+
+    scrollToError(config?: ScrollToErrorOpts): void { 
+        let scrollOpts: ScrollIntoViewOptions = config && config.scrollOpts ? config.scrollOpts : { behavior: 'smooth', block: 'start' };
+        let field = config && config.field;
+        let index = config && config.index;
+        let fieldDOM, errorDOM;
+        if (typeof index === 'number') {
+            const allErrorDOM = this._adapter.getAllErrorDOM();
+            let errorDOM = allErrorDOM[index];
+            if (errorDOM) {
+                fieldDOM = errorDOM.parentNode.parentNode;
+            }
+        } else if (field) {
+            // If field is specified, find the error dom of the corresponding field
+            errorDOM = this._adapter.getFieldErrorDOM(field);
+            if (errorDOM) {
+                fieldDOM = errorDOM.parentNode.parentNode;
+            }
+        } else if (typeof field === 'undefined') {
+            // If field is not specified, find all error doms and scroll to the first one
+            let allErrorDOM = this._adapter.getAllErrorDOM();
+            if (allErrorDOM && allErrorDOM.length) {
+                fieldDOM = allErrorDOM[0].parentNode.parentNode;
+            }
+        }
+
+        if (fieldDOM) {
+            scrollIntoView(fieldDOM as Element, scrollOpts);
+        }
+    }
 }

+ 9 - 1
packages/semi-foundation/form/interface.ts

@@ -22,6 +22,7 @@ export interface BaseFormAdapter<P = Record<string, any>, S = Record<string, any
     getFormProps: (keys: undefined | string | Array<string>) => any;
     getAllErrorDOM: () => NodeList;
     getFieldDOM: (field: string) => Node;
+    getFieldErrorDOM: (field: string) => Node;
     initFormId: () => void
 }
 
@@ -62,6 +63,12 @@ export type FieldPathValue<T, P extends FieldPath<T>> =
           ? T[P]
           : never;
 
+export type ScrollToErrorOptions<K> = {
+    field?: K;
+    index?: number;
+    scrollConfig?: ScrollIntoViewOptions
+}
+
 // use object replace Record<string, any>, fix issue 933
 export interface BaseFormApi<T extends object = any> {
     /** get value of field */
@@ -91,7 +98,8 @@ export interface BaseFormApi<T extends object = any> {
     getValues: () => T;
     /** set value of multiple fields */
     setValues: (fieldsValue: Partial<T>, config?: setValuesConfig) => void;
-    scrollToField: <K extends keyof T>(field: K, scrollConfig?: ScrollIntoViewOptions) => void
+    scrollToField: <K extends keyof T>(field: K, scrollConfig?: ScrollIntoViewOptions) => void;
+    scrollToError: <K extends keyof T>(config?: ScrollToErrorOptions<K>) => void
 }
 
 export interface CallOpts {

+ 1 - 1
packages/semi-foundation/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-foundation",
-    "version": "2.60.0",
+    "version": "2.61.0-alpha.0",
     "description": "",
     "scripts": {
         "build:lib": "node ./scripts/compileLib.js",

+ 1 - 1
packages/semi-next/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-next",
-    "version": "2.60.0",
+    "version": "2.61.0-alpha.0",
     "description": "Plugin that support Semi Design in Next.js",
     "author": "伍浩威 <[email protected]>",
     "homepage": "",

+ 0 - 1
packages/semi-next/tsconfig.json

@@ -10,7 +10,6 @@
         "moduleResolution": "node",
         "noImplicitAny": true,
         "declaration": true,
-        "suppressImplicitAnyIndexErrors": true,
         "forceConsistentCasingInFileNames": true,
         "allowSyntheticDefaultImports": true,
         "experimentalDecorators": true,

+ 90 - 0
packages/semi-ui/form/_story/FormApi/scrollToError.jsx

@@ -0,0 +1,90 @@
+import React, { useState, useLayoutEffect, useRef } from 'react';
+import {
+    Form,
+    Toast,
+    Button, Modal, TreeSelect, Row, Col, Avatar, Select as BasicSelect
+} from '../../../index';
+const { Input, Select, DatePicker, Switch, Slider, CheckboxGroup, Checkbox, RadioGroup, Radio, TimePicker, InputNumber, InputGroup } = Form;
+
+import { IconPlusCircle, IconMinusCircle } from '@douyinfe/semi-icons';
+
+class ScrollToErrorDemo extends React.Component {
+    constructor(props) {
+        super(props);
+        this.state = {
+            fields: Array.from({ length: 100 }, (v, i) => i + 1),
+        };
+        this.formApi = null;
+        this.getFormApi = this.getFormApi.bind(this);
+        this.validate = this.validate.bind(this);
+        this.scrollToError = this.scrollToError.bind(this);
+    }
+
+    getFormApi(formApi) {
+        this.formApi = formApi;
+    }
+
+    validate() {
+        let begin = new Date().valueOf();
+        let end, time;
+        this.formApi
+            .validate(['No22', 'No55', 'No88'])
+            .then(values => {
+                end = new Date().valueOf();
+                time = (end - begin) / 1000;
+                console.log(`validate用时:${ time }s`);
+                // debugger
+            })
+            .catch(err => {
+                end = new Date().valueOf();
+                time = (end - begin) / 1000;
+                console.log(`validate用时:${ time }s`);
+                // debugger
+            });
+    }
+
+    scrollToError(target) {
+        // this.formApi.scrollToError({ field: `No${targetIndex}`});
+        if (typeof target === 'string') {
+            this.formApi.scrollToError({ field: target });
+        } else if (typeof target === 'number') {
+            this.formApi.scrollToError({ index: target });
+        } else if (!target) {
+            this.formApi.scrollToError();
+        }
+    }
+
+    renderFields() {
+        const { fields } = this.state;
+
+        return fields.map(item => (
+            <Form.Input
+                key={`No${item}`}
+                field={`No${item}`}
+                rules={[
+                    { required: true, message: 'required error' },
+                    {
+                        pattern: /^[a-zA-Z0-9_]+$/,
+                        message: '限制输入字符为:a-z, A-Z, 0-9, _',
+                    },
+                ]}
+            />
+        ));
+    }
+    render() {
+        let fields = this.renderFields();
+        return (
+            <Form getFormApi={this.getFormApi}>
+                <div style={{ height: 500, overflow: 'scroll' }}>{fields}</div>
+                <Button type='primary' onClick={this.validate}>validate</Button>
+                <Button onClick={() => this.scrollToError(88)}>scrollTo 88 Error</Button>
+                <Button onClick={() => this.scrollToError()}>scrollTo first Error</Button>
+                <Button onClick={() => this.scrollToError('No55')}>scrollTo No55 Error</Button>
+                <Button htmlType="reset">reset</Button>
+                <Button htmlType="submit">submit</Button>
+            </Form>
+        );
+    }
+}
+
+export { ScrollToErrorDemo };

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

@@ -69,12 +69,16 @@ import {
 } from './Performance/performanceDemo';
 import { SetValuesDemo, SetValuesWithArrayField } from './FormApi/setValuesDemo';
 import { SetValueUsingParentPath } from './FormApi/formApiDemo';
+import { ScrollToErrorDemo } from './FormApi/scrollToError';
 import { FieldPathWithArrayDemo } from './Debug/bugDemo';
 import ChildDidMount from './Debug/childDidMount';
 
 export { default as FormSubmit } from './FormSubmit';
 export { default as TabelForm } from './TableDemo';
 
+export const ScrollToError = () => <ScrollToErrorDemo></ScrollToErrorDemo>
+// export { default as ScrollToError } from './FormApi/scrollToError'
+
 const {
   Input,
   Select,

+ 7 - 0
packages/semi-ui/form/baseForm.tsx

@@ -208,6 +208,13 @@ class Form<Values extends Record<string, any> = any> extends BaseComponent<BaseF
             },
             getFieldDOM: (field: string) =>
                 document.querySelector(`.${cssClasses.PREFIX}-field[x-field-id="${field}"]`),
+            getFieldErrorDOM: (field: string) => {
+                const { formId } = this.state;
+                const { id } = this.props;
+                const xId = id ? id : formId;
+                let selector = `form[x-form-id="${xId}"] .${cssClasses.PREFIX}-field[x-field-id="${field}"] .${cssClasses.PREFIX}-field-error-message`;
+                return document.querySelector(selector);
+            }
         };
     }
 

+ 2 - 2
packages/semi-ui/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-ui",
-    "version": "2.60.0",
+    "version": "2.61.0-alpha.0",
     "description": "A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.",
     "main": "lib/cjs/index.js",
     "module": "lib/es/index.js",
@@ -22,7 +22,7 @@
         "@dnd-kit/utilities": "^3.2.1",
         "@douyinfe/semi-animation": "2.60.0",
         "@douyinfe/semi-animation-react": "2.60.0",
-        "@douyinfe/semi-foundation": "2.60.0",
+        "@douyinfe/semi-foundation": "2.61.0-alpha.0",
         "@douyinfe/semi-icons": "2.60.0",
         "@douyinfe/semi-illustrations": "2.60.0",
         "@douyinfe/semi-theme-default": "2.60.0",