Skip to content

Commit

Permalink
fix: make validateAllFields more stable (#526)
Browse files Browse the repository at this point in the history
* fix: make validateAllFields more stable

* test: write test for stable validateAllFields functionality
  • Loading branch information
crutchcorn authored Dec 3, 2023
1 parent 10eb5b8 commit e6e3322
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 17 deletions.
23 changes: 10 additions & 13 deletions packages/form-core/src/FieldApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -393,7 +393,9 @@ export class FieldApi<
[getErrorMapKey(validateObj.cause)]: error,
},
}))
hasError = true
}
if (error) {
hasErrored = true
}
}
})
Expand All @@ -406,7 +408,7 @@ export class FieldApi<
if (
this.state.meta.errorMap[submitErrKey] &&
cause !== 'submit' &&
!hasError
!hasErrored
) {
this.setMeta((prev) => ({
...prev,
Expand All @@ -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 = () => {
Expand Down Expand Up @@ -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
}
Expand Down
8 changes: 4 additions & 4 deletions packages/form-core/src/FormApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,14 +256,14 @@ export class FormApi<TFormData, ValidatorType> {
Object.values(this.fieldInfo) as FieldInfo<any, ValidatorType>[]
).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)),
)
}
})
})
Expand Down
23 changes: 23 additions & 0 deletions packages/form-core/src/tests/FormApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
})

0 comments on commit e6e3322

Please sign in to comment.