From e6e3322b498f2dc3d32e312937f6ff021e60c40c Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 3 Dec 2023 05:57:25 -0800 Subject: [PATCH] fix: make validateAllFields more stable (#526) * fix: make validateAllFields more stable * test: write test for stable validateAllFields functionality --- packages/form-core/src/FieldApi.ts | 23 +++++++++----------- packages/form-core/src/FormApi.ts | 8 +++---- packages/form-core/src/tests/FormApi.spec.ts | 23 ++++++++++++++++++++ 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index b654c7dae..da46976d7 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -378,7 +378,7 @@ export class FieldApi< } // Needs type cast as eslint errantly believes this is always falsy - let hasError = false as boolean + let hasErrored = false as boolean this.form.store.batch(() => { for (const validateObj of validates) { @@ -393,7 +393,9 @@ export class FieldApi< [getErrorMapKey(validateObj.cause)]: error, }, })) - hasError = true + } + if (error) { + hasErrored = true } } }) @@ -406,7 +408,7 @@ export class FieldApi< if ( this.state.meta.errorMap[submitErrKey] && cause !== 'submit' && - !hasError + !hasErrored ) { this.setMeta((prev) => ({ ...prev, @@ -418,9 +420,11 @@ export class FieldApi< } // If a sync error is encountered for the errorMapKey (eg. onChange), cancel any async validation - if (hasError) { + if (hasErrored) { this.cancelValidateAsync() } + + return { hasErrored } } __leaseValidateAsync = () => { @@ -552,17 +556,10 @@ export class FieldApi< this.form.validate(cause) } catch (_) {} - // Store the previous error for the errorMapKey (eg. onChange, onBlur, onSubmit) - const errorMapKey = getErrorMapKey(cause) - const prevError = this.getMeta().errorMap[errorMapKey] - // Attempt to sync validate first - this.validateSync(value, cause) - - // If there is a new error mapped to the errorMapKey (eg. onChange, onBlur, onSubmit), return the errors array, do not attempt async validation - const newError = this.getMeta().errorMap[errorMapKey] + const { hasErrored } = this.validateSync(value, cause) - if (prevError !== newError) { + if (hasErrored) { if (!this.options.asyncAlways) { return this.state.meta.errors } diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 305f36f7e..7ad14314b 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -256,14 +256,14 @@ export class FormApi { Object.values(this.fieldInfo) as FieldInfo[] ).forEach((field) => { Object.values(field.instances).forEach((instance) => { + // Validate the field + fieldValidationPromises.push( + Promise.resolve().then(() => instance.validate(cause)), + ) // If any fields are not touched if (!instance.state.meta.isTouched) { // Mark them as touched instance.setMeta((prev) => ({ ...prev, isTouched: true })) - // Validate the field - fieldValidationPromises.push( - Promise.resolve().then(() => instance.validate(cause)), - ) } }) }) diff --git a/packages/form-core/src/tests/FormApi.spec.ts b/packages/form-core/src/tests/FormApi.spec.ts index 55e64a40c..1697151c2 100644 --- a/packages/form-core/src/tests/FormApi.spec.ts +++ b/packages/form-core/src/tests/FormApi.spec.ts @@ -816,4 +816,27 @@ describe('form api', () => { form.state.fieldMeta['firstName'].errorMap['onSubmit'], ).toBeUndefined() }) + + it('should validate all fields consistently', async () => { + const form = new FormApi({ + defaultValues: { + firstName: '', + lastName: '', + }, + }) + + const field = new FieldApi({ + form, + name: 'firstName', + onChange: (v) => (v.length > 0 ? undefined : 'first name is required'), + }) + + field.mount() + form.mount() + + await form.validateAllFields('change') + expect(field.getMeta().errorMap.onChange).toEqual('first name is required') + await form.validateAllFields('change') + expect(field.getMeta().errorMap.onChange).toEqual('first name is required') + }) })