Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(form): Add scrollToFirstError api (#3003) #3006

Merged
merged 3 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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