diff --git a/src/atoms/list-atom/listAtom.test.ts b/src/atoms/list-atom/listAtom.test.ts index 7398c86..755c65e 100644 --- a/src/atoms/list-atom/listAtom.test.ts +++ b/src/atoms/list-atom/listAtom.test.ts @@ -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 diff --git a/src/atoms/list-atom/listAtom.ts b/src/atoms/list-atom/listAtom.ts index e532e1d..66182d2 100644 --- a/src/atoms/list-atom/listAtom.ts +++ b/src/atoms/list-atom/listAtom.ts @@ -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 { @@ -116,7 +117,41 @@ export function listAtom< const touchedAtom = atomWithReset(false); const dirtyAtom = atom(false); - const errorsAtom = atom([]); + const listErrorsAtom = atom([]); + 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("valid"); const refAtom = atom(null); @@ -215,51 +250,27 @@ export function listAtom< const validateCallback: Validate = 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; }; @@ -293,3 +304,64 @@ export function listAtom< function isPromise(value: any): value is Promise { return typeof value === "object" && typeof value.then === "function"; } + +// TODO: reuse from formAtoms._validateFields +async function validateFormFields( + formAtom: FormAtom, + get: Getter, + set: Setter, + event: ValidateOn, +) { + const form = get(formAtom); + const fields = get(form.fields); + const promises: Promise[] = []; + + 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); + } + + 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); +}