From 8db6210f06582ebdff9277c3f08be747a9fcf039 Mon Sep 17 00:00:00 2001 From: Bruno Tot Date: Thu, 7 Sep 2023 12:27:00 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20[feat]:=20Implement=20'partial'=20o?= =?UTF-8?q?ption=20for=20EvaluatedStrategy=20type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/src/types/DetailedErrors.type.ts | 7 +- packages/core/src/types/Payload.type.ts | 7 +- packages/react/jest.config.js | 1 + packages/react/src/hooks/useForm/index.tsx | 87 ++++++++----------- packages/react/src/hooks/useForm/types.ts | 11 ++- .../react/src/hooks/useValidation/index.ts | 39 +++++---- 6 files changed, 77 insertions(+), 75 deletions(-) diff --git a/packages/core/src/types/DetailedErrors.type.ts b/packages/core/src/types/DetailedErrors.type.ts index f95d2cb13..3239a3272 100644 --- a/packages/core/src/types/DetailedErrors.type.ts +++ b/packages/core/src/types/DetailedErrors.type.ts @@ -1,4 +1,9 @@ import { EvaluatedStrategy } from "./EvaluatedStrategy"; import { ValidationResult } from "./ValidationResult.type"; +import { $ } from "./namespace/Utility.ns"; -export type DetailedErrors = EvaluatedStrategy; +export type DetailedErrors = EvaluatedStrategy< + T, + ValidationResult[], + $.TArgGet<"partial">["enabled"] +>; diff --git a/packages/core/src/types/Payload.type.ts b/packages/core/src/types/Payload.type.ts index 59db80158..79e942547 100644 --- a/packages/core/src/types/Payload.type.ts +++ b/packages/core/src/types/Payload.type.ts @@ -1,3 +1,8 @@ import { EvaluatedStrategy } from "./EvaluatedStrategy"; +import { $ } from "./namespace/Utility.ns"; -export type Payload = EvaluatedStrategy; +export type Payload = EvaluatedStrategy< + T, + undefined, + $.TArgGet<"partial">["enabled"] +>; diff --git a/packages/react/jest.config.js b/packages/react/jest.config.js index ef19697df..cb43faa1b 100644 --- a/packages/react/jest.config.js +++ b/packages/react/jest.config.js @@ -4,6 +4,7 @@ module.exports = { testMatch: ["**/*.test.ts", "**/*.test.tsx"], transformIgnorePatterns: ["./node_modules/", "./dist/"], reporters: ["/../../test-reporter/index.js"], + modulePathIgnorePatterns: ["/examples"], moduleFileExtensions: [ "js", "mjs", diff --git a/packages/react/src/hooks/useForm/index.tsx b/packages/react/src/hooks/useForm/index.tsx index 9bf493efe..abe3b0930 100644 --- a/packages/react/src/hooks/useForm/index.tsx +++ b/packages/react/src/hooks/useForm/index.tsx @@ -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"; /** @@ -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( +export default function useForm< + TClass, + TBody extends Payload = Payload +>( model: Class, - config?: ns.UseFormConfig + { + defaultValue, + onSubmit: onSubmitParam, + onSubmitValidationFail, + standalone, + validateImmediately, + validationGroups: groups, + onChange, + }: ns.UseFormConfig = { + onSubmit: async () => {}, + standalone: true, + validateImmediately: false, + validationGroups: [], + onChange: () => {}, + } ): ns.UseFormReturn { - 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(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) => { @@ -91,10 +87,7 @@ export default function useForm( }; //* 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(() => { @@ -107,30 +100,19 @@ export default function useForm( 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, @@ -140,12 +122,13 @@ export default function useForm( }); const data: ns.UseFormData = { + mutations: useMutations(model, { setForm }), isValid, isSubmitted, - mutations, onSubmit, providerProps, - errors: validateImmediately || isSubmitted ? errors : {}, + errors: isSubmitted ? errors : {}, + detailedErrors: isSubmitted ? detailedErrors : {}, reset, }; diff --git a/packages/react/src/hooks/useForm/types.ts b/packages/react/src/hooks/useForm/types.ts index f1e874936..96ccd6051 100644 --- a/packages/react/src/hooks/useForm/types.ts +++ b/packages/react/src/hooks/useForm/types.ts @@ -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 { @@ -10,7 +16,7 @@ namespace UseFormHook { standalone?: boolean; onSubmit?: () => Promise | void; onSubmitValidationFail?: (errors: Errors) => void; - whenChanged?: () => void; + onChange?: () => void; }; export type UseFormData = { @@ -20,6 +26,7 @@ namespace UseFormHook { mutations: UseFormChangeHandlerMap; providerProps: Omit; errors: Errors; + detailedErrors: DetailedErrors; reset: (...fieldPaths: PayloadFieldPath[]) => void; }; diff --git a/packages/react/src/hooks/useValidation/index.ts b/packages/react/src/hooks/useValidation/index.ts index 5eb483c64..ed15ce366 100644 --- a/packages/react/src/hooks/useValidation/index.ts +++ b/packages/react/src/hooks/useValidation/index.ts @@ -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( +export default function useValidation< + TClass, + TBody extends Payload = Payload +>( model: Class, - config?: ns.UseValidationConfig + { defaultValue, groups }: ns.UseValidationConfig = {} ): ns.UseValidationReturn { - const defaultValue = config?.defaultValue; - const groups = config?.groups ?? []; - const poc = useEntityProcessor(model, { groups, defaultValue }); - const initialForm = defaultValue ?? poc.noArgsInstance; - const [form, setForm] = useState(initialForm as TBody); + const processor = useEntityProcessor(model, { groups, defaultValue }); + const [form, setForm] = useState(processor.noArgsInstance); const [details, setDetails] = useState({} as DetailedErrors); const [simpleErrors, setSimpleErrors] = useState({} as Errors); - const payload = form as Payload; - 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 = { - isValid, - processor: poc, - errors: simpleErrors, - detailedErrors: details, - }; - - return [form, setForm, data]; + return [ + form, + setForm, + { + isValid: processor.isValid(form), + processor, + errors: simpleErrors, + detailedErrors: details, + }, + ]; }