u
This commit is contained in:
1 parent
ca2fa3543d
commit
7333e6695f
13 files changed
+211
-105
No files matched your search
@@ -1,19 +1,19 @@
|
||||
import type { ScrollView } from 'react-native'
|
||||
import React, { createContext, forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from 'react'
|
||||
import React, { createContext, forwardRef, useCallback, useImperativeHandle, useRef } from 'react'
|
||||
|
||||
export interface FormRef {
|
||||
validate: () => boolean
|
||||
scrollToField: (name: string) => void // 新增的滚动方法
|
||||
scrollToField: (name: string) => void
|
||||
}
|
||||
|
||||
interface FormProps<T extends Record<string, any>> {
|
||||
interface FormProps {
|
||||
children: React.ReactNode
|
||||
form: T
|
||||
scrollRef: React.RefObject<ScrollView>
|
||||
form: Record<string, any>
|
||||
scrollRef: React.RefObject<ScrollView | null>
|
||||
}
|
||||
|
||||
interface FormContextType<T = any> {
|
||||
form: T
|
||||
interface FormContextType {
|
||||
form: Record<string, any>
|
||||
registerValidator: (name: string, validator: () => boolean) => void
|
||||
unregisterValidator: (name: string) => void
|
||||
registerFieldPosition: (name: string, y: number) => void
|
||||
@@ -21,19 +21,16 @@ interface FormContextType<T = any> {
|
||||
|
||||
export const FormContext = createContext<FormContextType | undefined>(undefined)
|
||||
|
||||
const Form = forwardRef<FormRef, FormProps<any>>(({ children, form, scrollRef }, ref) => {
|
||||
// 使用 Array 来代替 Map
|
||||
const validatorsRef = useRef<Array<{ name: string, validator: () => boolean }>>([]) // 持有验证器顺序
|
||||
const fieldPositions = useRef<Map<string, number>>(new Map()) // 用来存储每个字段的 Y 坐标
|
||||
const Form = forwardRef<FormRef, FormProps>(({ children, form, scrollRef }, ref) => {
|
||||
const validatorsRef = useRef<Array<{ name: string, validator: () => boolean }>>([])
|
||||
const fieldPositions = useRef<Map<string, number>>(new Map())
|
||||
|
||||
const registerValidator = useCallback((name: string, validator: () => boolean) => {
|
||||
console.log(`注册:${name}`)
|
||||
validatorsRef.current.push({ name, validator })
|
||||
}, [])
|
||||
|
||||
const unregisterValidator = useCallback((name: string) => {
|
||||
console.log(`移除:${name}`)
|
||||
validatorsRef.current = validatorsRef.current.filter(validator => validator.name !== name)
|
||||
validatorsRef.current = validatorsRef.current.filter(v => v.name !== name)
|
||||
}, [])
|
||||
|
||||
const registerFieldPosition = useCallback((name: string, y: number) => {
|
||||
@@ -44,34 +41,24 @@ const Form = forwardRef<FormRef, FormProps<any>>(({ children, form, scrollRef },
|
||||
let firstErrorField: string | null = null
|
||||
let isValid = true
|
||||
|
||||
// 按顺序遍历所有的验证器
|
||||
for (const { name, validator } of validatorsRef.current) {
|
||||
const ok = validator()
|
||||
|
||||
if (!ok && !firstErrorField) {
|
||||
// 如果遇到第一个验证失败的字段,记录下来
|
||||
if (!validator() && !firstErrorField) {
|
||||
firstErrorField = name
|
||||
isValid = false
|
||||
}
|
||||
}
|
||||
|
||||
if (isValid)
|
||||
return true
|
||||
|
||||
// 滚动到第一个错误字段
|
||||
if (firstErrorField) {
|
||||
if (!isValid && firstErrorField) {
|
||||
scrollToField(firstErrorField)
|
||||
}
|
||||
|
||||
return false
|
||||
return isValid
|
||||
}
|
||||
|
||||
const scrollToField = (name: string) => {
|
||||
if (scrollRef.current) {
|
||||
const y = fieldPositions.current.get(name)
|
||||
if (y !== undefined && scrollRef.current) {
|
||||
scrollRef.current.scrollTo({ y, animated: true })
|
||||
}
|
||||
const y = fieldPositions.current.get(name)
|
||||
if (y !== undefined && scrollRef.current) {
|
||||
scrollRef.current.scrollTo({ y, animated: true })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import { View } from 'react-native'
|
||||
import {
|
||||
FormControl,
|
||||
FormControlError,
|
||||
@@ -11,10 +10,21 @@ import {
|
||||
import { AlertCircleIcon } from '@/components/ui/icon'
|
||||
import { FormContext } from './Form'
|
||||
|
||||
interface RuleType {
|
||||
validator?: (val: any) => boolean
|
||||
pattern?: RegExp
|
||||
message?: string
|
||||
len?: number
|
||||
min?: number
|
||||
max?: number
|
||||
required?: boolean
|
||||
whitespace?: boolean
|
||||
}
|
||||
|
||||
interface FormItemProps<T> {
|
||||
name: keyof T
|
||||
label?: string
|
||||
rules?: ((val: any) => string | null)[]
|
||||
rules?: RuleType[]
|
||||
children: React.ReactNode
|
||||
isRequired?: boolean
|
||||
}
|
||||
@@ -33,21 +43,87 @@ function FormItem<T extends Record<string, any>>({
|
||||
if (!context)
|
||||
return
|
||||
|
||||
const { registerValidator, unregisterValidator } = context
|
||||
const currentName = String(name)
|
||||
|
||||
const validator = () => {
|
||||
const value = context.form[name]
|
||||
// 如果 isRequired 为 true,且 rules 中没有 required: true,则自动加
|
||||
const hasRequiredInRules = rules.some(rule => rule.required)
|
||||
const allRules = [...rules]
|
||||
if (isRequired && !hasRequiredInRules) {
|
||||
allRules.unshift({
|
||||
required: true,
|
||||
message: label ? `${label} 是必填项` : `${currentName} 是必填项`,
|
||||
})
|
||||
}
|
||||
|
||||
if (isRequired && (value === undefined || value === null || value === '' || value.length === 0)) {
|
||||
setError(`${String(label || name)} 是必填项`)
|
||||
return false
|
||||
}
|
||||
const validateFn = () => {
|
||||
const value = context.form[currentName]
|
||||
|
||||
for (const rule of rules) {
|
||||
const msg = rule(value)
|
||||
if (msg) {
|
||||
setError(msg)
|
||||
return false
|
||||
for (const rule of allRules) {
|
||||
// 1. required 校验
|
||||
if (rule.required) {
|
||||
const empty
|
||||
= value === undefined
|
||||
|| value === null
|
||||
|| value === ''
|
||||
|| (Array.isArray(value) && value.length === 0)
|
||||
|| (rule.whitespace && typeof value === 'string' && value.trim() === '')
|
||||
if (empty) {
|
||||
setError(rule.message || `${label || currentName} 校验不通过`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 自定义 validator 返回 true/false
|
||||
if (rule.validator !== undefined) {
|
||||
const ok = rule.validator(value)
|
||||
if (!ok) {
|
||||
setError(rule.message || `${label || currentName} 校验不通过`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 3. pattern 正则
|
||||
if (rule.pattern && typeof value === 'string') {
|
||||
if (!rule.pattern.test(value)) {
|
||||
setError(rule.message || `${label || currentName} 格式不正确`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 4. len
|
||||
if (rule.len !== undefined) {
|
||||
if (
|
||||
(typeof value === 'string' && value.length !== rule.len)
|
||||
|| (typeof value === 'number' && value !== rule.len)
|
||||
|| (Array.isArray(value) && value.length !== rule.len)
|
||||
) {
|
||||
setError(rule.message || `${label || currentName} 长度/值必须为 ${rule.len}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 5. min
|
||||
if (rule.min !== undefined) {
|
||||
if (
|
||||
(typeof value === 'string' && value.length < rule.min)
|
||||
|| (typeof value === 'number' && value < rule.min)
|
||||
|| (Array.isArray(value) && value.length < rule.min)
|
||||
) {
|
||||
setError(rule.message || `${label || currentName} 最小值/长度为 ${rule.min}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 6. max
|
||||
if (rule.max !== undefined) {
|
||||
if (
|
||||
(typeof value === 'string' && value.length > rule.max)
|
||||
|| (typeof value === 'number' && value > rule.max)
|
||||
|| (Array.isArray(value) && value.length > rule.max)
|
||||
) {
|
||||
setError(rule.message || `${label || currentName} 最大值/长度为 ${rule.max}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,9 +131,12 @@ function FormItem<T extends Record<string, any>>({
|
||||
return true
|
||||
}
|
||||
|
||||
registerValidator(String(name), validator)
|
||||
return () => unregisterValidator(String(name))
|
||||
}, [name])
|
||||
context.registerValidator(currentName, validateFn)
|
||||
|
||||
return () => {
|
||||
context.unregisterValidator(currentName)
|
||||
}
|
||||
}, [context, name, label, rules, isRequired])
|
||||
|
||||
const handleLayout = (event: any) => {
|
||||
const { y } = event.nativeEvent.layout
|
||||
|
||||
Reference in new issue
Block a user