Skip to content

Commit

Permalink
feat(form): Add scrollToFirstError api
Browse files Browse the repository at this point in the history
  • Loading branch information
zyprepare committed Sep 20, 2024
1 parent 0b98947 commit fe6479a
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 5 deletions.
3 changes: 2 additions & 1 deletion packages/ui/form/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 5 additions & 2 deletions packages/ui/form/src/FormItem.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -21,7 +21,7 @@ export const FormItem: React.FC<FormItemProps> = ({
render,
...rest
}) => {
const { prefixCls, showRequiredOnValidateRequired } = useFormContext()
const { prefixCls, showRequiredOnValidateRequired, formItemsRef } = useFormContext()

const fieldRules = useFiledRules({ field, rules, valueType })
const { required } = rest
Expand All @@ -36,6 +36,9 @@ export const FormItem: React.FC<FormItemProps> = ({
return (
<FormLabel
{...rest}
ref={(ref) => {
field && formItemsRef.current.set(field.toString(), ref)
}}
required={showRequired}
// @ts-ignore
formMessage={<FormMessage field={field} className={`${prefixCls}-item__message`} />}
Expand Down
4 changes: 3 additions & 1 deletion packages/ui/form/src/context.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -10,6 +11,7 @@ export interface FormContextProps extends UseFormReturn {
showRequiredOnValidateRequired: boolean
showValidateMessage: boolean
prefixCls: string
formItemsRef: MutableRefObject<Map<FormFieldPath, HTMLDivElement | null>>
}

const formContext = createContext<Omit<FormContextProps, 'rootProps'> | null>(null)
Expand Down
43 changes: 42 additions & 1 deletion packages/ui/form/src/use-form.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -35,6 +38,7 @@ export const useForm = <Values = Record<string, any>>({
rules = EMPTY_RULES,
validateAfterTouched = false,
validateTrigger: validateTriggerProp = DEFAULT_VALIDATE_TRIGGER,
scrollToFirstError,
...rest
}: UseFormProps<Values>) => {
/**
Expand All @@ -44,6 +48,9 @@ export const useForm = <Values = Record<string, any>>({
// eslint-disable-next-line react-hooks/exhaustive-deps
const validateTriggersMemo = useMemo(() => validateTrigger, validateTrigger)

const formItemsMp = useMemo(() => new Map(), [])
const formItemsRef = useRef(formItemsMp)

/**
* 收集 Field 的校验器注册表
*/
Expand All @@ -69,6 +76,21 @@ export const useForm = <Values = Record<string, any>>({
// 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]
Expand Down Expand Up @@ -151,6 +173,8 @@ export const useForm = <Values = Record<string, any>>({
const fieldNames = getRegisteredKeys()
formDispatch({ type: 'SET_VALIDATING', payload: true })

let firstError = false

return Promise.all(
fieldNames.map((fieldName) => {
const value = getFieldValue(fieldName)
Expand All @@ -162,6 +186,11 @@ export const useForm = <Values = Record<string, any>>({

// catch 错误,保证检验所有表单项
return fieldValidation.validate(value).catch((error) => {
if (scrollToFirstError && !firstError) {
firstError = true
scrollToNode(fieldName, scrollToFirstError)
}

// 第一个出错,即退出校验
if (lazyValidate) {
throw error
Expand Down Expand Up @@ -229,7 +258,14 @@ export const useForm = <Values = Record<string, any>>({

return combinedError
})
}, [getRegisteredKeys, getFieldValue, getValidation, lazyValidate])
}, [
getFieldValue,
getRegisteredKeys,
getValidation,
lazyValidate,
scrollToFirstError,
scrollToNode,
])

/**
* 控件值更新策略
Expand Down Expand Up @@ -568,6 +604,7 @@ export const useForm = <Values = Record<string, any>>({
getFieldsValue,
setFieldsValue,
getFieldsError,
formItemsRef,
}
}

Expand Down Expand Up @@ -615,6 +652,10 @@ export interface UseFormProps<T = Record<string, any>> {
* 重置时回调
*/
onReset?: (values: T) => void | Promise<any>
/**
* 提交失败自动滚动到第一个错误字段
*/
scrollToFirstError?: boolean | Options
}

export type UseFormReturn = ReturnType<typeof useForm>
Expand Down
12 changes: 12 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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==

[email protected]:
version "0.0.1"
resolved "https://registry.npm.taobao.org/concat-map/download/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit fe6479a

Please sign in to comment.