1
1
import { useEffect , useMemo , useRef , useState } from 'react' ;
2
- import { addUserDefinedKeywords , getErrors , getInitial , getValue } from './utils' ;
2
+ import {
3
+ addUserDefinedKeywords ,
4
+ getErrors ,
5
+ getFormState ,
6
+ getInitial ,
7
+ getValue ,
8
+ } from './utils' ;
3
9
import { ajv as ajvInternal } from './utils/validation' ;
4
10
5
11
import { ErrorObject , JSONSchemaType , KeywordDefinition , SchemaObject } from 'ajv' ;
6
12
import { useDebounce } from './Hooks/useDebounce' ;
7
- import {
8
- AJVMessageFunction ,
9
- FormField ,
10
- IState ,
11
- UseFormReturn ,
12
- useFormErrors ,
13
- } from './utils/types' ;
13
+ import { AJVMessageFunction , FormField , IState , UseFormReturn } from './utils/types' ;
14
14
import Logger from './utils/Logger' ;
15
15
const useAJVForm = < T extends Record < string , any > > (
16
16
initial : T ,
@@ -26,20 +26,44 @@ const useAJVForm = <T extends Record<string, any>>(
26
26
} ,
27
27
) : UseFormReturn < T > => {
28
28
const ajvInstance = options ?. ajv || ajvInternal ;
29
- const initialStateRef = useRef < IState < T > > ( getInitial ( initial ) ) ;
30
29
31
- const [ state , setState ] = useState < IState < T > > ( getInitial ( initial ) ) ;
30
+ const initialStateRef = useRef < IState < T > > (
31
+ getInitial ( initial , schema ) as IState < T > ,
32
+ ) ;
33
+
34
+ const logger = useMemo (
35
+ ( ) => new Logger ( options ?. debug || false ) ,
36
+ [ options ?. debug ] ,
37
+ ) ;
38
+
39
+ // Precompute field dependencies
40
+ const fieldDependencies = useMemo ( ( ) => {
41
+ const dependencies : Record < string , string [ ] > = { } ;
42
+ if ( schema . allOf ) {
43
+ schema . allOf . forEach ( ( condition : any ) => {
44
+ if ( condition . if ) {
45
+ const parentField = Object . keys ( condition . if . properties || { } ) [ 0 ] ;
46
+ const dependentFields = condition . then ?. required || [ ] ;
47
+ dependencies [ parentField ] = [
48
+ ...( dependencies [ parentField ] || [ ] ) ,
49
+ ...dependentFields ,
50
+ ] ;
51
+ }
52
+ } ) ;
53
+ }
54
+
55
+ logger . log ( 'Precomputed field dependencies:' , dependencies ) ;
56
+ return dependencies ;
57
+ } , [ schema ] ) ;
58
+
59
+ const [ state , setState ] = useState < IState < T > > ( initialStateRef . current ) ;
32
60
33
61
const [ currentField , setCurrentField ] = useState < {
34
62
name : keyof T ;
35
63
editId : number ;
36
64
} | null > ( null ) ;
37
- const [ editCounter , setEditCounter ] = useState ( 0 ) ;
65
+
38
66
const debouncedField = useDebounce ( currentField , options ?. debounceTime || 1000 ) ;
39
- const logger = useMemo (
40
- ( ) => new Logger ( options ?. debug || false ) ,
41
- [ options ?. debug ] ,
42
- ) ;
43
67
44
68
if ( options ?. customKeywords ?. length ) {
45
69
addUserDefinedKeywords ( ajvInstance , options . customKeywords ) ;
@@ -49,13 +73,11 @@ const useAJVForm = <T extends Record<string, any>>(
49
73
50
74
const resetForm = ( ) => {
51
75
logger . log ( 'Form reset to initial state' ) ;
52
-
53
76
setState ( initialStateRef . current ) ;
54
77
} ;
55
78
56
79
const validateField = ( fieldName : keyof T ) => {
57
80
const fieldData = { [ fieldName ] : state [ fieldName ] . value } ;
58
-
59
81
const isValid = AJVValidate ( fieldData ) ;
60
82
const errors = AJVValidate . errors || [ ] ;
61
83
@@ -70,7 +92,6 @@ const useAJVForm = <T extends Record<string, any>>(
70
92
: getErrors ( errors , options ?. userDefinedMessages , logger , schema ) ;
71
93
72
94
const error = fieldErrors [ fieldName as string ] || '' ;
73
-
74
95
setState ( ( prevState ) => ( {
75
96
...prevState ,
76
97
[ fieldName ] : {
@@ -82,174 +103,113 @@ const useAJVForm = <T extends Record<string, any>>(
82
103
83
104
const handleBlur = ( fieldName : keyof T ) => {
84
105
logger . log ( `Field '${ String ( fieldName ) } ' blurred` ) ;
85
-
86
106
validateField ( fieldName ) ;
87
107
} ;
88
108
89
109
const setFormState = ( form : Partial < FormField < T > > ) => {
90
- setState ( ( current ) => {
91
- const newState = { ...current } ;
92
-
93
- Object . keys ( form ) . forEach ( ( key ) => {
94
- const name = key as keyof T ;
95
- newState [ name ] = {
96
- ...newState [ name ] ,
97
- value : getValue ( form [ name ] ) ,
98
- error : newState [ name ] ?. error || '' ,
99
- } ;
100
- } ) ;
101
-
102
- setCurrentField ( {
103
- name : Object . keys ( form ) [ 0 ] as keyof T ,
104
- editId : editCounter ,
105
- } ) ;
106
-
107
- setEditCounter ( editCounter + 1 ) ;
108
-
109
- return newState ;
110
+ setState ( getFormState ( state , form , fieldDependencies , schema ) as IState < T > ) ;
111
+ setCurrentField ( {
112
+ name : Object . keys ( form ) [ 0 ] as keyof T ,
113
+ editId : currentField ?. editId || 0 + 1 ,
110
114
} ) ;
111
115
} ;
112
116
113
- const _setErrors = ( errors : useFormErrors < T > ) => {
114
- return Object . keys ( errors ) . reduce (
115
- ( acc , fieldName ) => {
116
- const key = fieldName as keyof typeof state ;
117
-
118
- return {
119
- ...acc ,
120
- [ fieldName ] : {
121
- value : getValue ( state [ key ] . value ) ,
122
- error : errors [ fieldName ] || '' ,
123
- } ,
124
- } ;
125
- } ,
126
- { ...state } ,
127
- ) ;
128
- } ;
129
-
130
117
const validateForm = ( ) => {
131
- try {
132
- const data = Object . keys ( state ) . reduce ( ( acc , inputName ) => {
133
- return {
134
- ...acc ,
135
- [ inputName ] : getValue ( state [ inputName ] . value ) ,
136
- } ;
137
- } , { } as T ) ;
138
-
139
- logger . log ( 'Validating entire form:' , { data } ) ;
140
-
141
- const isValid = AJVValidate ( data ) ;
142
- if ( ! isValid && AJVValidate . errors ) {
143
- logger . error ( 'Form validation failed with errors:' , AJVValidate . errors ) ;
144
-
145
- const errors : useFormErrors < T > = getErrors (
146
- AJVValidate . errors ,
147
- options ?. userDefinedMessages ,
148
- logger ,
149
- ) ;
150
-
151
- setState ( ( currentState ) =>
152
- Object . keys ( currentState ) . reduce ( ( acc , inputName ) => {
153
- const currentField = currentState [ inputName as keyof T ] ;
154
-
155
- return {
156
- ...acc ,
157
- [ inputName ] : {
158
- ...currentField ,
159
- error : errors [ inputName ] || '' ,
160
- } ,
161
- } ;
162
- } , { } as IState < T > ) ,
163
- ) ;
118
+ const data = Object . keys ( state ) . reduce ( ( acc , inputName ) => {
119
+ acc [ inputName as keyof T ] = getValue ( state [ inputName ] . value ) as T [ keyof T ] ;
120
+ return acc ;
121
+ } , { } as T ) ;
164
122
165
- return { isValid : false , data : null } ;
166
- }
123
+ logger . log ( 'Validating entire form:' , { data } ) ;
124
+ const isValid = AJVValidate ( data ) ;
167
125
168
- setState ( ( currentState ) =>
169
- Object . keys ( currentState ) . reduce ( ( acc , inputName ) => {
170
- const currentField = currentState [ inputName as keyof T ] ;
126
+ if ( ! isValid && AJVValidate . errors ) {
127
+ logger . error ( 'Form validation failed with errors:' , AJVValidate . errors ) ;
171
128
129
+ const errors = getErrors (
130
+ AJVValidate . errors ,
131
+ options ?. userDefinedMessages ,
132
+ logger ,
133
+ ) ;
134
+ setState ( ( prevState ) =>
135
+ Object . keys ( prevState ) . reduce ( ( updatedState , fieldName ) => {
172
136
return {
173
- ...acc ,
174
- [ inputName ] : {
175
- ...currentField ,
176
- error : '' ,
137
+ ...updatedState ,
138
+ [ fieldName ] : {
139
+ ...prevState [ fieldName ] ,
140
+ error : errors [ fieldName ] || '' ,
177
141
} ,
178
142
} ;
179
143
} , { } as IState < T > ) ,
180
144
) ;
181
145
182
- logger . log ( 'Form is valid:' , data ) ;
183
-
184
- return { isValid : true , data } ;
185
- } catch ( error ) {
186
- logger . error ( 'Unexpected error during form validation:' , error ) ;
187
-
188
146
return { isValid : false , data : null } ;
189
147
}
190
- } ;
191
148
192
- const isFormDirty = (
193
- currentState : IState < T > ,
194
- initialState : IState < T > ,
195
- ) : boolean => {
196
- return Object . keys ( currentState ) . some (
197
- ( key ) => currentState [ key ] . value !== initialState [ key ] . value ,
198
- ) ;
199
- } ;
200
-
201
- const isFormValid = ( currentState : IState < T > ) : boolean => {
202
- const hasErrors = Object . keys ( currentState ) . some (
203
- ( key ) => currentState [ key ] . error !== '' ,
149
+ // Clear errors if valid
150
+ setState ( ( prevState ) =>
151
+ Object . keys ( prevState ) . reduce ( ( updatedState , fieldName ) => {
152
+ return {
153
+ ...updatedState ,
154
+ [ fieldName ] : {
155
+ ...prevState [ fieldName ] ,
156
+ error : '' ,
157
+ } ,
158
+ } ;
159
+ } , { } as IState < T > ) ,
204
160
) ;
205
161
206
- return ! hasErrors ;
162
+ logger . log ( 'Form is valid:' , data ) ;
163
+ return { isValid : true , data } ;
207
164
} ;
208
165
209
- const isValid = useMemo ( ( ) => {
210
- return isFormValid ( state ) ;
166
+ const isDirty = useMemo ( ( ) => {
167
+ return Object . keys ( state ) . some (
168
+ ( key ) => state [ key ] . value !== initialStateRef . current [ key ] . value ,
169
+ ) ;
211
170
} , [ state ] ) ;
212
171
213
- const isDirty = useMemo (
214
- ( ) => isFormDirty ( state , initialStateRef . current ) ,
172
+ const isValid = useMemo (
173
+ ( ) => Object . keys ( state ) . every ( ( key ) => ! state [ key ] . error ) ,
215
174
[ state ] ,
216
175
) ;
217
176
218
- const setErrors = ( errors : ErrorObject [ ] ) : void => {
219
- setState (
220
- _setErrors ( getErrors ( errors , options ?. userDefinedMessages , logger , schema ) ) ,
177
+ const setErrors = ( errors : ErrorObject [ ] ) => {
178
+ const newErrors = getErrors (
179
+ errors ,
180
+ options ?. userDefinedMessages ,
181
+ logger ,
182
+ schema ,
183
+ ) ;
184
+ setState ( ( prevState ) =>
185
+ Object . keys ( newErrors ) . reduce ( ( updatedState , fieldName ) => {
186
+ return {
187
+ ...updatedState ,
188
+ [ fieldName ] : {
189
+ ...prevState [ fieldName ] ,
190
+ error : newErrors [ fieldName ] || '' ,
191
+ } ,
192
+ } ;
193
+ } , { } as IState < T > ) ,
221
194
) ;
222
195
} ;
223
196
224
197
useEffect ( ( ) => {
225
198
if (
226
- options ?. shouldDebounceAndValidate === false ||
227
199
! debouncedField ||
228
- ! isDirty
200
+ ! isDirty ||
201
+ options ?. shouldDebounceAndValidate === false
229
202
) {
230
203
return ;
231
204
}
232
205
validateField ( debouncedField . name ) ;
233
206
} , [ debouncedField , isDirty ] ) ;
234
207
235
- useEffect ( ( ) => {
236
- if ( ! options ?. errors ?. length ) {
237
- return ;
238
- }
239
-
240
- setState (
241
- _setErrors (
242
- getErrors ( options ?. errors , options ?. userDefinedMessages , logger , schema ) ,
243
- ) ,
244
- ) ;
245
- } , [ options ?. errors ] ) ;
246
-
247
208
return {
248
209
reset : resetForm ,
249
210
set : setFormState ,
250
211
setErrors,
251
212
validate : validateForm ,
252
- onBlur : handleBlur ,
253
213
isValid,
254
214
isDirty,
255
215
data : Object . keys ( state ) . reduce ( ( acc , fieldName ) => {
@@ -259,6 +219,7 @@ const useAJVForm = <T extends Record<string, any>>(
259
219
} ;
260
220
} , { } as T ) ,
261
221
state,
222
+ onBlur : handleBlur ,
262
223
} ;
263
224
} ;
264
225
0 commit comments