From 9cf341f04661ee5d03341e564ee51a7a94da0168 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Mon, 8 Jan 2024 12:00:32 +1100 Subject: [PATCH 1/3] Allow delayed listeners --- packages/core/src/validator.ts | 62 ++++++++++++++++----------- packages/core/tests/validator.test.js | 26 +++++++++++ 2 files changed, 64 insertions(+), 24 deletions(-) diff --git a/packages/core/src/validator.ts b/packages/core/src/validator.ts index 7b5b8ba..593b981 100644 --- a/packages/core/src/validator.ts +++ b/packages/core/src/validator.ts @@ -29,12 +29,14 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor */ let validating = false - const setValidating = (value: boolean) => { + const setValidating = (value: boolean): (() => void)[] => { if (value !== validating) { validating = value - listeners.validatingChanged.forEach(callback => callback()) + return listeners.validatingChanged } + + return [] } /** @@ -42,34 +44,40 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor */ let validated: Array = [] - const setValidated = (value: Array) => { + const setValidated = (value: Array): (() => void)[] => { const uniqueNames = [...new Set(value)] if (validated.length !== uniqueNames.length || ! uniqueNames.every(name => validated.includes(name))) { validated = uniqueNames - listeners.validatedChanged.forEach(callback => callback()) + return listeners.validatedChanged } + + return [] } /** * Valid validation state. */ - const valid = () => validated.filter(name => typeof errors[name] === 'undefined') + const valid = () => { + return validated.filter(name => typeof errors[name] === 'undefined') + } /** * Touched input state. */ let touched: Array = [] - const setTouched = (value: Array) => { + const setTouched = (value: Array): (() => void)[] => { const uniqueNames = [...new Set(value)] if (touched.length !== uniqueNames.length || ! uniqueNames.every(name => touched.includes(name))) { touched = uniqueNames - listeners.touchedChanged.forEach(callback => callback()) + return listeners.touchedChanged } + + return [] } /** @@ -77,22 +85,24 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor */ let errors: ValidationErrors = {} - const setErrors = (value: ValidationErrors|SimpleValidationErrors) => { + const setErrors = (value: ValidationErrors|SimpleValidationErrors): (() => void)[] => { const prepared = toValidationErrors(value) if (! isequal(errors, prepared)) { errors = prepared - listeners.errorsChanged.forEach(callback => callback()) + return listeners.errorsChanged } + + return [] } - const forgetError = (name: string|NamedInputEvent) => { + const forgetError = (name: string|NamedInputEvent): (() => void)[] => { const newErrors = { ...errors } delete newErrors[resolveName(name)] - setErrors(newErrors) + return setErrors(newErrors) } /** @@ -163,19 +173,23 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor validate, timeout: config.timeout ?? 5000, onValidationError: (response, axiosError) => { - setValidated([...validated, ...validate]) - - setErrors(merge(omit({ ...errors }, validate), response.data.errors)) + [ + ...setValidated([...validated, ...validate]), + ...setErrors(merge(omit({ ...errors }, validate), response.data.errors)), + ].forEach(listener => listener()) return config.onValidationError ? config.onValidationError(response, axiosError) : Promise.reject(axiosError) }, onSuccess: () => { - setValidated([...validated, ...validate]) + setValidated([...validated, ...validate]).forEach(listener => listener()) }, onPrecognitionSuccess: (response) => { - setErrors(omit({ ...errors }, validate)) + [ + ...setValidated([...validated, ...validate]), + ...setErrors({}), + ].forEach(listener => listener()) return config.onPrecognitionSuccess ? config.onPrecognitionSuccess(response) @@ -203,12 +217,12 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor return true }, onStart: () => { - setValidating(true); + setValidating(true).forEach(listener => listener()); (config.onStart ?? (() => null))() }, onFinish: () => { - setValidating(false) + setValidating(false).forEach(listener => listener()) oldTouched = validatingTouched! @@ -240,7 +254,7 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor name = resolveName(name) if (get(oldData, name) !== value) { - setTouched([name, ...touched]) + setTouched([name, ...touched]).forEach(listener => listener()) } if (touched.length === 0) { @@ -272,7 +286,7 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor ? input : [resolveName(input)] - setTouched([...touched, ...inputs]) + setTouched([...touched, ...inputs]).forEach(listener => listener()) return form }, @@ -281,18 +295,18 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor errors: () => errors, hasErrors, setErrors(value) { - setErrors(value) + setErrors(value).forEach(listener => listener()) return form }, forgetError(name) { - forgetError(name) + forgetError(name).forEach(listener => listener()) return form }, reset(...names) { if (names.length === 0) { - setTouched([]) + setTouched([]).forEach(listener => listener()) } else { const newTouched = [...touched] @@ -304,7 +318,7 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor set(oldData, name, get(initialData, name)) }) - setTouched(newTouched) + setTouched(newTouched).forEach(listener => listener()) } return form diff --git a/packages/core/tests/validator.test.js b/packages/core/tests/validator.test.js index c7188d9..9b4582f 100644 --- a/packages/core/tests/validator.test.js +++ b/packages/core/tests/validator.test.js @@ -436,3 +436,29 @@ it('can validate without needing to specify a field', async () => { validator.touch(['name', 'framework']).validate() expect(requests).toBe(1) }) + +it('marks fields as valid on precognition success', async () => { + expect.assertions(5) + + let requests = 0 + axios.request.mockImplementation(() => { + requests++ + + return Promise.resolve({ headers: { precognition: 'true', 'precognition-success': 'true' }, status: 204, data: '' }) + }) + const validator = createValidator((client) => client.post('/foo', {})) + let valid = null + validator.setErrors({name: 'Required'}).touch('name').on('errorsChanged', () => { + valid = validator.valid() + }) + + expect(validator.valid()).toStrictEqual([]) + expect(valid).toBeNull() + + validator.validate() + await vi.runAllTimersAsync() + + expect(requests).toBe(1) + expect(validator.valid()).toStrictEqual(['name']) + expect(valid).toStrictEqual(['name']) +}) From 087f253394f30745382801b43f14ab4d6515963c Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Mon, 8 Jan 2024 12:02:21 +1100 Subject: [PATCH 2/3] Update valid state on error change --- packages/alpine/src/index.ts | 2 ++ packages/react/src/index.ts | 2 ++ packages/vue/src/index.ts | 3 +++ 3 files changed, 7 insertions(+) diff --git a/packages/alpine/src/index.ts b/packages/alpine/src/index.ts index 0af038b..bed3d69 100644 --- a/packages/alpine/src/index.ts +++ b/packages/alpine/src/index.ts @@ -47,6 +47,8 @@ export default function (Alpine: TAlpine) { form.hasErrors = validator.hasErrors() form.errors = toSimpleValidationErrors(validator.errors()) + + state.valid = validator.valid() }) /** diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index ab2698f..5ebb731 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -87,6 +87,8 @@ export const useForm = >(method: RequestMet // @ts-expect-error setErrors(toSimpleValidationErrors(validator.current!.errors())) + + setValid(validator.current!.valid()) }) } diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index 447b35a..8e2038f 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -48,6 +48,9 @@ export const useForm = >(method: RequestMet // @ts-expect-error form.errors = toSimpleValidationErrors(validator.errors()) + + // @ts-expect-error + valid.value = validator.valid() }) /** From a82e9c8758decd7c3ac3c106c8ead46cc31ca649 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Mon, 8 Jan 2024 12:49:43 +1100 Subject: [PATCH 3/3] formatting --- packages/core/src/validator.ts | 36 ++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/packages/core/src/validator.ts b/packages/core/src/validator.ts index 593b981..fbcf028 100644 --- a/packages/core/src/validator.ts +++ b/packages/core/src/validator.ts @@ -29,6 +29,12 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor */ let validating = false + /** + * Set the validating inputs. + * + * Returns an array of listeners that should be invoked once all state + * changes have taken place. + */ const setValidating = (value: boolean): (() => void)[] => { if (value !== validating) { validating = value @@ -44,6 +50,12 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor */ let validated: Array = [] + /** + * Set the validated inputs. + * + * Returns an array of listeners that should be invoked once all state + * changes have taken place. + */ const setValidated = (value: Array): (() => void)[] => { const uniqueNames = [...new Set(value)] @@ -59,15 +71,19 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor /** * Valid validation state. */ - const valid = () => { - return validated.filter(name => typeof errors[name] === 'undefined') - } + const valid = () => validated.filter(name => typeof errors[name] === 'undefined') /** * Touched input state. */ let touched: Array = [] + /** + * Set the touched inputs. + * + * Returns an array of listeners that should be invoked once all state + * changes have taken place. + */ const setTouched = (value: Array): (() => void)[] => { const uniqueNames = [...new Set(value)] @@ -85,6 +101,12 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor */ let errors: ValidationErrors = {} + /** + * Set the input errors. + * + * Returns an array of listeners that should be invoked once all state + * changes have taken place. + */ const setErrors = (value: ValidationErrors|SimpleValidationErrors): (() => void)[] => { const prepared = toValidationErrors(value) @@ -97,6 +119,12 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor return [] } + /** + * Forget the given input's errors. + * + * Returns an array of listeners that should be invoked once all state + * changes have taken place. + */ const forgetError = (name: string|NamedInputEvent): (() => void)[] => { const newErrors = { ...errors } @@ -188,7 +216,7 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor onPrecognitionSuccess: (response) => { [ ...setValidated([...validated, ...validate]), - ...setErrors({}), + ...setErrors(omit({ ...errors }, validate)), ].forEach(listener => listener()) return config.onPrecognitionSuccess