@coxy/react-validator 3.0.0 → 5.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.js +206 -2
- package/example/example.tsx +19 -51
- package/example/index.html +2 -7
- package/example/tsconfig.json +6 -6
- package/package.json +14 -14
- package/src/context.ts +8 -4
- package/src/index.test.ts +19 -4
- package/src/index.ts +4 -3
- package/src/rules.test.ts +16 -106
- package/src/rules.ts +5 -66
- package/src/types.ts +2 -8
- package/src/use-validator.test.tsx +4 -5
- package/src/use-validator.ts +1 -1
- package/src/validator-field.test.tsx +27 -22
- package/src/validator-field.tsx +37 -54
- package/src/validator-wrapper.test.tsx +36 -18
- package/src/validator-wrapper.tsx +45 -58
- package/src/validator.test.tsx +11 -1
- package/src/validator.ts +25 -20
- package/dist/index.d.mts +0 -116
- package/dist/index.d.ts +0 -116
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -2
- package/dist/index.mjs.map +0 -1
- package/src/jest.d.ts +0 -2
- package/tsup.config.ts +0 -14
package/src/rules.ts
CHANGED
|
@@ -1,70 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { z } from 'zod'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
const emailReg =
|
|
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
|
-
|
|
7
|
-
export type ValidatorRules = ValidatorRule[]
|
|
3
|
+
export type ValidatorRules = z.ZodType[] | z.ZodType
|
|
8
4
|
|
|
9
5
|
export const rules = {
|
|
10
|
-
notEmpty: [
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
message: 'Value is required',
|
|
14
|
-
},
|
|
15
|
-
],
|
|
16
|
-
|
|
17
|
-
bool: [
|
|
18
|
-
{
|
|
19
|
-
rule: (value) => !!value,
|
|
20
|
-
message: 'Value is required',
|
|
21
|
-
},
|
|
22
|
-
],
|
|
23
|
-
|
|
24
|
-
password: [
|
|
25
|
-
{
|
|
26
|
-
rule: (value) => value.length > 0,
|
|
27
|
-
message: 'Password field cannot be empty',
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
rule: (value) => value.length > 5,
|
|
31
|
-
message: 'Password field can not be less than 6 characters',
|
|
32
|
-
},
|
|
33
|
-
],
|
|
34
|
-
|
|
35
|
-
email: [
|
|
36
|
-
{
|
|
37
|
-
rule: (value) => !!value && value !== '' && value.length !== 0,
|
|
38
|
-
message: 'Email is required',
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
rule: (value) => emailReg.test(String(value).toLowerCase()),
|
|
42
|
-
message: 'Email is invalid',
|
|
43
|
-
},
|
|
44
|
-
],
|
|
45
|
-
|
|
46
|
-
min: (min) => [
|
|
47
|
-
{
|
|
48
|
-
rule: (value) => Number.parseFloat(value) > min,
|
|
49
|
-
message: `The value must be greater than ${min}`,
|
|
50
|
-
},
|
|
51
|
-
],
|
|
52
|
-
|
|
53
|
-
max: (max) => [
|
|
54
|
-
{
|
|
55
|
-
rule: (value) => Number.parseFloat(value) < max,
|
|
56
|
-
message: `The value must be smaller ${max}`,
|
|
57
|
-
},
|
|
58
|
-
],
|
|
59
|
-
|
|
60
|
-
length: (min, max?) => [
|
|
61
|
-
{
|
|
62
|
-
rule: (value) => String(value).length >= min,
|
|
63
|
-
message: `No less than ${min} symbols`,
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
rule: (value) => (max !== undefined ? String(value).length <= max : true),
|
|
67
|
-
message: `No more than ${max} symbols`,
|
|
68
|
-
},
|
|
69
|
-
],
|
|
6
|
+
notEmpty: [z.string().min(1, { error: 'Field is required' })],
|
|
7
|
+
isTrue: [z.boolean({ error: 'Value is required' }).and(z.literal(true))],
|
|
8
|
+
email: [z.string().min(1, { error: 'Email is required' }), z.email({ message: 'Email is invalid' })],
|
|
70
9
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
|
+
import type { ZodSafeParseResult } from 'zod/index'
|
|
1
2
|
import type { ValidatorRules } from './rules'
|
|
2
3
|
import type { Value } from './validator-field'
|
|
3
4
|
|
|
4
|
-
type Fn = (value: Value) => string
|
|
5
|
-
|
|
6
|
-
export interface ValidatorRule {
|
|
7
|
-
rule: (value: Value) => boolean
|
|
8
|
-
message: string | Fn
|
|
9
|
-
}
|
|
10
|
-
|
|
11
5
|
export interface ErrorMessage {
|
|
12
6
|
message: string
|
|
13
7
|
isValid: boolean
|
|
@@ -16,7 +10,7 @@ export interface ErrorMessage {
|
|
|
16
10
|
export interface Validity {
|
|
17
11
|
message: string
|
|
18
12
|
isValid: boolean
|
|
19
|
-
|
|
13
|
+
result: ZodSafeParseResult<unknown>
|
|
20
14
|
id?: string | number
|
|
21
15
|
}
|
|
22
16
|
|
|
@@ -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'
|
|
@@ -25,14 +24,14 @@ jest.useFakeTimers()
|
|
|
25
24
|
it('check state change and hide field', () => {
|
|
26
25
|
function Comp() {
|
|
27
26
|
const [value, setValue] = useState(false)
|
|
28
|
-
const [isValid, validateObject] = useValidator(value, rules.
|
|
27
|
+
const [isValid, validateObject] = useValidator(value, rules.isTrue)
|
|
29
28
|
|
|
30
29
|
useEffect(() => {
|
|
31
30
|
setTimeout(() => {
|
|
32
31
|
act(() => {
|
|
33
32
|
setValue(true)
|
|
34
33
|
})
|
|
35
|
-
},
|
|
34
|
+
}, 200)
|
|
36
35
|
}, [])
|
|
37
36
|
|
|
38
37
|
return (
|
|
@@ -47,7 +46,7 @@ it('check state change and hide field', () => {
|
|
|
47
46
|
})
|
|
48
47
|
|
|
49
48
|
expect(screen.getByTestId('test1').textContent).toContain('false')
|
|
50
|
-
expect(screen.getByTestId('test2').textContent).toContain('
|
|
49
|
+
expect(screen.getByTestId('test2').textContent).toContain('Invalid input: expected true')
|
|
51
50
|
|
|
52
51
|
jest.runAllTimers()
|
|
53
52
|
|
package/src/use-validator.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Validity } from './types'
|
|
|
3
3
|
import { Validator } from './validator'
|
|
4
4
|
import type { Value } from './validator-field'
|
|
5
5
|
|
|
6
|
-
export function useValidator(value: Value, rules: ValidatorRules): [boolean, Pick<Validity, 'message'
|
|
6
|
+
export function useValidator(value: Value, rules: ValidatorRules): [boolean, Pick<Validity, 'message'>] {
|
|
7
7
|
const validator = new Validator()
|
|
8
8
|
validator.addField({ value, rules })
|
|
9
9
|
const { isValid, ...validateObject } = validator.validate()
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
import { act, render } from '@testing-library/react'
|
|
6
6
|
import { createRef, useEffect, useState } from 'react'
|
|
7
|
-
|
|
8
7
|
import { rules } from './rules'
|
|
9
8
|
import { ValidatorField } from './validator-field'
|
|
10
9
|
import { ValidatorWrapper } from './validator-wrapper'
|
|
@@ -21,7 +20,7 @@ it('check context validator', () => {
|
|
|
21
20
|
const validator = createRef<ValidatorWrapper>()
|
|
22
21
|
render(
|
|
23
22
|
<ValidatorWrapper ref={validator}>
|
|
24
|
-
<ValidatorField rules={[]} />
|
|
23
|
+
<ValidatorField rules={[]} value="" />
|
|
25
24
|
</ValidatorWrapper>,
|
|
26
25
|
)
|
|
27
26
|
|
|
@@ -51,13 +50,11 @@ it('check failed validation', () => {
|
|
|
51
50
|
|
|
52
51
|
expect(validateResult1.isValid).toBe(false)
|
|
53
52
|
expect(validateResult1.message).toBe('Email is invalid')
|
|
54
|
-
expect(validateResult1.errors.length).toBe(1)
|
|
55
53
|
|
|
56
54
|
const validateResult2 = validator2.current.validate()
|
|
57
55
|
|
|
58
56
|
expect(validateResult2.isValid).toBe(false)
|
|
59
57
|
expect(validateResult2.message).toBe('Email is required')
|
|
60
|
-
expect(validateResult2.errors.length).toBe(1)
|
|
61
58
|
})
|
|
62
59
|
})
|
|
63
60
|
|
|
@@ -93,7 +90,6 @@ it('check state change and hide field', () => {
|
|
|
93
90
|
|
|
94
91
|
expect(validateResult1.isValid).toBe(false)
|
|
95
92
|
expect(validateResult1.message).toBe('Email is invalid')
|
|
96
|
-
expect(validateResult1.errors.length).toBe(1)
|
|
97
93
|
})
|
|
98
94
|
|
|
99
95
|
it('check success validation', () => {
|
|
@@ -126,24 +122,33 @@ it('check success validation fot child function', () => {
|
|
|
126
122
|
expect(validateResult.message).toBe('')
|
|
127
123
|
})
|
|
128
124
|
|
|
129
|
-
|
|
125
|
+
jest.useFakeTimers()
|
|
126
|
+
|
|
127
|
+
it('re-renders the same field to cover handleRef initialization false branch and else-validate path', () => {
|
|
130
128
|
const validator = createRef<ValidatorWrapper>()
|
|
131
|
-
const rule = [
|
|
132
|
-
{
|
|
133
|
-
rule: (value) => value !== 'test',
|
|
134
|
-
message: (value) => `test message ${value}`,
|
|
135
|
-
},
|
|
136
|
-
]
|
|
137
|
-
render(
|
|
138
|
-
<ValidatorWrapper ref={validator}>
|
|
139
|
-
<ValidatorField rules={rule} value="test">
|
|
140
|
-
{({ isValid, message }) => <>{!isValid && <div>{message}</div>}</>}
|
|
141
|
-
</ValidatorField>
|
|
142
|
-
</ValidatorWrapper>,
|
|
143
|
-
)
|
|
144
129
|
|
|
145
|
-
|
|
130
|
+
function Comp() {
|
|
131
|
+
const [val, setVal] = useState('')
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
setTimeout(() => {
|
|
134
|
+
act(() => setVal('abc'))
|
|
135
|
+
}, 50)
|
|
136
|
+
}, [])
|
|
137
|
+
return (
|
|
138
|
+
<ValidatorWrapper ref={validator}>
|
|
139
|
+
<ValidatorField id="rerender" rules={rules.notEmpty} value={val}>
|
|
140
|
+
{() => null}
|
|
141
|
+
</ValidatorField>
|
|
142
|
+
</ValidatorWrapper>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
146
145
|
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
render(<Comp />)
|
|
147
|
+
// initial: invalid
|
|
148
|
+
let field = validator.current.getField('rerender')
|
|
149
|
+
expect(field.validate().isValid).toBe(false)
|
|
150
|
+
|
|
151
|
+
jest.runAllTimers()
|
|
152
|
+
field = validator.current.getField('rerender')
|
|
153
|
+
expect(field.validate().isValid).toBe(true)
|
|
149
154
|
})
|
package/src/validator-field.tsx
CHANGED
|
@@ -1,69 +1,52 @@
|
|
|
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 { registerField, unregisterField } = useContext(Context)
|
|
19
|
+
|
|
20
|
+
const propsRef = useRef(props)
|
|
21
|
+
propsRef.current = props
|
|
22
|
+
|
|
23
|
+
const handleRef = useRef<RegisteredFieldHandle | null>(null)
|
|
24
|
+
if (!handleRef.current) {
|
|
25
|
+
handleRef.current = {
|
|
26
|
+
get props() {
|
|
27
|
+
return propsRef.current
|
|
28
|
+
},
|
|
29
|
+
validate: () => {
|
|
30
|
+
const curr = propsRef.current
|
|
31
|
+
const field = new Field({
|
|
32
|
+
rules: curr.rules,
|
|
33
|
+
required: curr.required,
|
|
34
|
+
value: curr.value,
|
|
35
|
+
id: curr.id,
|
|
36
|
+
})
|
|
37
|
+
return field.validate()
|
|
38
|
+
},
|
|
39
|
+
}
|
|
31
40
|
}
|
|
32
41
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return customError
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
registerField(handleRef.current as RegisteredFieldHandle)
|
|
44
|
+
return () => {
|
|
45
|
+
unregisterField(handleRef.current as RegisteredFieldHandle)
|
|
38
46
|
}
|
|
47
|
+
}, [registerField, unregisterField])
|
|
39
48
|
|
|
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
|
-
}
|
|
49
|
+
const validity = handleRef.current.validate()
|
|
55
50
|
|
|
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
|
-
}
|
|
51
|
+
return typeof children === 'function' ? (children as Fn)(validity, value) : (children as ReactNode)
|
|
52
|
+
})
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { render } from '@testing-library/react'
|
|
6
6
|
import { createRef } from 'react'
|
|
7
|
-
|
|
7
|
+
import type { Validity } from 'types'
|
|
8
8
|
import { rules } from './rules'
|
|
9
9
|
import { ValidatorField } from './validator-field'
|
|
10
10
|
import { ValidatorWrapper } from './validator-wrapper'
|
|
@@ -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
|
|
|
@@ -62,20 +62,20 @@ it('check stopAtFirstError validator', () => {
|
|
|
62
62
|
<ValidatorWrapper ref={validator} stopAtFirstError>
|
|
63
63
|
<ValidatorField rules={[]} value="test" />
|
|
64
64
|
<ValidatorField rules={rules.email} value="test" />
|
|
65
|
-
<ValidatorField rules={rules.
|
|
65
|
+
<ValidatorField rules={rules.isTrue} value={false} />
|
|
66
66
|
</ValidatorWrapper>,
|
|
67
67
|
)
|
|
68
|
+
|
|
68
69
|
const fieldValidate = validator.current.validate()
|
|
69
70
|
expect(fieldValidate.isValid).toBe(false)
|
|
70
71
|
expect(fieldValidate.message).toBe('Email is invalid')
|
|
71
|
-
expect(fieldValidate.errors.length).toBe(1)
|
|
72
72
|
})
|
|
73
73
|
|
|
74
74
|
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
|
)
|
|
@@ -107,7 +107,7 @@ it('check wrapper in wrapper', () => {
|
|
|
107
107
|
<ValidatorWrapper ref={validatorOut}>
|
|
108
108
|
<ValidatorField rules={rules.email} value="" />
|
|
109
109
|
<ValidatorWrapper ref={validatorIn}>
|
|
110
|
-
<ValidatorField rules={rules.
|
|
110
|
+
<ValidatorField rules={rules.isTrue} value={true} />
|
|
111
111
|
</ValidatorWrapper>
|
|
112
112
|
</ValidatorWrapper>,
|
|
113
113
|
)
|
|
@@ -121,7 +121,7 @@ it('check two validators', () => {
|
|
|
121
121
|
render(
|
|
122
122
|
<>
|
|
123
123
|
<ValidatorWrapper ref={validatorSuccess}>
|
|
124
|
-
<ValidatorField rules={rules.
|
|
124
|
+
<ValidatorField rules={rules.notEmpty} value="successpasswword" />
|
|
125
125
|
</ValidatorWrapper>
|
|
126
126
|
<ValidatorWrapper ref={validatorFailed}>
|
|
127
127
|
<ValidatorField rules={rules.email} value="" />
|
|
@@ -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: (): Validity => ({ isValid: true, message: '', result: { success: true, data: null } }),
|
|
150
|
+
}
|
|
151
|
+
validator.current.unregisterField(dummy)
|
|
152
|
+
})
|
|
@@ -1,75 +1,62 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { forwardRef, type ReactNode, useCallback, useImperativeHandle, useMemo, useRef } 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
|
-
constructor(props) {
|
|
20
|
-
super(props)
|
|
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
|
+
}
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
export const ValidatorWrapper = forwardRef<ValidatorWrapper, ComponentProps>(function ValidatorWrapper(
|
|
20
|
+
{ children, stopAtFirstError },
|
|
21
|
+
ref,
|
|
22
|
+
) {
|
|
23
|
+
const fieldsRef = useRef<RegisteredFieldHandle[]>([])
|
|
28
24
|
|
|
29
|
-
registerField(field) {
|
|
30
|
-
if (field && !
|
|
31
|
-
|
|
25
|
+
const registerField = useCallback((field: RegisteredFieldHandle) => {
|
|
26
|
+
if (field && !fieldsRef.current.includes(field)) {
|
|
27
|
+
fieldsRef.current.push(field)
|
|
32
28
|
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
unregisterField(field) {
|
|
36
|
-
const index = this.fields.indexOf(field)
|
|
37
|
-
if (index > -1) this.fields.splice(index, 1)
|
|
38
|
-
}
|
|
29
|
+
}, [])
|
|
39
30
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
31
|
+
const unregisterField = useCallback((field: RegisteredFieldHandle) => {
|
|
32
|
+
const index = fieldsRef.current.indexOf(field)
|
|
33
|
+
if (index > -1) fieldsRef.current.splice(index, 1)
|
|
34
|
+
}, [])
|
|
43
35
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
})
|
|
48
|
-
}
|
|
36
|
+
const getField = useCallback<ValidatorWrapper['getField']>((id) => {
|
|
37
|
+
return fieldsRef.current.find((field) => field?.props?.id === id) || null
|
|
38
|
+
}, [])
|
|
49
39
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
validate(): Validity {
|
|
55
|
-
const validator = new Validator({ stopAtFirstError: this.props.stopAtFirstError })
|
|
56
|
-
for (const comp of this.fields) {
|
|
40
|
+
const validate = useCallback<ValidatorWrapper['validate']>(() => {
|
|
41
|
+
const validator = new Validator({ stopAtFirstError })
|
|
42
|
+
for (const comp of fieldsRef.current) {
|
|
57
43
|
validator.addField(comp.props)
|
|
58
44
|
}
|
|
59
45
|
return validator.validate()
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
46
|
+
}, [stopAtFirstError])
|
|
47
|
+
|
|
48
|
+
useImperativeHandle(
|
|
49
|
+
ref,
|
|
50
|
+
() => ({
|
|
51
|
+
validate,
|
|
52
|
+
getField,
|
|
53
|
+
registerField,
|
|
54
|
+
unregisterField,
|
|
55
|
+
}),
|
|
56
|
+
[validate, getField, registerField, unregisterField],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const contextValue = useMemo(() => ({ registerField, unregisterField }), [registerField, unregisterField])
|
|
60
|
+
|
|
61
|
+
return <Context.Provider value={contextValue}>{children}</Context.Provider>
|
|
62
|
+
})
|
package/src/validator.test.tsx
CHANGED
|
@@ -10,7 +10,7 @@ it('check normal create validator', () => {
|
|
|
10
10
|
it('check normal add and remove fields', () => {
|
|
11
11
|
const validator = new Validator({ stopAtFirstError: true })
|
|
12
12
|
const fieldPassword = validator.addField({
|
|
13
|
-
rules: rules.
|
|
13
|
+
rules: rules.email,
|
|
14
14
|
value: '',
|
|
15
15
|
id: 'for-remove',
|
|
16
16
|
})
|
|
@@ -28,3 +28,13 @@ it('check normal add and remove fields', () => {
|
|
|
28
28
|
newFieldSearchPassword = validator.getField('for-remove')
|
|
29
29
|
expect(newFieldSearchPassword === null).toBe(true)
|
|
30
30
|
})
|
|
31
|
+
|
|
32
|
+
it('removeField does nothing when field is not registered', () => {
|
|
33
|
+
const validator = new Validator()
|
|
34
|
+
// create a field-like object that validator doesn't know about
|
|
35
|
+
// import Field from the module is possible, but we can emulate shape using any
|
|
36
|
+
// Should not throw and should not alter state
|
|
37
|
+
// @ts-expect-error.
|
|
38
|
+
expect(() => validator.removeField({})).not.toThrow()
|
|
39
|
+
expect(validator.getField('unknown')).toBe(null)
|
|
40
|
+
})
|