Skip to content

Commit 78cb86c

Browse files
authored
[0.3.x] Ensure marked as valid once response returns (#19)
* Ensure marked as valid once response returns * Update packages * lint * Update handler
1 parent 08be39e commit 78cb86c

File tree

7 files changed

+164
-46
lines changed

7 files changed

+164
-46
lines changed

packages/alpine/src/index.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,13 @@ export default function (Alpine: TAlpine) {
4040
.on('validatingChanged', () => {
4141
form.validating = validator.validating()
4242
})
43-
.on('touchedChanged', () => {
43+
.on('validatedChanged', () => {
4444
state.valid = validator.valid()
45-
45+
})
46+
.on('touchedChanged', () => {
4647
state.touched = validator.touched()
4748
})
4849
.on('errorsChanged', () => {
49-
state.valid = validator.valid()
50-
5150
form.hasErrors = validator.hasErrors()
5251

5352
form.errors = toSimpleValidationErrors(validator.errors())

packages/core/src/client.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,19 +66,27 @@ const request = (userConfig: Config = {}): Promise<unknown> => {
6666

6767
(config.onStart ?? (() => null))()
6868

69-
return axiosClient.request(config).then(response => {
69+
return axiosClient.request(config).then(async response => {
7070
if (config.precognitive) {
7171
validatePrecognitionResponse(response)
7272
}
7373

74-
if (config.precognitive && config.onPrecognitionSuccess && successResolver(response)) {
75-
return config.onPrecognitionSuccess(response)
74+
const status = response.status
75+
76+
let payload: any = response
77+
78+
if (config.precognitive && config.onPrecognitionSuccess && successResolver(payload)) {
79+
payload = await Promise.resolve(config.onPrecognitionSuccess(payload) ?? payload)
80+
}
81+
82+
if (config.onSuccess && isSuccess(status)) {
83+
payload = await Promise.resolve(config.onSuccess(payload) ?? payload)
7684
}
7785

78-
const statusHandler = resolveStatusHandler(config, response.status)
86+
const statusHandler = resolveStatusHandler(config, status)
7987
?? ((response) => response)
8088

81-
return statusHandler(response)
89+
return statusHandler(payload) ?? payload
8290
}, error => {
8391
if (isNotServerGeneratedError(error)) {
8492
return Promise.reject(error)
@@ -117,6 +125,11 @@ const resolveConfig = (config: Config): Config => ({
117125
},
118126
})
119127

128+
/**
129+
* Determine if the status is successful.
130+
*/
131+
const isSuccess = (status: number) => status >= 200 && status < 300
132+
120133
/**
121134
* Abort an existing request with the same configured fingerprint.
122135
*/

packages/core/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export type Config = AxiosRequestConfig&{
1212
fingerprint?: string|null,
1313
onBefore?: () => boolean|undefined,
1414
onStart?: () => void,
15+
onSuccess?: (response: AxiosResponse) => unknown,
1516
onPrecognitionSuccess?: (response: AxiosResponse) => unknown,
1617
onValidationError?: StatusHandler,
1718
onUnauthorized?: StatusHandler,
@@ -64,6 +65,7 @@ export interface ValidatorListeners {
6465
errorsChanged: Array<() => void>,
6566
validatingChanged: Array<() => void>,
6667
touchedChanged: Array<() => void>,
68+
validatedChanged: Array<() => void>,
6769
}
6870

6971
export type RequestMethod = 'get'|'post'|'patch'|'put'|'delete'

packages/core/src/validator.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
1616
errorsChanged: [],
1717
touchedChanged: [],
1818
validatingChanged: [],
19+
validatedChanged: [],
1920
}
2021

2122
/**
@@ -36,6 +37,26 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
3637
}
3738
}
3839

40+
/**
41+
* Inputs that have been validated.
42+
*/
43+
let validated: Array<string> = []
44+
45+
const setValidated = (value: Array<string>) => {
46+
const uniqueNames = [...new Set(value)]
47+
48+
if (validated.length !== uniqueNames.length || ! uniqueNames.every(name => validated.includes(name))) {
49+
validated = uniqueNames
50+
51+
listeners.validatedChanged.forEach(callback => callback())
52+
}
53+
}
54+
55+
/**
56+
* Valid validation state.
57+
*/
58+
const valid = () => validated.filter(name => typeof errors[name] === 'undefined')
59+
3960
/**
4061
* Touched input state.
4162
*/
@@ -79,11 +100,6 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
79100
*/
80101
const hasErrors = () => Object.keys(errors).length > 0
81102

82-
/**
83-
* Valid validation state.
84-
*/
85-
const valid = () => touched.filter(name => typeof errors[name] === 'undefined')
86-
87103
/**
88104
* Debouncing timeout state.
89105
*/
@@ -134,12 +150,17 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor
134150
validate,
135151
timeout: config.timeout ?? 5000,
136152
onValidationError: (response, axiosError) => {
153+
setValidated([...validated, ...validate])
154+
137155
setErrors(merge(omit({ ...errors }, validate), response.data.errors))
138156

139157
return config.onValidationError
140158
? config.onValidationError(response, axiosError)
141159
: Promise.reject(axiosError)
142160
},
161+
onSuccess: () => {
162+
setValidated([...validated, ...validate])
163+
},
143164
onPrecognitionSuccess: (response) => {
144165
setErrors(omit({ ...errors }, validate))
145166

packages/core/tests/validator.test.js

Lines changed: 108 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -203,28 +203,6 @@ it('does not validate if the field has not been changed', async () => {
203203
expect(requestMade).toBe(false)
204204
})
205205

206-
it('is valid after field has changed and successful validation has triggered', async () => {
207-
let requestMade = false
208-
let promise = Promise.resolve(null)
209-
axios.request.mockImplementation(() => {
210-
requestMade = true
211-
return promise = Promise.resolve({
212-
status: 201,
213-
headers: { precognition: 'true' },
214-
data: {},
215-
})
216-
})
217-
const validator = createValidator((client) => client.post('/foo', {}), {
218-
name: 'Tim',
219-
})
220-
221-
validator.validate('name', 'Taylor')
222-
await promise
223-
224-
expect(requestMade).toBe(true)
225-
expect(validator.valid()).toEqual(['name'])
226-
})
227-
228206
it('filters out files', () => {
229207
let config
230208
axios.request.mockImplementationOnce((c) => {
@@ -289,3 +267,111 @@ it('filters out files', () => {
289267
}
290268
})
291269
})
270+
271+
it('doesnt mark fields as validated while response is pending', async () => {
272+
expect.assertions(10)
273+
274+
let resolver = null
275+
let promise = null
276+
let onValidatedChangedCalledTimes = 0
277+
axios.request.mockImplementation(() => {
278+
promise = new Promise(resolve => {
279+
resolver = resolve
280+
})
281+
282+
return promise
283+
})
284+
let data = {}
285+
const validator = createValidator((client) => client.post('/foo', data))
286+
validator.on('validatedChanged', () => onValidatedChangedCalledTimes++)
287+
288+
expect(validator.valid()).toEqual([])
289+
expect(onValidatedChangedCalledTimes).toEqual(0)
290+
291+
data = { app: 'Laravel' }
292+
expect(validator.valid()).toEqual([])
293+
294+
validator.validate('app', 'Laravel')
295+
expect(validator.valid()).toEqual([])
296+
297+
resolver({ headers: { precognition: 'true' }, status: 204 })
298+
await vi.runAllTimersAsync()
299+
expect(validator.valid()).toEqual(['app'])
300+
expect(onValidatedChangedCalledTimes).toEqual(1)
301+
302+
data = { app: 'Laravel', version: '10' }
303+
expect(validator.valid()).toEqual(['app'])
304+
305+
validator.validate('version', '10')
306+
expect(validator.valid()).toEqual(['app'])
307+
308+
axios.isAxiosError.mockReturnValueOnce(true)
309+
resolver({ headers: { precognition: 'true' }, status: 422, data: { errors: {}} })
310+
await vi.runAllTimersAsync()
311+
expect(validator.valid()).toEqual(['app', 'version'])
312+
expect(onValidatedChangedCalledTimes).toEqual(2)
313+
})
314+
315+
it('doesnt mark fields as validated on error status', async () => {
316+
expect.assertions(6)
317+
318+
let resolver = null
319+
let promise = null
320+
let onValidatedChangedCalledTimes = 0
321+
axios.request.mockImplementation(() => {
322+
promise = new Promise(resolve => {
323+
resolver = resolve
324+
})
325+
326+
return promise
327+
})
328+
let data = {}
329+
const validator = createValidator((client) => client.post('/foo', data))
330+
validator.on('validatedChanged', () => onValidatedChangedCalledTimes++)
331+
332+
expect(validator.valid()).toEqual([])
333+
expect(onValidatedChangedCalledTimes).toEqual(0)
334+
335+
data = { app: 'Laravel' }
336+
expect(validator.valid()).toEqual([])
337+
338+
validator.validate('app', 'Laravel')
339+
expect(validator.valid()).toEqual([])
340+
341+
resolver({ headers: { precognition: 'true' }, status: 401 })
342+
await vi.runAllTimersAsync()
343+
expect(validator.valid()).toEqual([])
344+
expect(onValidatedChangedCalledTimes).toEqual(0)
345+
})
346+
347+
it('does mark fields as validated on success status', async () => {
348+
expect.assertions(6)
349+
350+
let resolver = null
351+
let promise = null
352+
let onValidatedChangedCalledTimes = 0
353+
axios.request.mockImplementation(() => {
354+
promise = new Promise(resolve => {
355+
resolver = resolve
356+
})
357+
358+
return promise
359+
})
360+
let data = {}
361+
const validator = createValidator((client) => client.post('/foo', data))
362+
validator.on('validatedChanged', () => onValidatedChangedCalledTimes++)
363+
364+
expect(validator.valid()).toEqual([])
365+
expect(onValidatedChangedCalledTimes).toEqual(0)
366+
367+
data = { app: 'Laravel' }
368+
expect(validator.valid()).toEqual([])
369+
370+
validator.validate('app', 'Laravel')
371+
expect(validator.valid()).toEqual([])
372+
373+
resolver({ headers: { precognition: 'true' }, status: 200 })
374+
await vi.runAllTimersAsync()
375+
expect(validator.valid()).toEqual(['app'])
376+
expect(onValidatedChangedCalledTimes).toEqual(1)
377+
})

packages/react/src/index.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,15 @@ export const useForm = <Data extends Record<string, unknown>>(method: RequestMet
7777
.on('validatingChanged', () => {
7878
setValidating(validator.current!.validating())
7979
})
80+
.on('validatedChanged', () => {
81+
setValid(validator.current!.valid())
82+
})
8083
.on('touchedChanged', () => {
8184
setTouched(validator.current!.touched())
82-
83-
setValid(validator.current!.valid())
8485
})
8586
.on('errorsChanged', () => {
8687
setHasErrors(validator.current!.hasErrors())
8788

88-
setValid(validator.current!.valid())
89-
9089
// @ts-expect-error
9190
setErrors(toSimpleValidationErrors(validator.current!.errors()))
9291
})

packages/vue/src/index.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,17 @@ export const useForm = <Data extends Record<string, unknown>>(method: RequestMet
3737
.on('validatingChanged', () => {
3838
form.validating = validator.validating()
3939
})
40+
.on('validatedChanged', () => {
41+
// @ts-expect-error
42+
valid.value = validator.valid()
43+
})
4044
.on('touchedChanged', () => {
4145
// @ts-expect-error
4246
touched.value = validator.touched()
43-
44-
// @ts-expect-error
45-
valid.value = validator.valid()
4647
})
4748
.on('errorsChanged', () => {
4849
form.hasErrors = validator.hasErrors()
4950

50-
// @ts-expect-error
51-
valid.value = validator.valid()
52-
5351
// @ts-expect-error
5452
form.errors = toSimpleValidationErrors(validator.errors())
5553
})

0 commit comments

Comments
 (0)