@coxy/react-validator 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/biome.json +4 -2
- package/dist/index.d.mts +32 -42
- package/dist/index.d.ts +32 -42
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/example/example.tsx +21 -25
- package/example/index.html +2 -7
- package/example/tsconfig.json +6 -6
- package/package.json +12 -13
- package/src/context.ts +8 -3
- package/src/custom-errors.test.tsx +73 -0
- package/src/index.test.ts +19 -4
- package/src/index.ts +4 -3
- package/src/rules.test.ts +9 -4
- package/src/rules.ts +14 -14
- package/src/use-validator.test.tsx +1 -2
- package/src/validator-field.test.tsx +34 -3
- package/src/validator-field.tsx +44 -54
- package/src/validator-wrapper.test.tsx +31 -13
- package/src/validator-wrapper.tsx +58 -55
- package/src/validator.test.tsx +10 -0
- package/src/validator.ts +3 -3
- package/src/jest.d.ts +0 -2
package/src/context.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { createContext } from 'react'
|
|
2
2
|
|
|
3
|
-
import type { Validity } from './types'
|
|
3
|
+
import type { FieldParams, Validity } from './types'
|
|
4
|
+
|
|
5
|
+
export interface RegisteredFieldHandle {
|
|
6
|
+
props: FieldParams
|
|
7
|
+
validate: () => Validity
|
|
8
|
+
}
|
|
4
9
|
|
|
5
10
|
export const Context = createContext<{
|
|
6
|
-
registerField: (field:
|
|
7
|
-
unregisterField: (field:
|
|
11
|
+
registerField: (field: RegisteredFieldHandle) => void
|
|
12
|
+
unregisterField: (field: RegisteredFieldHandle) => void
|
|
8
13
|
customErrors: Array<Validity>
|
|
9
14
|
}>(null)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { act, render } from '@testing-library/react'
|
|
6
|
+
import { createRef } from 'react'
|
|
7
|
+
|
|
8
|
+
import { rules } from './rules'
|
|
9
|
+
import { ValidatorField } from './validator-field'
|
|
10
|
+
import { ValidatorWrapper, type ValidatorWrapper as ValidatorWrapperHandle } from './validator-wrapper'
|
|
11
|
+
|
|
12
|
+
it('setCustomError overrides field validation result and clearCustomErrors restores it', () => {
|
|
13
|
+
const validator = createRef<ValidatorWrapperHandle>()
|
|
14
|
+
|
|
15
|
+
render(
|
|
16
|
+
<ValidatorWrapper ref={validator}>
|
|
17
|
+
<ValidatorField id="email-field" rules={rules.email} value="user@example.com" />
|
|
18
|
+
</ValidatorWrapper>,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// Initially valid
|
|
22
|
+
const fieldBefore = validator.current.getField('email-field')
|
|
23
|
+
const validityBefore = fieldBefore.validate()
|
|
24
|
+
expect(validityBefore.isValid).toBe(true)
|
|
25
|
+
expect(validityBefore.message).toBe('')
|
|
26
|
+
|
|
27
|
+
// Set a custom error
|
|
28
|
+
act(() => {
|
|
29
|
+
validator.current.setCustomError({ id: 'email-field', isValid: false, message: 'Custom error' })
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const fieldWithCustom = validator.current.getField('email-field')
|
|
33
|
+
const validityWithCustom = fieldWithCustom.validate()
|
|
34
|
+
expect(validityWithCustom.isValid).toBe(false)
|
|
35
|
+
expect(validityWithCustom.message).toBe('Custom error')
|
|
36
|
+
|
|
37
|
+
// Clear custom errors
|
|
38
|
+
act(() => {
|
|
39
|
+
validator.current.clearCustomErrors()
|
|
40
|
+
})
|
|
41
|
+
const fieldAfter = validator.current.getField('email-field')
|
|
42
|
+
const validityAfter = fieldAfter.validate()
|
|
43
|
+
expect(validityAfter.isValid).toBe(true)
|
|
44
|
+
expect(validityAfter.message).toBe('')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('custom error is used inside ValidatorField render-prop child', () => {
|
|
48
|
+
const validator = createRef<ValidatorWrapperHandle>()
|
|
49
|
+
const messages: string[] = []
|
|
50
|
+
|
|
51
|
+
render(
|
|
52
|
+
<ValidatorWrapper ref={validator}>
|
|
53
|
+
<ValidatorField id="field-x" rules={rules.password} value="strongpassword">
|
|
54
|
+
{({ message }) => {
|
|
55
|
+
messages.push(message)
|
|
56
|
+
return null
|
|
57
|
+
}}
|
|
58
|
+
</ValidatorField>
|
|
59
|
+
</ValidatorWrapper>,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
// Initially valid → message pushed should be ''
|
|
63
|
+
expect(messages[messages.length - 1]).toBe('')
|
|
64
|
+
|
|
65
|
+
// After setting custom error, the render-prop should see the custom message.
|
|
66
|
+
act(() => {
|
|
67
|
+
validator.current.setCustomError({ id: 'field-x', isValid: false, message: 'Injected' })
|
|
68
|
+
})
|
|
69
|
+
const field = validator.current.getField('field-x')
|
|
70
|
+
const res = field.validate()
|
|
71
|
+
expect(res.isValid).toBe(false)
|
|
72
|
+
expect(res.message).toBe('Injected')
|
|
73
|
+
})
|
package/src/index.test.ts
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { rules, useValidator, Validator, ValidatorField, ValidatorWrapper } from './index'
|
|
2
2
|
|
|
3
|
-
it('
|
|
4
|
-
expect(typeof ValidatorWrapper).toBe('
|
|
5
|
-
expect(typeof ValidatorField).toBe('
|
|
3
|
+
it('exports surface is available', () => {
|
|
4
|
+
expect(typeof ValidatorWrapper).toBe('object')
|
|
5
|
+
expect(typeof ValidatorField).toBe('object')
|
|
6
6
|
expect(typeof rules).toBe('object')
|
|
7
|
+
expect(typeof Validator).toBe('function')
|
|
8
|
+
expect(typeof useValidator).toBe('function')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('use exports to execute a basic validation flow', () => {
|
|
12
|
+
// use Validator (class)
|
|
13
|
+
const validator = new Validator()
|
|
14
|
+
validator.addField({ value: 'test@example.com', rules: rules.email })
|
|
15
|
+
const res = validator.validate()
|
|
16
|
+
expect(res.isValid).toBe(true)
|
|
17
|
+
|
|
18
|
+
// use useValidator (hook-like util function)
|
|
19
|
+
const [isValid, { message }] = useValidator('bad-email', rules.email)
|
|
20
|
+
expect(isValid).toBe(false)
|
|
21
|
+
expect(message).toBe('Email is invalid')
|
|
7
22
|
})
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { rules } from './rules'
|
|
2
|
+
|
|
3
|
+
export type { ErrorMessage, FieldParams, ValidatorRule, Validity } from './types'
|
|
4
|
+
export { useValidator } from './use-validator'
|
|
5
|
+
export { Validator } from './validator'
|
|
2
6
|
export { ValidatorField } from './validator-field'
|
|
3
7
|
export { ValidatorWrapper } from './validator-wrapper'
|
|
4
|
-
export { Validator } from './validator'
|
|
5
|
-
export { useValidator } from './use-validator'
|
|
6
|
-
export * from './types'
|
package/src/rules.test.ts
CHANGED
|
@@ -33,6 +33,7 @@ it('check rule password', () => {
|
|
|
33
33
|
it('check rule bool', () => {
|
|
34
34
|
expect(rules.bool.length).toBe(1)
|
|
35
35
|
|
|
36
|
+
// @ts-expect-error
|
|
36
37
|
const result = rules.bool[0].rule(true)
|
|
37
38
|
expect(result).toBe(true)
|
|
38
39
|
})
|
|
@@ -56,18 +57,20 @@ it('check rule min', () => {
|
|
|
56
57
|
result = rules.min(10)[0].rule('')
|
|
57
58
|
expect(result).toBe(false)
|
|
58
59
|
|
|
59
|
-
result = rules.min(9)[0].rule('
|
|
60
|
+
result = rules.min(9)[0].rule('test-test-test')
|
|
60
61
|
expect(result).toBe(false)
|
|
61
62
|
|
|
62
63
|
result = rules.min(9)[0].rule('11')
|
|
63
64
|
expect(result).toBe(true)
|
|
64
65
|
|
|
66
|
+
// @ts-expect-error
|
|
65
67
|
result = rules.min(9)[0].rule(10)
|
|
66
68
|
expect(result).toBe(true)
|
|
67
69
|
|
|
68
70
|
result = rules.min(9)[0].rule('8')
|
|
69
71
|
expect(result).toBe(false)
|
|
70
72
|
|
|
73
|
+
// @ts-expect-error
|
|
71
74
|
result = rules.min(9)[0].rule(7)
|
|
72
75
|
expect(result).toBe(false)
|
|
73
76
|
|
|
@@ -80,18 +83,20 @@ it('check rule max', () => {
|
|
|
80
83
|
result = rules.max(10)[0].rule('')
|
|
81
84
|
expect(result).toBe(false)
|
|
82
85
|
|
|
83
|
-
result = rules.max(9)[0].rule('
|
|
86
|
+
result = rules.max(9)[0].rule('test-test-test')
|
|
84
87
|
expect(result).toBe(false)
|
|
85
88
|
|
|
86
89
|
result = rules.max(9)[0].rule('11')
|
|
87
90
|
expect(result).toBe(false)
|
|
88
91
|
|
|
92
|
+
// @ts-expect-error
|
|
89
93
|
result = rules.max(9)[0].rule(10)
|
|
90
94
|
expect(result).toBe(false)
|
|
91
95
|
|
|
92
96
|
result = rules.max(9)[0].rule('5')
|
|
93
97
|
expect(result).toBe(true)
|
|
94
98
|
|
|
99
|
+
// @ts-expect-error
|
|
95
100
|
result = rules.max(9)[0].rule(5)
|
|
96
101
|
expect(result).toBe(true)
|
|
97
102
|
|
|
@@ -107,10 +112,10 @@ it('check rule length', () => {
|
|
|
107
112
|
result = rules.length(1)[0].rule('1')
|
|
108
113
|
expect(result).toBe(true)
|
|
109
114
|
|
|
110
|
-
result = rules.length(1, 10)[0].rule('
|
|
115
|
+
result = rules.length(1, 10)[0].rule('test-test-test')
|
|
111
116
|
expect(result).toBe(true)
|
|
112
117
|
|
|
113
|
-
result = rules.length(1, 10)[1].rule('
|
|
118
|
+
result = rules.length(1, 10)[1].rule('test-test-test')
|
|
114
119
|
expect(result).toBe(false)
|
|
115
120
|
|
|
116
121
|
result = rules.length(1, 10)[0].rule('lol')
|
package/src/rules.ts
CHANGED
|
@@ -2,68 +2,68 @@ import type { ValidatorRule } from './types'
|
|
|
2
2
|
|
|
3
3
|
// eslint-disable-next-line
|
|
4
4
|
const emailReg =
|
|
5
|
-
/^(([^<>()
|
|
5
|
+
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
|
6
6
|
|
|
7
7
|
export type ValidatorRules = ValidatorRule[]
|
|
8
8
|
|
|
9
9
|
export const rules = {
|
|
10
10
|
notEmpty: [
|
|
11
11
|
{
|
|
12
|
-
rule: (value) => value !== '' && value.length > 0,
|
|
12
|
+
rule: (value: string) => value !== '' && value.length > 0,
|
|
13
13
|
message: 'Value is required',
|
|
14
14
|
},
|
|
15
15
|
],
|
|
16
16
|
|
|
17
17
|
bool: [
|
|
18
18
|
{
|
|
19
|
-
rule: (value) => !!value,
|
|
19
|
+
rule: (value: string) => !!value,
|
|
20
20
|
message: 'Value is required',
|
|
21
21
|
},
|
|
22
22
|
],
|
|
23
23
|
|
|
24
24
|
password: [
|
|
25
25
|
{
|
|
26
|
-
rule: (value) => value.length > 0,
|
|
26
|
+
rule: (value: string) => value.length > 0,
|
|
27
27
|
message: 'Password field cannot be empty',
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
|
-
rule: (value) => value.length > 5,
|
|
30
|
+
rule: (value: string) => value.length > 5,
|
|
31
31
|
message: 'Password field can not be less than 6 characters',
|
|
32
32
|
},
|
|
33
33
|
],
|
|
34
34
|
|
|
35
35
|
email: [
|
|
36
36
|
{
|
|
37
|
-
rule: (value) => !!value && value !== '' && value.length !== 0,
|
|
37
|
+
rule: (value: string) => !!value && value !== '' && value.length !== 0,
|
|
38
38
|
message: 'Email is required',
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
|
-
rule: (value) => emailReg.test(String(value).toLowerCase()),
|
|
41
|
+
rule: (value: string) => emailReg.test(String(value).toLowerCase()),
|
|
42
42
|
message: 'Email is invalid',
|
|
43
43
|
},
|
|
44
44
|
],
|
|
45
45
|
|
|
46
|
-
min: (min) => [
|
|
46
|
+
min: (min: number) => [
|
|
47
47
|
{
|
|
48
|
-
rule: (value) => Number.parseFloat(value) > min,
|
|
48
|
+
rule: (value: string) => Number.parseFloat(value) > min,
|
|
49
49
|
message: `The value must be greater than ${min}`,
|
|
50
50
|
},
|
|
51
51
|
],
|
|
52
52
|
|
|
53
|
-
max: (max) => [
|
|
53
|
+
max: (max: number) => [
|
|
54
54
|
{
|
|
55
|
-
rule: (value) => Number.parseFloat(value) < max,
|
|
55
|
+
rule: (value: string) => Number.parseFloat(value) < max,
|
|
56
56
|
message: `The value must be smaller ${max}`,
|
|
57
57
|
},
|
|
58
58
|
],
|
|
59
59
|
|
|
60
|
-
length: (min, max
|
|
60
|
+
length: (min: number, max?: number) => [
|
|
61
61
|
{
|
|
62
|
-
rule: (value) => String(value).length >= min,
|
|
62
|
+
rule: (value: string) => String(value).length >= min,
|
|
63
63
|
message: `No less than ${min} symbols`,
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
|
-
rule: (value) => (max !== undefined ? String(value).length <= max : true),
|
|
66
|
+
rule: (value: string) => (max !== undefined ? String(value).length <= max : true),
|
|
67
67
|
message: `No more than ${max} symbols`,
|
|
68
68
|
},
|
|
69
69
|
],
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* @jest-environment jsdom
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { render, screen } from '@testing-library/react'
|
|
6
|
-
import { act } from '@testing-library/react'
|
|
5
|
+
import { act, render, screen } from '@testing-library/react'
|
|
7
6
|
import { useEffect, useState } from 'react'
|
|
8
7
|
|
|
9
8
|
import { rules } from './rules'
|
|
@@ -21,7 +21,7 @@ it('check context validator', () => {
|
|
|
21
21
|
const validator = createRef<ValidatorWrapper>()
|
|
22
22
|
render(
|
|
23
23
|
<ValidatorWrapper ref={validator}>
|
|
24
|
-
<ValidatorField rules={[]} />
|
|
24
|
+
<ValidatorField rules={[]} value="" />
|
|
25
25
|
</ValidatorWrapper>,
|
|
26
26
|
)
|
|
27
27
|
|
|
@@ -130,8 +130,8 @@ it('check custom rule message function', () => {
|
|
|
130
130
|
const validator = createRef<ValidatorWrapper>()
|
|
131
131
|
const rule = [
|
|
132
132
|
{
|
|
133
|
-
rule: (value) => value !== 'test',
|
|
134
|
-
message: (value) => `test message ${value}`,
|
|
133
|
+
rule: (value: string) => value !== 'test',
|
|
134
|
+
message: (value: string) => `test message ${value}`,
|
|
135
135
|
},
|
|
136
136
|
]
|
|
137
137
|
render(
|
|
@@ -147,3 +147,34 @@ it('check custom rule message function', () => {
|
|
|
147
147
|
expect(validateResult.isValid).toBe(false)
|
|
148
148
|
expect(validateResult.message).toBe('test message test')
|
|
149
149
|
})
|
|
150
|
+
|
|
151
|
+
jest.useFakeTimers()
|
|
152
|
+
|
|
153
|
+
it('re-renders the same field to cover handleRef initialization false branch and else-validate path', () => {
|
|
154
|
+
const validator = createRef<ValidatorWrapper>()
|
|
155
|
+
|
|
156
|
+
function Comp() {
|
|
157
|
+
const [val, setVal] = useState('')
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
setTimeout(() => {
|
|
160
|
+
act(() => setVal('abc'))
|
|
161
|
+
}, 50)
|
|
162
|
+
}, [])
|
|
163
|
+
return (
|
|
164
|
+
<ValidatorWrapper ref={validator}>
|
|
165
|
+
<ValidatorField id="rerender" rules={rules.notEmpty} value={val}>
|
|
166
|
+
{() => null}
|
|
167
|
+
</ValidatorField>
|
|
168
|
+
</ValidatorWrapper>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
render(<Comp />)
|
|
173
|
+
// initial: invalid
|
|
174
|
+
let field = validator.current.getField('rerender')
|
|
175
|
+
expect(field.validate().isValid).toBe(false)
|
|
176
|
+
|
|
177
|
+
jest.runAllTimers()
|
|
178
|
+
field = validator.current.getField('rerender')
|
|
179
|
+
expect(field.validate().isValid).toBe(true)
|
|
180
|
+
})
|
package/src/validator-field.tsx
CHANGED
|
@@ -1,69 +1,59 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { Validity } from 'types'
|
|
1
|
+
import { forwardRef, type ReactNode, useContext, useEffect, useRef } from 'react'
|
|
2
|
+
import type { FieldParams, Validity } from 'types'
|
|
3
3
|
|
|
4
|
-
import { Context } from './context'
|
|
5
|
-
import type { ValidatorRules } from './rules'
|
|
4
|
+
import { Context, type RegisteredFieldHandle } from './context'
|
|
6
5
|
import { Field } from './validator'
|
|
7
6
|
|
|
8
|
-
// biome-ignore lint/suspicious/noExplicitAny: <
|
|
7
|
+
// biome-ignore lint/suspicious/noExplicitAny: <need>
|
|
9
8
|
export type Value = any
|
|
10
9
|
|
|
11
10
|
type Fn = (validity: Validity, value: Value) => ReactNode
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
rules?: ValidatorRules
|
|
15
|
-
required?: boolean
|
|
16
|
-
value?: Value
|
|
17
|
-
id?: string | number
|
|
12
|
+
type Props = FieldParams & {
|
|
18
13
|
children?: ReactNode | Fn
|
|
19
|
-
unregisterField: (val: Value) => void
|
|
20
|
-
registerField: (val: Value) => void
|
|
21
|
-
customErrors: Array<Validity>
|
|
22
14
|
}
|
|
23
15
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
16
|
+
export const ValidatorField = forwardRef<unknown, Props>(function ValidatorField(props: Props, _ref) {
|
|
17
|
+
const { children, value } = props
|
|
18
|
+
const { customErrors, registerField, unregisterField } = useContext(Context)
|
|
19
|
+
|
|
20
|
+
const propsRef = useRef(props)
|
|
21
|
+
propsRef.current = props
|
|
22
|
+
|
|
23
|
+
const customErrorsRef = useRef(customErrors)
|
|
24
|
+
customErrorsRef.current = customErrors
|
|
25
|
+
|
|
26
|
+
const handleRef = useRef<RegisteredFieldHandle | null>(null)
|
|
27
|
+
if (!handleRef.current) {
|
|
28
|
+
handleRef.current = {
|
|
29
|
+
get props() {
|
|
30
|
+
return propsRef.current
|
|
31
|
+
},
|
|
32
|
+
validate: () => {
|
|
33
|
+
const curr = propsRef.current
|
|
34
|
+
const customError = customErrorsRef.current.find((item) => item.id === curr.id)
|
|
35
|
+
if (customError) {
|
|
36
|
+
return customError
|
|
37
|
+
}
|
|
38
|
+
const field = new Field({
|
|
39
|
+
rules: curr.rules,
|
|
40
|
+
required: curr.required,
|
|
41
|
+
value: curr.value,
|
|
42
|
+
id: curr.id,
|
|
43
|
+
})
|
|
44
|
+
return field.validate()
|
|
45
|
+
},
|
|
46
|
+
}
|
|
31
47
|
}
|
|
32
48
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return customError
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
registerField(handleRef.current as RegisteredFieldHandle)
|
|
51
|
+
return () => {
|
|
52
|
+
unregisterField(handleRef.current as RegisteredFieldHandle)
|
|
38
53
|
}
|
|
54
|
+
}, [registerField, unregisterField])
|
|
39
55
|
|
|
40
|
-
|
|
41
|
-
rules: props.rules,
|
|
42
|
-
required: props.required,
|
|
43
|
-
value: props.value,
|
|
44
|
-
id: props.id,
|
|
45
|
-
})
|
|
46
|
-
return field.validate()
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
render() {
|
|
50
|
-
const { children, value } = this.props
|
|
51
|
-
const validity = this.validate()
|
|
52
|
-
return typeof children === 'function' ? children(validity, value) : children
|
|
53
|
-
}
|
|
54
|
-
}
|
|
56
|
+
const validity = handleRef.current.validate()
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
<Context.Consumer>
|
|
59
|
-
{(data) => (
|
|
60
|
-
<ValidationFieldWrapper
|
|
61
|
-
{...props}
|
|
62
|
-
customErrors={data.customErrors}
|
|
63
|
-
registerField={data.registerField}
|
|
64
|
-
unregisterField={data.unregisterField}
|
|
65
|
-
/>
|
|
66
|
-
)}
|
|
67
|
-
</Context.Consumer>
|
|
68
|
-
)
|
|
69
|
-
}
|
|
58
|
+
return typeof children === 'function' ? (children as Fn)(validity, value) : (children as ReactNode)
|
|
59
|
+
})
|
|
@@ -13,12 +13,12 @@ it('check wrapper validator', () => {
|
|
|
13
13
|
const validator = createRef<ValidatorWrapper>()
|
|
14
14
|
render(
|
|
15
15
|
<ValidatorWrapper ref={validator}>
|
|
16
|
-
<ValidatorField rules={[]} />
|
|
17
|
-
<ValidatorField rules={[]} />
|
|
18
|
-
<ValidatorField rules={[]} />
|
|
19
|
-
<ValidatorField rules={[]} />
|
|
20
|
-
<ValidatorField rules={[]} />
|
|
21
|
-
<ValidatorField rules={[]} />
|
|
16
|
+
<ValidatorField rules={[]} value="" />
|
|
17
|
+
<ValidatorField rules={[]} value="" />
|
|
18
|
+
<ValidatorField rules={[]} value="" />
|
|
19
|
+
<ValidatorField rules={[]} value="" />
|
|
20
|
+
<ValidatorField rules={[]} value="" />
|
|
21
|
+
<ValidatorField rules={[]} value="" />
|
|
22
22
|
</ValidatorWrapper>,
|
|
23
23
|
)
|
|
24
24
|
|
|
@@ -30,8 +30,8 @@ it('check getField validator', () => {
|
|
|
30
30
|
const validator = createRef<ValidatorWrapper>()
|
|
31
31
|
render(
|
|
32
32
|
<ValidatorWrapper ref={validator}>
|
|
33
|
-
<ValidatorField rules={[]} id="test" />
|
|
34
|
-
<ValidatorField rules={[]} id="test-fields" />
|
|
33
|
+
<ValidatorField rules={[]} id="test" value="" />
|
|
34
|
+
<ValidatorField rules={[]} id="test-fields" value="" />
|
|
35
35
|
</ValidatorWrapper>,
|
|
36
36
|
)
|
|
37
37
|
expect(typeof validator.current.getField).toBe('function')
|
|
@@ -48,7 +48,7 @@ it('check getField undefined field', () => {
|
|
|
48
48
|
const validator = createRef<ValidatorWrapper>()
|
|
49
49
|
render(
|
|
50
50
|
<ValidatorWrapper ref={validator}>
|
|
51
|
-
<ValidatorField rules={[]} id="test-empty-field" />
|
|
51
|
+
<ValidatorField rules={[]} id="test-empty-field" value="" />
|
|
52
52
|
</ValidatorWrapper>,
|
|
53
53
|
)
|
|
54
54
|
|
|
@@ -75,7 +75,7 @@ it('check unregisterField, registerField', () => {
|
|
|
75
75
|
const validator = createRef<ValidatorWrapper>()
|
|
76
76
|
render(
|
|
77
77
|
<ValidatorWrapper ref={validator}>
|
|
78
|
-
<ValidatorField rules={[]} id="test-register-field" />
|
|
78
|
+
<ValidatorField rules={[]} id="test-register-field" value="" />
|
|
79
79
|
</ValidatorWrapper>,
|
|
80
80
|
)
|
|
81
81
|
|
|
@@ -87,9 +87,9 @@ it('check filed in field', () => {
|
|
|
87
87
|
const validator = createRef<ValidatorWrapper>()
|
|
88
88
|
render(
|
|
89
89
|
<ValidatorWrapper ref={validator}>
|
|
90
|
-
<ValidatorField rules={[]}>
|
|
91
|
-
<ValidatorField rules={[]} id="check-validate-field-1" />
|
|
92
|
-
<ValidatorField rules={[]} id="check-validate-field-2" />
|
|
90
|
+
<ValidatorField rules={[]} value="">
|
|
91
|
+
<ValidatorField rules={[]} id="check-validate-field-1" value="" />
|
|
92
|
+
<ValidatorField rules={[]} id="check-validate-field-2" value="" />
|
|
93
93
|
</ValidatorField>
|
|
94
94
|
</ValidatorWrapper>,
|
|
95
95
|
)
|
|
@@ -132,3 +132,21 @@ it('check two validators', () => {
|
|
|
132
132
|
expect(validatorFailed.current.validate().isValid).toBe(false)
|
|
133
133
|
expect(validatorSuccess.current.validate().isValid).toBe(true)
|
|
134
134
|
})
|
|
135
|
+
|
|
136
|
+
it('covers registerField duplicate and unregisterField non-existing branches', () => {
|
|
137
|
+
const validator = createRef<ValidatorWrapper>()
|
|
138
|
+
render(
|
|
139
|
+
<ValidatorWrapper ref={validator}>
|
|
140
|
+
<ValidatorField rules={[]} id="dup-field" value="" />
|
|
141
|
+
</ValidatorWrapper>,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
const handle = validator.current.getField('dup-field')
|
|
145
|
+
validator.current.registerField(handle)
|
|
146
|
+
validator.current.unregisterField(handle)
|
|
147
|
+
const dummy = {
|
|
148
|
+
props: { value: '', rules: [], id: 'dummy' },
|
|
149
|
+
validate: () => ({ isValid: true, message: '' }),
|
|
150
|
+
}
|
|
151
|
+
validator.current.unregisterField(dummy)
|
|
152
|
+
})
|
|
@@ -1,75 +1,78 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { forwardRef, type ReactNode, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
|
2
2
|
|
|
3
|
-
import { Context } from './context'
|
|
3
|
+
import { Context, type RegisteredFieldHandle } from './context'
|
|
4
4
|
import type { Validity } from './types'
|
|
5
|
-
import {
|
|
5
|
+
import { Validator } from './validator'
|
|
6
6
|
|
|
7
7
|
interface ComponentProps {
|
|
8
8
|
children?: ReactNode
|
|
9
9
|
stopAtFirstError?: boolean
|
|
10
|
-
ref?: RefObject<ValidatorWrapper>
|
|
11
10
|
}
|
|
12
11
|
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
this.registerField = this.registerField.bind(this)
|
|
22
|
-
this.unregisterField = this.unregisterField.bind(this)
|
|
23
|
-
}
|
|
12
|
+
export interface ValidatorWrapper {
|
|
13
|
+
validate: () => Validity
|
|
14
|
+
getField: (id: string | number) => RegisteredFieldHandle | null
|
|
15
|
+
registerField: (field: RegisteredFieldHandle) => void
|
|
16
|
+
unregisterField: (field: RegisteredFieldHandle) => void
|
|
17
|
+
setCustomError: (customError: Validity) => void
|
|
18
|
+
clearCustomErrors: () => void
|
|
19
|
+
}
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
export const ValidatorWrapper = forwardRef<ValidatorWrapper, ComponentProps>(function ValidatorWrapper(
|
|
22
|
+
{ children, stopAtFirstError },
|
|
23
|
+
ref,
|
|
24
|
+
) {
|
|
25
|
+
const fieldsRef = useRef<RegisteredFieldHandle[]>([])
|
|
26
|
+
const [customErrors, setCustomErrors] = useState<Validity[]>([])
|
|
28
27
|
|
|
29
|
-
registerField(field) {
|
|
30
|
-
if (field && !
|
|
31
|
-
|
|
28
|
+
const registerField = useCallback((field: RegisteredFieldHandle) => {
|
|
29
|
+
if (field && !fieldsRef.current.includes(field)) {
|
|
30
|
+
fieldsRef.current.push(field)
|
|
32
31
|
}
|
|
33
|
-
}
|
|
32
|
+
}, [])
|
|
34
33
|
|
|
35
|
-
unregisterField(field) {
|
|
36
|
-
const index =
|
|
37
|
-
if (index > -1)
|
|
38
|
-
}
|
|
34
|
+
const unregisterField = useCallback((field: RegisteredFieldHandle) => {
|
|
35
|
+
const index = fieldsRef.current.indexOf(field)
|
|
36
|
+
if (index > -1) fieldsRef.current.splice(index, 1)
|
|
37
|
+
}, [])
|
|
39
38
|
|
|
40
|
-
getField(id)
|
|
41
|
-
return
|
|
42
|
-
}
|
|
39
|
+
const getField = useCallback<ValidatorWrapper['getField']>((id) => {
|
|
40
|
+
return fieldsRef.current.find((field) => field?.props?.id === id) || null
|
|
41
|
+
}, [])
|
|
43
42
|
|
|
44
|
-
setCustomError(customError
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
})
|
|
48
|
-
}
|
|
43
|
+
const setCustomError = useCallback<ValidatorWrapper['setCustomError']>((customError) => {
|
|
44
|
+
setCustomErrors((prev) => [...prev, customError])
|
|
45
|
+
}, [])
|
|
49
46
|
|
|
50
|
-
clearCustomErrors() {
|
|
51
|
-
|
|
52
|
-
}
|
|
47
|
+
const clearCustomErrors = useCallback<ValidatorWrapper['clearCustomErrors']>(() => {
|
|
48
|
+
setCustomErrors([])
|
|
49
|
+
}, [])
|
|
53
50
|
|
|
54
|
-
validate()
|
|
55
|
-
const validator = new Validator({ stopAtFirstError
|
|
56
|
-
for (const comp of
|
|
51
|
+
const validate = useCallback<ValidatorWrapper['validate']>(() => {
|
|
52
|
+
const validator = new Validator({ stopAtFirstError })
|
|
53
|
+
for (const comp of fieldsRef.current) {
|
|
57
54
|
validator.addField(comp.props)
|
|
58
55
|
}
|
|
59
56
|
return validator.validate()
|
|
60
|
-
}
|
|
57
|
+
}, [stopAtFirstError])
|
|
61
58
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
59
|
+
useImperativeHandle(
|
|
60
|
+
ref,
|
|
61
|
+
() => ({
|
|
62
|
+
validate,
|
|
63
|
+
getField,
|
|
64
|
+
registerField,
|
|
65
|
+
unregisterField,
|
|
66
|
+
setCustomError,
|
|
67
|
+
clearCustomErrors,
|
|
68
|
+
}),
|
|
69
|
+
[validate, getField, registerField, unregisterField, setCustomError, clearCustomErrors],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const contextValue = useMemo(
|
|
73
|
+
() => ({ customErrors, registerField, unregisterField }),
|
|
74
|
+
[customErrors, registerField, unregisterField],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return <Context.Provider value={contextValue}>{children}</Context.Provider>
|
|
78
|
+
})
|