Skip to content

Commit

Permalink
fix(listAtom): combined errors from nested forms & field itself
Browse files Browse the repository at this point in the history
  • Loading branch information
MiroslavPetrik committed Jan 11, 2024
1 parent 044343a commit 9e181f3
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 39 deletions.
2 changes: 1 addition & 1 deletion src/atoms/list-atom/listAtom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ describe("listAtom()", () => {
expect(fieldError.current.error).toBe("There are some errors");
});

it.only("should loose invalidItemError, when the nested item error is fixed", async () => {
it("should lose invalidItemError, when the nested item error is fixed", async () => {
const field = listAtom({
validate: HACK_validate,
value: [undefined], // empty value for a required number will cause error
Expand Down
148 changes: 110 additions & 38 deletions src/atoms/list-atom/listAtom.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import {
FieldAtomConfig,
FormAtom,
FormFields,
Validate,
ValidateOn,
ValidateStatus,
formAtom,
walkFields,
} from "form-atoms";
import { Atom, PrimitiveAtom, WritableAtom, atom } from "jotai";
import { Atom, Getter, PrimitiveAtom, Setter, WritableAtom, atom } from "jotai";
import { RESET, atomWithReset, splitAtom } from "jotai/utils";

import {
Expand Down Expand Up @@ -116,7 +117,41 @@ export function listAtom<

const touchedAtom = atomWithReset(false);
const dirtyAtom = atom(false);
const errorsAtom = atom<string[]>([]);
const listErrorsAtom = atom<string[]>([]);
const itemErrorsAtom = atom((get) => {
// get errors from the nested forms
const hasInvalidForm = get(_formListAtom)
.map((formAtom) => {
const form = get(formAtom);
let invalid = false;

walkFields(get(form.fields), (field) => {
const atoms = get(field);
const errors = get(atoms.errors);

if (errors.length) {
invalid = true;
return false;
}
});

// does not work with async
// state.get(form.validateStatus) === "invalid";
return invalid;
})
.some((invalid) => invalid);

return hasInvalidForm
? [config.invalidItemError ?? "Some list items contain errors."]
: [];
});
const errorsAtom = atom(
(get) => [...get(listErrorsAtom), ...get(itemErrorsAtom)],
(_, set, value: string[]) => {
set(listErrorsAtom, value);
},
);

const validateCountAtom = atom(0);
const validateResultAtom = atom<ValidateStatus>("valid");
const refAtom = atom<HTMLFieldSetElement | null>(null);
Expand Down Expand Up @@ -215,51 +250,27 @@ export function listAtom<

const validateCallback: Validate<Value> = async (state) => {
// run validation for nested forms
state.get(_formListAtom).map((formAtom) => {
const form = state.get(formAtom);
state.set(form.validate, state.event);
});
await Promise.all(
state
.get(_formListAtom)
.map((formAtom) =>
validateFormFields(
formAtom as any,
state.get,
state.set,
state.event,
),
),
);

// validate the listAtom itself
const listValidate = config.validate?.(state);

const listError = isPromise(listValidate)
? await listValidate
: listValidate;

// get errors from the nested forms
const hasInvalidForm = state
.get(_formListAtom)
.map((formAtom) => {
const form = state.get(formAtom);

let invalid = false;

walkFields(state.get(form.fields), (field) => {
const atoms = state.get(field);

const errors = state.get(atoms.errors);

if (errors.length) {
invalid = true;
return false;
}
});

// does not work with async
// state.get(form.validateStatus) === "invalid";
return invalid;
})
.some((invalid) => invalid);

const errors = listError ?? [];

if (hasInvalidForm) {
errors.push(config.invalidItemError ?? "Some list items contain errors.");
}

state.set(errorsAtom, errors);

return errors;
};

Expand Down Expand Up @@ -293,3 +304,64 @@ export function listAtom<
function isPromise(value: any): value is Promise<any> {
return typeof value === "object" && typeof value.then === "function";
}

// TODO: reuse from formAtoms._validateFields
async function validateFormFields(
formAtom: FormAtom<FormFields>,
get: Getter,
set: Setter,
event: ValidateOn,
) {
const form = get(formAtom);
const fields = get(form.fields);
const promises: Promise<boolean>[] = [];

walkFields(fields, (nextField) => {
async function validate(field: typeof nextField) {
const fieldAtom = get(field);
const value = get(fieldAtom.value);
const dirty = get(fieldAtom.dirty);
// This pointer prevents a stale validation result from being
// set after the most recent validation has been performed.
const ptr = get(fieldAtom._validateCount) + 1;
set(fieldAtom._validateCount, ptr);

if (event === "user" || event === "submit") {
set(fieldAtom.touched, true);
}

const maybePromise = fieldAtom._validateCallback?.({
get,
set,
value,
dirty,
touched: get(fieldAtom.touched),
event,
});

let errors: string[];

if (isPromise(maybePromise)) {
set(fieldAtom.validateStatus, "validating");
errors = (await maybePromise) ?? get(fieldAtom.errors);
} else {
errors = maybePromise ?? get(fieldAtom.errors);
}

Check warning on line 349 in src/atoms/list-atom/listAtom.ts

View check run for this annotation

Codecov / codecov/patch

src/atoms/list-atom/listAtom.ts#L348-L349

Added lines #L348 - L349 were not covered by tests

if (ptr === get(fieldAtom._validateCount)) {
set(fieldAtom.errors, errors);
set(fieldAtom.validateStatus, errors.length > 0 ? "invalid" : "valid");
}

if (errors && errors.length) {
return false;
}

return true;
}

promises.push(validate(nextField));
});

await Promise.all(promises);
}

0 comments on commit 9e181f3

Please sign in to comment.