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/core/src/validator.ts b/packages/core/src/validator.ts index 7b5b8ba..fbcf028 100644 --- a/packages/core/src/validator.ts +++ b/packages/core/src/validator.ts @@ -29,12 +29,20 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor */ let validating = false - const setValidating = (value: boolean) => { + /** + * 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 - listeners.validatingChanged.forEach(callback => callback()) + return listeners.validatingChanged } + + return [] } /** @@ -42,14 +50,22 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor */ let validated: Array = [] - const setValidated = (value: 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)] if (validated.length !== uniqueNames.length || ! uniqueNames.every(name => validated.includes(name))) { validated = uniqueNames - listeners.validatedChanged.forEach(callback => callback()) + return listeners.validatedChanged } + + return [] } /** @@ -62,14 +78,22 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor */ let touched: Array = [] - const setTouched = (value: 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)] if (touched.length !== uniqueNames.length || ! uniqueNames.every(name => touched.includes(name))) { touched = uniqueNames - listeners.touchedChanged.forEach(callback => callback()) + return listeners.touchedChanged } + + return [] } /** @@ -77,22 +101,36 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor */ let errors: ValidationErrors = {} - const setErrors = (value: ValidationErrors|SimpleValidationErrors) => { + /** + * 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) if (! isequal(errors, prepared)) { errors = prepared - listeners.errorsChanged.forEach(callback => callback()) + return listeners.errorsChanged } + + return [] } - const forgetError = (name: string|NamedInputEvent) => { + /** + * 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 } delete newErrors[resolveName(name)] - setErrors(newErrors) + return setErrors(newErrors) } /** @@ -163,19 +201,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(omit({ ...errors }, validate)), + ].forEach(listener => listener()) return config.onPrecognitionSuccess ? config.onPrecognitionSuccess(response) @@ -203,12 +245,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 +282,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 +314,7 @@ export const createValidator = (callback: ValidationCallback, initialData: Recor ? input : [resolveName(input)] - setTouched([...touched, ...inputs]) + setTouched([...touched, ...inputs]).forEach(listener => listener()) return form }, @@ -281,18 +323,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 +346,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']) +}) 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() }) /**