Skip to content

Commit 492ce3c

Browse files
authored
[0.8.x] Fix incorrectly reported valid inputs (#60)
* Allow delayed listeners * Update valid state on error change * formatting
1 parent d891dad commit 492ce3c

File tree

5 files changed

+98
-23
lines changed

5 files changed

+98
-23
lines changed

packages/alpine/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ export default function (Alpine: TAlpine) {
4747
form.hasErrors = validator.hasErrors()
4848

4949
form.errors = toSimpleValidationErrors(validator.errors())
50+
51+
state.valid = validator.valid()
5052
})
5153

5254
/**

packages/core/src/validator.ts

Lines changed: 65 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,27 +29,43 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
2929
*/
3030
let validating = false
3131

32-
const setValidating = (value: boolean) => {
32+
/**
33+
* Set the validating inputs.
34+
*
35+
* Returns an array of listeners that should be invoked once all state
36+
* changes have taken place.
37+
*/
38+
const setValidating = (value: boolean): (() => void)[] => {
3339
if (value !== validating) {
3440
validating = value
3541

36-
listeners.validatingChanged.forEach(callback => callback())
42+
return listeners.validatingChanged
3743
}
44+
45+
return []
3846
}
3947

4048
/**
4149
* Inputs that have been validated.
4250
*/
4351
let validated: Array<string> = []
4452

45-
const setValidated = (value: Array<string>) => {
53+
/**
54+
* Set the validated inputs.
55+
*
56+
* Returns an array of listeners that should be invoked once all state
57+
* changes have taken place.
58+
*/
59+
const setValidated = (value: Array<string>): (() => void)[] => {
4660
const uniqueNames = [...new Set(value)]
4761

4862
if (validated.length !== uniqueNames.length || ! uniqueNames.every(name => validated.includes(name))) {
4963
validated = uniqueNames
5064

51-
listeners.validatedChanged.forEach(callback => callback())
65+
return listeners.validatedChanged
5266
}
67+
68+
return []
5369
}
5470

5571
/**
@@ -62,37 +78,59 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
6278
*/
6379
let touched: Array<string> = []
6480

65-
const setTouched = (value: Array<string>) => {
81+
/**
82+
* Set the touched inputs.
83+
*
84+
* Returns an array of listeners that should be invoked once all state
85+
* changes have taken place.
86+
*/
87+
const setTouched = (value: Array<string>): (() => void)[] => {
6688
const uniqueNames = [...new Set(value)]
6789

6890
if (touched.length !== uniqueNames.length || ! uniqueNames.every(name => touched.includes(name))) {
6991
touched = uniqueNames
7092

71-
listeners.touchedChanged.forEach(callback => callback())
93+
return listeners.touchedChanged
7294
}
95+
96+
return []
7397
}
7498

7599
/**
76100
* Validation errors state.
77101
*/
78102
let errors: ValidationErrors = {}
79103

80-
const setErrors = (value: ValidationErrors|SimpleValidationErrors) => {
104+
/**
105+
* Set the input errors.
106+
*
107+
* Returns an array of listeners that should be invoked once all state
108+
* changes have taken place.
109+
*/
110+
const setErrors = (value: ValidationErrors|SimpleValidationErrors): (() => void)[] => {
81111
const prepared = toValidationErrors(value)
82112

83113
if (! isequal(errors, prepared)) {
84114
errors = prepared
85115

86-
listeners.errorsChanged.forEach(callback => callback())
116+
return listeners.errorsChanged
87117
}
118+
119+
return []
88120
}
89121

90-
const forgetError = (name: string|NamedInputEvent) => {
122+
/**
123+
* Forget the given input's errors.
124+
*
125+
* Returns an array of listeners that should be invoked once all state
126+
* changes have taken place.
127+
*/
128+
const forgetError = (name: string|NamedInputEvent): (() => void)[] => {
91129
const newErrors = { ...errors }
92130

93131
delete newErrors[resolveName(name)]
94132

95-
setErrors(newErrors)
133+
return setErrors(newErrors)
96134
}
97135

98136
/**
@@ -163,19 +201,23 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
163201
validate,
164202
timeout: config.timeout ?? 5000,
165203
onValidationError: (response, axiosError) => {
166-
setValidated([...validated, ...validate])
167-
168-
setErrors(merge(omit({ ...errors }, validate), response.data.errors))
204+
[
205+
...setValidated([...validated, ...validate]),
206+
...setErrors(merge(omit({ ...errors }, validate), response.data.errors)),
207+
].forEach(listener => listener())
169208

170209
return config.onValidationError
171210
? config.onValidationError(response, axiosError)
172211
: Promise.reject(axiosError)
173212
},
174213
onSuccess: () => {
175-
setValidated([...validated, ...validate])
214+
setValidated([...validated, ...validate]).forEach(listener => listener())
176215
},
177216
onPrecognitionSuccess: (response) => {
178-
setErrors(omit({ ...errors }, validate))
217+
[
218+
...setValidated([...validated, ...validate]),
219+
...setErrors(omit({ ...errors }, validate)),
220+
].forEach(listener => listener())
179221

180222
return config.onPrecognitionSuccess
181223
? config.onPrecognitionSuccess(response)
@@ -203,12 +245,12 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
203245
return true
204246
},
205247
onStart: () => {
206-
setValidating(true);
248+
setValidating(true).forEach(listener => listener());
207249

208250
(config.onStart ?? (() => null))()
209251
},
210252
onFinish: () => {
211-
setValidating(false)
253+
setValidating(false).forEach(listener => listener())
212254

213255
oldTouched = validatingTouched!
214256

@@ -240,7 +282,7 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
240282
name = resolveName(name)
241283

242284
if (get(oldData, name) !== value) {
243-
setTouched([name, ...touched])
285+
setTouched([name, ...touched]).forEach(listener => listener())
244286
}
245287

246288
if (touched.length === 0) {
@@ -272,7 +314,7 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
272314
? input
273315
: [resolveName(input)]
274316

275-
setTouched([...touched, ...inputs])
317+
setTouched([...touched, ...inputs]).forEach(listener => listener())
276318

277319
return form
278320
},
@@ -281,18 +323,18 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
281323
errors: () => errors,
282324
hasErrors,
283325
setErrors(value) {
284-
setErrors(value)
326+
setErrors(value).forEach(listener => listener())
285327

286328
return form
287329
},
288330
forgetError(name) {
289-
forgetError(name)
331+
forgetError(name).forEach(listener => listener())
290332

291333
return form
292334
},
293335
reset(...names) {
294336
if (names.length === 0) {
295-
setTouched([])
337+
setTouched([]).forEach(listener => listener())
296338
} else {
297339
const newTouched = [...touched]
298340

@@ -304,7 +346,7 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
304346
set(oldData, name, get(initialData, name))
305347
})
306348

307-
setTouched(newTouched)
349+
setTouched(newTouched).forEach(listener => listener())
308350
}
309351

310352
return form

packages/core/tests/validator.test.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,3 +436,29 @@ it('can validate without needing to specify a field', async () => {
436436
validator.touch(['name', 'framework']).validate()
437437
expect(requests).toBe(1)
438438
})
439+
440+
it('marks fields as valid on precognition success', async () => {
441+
expect.assertions(5)
442+
443+
let requests = 0
444+
axios.request.mockImplementation(() => {
445+
requests++
446+
447+
return Promise.resolve({ headers: { precognition: 'true', 'precognition-success': 'true' }, status: 204, data: '' })
448+
})
449+
const validator = createValidator((client) => client.post('/foo', {}))
450+
let valid = null
451+
validator.setErrors({name: 'Required'}).touch('name').on('errorsChanged', () => {
452+
valid = validator.valid()
453+
})
454+
455+
expect(validator.valid()).toStrictEqual([])
456+
expect(valid).toBeNull()
457+
458+
validator.validate()
459+
await vi.runAllTimersAsync()
460+
461+
expect(requests).toBe(1)
462+
expect(validator.valid()).toStrictEqual(['name'])
463+
expect(valid).toStrictEqual(['name'])
464+
})

packages/react/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ export const useForm = <Data extends Record<string, unknown>>(method: RequestMet
8787

8888
// @ts-expect-error
8989
setErrors(toSimpleValidationErrors(validator.current!.errors()))
90+
91+
setValid(validator.current!.valid())
9092
})
9193
}
9294

packages/vue/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ export const useForm = <Data extends Record<string, unknown>>(method: RequestMet
4848

4949
// @ts-expect-error
5050
form.errors = toSimpleValidationErrors(validator.errors())
51+
52+
// @ts-expect-error
53+
valid.value = validator.valid()
5154
})
5255

5356
/**

0 commit comments

Comments
 (0)