Skip to content

Commit

Permalink
feat(core): use formts validation and submit handling (#18)
Browse files Browse the repository at this point in the history
* feat(core): useFormts validation handling p1

- store errors state as dictionary
- expose getter/setter for field errors
- expose list of all form errors
- expose computed isValid property for fields and form

* feat(core): useFormts validation handling p2

call validator when:
- field value is set
- field is touched
- field validate method is called
- form validate method is called

* feat(core): useFormts submit handling

* feat(core): simplify form.reset() and add tests

* feat(core): useFormts basic async validation support

Co-authored-by: Mikołaj Klaman <mklaman@virtuslab.com>
  • Loading branch information
mixvar and Mikołaj Klaman authored Oct 19, 2020
1 parent 6a3059b commit f501e71
Show file tree
Hide file tree
Showing 9 changed files with 790 additions and 63 deletions.
62 changes: 51 additions & 11 deletions src/core/hooks/use-formts/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,30 @@ import { createInitialValues } from "./create-initial-values";
import { makeTouchedValues, makeUntouchedValues } from "./make-touched-values";
import { FormtsOptions } from "./use-formts";

export type FormtsAction<Values> =
export type FormtsAction<Values, Err> =
| { type: "reset"; payload: { values: Values } }
| { type: "touchValue"; payload: { path: string } }
| { type: "setValue"; payload: { path: string; value: any } };
| { type: "setValue"; payload: { path: string; value: any } }
| { type: "setErrors"; payload: Array<{ path: string; error: Err | null }> }
| { type: "setIsValidating"; payload: { isValidating: boolean } }
| { type: "setIsSubmitting"; payload: { isSubmitting: boolean } };

export const createReducer = <Values extends object>(): Reducer<
FormtsState<Values>,
FormtsAction<Values>
export const createReducer = <Values extends object, Err>(): Reducer<
FormtsState<Values, Err>,
FormtsAction<Values, Err>
> => (state, action) => {
switch (action.type) {
case "reset": {
const { values } = action.payload;

const touched = makeUntouchedValues(values);

return { values, touched };
return {
values,
touched,
errors: {},
isValidating: false,
isSubmitting: false,
};
}

case "touchValue": {
Expand All @@ -40,16 +48,48 @@ export const createReducer = <Values extends object>(): Reducer<
const values = set(state.values, path, value);
const touched = set(state.touched, path, makeTouchedValues(value));

return { values, touched };
return { ...state, values, touched };
}

case "setErrors": {
const errors = action.payload.reduce(
(dict, { path, error }) => {
if (error != null) {
dict[path] = error;
} else {
delete dict[path];
}
return dict;
},
{ ...state.errors }
);

return { ...state, errors };
}

case "setIsValidating": {
const { isValidating } = action.payload;
return { ...state, isValidating };
}
case "setIsSubmitting": {
const { isSubmitting } = action.payload;
return { ...state, isSubmitting };
}
}
};

export const getInitialState = <Values extends object>({
export const getInitialState = <Values extends object, Err>({
Schema,
initialValues,
}: FormtsOptions<Values, any>): FormtsState<Values> => {
}: FormtsOptions<Values, any>): FormtsState<Values, Err> => {
const values = createInitialValues(Schema, initialValues);
const touched = makeUntouchedValues(values);
return { values, touched };

return {
values,
touched,
errors: {},
isValidating: false,
isSubmitting: false,
};
};
69 changes: 69 additions & 0 deletions src/core/hooks/use-formts/resolve-is-valid.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { FieldErrors } from "../../types/formts-state";

import { resolveIsValid } from "./resolve-is-valid";

describe("resolveIsValid", () => {
it("handles primitive field errors", () => {
const errors: FieldErrors<string> = {
foo: "error!",
bar: undefined,
};

expect(resolveIsValid(errors, "foo")).toBe(false);
expect(resolveIsValid(errors, "bar")).toBe(true);
expect(resolveIsValid(errors, "baz")).toBe(true);
});

it("handles root array field errors", () => {
const errors: FieldErrors<string> = {
array: "error!",
};

expect(resolveIsValid(errors, "array")).toBe(false);
expect(resolveIsValid(errors, "array[42]")).toBe(true);
});

it("handles array item field errors", () => {
const errors: FieldErrors<string> = {
"array[0]": "error!",
};

expect(resolveIsValid(errors, "array")).toBe(false);
expect(resolveIsValid(errors, "array[0]")).toBe(false);
expect(resolveIsValid(errors, "array[42]")).toBe(true);
});

it("handles root object field errors", () => {
const errors: FieldErrors<string> = {
object: "error!",
};

expect(resolveIsValid(errors, "object")).toBe(false);
expect(resolveIsValid(errors, "object.prop")).toBe(true);
});

it("handles object property field errors", () => {
const errors: FieldErrors<string> = {
"object.prop": "error!",
};

expect(resolveIsValid(errors, "object")).toBe(false);
expect(resolveIsValid(errors, "object.prop")).toBe(false);
expect(resolveIsValid(errors, "object.otherProp")).toBe(true);
});

it("handles nested object and array fields", () => {
const errors: FieldErrors<string> = {
"nested.nestedArr[42].foo": "error!",
};

expect(resolveIsValid(errors, "nested")).toBe(false);
expect(resolveIsValid(errors, "nested.nestedArr")).toBe(false);
expect(resolveIsValid(errors, "nested.nestedArr[42]")).toBe(false);
expect(resolveIsValid(errors, "nested.nestedArr[42].foo")).toBe(false);

expect(resolveIsValid(errors, "nested.otherProp")).toBe(true);
expect(resolveIsValid(errors, "nested.nestedArr[43]")).toBe(true);
expect(resolveIsValid(errors, "nested.nestedArr[42].otherProp")).toBe(true);
});
});
19 changes: 19 additions & 0 deletions src/core/hooks/use-formts/resolve-is-valid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { entries } from "../../../utils";
import { FieldErrors } from "../../types/formts-state";

export const resolveIsValid = <Err>(
errors: FieldErrors<Err>,
path: string
): boolean => {
if (errors[path] != null) {
return false;
}

return not(
entries(errors).some(
([errorPath, error]) => error != null && errorPath.startsWith(path)
)
);
};

const not = (bool: boolean) => !bool;
Loading

0 comments on commit f501e71

Please sign in to comment.