Skip to content

Commit

Permalink
feat(form): Add scrollToFirstError api (#3003) (#3006)
Browse files Browse the repository at this point in the history
* feat(form): Add scrollToFirstError api

* chore(form): 处理类型问题

* feat(form): Add scrollToFirstError api (#3003)
  • Loading branch information
zyprepare authored Sep 26, 2024
1 parent 9b9a448 commit 4540c21
Show file tree
Hide file tree
Showing 9 changed files with 306 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/nasty-geckos-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hi-ui/hiui": patch
---

feat(form): Add scrollToFirstError api
5 changes: 5 additions & 0 deletions .changeset/six-carrots-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hi-ui/form": minor
---

feat: Add scrollToFirstError api
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
46 changes: 45 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,14 @@ export const useForm = <Values = Record<string, any>>({

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

// 第一个出错,即退出校验
if (lazyValidate) {
throw error
Expand Down Expand Up @@ -229,7 +261,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 +607,7 @@ export const useForm = <Values = Record<string, any>>({
getFieldsValue,
setFieldsValue,
getFieldsError,
formItemsRef,
}
}

Expand Down Expand Up @@ -615,6 +655,10 @@ export interface UseFormProps<T = Record<string, any>> {
* 重置时回调
*/
onReset?: (values: T) => void | Promise<any>
/**
* 提交失败自动滚动到第一个错误字段,配置参考:https://github.com/scroll-into-view/scroll-into-view-if-needed?tab=readme-ov-file#options
*/
scrollToFirstError?: boolean | ScrollOptions
}

export type UseFormReturn = ReturnType<typeof useForm>
Expand Down
1 change: 1 addition & 0 deletions packages/ui/form/stories/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './placement.stories'
export * from './validate.stories'
export * from './validate-field.stories'
export * from './validate-message.stories'
export * from './scroll-to-error.stories'
export * from './set-values.stories'
export * from './get-values.stories'
export * from './render.stories'
Expand Down
Loading

0 comments on commit 4540c21

Please sign in to comment.