Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

68 the validateatom of listatom should listen to the validation of inner fields #69

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import tsconfigPaths from "vite-tsconfig-paths";

export default {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
staticDirs: ["../public"],
Expand All @@ -16,11 +14,10 @@ export default {
async viteFinal(config) {
return {
...config,
plugins: [...(config.plugins || []), tsconfigPaths()],
define: {
...(config.define || {}),
'process.env': {}
}
"process.env": {},
},
};
},
};
11 changes: 5 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"build:storybook": "storybook build",
"test": "vitest",
"test:cov": "vitest --coverage",
"typecheck": "vitest typecheck",
"typecheck": "vitest --typecheck.only",
"storybook": "storybook dev",
"sb": "yarn storybook",
"docs": "storybook dev --docs",
Expand Down Expand Up @@ -76,8 +76,8 @@
"@types/semantic-release": "^20.0.6",
"@typescript-eslint/eslint-plugin": "6.13.1",
"@typescript-eslint/parser": "6.13.1",
"@vitejs/plugin-react": "^4.2.0",
"@vitest/coverage-v8": "^0.34.6",
"@vitejs/plugin-react": "^4.2.1",
"@vitest/coverage-v8": "^1.1.3",
"eslint": "8.54.0",
"eslint-config-prettier": "9.0.0",
"eslint-import-resolver-typescript": "^3.6.1",
Expand All @@ -95,9 +95,8 @@
"semantic-release": "^22.0.8",
"storybook": "7.6.6",
"typescript": "5.3.2",
"vite": "^5.0.4",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "^0.34.6",
"vite": "^5.0.11",
"vitest": "^1.1.3",
"zod": "3.22.4"
},
"dependencies": {
Expand Down
33 changes: 33 additions & 0 deletions src/atoms/list-atom/listAtom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,5 +170,38 @@ describe("listAtom()", () => {

expect(fieldError.current.error).toBe("There are some errors");
});

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
builder: (value) => numberField({ value }),
});

const form = formAtom({ field });

const { result: submit } = renderHook(() => useFormSubmit(form));
const { result: fieldError } = renderHook(() => useFieldError(field));
const { result: formFields } = renderHook(() =>
useAtomValue(useAtomValue(field)._formFields),
);

const { result: inputActions } = renderHook(() =>
useFieldActions(formFields.current[0]!),
);

expect(fieldError.current.error).toBe(undefined);

await act(async () => submit.current(vi.fn())());
await act(
() => new Promise<void>((resolve) => setTimeout(() => resolve(), 0)),
);

expect(fieldError.current.error).toBe("Some list items contain errors.");

await act(async () => inputActions.current.setValue(5));

expect(fieldError.current.error).toBe(undefined);
});
});
});
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 @@

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 @@ -152,8 +187,8 @@
formBuilder(value).map((fields) => formAtom({ fields })),
);
} else {
throw Error("Writing unsupported value to listFieldAtom value!");
}

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

View check run for this annotation

Codecov / codecov/patch

src/atoms/list-atom/listAtom.ts#L190-L191

Added lines #L190 - L191 were not covered by tests
},
);

Expand Down Expand Up @@ -181,8 +216,8 @@
const value = get(valueAtom);

if (event === "user" || event === "submit") {
set(touchedAtom, true);
}

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

View check run for this annotation

Codecov / codecov/patch

src/atoms/list-atom/listAtom.ts#L219-L220

Added lines #L219 - L220 were not covered by tests

let errors: string[] = [];

Expand Down Expand Up @@ -215,51 +250,27 @@

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 @@
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);
}
6 changes: 4 additions & 2 deletions vitest.config.ts → vitest.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ export default defineConfig({
setupFiles: ["./setup.ts"],
coverage: {
provider: "v8",
include: ["src/@(atoms|components|fields|hooks)"],
exclude: ["**/*.@(mock|stories|test-d).@(ts|tsx)"],
},
},
define: {
'process.env': {}
}
"process.env": {},
},
});
Loading
Loading