Skip to content

Commit

Permalink
✨ [feat]: Implement 'partial' option for EvaluatedStrategy type
Browse files Browse the repository at this point in the history
  • Loading branch information
brunotot committed Sep 7, 2023
1 parent 147d81a commit 8db6210
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 75 deletions.
7 changes: 6 additions & 1 deletion packages/core/src/types/DetailedErrors.type.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { EvaluatedStrategy } from "./EvaluatedStrategy";
import { ValidationResult } from "./ValidationResult.type";
import { $ } from "./namespace/Utility.ns";

export type DetailedErrors<T> = EvaluatedStrategy<T, ValidationResult[]>;
export type DetailedErrors<T> = EvaluatedStrategy<
T,
ValidationResult[],
$.TArgGet<"partial">["enabled"]
>;
7 changes: 6 additions & 1 deletion packages/core/src/types/Payload.type.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import { EvaluatedStrategy } from "./EvaluatedStrategy";
import { $ } from "./namespace/Utility.ns";

export type Payload<T> = EvaluatedStrategy<T>;
export type Payload<T> = EvaluatedStrategy<
T,
undefined,
$.TArgGet<"partial">["enabled"]
>;
1 change: 1 addition & 0 deletions packages/react/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module.exports = {
testMatch: ["**/*.test.ts", "**/*.test.tsx"],
transformIgnorePatterns: ["./node_modules/", "./dist/"],
reporters: ["<rootDir>/../../test-reporter/index.js"],
modulePathIgnorePatterns: ["<rootDir>/examples"],
moduleFileExtensions: [
"js",
"mjs",
Expand Down
87 changes: 35 additions & 52 deletions packages/react/src/hooks/useForm/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { useContext, useEffect, useMemo, useState } from "react";
import { Class } from "tdv-core";
import { useContext, useEffect, useState } from "react";
import { Class, Payload } from "tdv-core";
import { FormContext } from "../../contexts/FormContext";
import useEffectWhenMounted from "../useAfterMount";
import useMutations from "../useMutations";
import useReset from "../useReset";
import useValidation from "../useValidation";
import FormContextNamespace from "./../../contexts/FormContext/types";
import ns from "./types";

/**
Expand Down Expand Up @@ -36,43 +35,40 @@ import ns from "./types";
* @typeParam TClass - represents parent form class model holding context of current compontent
* @typeParam TBody - represents writable scope of `TClass` (it can be TClass itself or a chunk of its fields)
*/
export default function useForm<TClass, TBody = TClass>(
export default function useForm<
TClass,
TBody extends Payload<TClass> = Payload<TClass>
>(
model: Class<TClass>,
config?: ns.UseFormConfig<TClass, TBody>
{
defaultValue,
onSubmit: onSubmitParam,
onSubmitValidationFail,
standalone,
validateImmediately,
validationGroups: groups,
onChange,
}: ns.UseFormConfig<TClass, TBody> = {
onSubmit: async () => {},
standalone: true,
validateImmediately: false,
validationGroups: [],
onChange: () => {},
}
): ns.UseFormReturn<TClass, TBody> {
const defaultValue0 = config?.defaultValue;
const whenChanged = config?.whenChanged ?? (() => {});
const groups = config?.validationGroups ?? [];
const onSubmitParam = config?.onSubmit ?? (async () => {});
const validateImmediatelyParam =
config?.validateImmediately === undefined
? false
: config?.validateImmediately!;
const standalone =
config?.standalone === undefined ? true : config.standalone!;
const onSubmitValidationFail = config?.onSubmitValidationFail;
const noArgsConstructedInstance = useMemo(() => new model(), []);
const defaultValue =
defaultValue0 ?? (noArgsConstructedInstance as unknown as TBody);
const ctx = useContext(FormContext);
const initialSubmitted = !standalone && !!ctx && ctx.submitted;
const validateImmediately = standalone
? validateImmediatelyParam
: ctx
? ctx.validateImmediately
: validateImmediatelyParam;

const [submitted, setSubmitted] = useState(initialSubmitted);
const isSubmitted = validateImmediately || submitted;
// prettier-ignore
const [submitted, setSubmitted] = useState(!standalone && !!ctx && ctx.submitted);
// prettier-ignore
const instantContextValidation = standalone ? validateImmediately! : ctx? ctx.validateImmediately : validateImmediately!;
const isSubmitted = instantContextValidation || submitted;

const [form, setForm, { errors: errorsSnapshot, isValid, processor }] =
const [form, setForm, { errors, detailedErrors, isValid, processor }] =
useValidation<TClass, TBody>(model, {
defaultValue,
groups,
});

const [errors, setErrors] = useState(errorsSnapshot);

//* Dispatcher function which fires only when
//* itself isn't a parent and context exists.
const dispatchContext = (bool?: boolean) => {
Expand All @@ -91,10 +87,7 @@ export default function useForm<TClass, TBody = TClass>(
};

//* When input data changes execute callback.
useEffectWhenMounted(() => whenChanged(), [form]);

//* When useValidation returns fresh errors object data.
useEffectWhenMounted(() => setErrors(errorsSnapshot), [errorsSnapshot]);
useEffectWhenMounted(() => onChange?.(), [form]);

//* When submitted flag from context gets changed.
useEffect(() => {
Expand All @@ -107,30 +100,19 @@ export default function useForm<TClass, TBody = TClass>(

const onSubmit = async () => {
handleSetSubmitted(true);

if (!isValid) {
const newErrors = isSubmitted ? structuredClone(errors) : errors;
if (isSubmitted) {
setErrors(newErrors);
}
onSubmitValidationFail?.(newErrors);
onSubmitValidationFail?.(errors);
return;
}

await onSubmitParam();
await onSubmitParam?.();
};

const providerProps: Omit<
FormContextNamespace.FormProviderProps,
"children"
> = {
const providerProps = {
submitted: submitted,
setSubmitted: handleSetSubmitted,
validateImmediately,
validateImmediately: instantContextValidation,
};

const mutations = useMutations(model, { setForm });

const reset = useReset({
form,
handleSetSubmitted,
Expand All @@ -140,12 +122,13 @@ export default function useForm<TClass, TBody = TClass>(
});

const data: ns.UseFormData<TClass, TBody> = {
mutations: useMutations(model, { setForm }),
isValid,
isSubmitted,
mutations,
onSubmit,
providerProps,
errors: validateImmediately || isSubmitted ? errors : {},
errors: isSubmitted ? errors : {},
detailedErrors: isSubmitted ? detailedErrors : {},
reset,
};

Expand Down
11 changes: 9 additions & 2 deletions packages/react/src/hooks/useForm/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { Dispatch, SetStateAction } from "react";
import { Condition, Errors, TypeUtils, ValidationGroup } from "tdv-core";
import {
Condition,
DetailedErrors,
Errors,
TypeUtils,
ValidationGroup,
} from "tdv-core";
import FormContextNamespace from "../../contexts/FormContext/types";

namespace UseFormHook {
Expand All @@ -10,7 +16,7 @@ namespace UseFormHook {
standalone?: boolean;
onSubmit?: () => Promise<void> | void;
onSubmitValidationFail?: (errors: Errors<TClass>) => void;
whenChanged?: () => void;
onChange?: () => void;
};

export type UseFormData<TClass, TBody = TClass> = {
Expand All @@ -20,6 +26,7 @@ namespace UseFormHook {
mutations: UseFormChangeHandlerMap<TBody>;
providerProps: Omit<FormContextNamespace.FormProviderProps, "children">;
errors: Errors<TClass>;
detailedErrors: DetailedErrors<TClass>;
reset: (...fieldPaths: PayloadFieldPath<TBody>[]) => void;
};

Expand Down
39 changes: 20 additions & 19 deletions packages/react/src/hooks/useValidation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,31 +27,32 @@ import ns from "./types";
* @typeParam TClass - represents parent form class model holding context of current compontent
* @typeParam TBody - represents writable scope of `TClass` (it can be TClass itself or a chunk of its fields)
*/
export default function useValidation<TClass, TBody = TClass>(
export default function useValidation<
TClass,
TBody extends Payload<TClass> = Payload<TClass>
>(
model: Class<TClass>,
config?: ns.UseValidationConfig<TBody>
{ defaultValue, groups }: ns.UseValidationConfig<TBody> = {}
): ns.UseValidationReturn<TClass, TBody> {
const defaultValue = config?.defaultValue;
const groups = config?.groups ?? [];
const poc = useEntityProcessor(model, { groups, defaultValue });
const initialForm = defaultValue ?? poc.noArgsInstance;
const [form, setForm] = useState<TBody>(initialForm as TBody);
const processor = useEntityProcessor(model, { groups, defaultValue });
const [form, setForm] = useState<TBody>(processor.noArgsInstance);
const [details, setDetails] = useState({} as DetailedErrors<TClass>);
const [simpleErrors, setSimpleErrors] = useState({} as Errors<TClass>);
const payload = form as Payload<TClass>;
const isValid = poc.isValid(payload);

useEffect(() => {
setDetails(poc.getDetailedErrors(payload));
setSimpleErrors(poc.getErrors(payload));
const { errors, detailedErrors } = processor.validate(form);
setDetails(detailedErrors);
setSimpleErrors(errors);
}, [form]);

const data: ns.UseValidationData<TClass, TBody> = {
isValid,
processor: poc,
errors: simpleErrors,
detailedErrors: details,
};

return [form, setForm, data];
return [
form,
setForm,
{
isValid: processor.isValid(form),
processor,
errors: simpleErrors,
detailedErrors: details,
},
];
}

0 comments on commit 8db6210

Please sign in to comment.