@coxy/react-validator 2.0.7 → 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/README.md +145 -120
- package/biome.json +34 -0
- package/dist/index.d.mts +106 -0
- package/dist/index.d.ts +106 -6
- package/dist/index.js +2 -28
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/example/example.tsx +26 -26
- package/example/index.html +2 -7
- package/example/tsconfig.json +6 -11
- package/package.json +26 -36
- 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 +15 -10
- package/src/rules.ts +15 -15
- package/src/types.ts +3 -2
- package/src/use-validator.test.tsx +4 -7
- package/src/use-validator.ts +3 -3
- package/src/validator-field.test.tsx +45 -18
- package/src/validator-field.tsx +44 -53
- package/src/validator-wrapper.test.tsx +41 -27
- package/src/validator-wrapper.tsx +60 -57
- package/src/validator.test.tsx +10 -0
- package/src/validator.ts +10 -10
- package/tsconfig.json +6 -16
- package/tsup.config.ts +14 -0
- package/.eslintignore +0 -2
- package/.eslintrc.js +0 -5
- package/.prettierrc.js +0 -10
- package/CHANGELOG.md +0 -49
- package/dist/context.d.ts +0 -7
- package/dist/context.js +0 -5
- package/dist/rules.d.ts +0 -32
- package/dist/rules.js +0 -60
- package/dist/types.d.ts +0 -24
- package/dist/types.js +0 -2
- package/dist/use-validator.d.ts +0 -4
- package/dist/use-validator.js +0 -22
- package/dist/validator-field.d.ts +0 -17
- package/dist/validator-field.js +0 -38
- package/dist/validator-wrapper.d.ts +0 -24
- package/dist/validator-wrapper.js +0 -57
- package/dist/validator.d.ts +0 -21
- package/dist/validator.js +0 -72
- package/src/jest.d.ts +0 -1
- package/tsconfig.build.json +0 -12
|
@@ -2,22 +2,19 @@
|
|
|
2
2
|
* @jest-environment jsdom
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { render, screen } from '@testing-library/react'
|
|
7
|
-
import { act } from 'react-dom/test-utils'
|
|
5
|
+
import { act, render, screen } from '@testing-library/react'
|
|
8
6
|
import { useEffect, useState } from 'react'
|
|
9
7
|
|
|
10
8
|
import { rules } from './rules'
|
|
11
9
|
import { useValidator } from './use-validator'
|
|
12
10
|
|
|
13
|
-
let container
|
|
11
|
+
let container: HTMLDivElement | null
|
|
14
12
|
beforeEach(() => {
|
|
15
13
|
container = document.createElement('div')
|
|
16
14
|
document.body.appendChild(container)
|
|
17
15
|
})
|
|
18
16
|
|
|
19
17
|
afterEach(() => {
|
|
20
|
-
unmountComponentAtNode(container)
|
|
21
18
|
container.remove()
|
|
22
19
|
container = null
|
|
23
20
|
})
|
|
@@ -39,8 +36,8 @@ it('check state change and hide field', () => {
|
|
|
39
36
|
|
|
40
37
|
return (
|
|
41
38
|
<>
|
|
42
|
-
<span data-testid=
|
|
43
|
-
<span data-testid=
|
|
39
|
+
<span data-testid="test1">{isValid ? 'true' : 'false'}</span>
|
|
40
|
+
<span data-testid="test2">{validateObject.message || 'true'}</span>
|
|
44
41
|
</>
|
|
45
42
|
)
|
|
46
43
|
}
|
package/src/use-validator.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import type { ValidatorRules } from './rules'
|
|
2
|
+
import type { Validity } from './types'
|
|
1
3
|
import { Validator } from './validator'
|
|
2
|
-
import {
|
|
3
|
-
import { Value } from './validator-field'
|
|
4
|
-
import { Validity } from './types'
|
|
4
|
+
import type { Value } from './validator-field'
|
|
5
5
|
|
|
6
6
|
export function useValidator(value: Value, rules: ValidatorRules): [boolean, Pick<Validity, 'message' | 'errors'>] {
|
|
7
7
|
const validator = new Validator()
|
|
@@ -2,21 +2,17 @@
|
|
|
2
2
|
* @jest-environment jsdom
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { act, render } from '@testing-library/react'
|
|
5
6
|
import { createRef, useEffect, useState } from 'react'
|
|
6
|
-
import { render, act } from '@testing-library/react'
|
|
7
7
|
|
|
8
|
-
import { ValidatorWrapper } from './validator-wrapper'
|
|
9
|
-
import { ValidatorField } from './validator-field'
|
|
10
8
|
import { rules } from './rules'
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
expect(() => shallow(<ValidatorField />)).toThrowError()
|
|
14
|
-
})
|
|
9
|
+
import { ValidatorField } from './validator-field'
|
|
10
|
+
import { ValidatorWrapper } from './validator-wrapper'
|
|
15
11
|
|
|
16
12
|
it('normal render', () => {
|
|
17
13
|
render(
|
|
18
14
|
<ValidatorWrapper>
|
|
19
|
-
<ValidatorField rules={[]} value=
|
|
15
|
+
<ValidatorField rules={[]} value="" />
|
|
20
16
|
</ValidatorWrapper>,
|
|
21
17
|
)
|
|
22
18
|
})
|
|
@@ -25,7 +21,7 @@ it('check context validator', () => {
|
|
|
25
21
|
const validator = createRef<ValidatorWrapper>()
|
|
26
22
|
render(
|
|
27
23
|
<ValidatorWrapper ref={validator}>
|
|
28
|
-
<ValidatorField rules={[]} />
|
|
24
|
+
<ValidatorField rules={[]} value="" />
|
|
29
25
|
</ValidatorWrapper>,
|
|
30
26
|
)
|
|
31
27
|
|
|
@@ -42,10 +38,10 @@ it('check failed validation', () => {
|
|
|
42
38
|
render(
|
|
43
39
|
<>
|
|
44
40
|
<ValidatorWrapper ref={validator1}>
|
|
45
|
-
<ValidatorField rules={rules.email} value=
|
|
41
|
+
<ValidatorField rules={rules.email} value="test" />
|
|
46
42
|
</ValidatorWrapper>
|
|
47
43
|
<ValidatorWrapper ref={validator2}>
|
|
48
|
-
<ValidatorField rules={rules.email} value=
|
|
44
|
+
<ValidatorField rules={rules.email} value="" />
|
|
49
45
|
</ValidatorWrapper>
|
|
50
46
|
</>,
|
|
51
47
|
)
|
|
@@ -83,8 +79,8 @@ it('check state change and hide field', () => {
|
|
|
83
79
|
|
|
84
80
|
return (
|
|
85
81
|
<ValidatorWrapper ref={validator1}>
|
|
86
|
-
<ValidatorField rules={rules.email} value=
|
|
87
|
-
{st && <ValidatorField rules={rules.email} value=
|
|
82
|
+
<ValidatorField rules={rules.email} value="test" />
|
|
83
|
+
{st && <ValidatorField rules={rules.email} value="" />}
|
|
88
84
|
</ValidatorWrapper>
|
|
89
85
|
)
|
|
90
86
|
}
|
|
@@ -104,7 +100,7 @@ it('check success validation', () => {
|
|
|
104
100
|
const validator = createRef<ValidatorWrapper>()
|
|
105
101
|
render(
|
|
106
102
|
<ValidatorWrapper ref={validator}>
|
|
107
|
-
<ValidatorField rules={rules.email} value=
|
|
103
|
+
<ValidatorField rules={rules.email} value="email@email.com" />
|
|
108
104
|
</ValidatorWrapper>,
|
|
109
105
|
)
|
|
110
106
|
|
|
@@ -118,7 +114,7 @@ it('check success validation fot child function', () => {
|
|
|
118
114
|
const validator = createRef<ValidatorWrapper>()
|
|
119
115
|
render(
|
|
120
116
|
<ValidatorWrapper ref={validator}>
|
|
121
|
-
<ValidatorField rules={rules.email} value=
|
|
117
|
+
<ValidatorField rules={rules.email} value="email@email.com">
|
|
122
118
|
{({ isValid, message }) => <>{!isValid && <div>{message}</div>}</>}
|
|
123
119
|
</ValidatorField>
|
|
124
120
|
</ValidatorWrapper>,
|
|
@@ -134,13 +130,13 @@ it('check custom rule message function', () => {
|
|
|
134
130
|
const validator = createRef<ValidatorWrapper>()
|
|
135
131
|
const rule = [
|
|
136
132
|
{
|
|
137
|
-
rule: (value) => value !== 'test',
|
|
138
|
-
message: (value) => `test message ${value}`,
|
|
133
|
+
rule: (value: string) => value !== 'test',
|
|
134
|
+
message: (value: string) => `test message ${value}`,
|
|
139
135
|
},
|
|
140
136
|
]
|
|
141
137
|
render(
|
|
142
138
|
<ValidatorWrapper ref={validator}>
|
|
143
|
-
<ValidatorField rules={rule} value=
|
|
139
|
+
<ValidatorField rules={rule} value="test">
|
|
144
140
|
{({ isValid, message }) => <>{!isValid && <div>{message}</div>}</>}
|
|
145
141
|
</ValidatorField>
|
|
146
142
|
</ValidatorWrapper>,
|
|
@@ -151,3 +147,34 @@ it('check custom rule message function', () => {
|
|
|
151
147
|
expect(validateResult.isValid).toBe(false)
|
|
152
148
|
expect(validateResult.message).toBe('test message test')
|
|
153
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,68 +1,59 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { 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'
|
|
4
|
+
import { Context, type RegisteredFieldHandle } from './context'
|
|
5
5
|
import { Field } from './validator'
|
|
6
|
-
import { ValidatorRules } from './rules'
|
|
7
6
|
|
|
7
|
+
// biome-ignore lint/suspicious/noExplicitAny: <need>
|
|
8
8
|
export type Value = any
|
|
9
9
|
|
|
10
10
|
type Fn = (validity: Validity, value: Value) => ReactNode
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
rules?: ValidatorRules
|
|
14
|
-
required?: boolean
|
|
15
|
-
value?: Value
|
|
16
|
-
id?: string | number
|
|
12
|
+
type Props = FieldParams & {
|
|
17
13
|
children?: ReactNode | Fn
|
|
18
|
-
unregisterField: (val: any) => void
|
|
19
|
-
registerField: (val: any) => void
|
|
20
|
-
customErrors: Array<Validity>
|
|
21
14
|
}
|
|
22
15
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
+
}
|
|
30
47
|
}
|
|
31
48
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return customError
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
registerField(handleRef.current as RegisteredFieldHandle)
|
|
51
|
+
return () => {
|
|
52
|
+
unregisterField(handleRef.current as RegisteredFieldHandle)
|
|
37
53
|
}
|
|
54
|
+
}, [registerField, unregisterField])
|
|
38
55
|
|
|
39
|
-
|
|
40
|
-
rules: props.rules,
|
|
41
|
-
required: props.required,
|
|
42
|
-
value: props.value,
|
|
43
|
-
id: props.id,
|
|
44
|
-
})
|
|
45
|
-
return field.validate()
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
render() {
|
|
49
|
-
const { children, value } = this.props
|
|
50
|
-
const validity = this.validate()
|
|
51
|
-
return typeof children === 'function' ? children(validity, value) : children
|
|
52
|
-
}
|
|
53
|
-
}
|
|
56
|
+
const validity = handleRef.current.validate()
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
<Context.Consumer>
|
|
58
|
-
{(data) => (
|
|
59
|
-
<ValidationFieldWrapper
|
|
60
|
-
{...props}
|
|
61
|
-
customErrors={data?.customErrors}
|
|
62
|
-
registerField={data?.registerField}
|
|
63
|
-
unregisterField={data?.unregisterField}
|
|
64
|
-
/>
|
|
65
|
-
)}
|
|
66
|
-
</Context.Consumer>
|
|
67
|
-
)
|
|
68
|
-
}
|
|
58
|
+
return typeof children === 'function' ? (children as Fn)(validity, value) : (children as ReactNode)
|
|
59
|
+
})
|
|
@@ -2,27 +2,23 @@
|
|
|
2
2
|
* @jest-environment jsdom
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { createRef } from 'react'
|
|
6
5
|
import { render } from '@testing-library/react'
|
|
6
|
+
import { createRef } from 'react'
|
|
7
7
|
|
|
8
|
-
import { ValidatorWrapper } from './validator-wrapper'
|
|
9
|
-
import { ValidatorField } from './validator-field'
|
|
10
8
|
import { rules } from './rules'
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
expect(() => shallow(<ValidatorWrapper />)).toThrowError()
|
|
14
|
-
})
|
|
9
|
+
import { ValidatorField } from './validator-field'
|
|
10
|
+
import { ValidatorWrapper } from './validator-wrapper'
|
|
15
11
|
|
|
16
12
|
it('check wrapper validator', () => {
|
|
17
13
|
const validator = createRef<ValidatorWrapper>()
|
|
18
14
|
render(
|
|
19
15
|
<ValidatorWrapper ref={validator}>
|
|
20
|
-
<ValidatorField rules={[]} />
|
|
21
|
-
<ValidatorField rules={[]} />
|
|
22
|
-
<ValidatorField rules={[]} />
|
|
23
|
-
<ValidatorField rules={[]} />
|
|
24
|
-
<ValidatorField rules={[]} />
|
|
25
|
-
<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="" />
|
|
26
22
|
</ValidatorWrapper>,
|
|
27
23
|
)
|
|
28
24
|
|
|
@@ -34,8 +30,8 @@ it('check getField validator', () => {
|
|
|
34
30
|
const validator = createRef<ValidatorWrapper>()
|
|
35
31
|
render(
|
|
36
32
|
<ValidatorWrapper ref={validator}>
|
|
37
|
-
<ValidatorField rules={[]} id=
|
|
38
|
-
<ValidatorField rules={[]} id=
|
|
33
|
+
<ValidatorField rules={[]} id="test" value="" />
|
|
34
|
+
<ValidatorField rules={[]} id="test-fields" value="" />
|
|
39
35
|
</ValidatorWrapper>,
|
|
40
36
|
)
|
|
41
37
|
expect(typeof validator.current.getField).toBe('function')
|
|
@@ -52,7 +48,7 @@ it('check getField undefined field', () => {
|
|
|
52
48
|
const validator = createRef<ValidatorWrapper>()
|
|
53
49
|
render(
|
|
54
50
|
<ValidatorWrapper ref={validator}>
|
|
55
|
-
<ValidatorField rules={[]} id=
|
|
51
|
+
<ValidatorField rules={[]} id="test-empty-field" value="" />
|
|
56
52
|
</ValidatorWrapper>,
|
|
57
53
|
)
|
|
58
54
|
|
|
@@ -64,9 +60,9 @@ it('check stopAtFirstError validator', () => {
|
|
|
64
60
|
const validator = createRef<ValidatorWrapper>()
|
|
65
61
|
render(
|
|
66
62
|
<ValidatorWrapper ref={validator} stopAtFirstError>
|
|
67
|
-
<ValidatorField rules={[]} value=
|
|
68
|
-
<ValidatorField rules={rules.email} value=
|
|
69
|
-
<ValidatorField rules={rules.password} value=
|
|
63
|
+
<ValidatorField rules={[]} value="test" />
|
|
64
|
+
<ValidatorField rules={rules.email} value="test" />
|
|
65
|
+
<ValidatorField rules={rules.password} value="" />
|
|
70
66
|
</ValidatorWrapper>,
|
|
71
67
|
)
|
|
72
68
|
const fieldValidate = validator.current.validate()
|
|
@@ -79,7 +75,7 @@ it('check unregisterField, registerField', () => {
|
|
|
79
75
|
const validator = createRef<ValidatorWrapper>()
|
|
80
76
|
render(
|
|
81
77
|
<ValidatorWrapper ref={validator}>
|
|
82
|
-
<ValidatorField rules={[]} id=
|
|
78
|
+
<ValidatorField rules={[]} id="test-register-field" value="" />
|
|
83
79
|
</ValidatorWrapper>,
|
|
84
80
|
)
|
|
85
81
|
|
|
@@ -91,9 +87,9 @@ it('check filed in field', () => {
|
|
|
91
87
|
const validator = createRef<ValidatorWrapper>()
|
|
92
88
|
render(
|
|
93
89
|
<ValidatorWrapper ref={validator}>
|
|
94
|
-
<ValidatorField rules={[]}>
|
|
95
|
-
<ValidatorField rules={[]} id=
|
|
96
|
-
<ValidatorField rules={[]} id=
|
|
90
|
+
<ValidatorField rules={[]} value="">
|
|
91
|
+
<ValidatorField rules={[]} id="check-validate-field-1" value="" />
|
|
92
|
+
<ValidatorField rules={[]} id="check-validate-field-2" value="" />
|
|
97
93
|
</ValidatorField>
|
|
98
94
|
</ValidatorWrapper>,
|
|
99
95
|
)
|
|
@@ -109,9 +105,9 @@ it('check wrapper in wrapper', () => {
|
|
|
109
105
|
const validatorIn = createRef<ValidatorWrapper>()
|
|
110
106
|
render(
|
|
111
107
|
<ValidatorWrapper ref={validatorOut}>
|
|
112
|
-
<ValidatorField rules={rules.email} value=
|
|
108
|
+
<ValidatorField rules={rules.email} value="" />
|
|
113
109
|
<ValidatorWrapper ref={validatorIn}>
|
|
114
|
-
<ValidatorField rules={rules.password} value=
|
|
110
|
+
<ValidatorField rules={rules.password} value="successpasswword" />
|
|
115
111
|
</ValidatorWrapper>
|
|
116
112
|
</ValidatorWrapper>,
|
|
117
113
|
)
|
|
@@ -125,10 +121,10 @@ it('check two validators', () => {
|
|
|
125
121
|
render(
|
|
126
122
|
<>
|
|
127
123
|
<ValidatorWrapper ref={validatorSuccess}>
|
|
128
|
-
<ValidatorField rules={rules.password} value=
|
|
124
|
+
<ValidatorField rules={rules.password} value="successpasswword" />
|
|
129
125
|
</ValidatorWrapper>
|
|
130
126
|
<ValidatorWrapper ref={validatorFailed}>
|
|
131
|
-
<ValidatorField rules={rules.email} value=
|
|
127
|
+
<ValidatorField rules={rules.email} value="" />
|
|
132
128
|
</ValidatorWrapper>
|
|
133
129
|
</>,
|
|
134
130
|
)
|
|
@@ -136,3 +132,21 @@ it('check two validators', () => {
|
|
|
136
132
|
expect(validatorFailed.current.validate().isValid).toBe(false)
|
|
137
133
|
expect(validatorSuccess.current.validate().isValid).toBe(true)
|
|
138
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 {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { Context, type RegisteredFieldHandle } from './context'
|
|
4
|
+
import type { Validity } from './types'
|
|
5
|
+
import { Validator } from './validator'
|
|
6
6
|
|
|
7
7
|
interface ComponentProps {
|
|
8
8
|
children?: ReactNode
|
|
9
9
|
stopAtFirstError?: boolean
|
|
10
|
-
ref?: RefObject<any>
|
|
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
|
-
|
|
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
|
+
})
|
package/src/validator.test.tsx
CHANGED
|
@@ -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
|
+
})
|
package/src/validator.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import type { ValidatorRules } from './rules'
|
|
2
|
+
import type { FieldParams, Validity } from './types'
|
|
3
|
+
import type { Value } from './validator-field'
|
|
4
4
|
|
|
5
5
|
export class Field {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
protected rules: ValidatorRules
|
|
7
|
+
protected required: boolean
|
|
8
|
+
protected value: Value
|
|
9
9
|
public id: string | number
|
|
10
10
|
|
|
11
11
|
constructor({ rules, required, value, id }: FieldParams) {
|
|
@@ -20,12 +20,12 @@ export class Field {
|
|
|
20
20
|
let message = ''
|
|
21
21
|
const { rules, value, required, id } = this
|
|
22
22
|
|
|
23
|
-
const isEmptyValue = !value && parseFloat(value) !== 0
|
|
23
|
+
const isEmptyValue = !value && Number.parseFloat(value) !== 0
|
|
24
24
|
|
|
25
25
|
if (!rules.length || (isEmptyValue && required === false)) {
|
|
26
26
|
return { isValid, message, id }
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
for (const instance of rules) {
|
|
29
29
|
if (isValid) {
|
|
30
30
|
isValid = instance.rule(value)
|
|
31
31
|
if (!isValid) {
|
|
@@ -36,7 +36,7 @@ export class Field {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
}
|
|
39
|
+
}
|
|
40
40
|
return { isValid, message, id }
|
|
41
41
|
}
|
|
42
42
|
}
|
|
@@ -70,7 +70,7 @@ export class Validator {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
validate(): Validity {
|
|
73
|
-
let prevResult
|
|
73
|
+
let prevResult: Validity | null
|
|
74
74
|
const statuses = this.fields.map((field) => {
|
|
75
75
|
if (this.params?.stopAtFirstError && prevResult && prevResult.isValid === false) {
|
|
76
76
|
return null
|
package/tsconfig.json
CHANGED
|
@@ -6,21 +6,17 @@
|
|
|
6
6
|
"removeComments": true,
|
|
7
7
|
"emitDecoratorMetadata": true,
|
|
8
8
|
"experimentalDecorators": true,
|
|
9
|
-
"target": "
|
|
9
|
+
"target": "ESNext",
|
|
10
10
|
"sourceMap": false,
|
|
11
|
-
"composite":
|
|
12
|
-
"incremental":
|
|
11
|
+
"composite": false,
|
|
12
|
+
"incremental": false,
|
|
13
13
|
"noUnusedLocals": true,
|
|
14
14
|
"noUnusedParameters": false,
|
|
15
15
|
"resolveJsonModule": true,
|
|
16
16
|
"outDir": "./dist",
|
|
17
17
|
"rootDir": "src",
|
|
18
18
|
"baseUrl": "./src",
|
|
19
|
-
"lib": [
|
|
20
|
-
"dom",
|
|
21
|
-
"dom.iterable",
|
|
22
|
-
"esnext"
|
|
23
|
-
],
|
|
19
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
24
20
|
"allowJs": true,
|
|
25
21
|
"skipLibCheck": true,
|
|
26
22
|
"strict": false,
|
|
@@ -28,12 +24,6 @@
|
|
|
28
24
|
"moduleResolution": "node",
|
|
29
25
|
"jsx": "react-jsx"
|
|
30
26
|
},
|
|
31
|
-
"include": [
|
|
32
|
-
|
|
33
|
-
"src/**/*"
|
|
34
|
-
],
|
|
35
|
-
"exclude": [
|
|
36
|
-
"node_modules",
|
|
37
|
-
"dist"
|
|
38
|
-
]
|
|
27
|
+
"include": ["src", "src/**/*"],
|
|
28
|
+
"exclude": ["node_modules", "dist"]
|
|
39
29
|
}
|