Skip to content

Commit

Permalink
Don't clear async errors between validations (#440)
Browse files Browse the repository at this point in the history
* Don't clear async errors between validations

* Bump bundle size
  • Loading branch information
erikras committed Dec 6, 2021
1 parent 6a28f52 commit 3c3029f
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 8 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,11 @@
"bundlesize": [
{
"path": "dist/final-form.umd.min.js",
"maxSize": "5.5kB"
"maxSize": "5.6kB"
},
{
"path": "dist/final-form.es.js",
"maxSize": "9.8kB"
"maxSize": "9.9kB"
},
{
"path": "dist/final-form.cjs.js",
Expand Down
21 changes: 16 additions & 5 deletions src/FinalForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ function createForm<FormValues: FormValuesShape>(
fieldSubscribers: {},
fields: {},
formState: {
asyncErrors: {},
dirtySinceLastSubmit: false,
modifiedSinceLastSubmit: false,
errors: {},
Expand Down Expand Up @@ -288,15 +289,17 @@ function createForm<FormValues: FormValuesShape>(
: {};

const runRecordLevelValidation = (
setErrors: (errors: Object) => void,
setErrors: (errors: Object, async: boolean) => void,
): Promise<*>[] => {
const promises = [];
if (validate) {
const errorsOrPromise = validate({ ...state.formState.values }); // clone to avoid writing
if (isPromise(errorsOrPromise)) {
promises.push(errorsOrPromise.then(setErrors));
promises.push(
errorsOrPromise.then((errors) => setErrors(errors, true)),
);
} else {
setErrors(errorsOrPromise);
setErrors(errorsOrPromise, false);
}
}
return promises;
Expand Down Expand Up @@ -381,11 +384,16 @@ function createForm<FormValues: FormValuesShape>(
}

let recordLevelErrors: Object = {};
let asyncRecordLevelErrors: Object = {};
const fieldLevelErrors = {};

const promises = [
...runRecordLevelValidation((errors) => {
recordLevelErrors = errors || {};
...runRecordLevelValidation((errors, wasAsync) => {
if (wasAsync) {
asyncRecordLevelErrors = errors || {};
} else {
recordLevelErrors = errors || {};
}
}),
...fieldKeys.reduce(
(result, name) =>
Expand Down Expand Up @@ -413,6 +421,8 @@ function createForm<FormValues: FormValuesShape>(
let merged = {
...(limitedFieldLevelValidation ? formState.errors : {}),
...recordLevelErrors,
...formState.asyncErrors, // previous async errors
...asyncRecordLevelErrors, // new async errors
};
const forEachError = (fn: (name: string, error: any) => void) => {
fieldKeys.forEach((name) => {
Expand Down Expand Up @@ -450,6 +460,7 @@ function createForm<FormValues: FormValuesShape>(
if (!shallowEqual(formState.errors, merged)) {
formState.errors = merged;
}
formState.asyncErrors = asyncRecordLevelErrors;
formState.error = recordLevelErrors[FORM_ERROR];
};

Expand Down
73 changes: 72 additions & 1 deletion src/FinalForm.validating.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ describe("Field.validation", () => {
if (values.username === "erikras") {
errors.username = "Username taken";
}
sleep(delay);
await sleep(delay);
return errors;
},
});
Expand Down Expand Up @@ -404,6 +404,77 @@ describe("Field.validation", () => {
expect(spy).toHaveBeenCalledTimes(3);
});

it.only("should not reset record-level async validation results until they have been replaced", async () => {
const delay = 50;
const form = createForm({
onSubmit: onSubmitMock,
validate: async (values) => {
const errors = {};
if (values.username === "erikras") {
errors.username = "Username taken";
}
if (values.username?.length > 7) {
errors.username = "Too long";
}
await sleep(delay);
return errors;
},
});
const spy = jest.fn();
form.registerField("username", spy, { error: true });
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0].error).toBeUndefined();

const { change } = spy.mock.calls[0][0];

await sleep(delay * 2);

// error hasn't changed
expect(spy).toHaveBeenCalledTimes(1);

change("erikras"); // username taken

await sleep(delay * 2);

// we have an error now
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[1][0].error).toBe("Username taken");

change("erikrasm"); // too long

await sleep(delay / 2);

// should not call spy again until validation has completed
expect(spy).toHaveBeenCalledTimes(2);

// wait for validation to return
await sleep(delay * 2);

// New error!
expect(spy).toHaveBeenCalledTimes(3);
expect(spy.mock.calls[2][0].error).toBe("Too long");

change("okay");

await sleep(delay / 2);

// should not call spy again until validation has completed
expect(spy).toHaveBeenCalledTimes(3);

// wait for validation to return
await sleep(delay * 2);

// spy called because sync validation passed
expect(spy).toHaveBeenCalledTimes(4);
expect(spy.mock.calls[3][0].error).toBeUndefined();

// wait again just for good measure
await sleep(delay * 2);

// spy not called because sync validation already cleared error
expect(spy).toHaveBeenCalledTimes(4);
});

it("should ignore old validation promise results", async () => {
const delay = 10;
const validate = jest.fn((values) => {
Expand Down
1 change: 1 addition & 0 deletions src/types.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export type InternalFieldState = {

export type InternalFormState<FormValues: FormValuesShape> = {
active?: string,
asyncErrors: Object,
dirtySinceLastSubmit: boolean,
modifiedSinceLastSubmit: boolean,
error?: any,
Expand Down

0 comments on commit 3c3029f

Please sign in to comment.