From fe6479a95f741cc73ff055a76d671d00fd9b85ef Mon Sep 17 00:00:00 2001 From: zhouyun1 Date: Fri, 20 Sep 2024 19:42:51 +0800 Subject: [PATCH] feat(form): Add scrollToFirstError api --- packages/ui/form/package.json | 3 ++- packages/ui/form/src/FormItem.tsx | 7 +++-- packages/ui/form/src/context.ts | 4 ++- packages/ui/form/src/use-form.ts | 43 ++++++++++++++++++++++++++++++- yarn.lock | 12 +++++++++ 5 files changed, 64 insertions(+), 5 deletions(-) diff --git a/packages/ui/form/package.json b/packages/ui/form/package.json index 09a70f45e..f277b6b13 100644 --- a/packages/ui/form/package.json +++ b/packages/ui/form/package.json @@ -52,7 +52,8 @@ "@hi-ui/object-utils": "^4.0.4", "@hi-ui/type-assertion": "^4.0.4", "@hi-ui/use-latest": "^4.0.4", - "async-validator": "^4.0.7" + "async-validator": "^4.0.7", + "scroll-into-view-if-needed": "^3.1.0" }, "peerDependencies": { "@hi-ui/core": ">=4.0.8", diff --git a/packages/ui/form/src/FormItem.tsx b/packages/ui/form/src/FormItem.tsx index 54cd8ad45..1e19b5dc5 100644 --- a/packages/ui/form/src/FormItem.tsx +++ b/packages/ui/form/src/FormItem.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react' +import React, { useMemo, useRef } from 'react' import { __DEV__ } from '@hi-ui/env' import { useFiledRules, UseFormFieldProps } from './use-form-field' import { FormLabel, FormLabelProps } from './FormLabel' @@ -21,7 +21,7 @@ export const FormItem: React.FC = ({ render, ...rest }) => { - const { prefixCls, showRequiredOnValidateRequired } = useFormContext() + const { prefixCls, showRequiredOnValidateRequired, formItemsRef } = useFormContext() const fieldRules = useFiledRules({ field, rules, valueType }) const { required } = rest @@ -36,6 +36,9 @@ export const FormItem: React.FC = ({ return ( { + field && formItemsRef.current.set(field.toString(), ref) + }} required={showRequired} // @ts-ignore formMessage={} diff --git a/packages/ui/form/src/context.ts b/packages/ui/form/src/context.ts index e1aff7fa7..05dbf3cf6 100644 --- a/packages/ui/form/src/context.ts +++ b/packages/ui/form/src/context.ts @@ -1,6 +1,7 @@ -import React, { createContext, useContext } from 'react' +import React, { createContext, MutableRefObject, useContext } from 'react' import { UseFormReturn } from './use-form' +import { FormFieldPath } from './types' export interface FormContextProps extends UseFormReturn { labelWidth: React.ReactText @@ -10,6 +11,7 @@ export interface FormContextProps extends UseFormReturn { showRequiredOnValidateRequired: boolean showValidateMessage: boolean prefixCls: string + formItemsRef: MutableRefObject> } const formContext = createContext | null>(null) diff --git a/packages/ui/form/src/use-form.ts b/packages/ui/form/src/use-form.ts index 1fcc1b6ab..4b6fffb6f 100644 --- a/packages/ui/form/src/use-form.ts +++ b/packages/ui/form/src/use-form.ts @@ -1,5 +1,8 @@ import { stringify, parse, isValidField, mergeValues } from './utils' import React, { useCallback, useMemo, useReducer, useRef } from 'react' +import scrollIntoView, { + StandardBehaviorOptions as ScrollOptions, +} from 'scroll-into-view-if-needed' import { FormAction, FormState, @@ -35,6 +38,7 @@ export const useForm = >({ rules = EMPTY_RULES, validateAfterTouched = false, validateTrigger: validateTriggerProp = DEFAULT_VALIDATE_TRIGGER, + scrollToFirstError, ...rest }: UseFormProps) => { /** @@ -44,6 +48,9 @@ export const useForm = >({ // eslint-disable-next-line react-hooks/exhaustive-deps const validateTriggersMemo = useMemo(() => validateTrigger, validateTrigger) + const formItemsMp = useMemo(() => new Map(), []) + const formItemsRef = useRef(formItemsMp) + /** * 收集 Field 的校验器注册表 */ @@ -69,6 +76,21 @@ export const useForm = >({ // formStateRef, // ]) + const getFormItemNode = useCallback((fieldName: FormFieldPath) => { + return formItemsRef.current.get(fieldName.toString()) + }, []) + + const scrollToNode = useCallback( + (fieldName: FormFieldPath, options: ScrollOptions = {}) => { + scrollIntoView(getFormItemNode(fieldName), { + scrollMode: 'if-needed', + block: 'nearest', + ...options, + }) + }, + [getFormItemNode] + ) + const getFieldValue = useCallback( (fieldName: FormFieldPath) => getNested(formStateRef.current.values, fieldName), [formStateRef] @@ -151,6 +173,8 @@ export const useForm = >({ const fieldNames = getRegisteredKeys() formDispatch({ type: 'SET_VALIDATING', payload: true }) + let firstError = false + return Promise.all( fieldNames.map((fieldName) => { const value = getFieldValue(fieldName) @@ -162,6 +186,11 @@ export const useForm = >({ // catch 错误,保证检验所有表单项 return fieldValidation.validate(value).catch((error) => { + if (scrollToFirstError && !firstError) { + firstError = true + scrollToNode(fieldName, scrollToFirstError) + } + // 第一个出错,即退出校验 if (lazyValidate) { throw error @@ -229,7 +258,14 @@ export const useForm = >({ return combinedError }) - }, [getRegisteredKeys, getFieldValue, getValidation, lazyValidate]) + }, [ + getFieldValue, + getRegisteredKeys, + getValidation, + lazyValidate, + scrollToFirstError, + scrollToNode, + ]) /** * 控件值更新策略 @@ -568,6 +604,7 @@ export const useForm = >({ getFieldsValue, setFieldsValue, getFieldsError, + formItemsRef, } } @@ -615,6 +652,10 @@ export interface UseFormProps> { * 重置时回调 */ onReset?: (values: T) => void | Promise + /** + * 提交失败自动滚动到第一个错误字段 + */ + scrollToFirstError?: boolean | Options } export type UseFormReturn = ReturnType diff --git a/yarn.lock b/yarn.lock index 3af2ccbea..3d23142ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7349,6 +7349,11 @@ compute-scroll-into-view@^1.0.17: version "1.0.17" resolved "https://registry.npm.taobao.org/compute-scroll-into-view/download/compute-scroll-into-view-1.0.17.tgz?cache=0&sync_timestamp=1614042424875&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcompute-scroll-into-view%2Fdownload%2Fcompute-scroll-into-view-1.0.17.tgz#6a88f18acd9d42e9cf4baa6bec7e0522607ab7ab" +compute-scroll-into-view@^3.0.2: + version "3.1.0" + resolved "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz#753f11d972596558d8fe7c6bcbc8497690ab4c87" + integrity sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.npm.taobao.org/concat-map/download/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -16680,6 +16685,13 @@ schema-utils@^3.0.0: ajv "^6.12.5" ajv-keywords "^3.5.2" +scroll-into-view-if-needed@^3.1.0: + version "3.1.0" + resolved "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz#fa9524518c799b45a2ef6bbffb92bcad0296d01f" + integrity sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ== + dependencies: + compute-scroll-into-view "^3.0.2" + semver-compare@^1.0.0: version "1.0.0" resolved "https://registry.npm.taobao.org/semver-compare/download/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"