From 76deb855a4637f56af4b2012984db6efa142c7e6 Mon Sep 17 00:00:00 2001 From: Yoann Fleury Date: Thu, 12 Sep 2024 14:57:21 +0200 Subject: [PATCH] feat: add watch to trigger hook (#526) * feat: add watch to trigger hook * docs: improve comment by adding example --- .../form/useWatchToTrigger/docs.stories.tsx | 134 ++++++++++++++++++ src/lib/form/useWatchToTrigger/index.ts | 47 ++++++ 2 files changed, 181 insertions(+) create mode 100644 src/lib/form/useWatchToTrigger/docs.stories.tsx create mode 100644 src/lib/form/useWatchToTrigger/index.ts diff --git a/src/lib/form/useWatchToTrigger/docs.stories.tsx b/src/lib/form/useWatchToTrigger/docs.stories.tsx new file mode 100644 index 000000000..dee73234d --- /dev/null +++ b/src/lib/form/useWatchToTrigger/docs.stories.tsx @@ -0,0 +1,134 @@ +import { Button, Stack } from '@chakra-ui/react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { + Form, + FormField, + FormFieldController, + FormFieldLabel, +} from '@/components/Form'; +import { getFieldPath } from '@/lib/form/getFieldPath'; + +import { useWatchToTrigger } from '.'; + +export default { + title: 'Hooks/useWatchToTrigger', +}; + +type FormType = z.infer>; +const formSchema = () => + z + .object({ min: z.number(), default: z.number(), max: z.number() }) + .superRefine((val, ctx) => { + if (val.min > val.default) { + ctx.addIssue({ + code: 'custom', + path: getFieldPath('min'), + message: 'The min should be <= to default', + }); + } + + if (val.default > val.max) { + ctx.addIssue({ + code: 'custom', + path: getFieldPath('default'), + message: 'The default should be <= to the max', + }); + } + }); + +export const WithoutHook = () => { + const form = useForm({ + mode: 'onBlur', + resolver: zodResolver(formSchema()), + defaultValues: { + min: 2, + default: 4, + max: 6, + }, + }); + + return ( +
+ + + Min + + + + Default + + + + Max + + + + +
+ ); +}; + +export const WithHook = () => { + const form = useForm({ + mode: 'onBlur', + resolver: zodResolver(formSchema()), + defaultValues: { + min: 2, + default: 4, + max: 6, + }, + }); + + useWatchToTrigger({ form, names: ['min', 'default', 'max'] }); + + return ( +
+ + + Min + + + + Default + + + + Max + + + + +
+ ); +}; diff --git a/src/lib/form/useWatchToTrigger/index.ts b/src/lib/form/useWatchToTrigger/index.ts new file mode 100644 index 000000000..fcb54a87b --- /dev/null +++ b/src/lib/form/useWatchToTrigger/index.ts @@ -0,0 +1,47 @@ +import { useEffect } from 'react'; + +import { FieldPath, FieldValues, UseFormReturn } from 'react-hook-form'; + +/** + * Use this hook to subscribe to fields and listen for changes to revalidate the + * form. + * + * Using the form "onBlur" validation will validate the field you just updated. + * But imagine a field that has to validate itself based on an another field update. + * That's the point of this hook. + * + * Example: imagine those fields: `min`, `default`, `max`. The `min` should be + * lower than the `default` and the `default` should lower than the `max`. + * `min` is equal to 2, `default` is equal to 4 and `max` is equal to 6. + * You update the `min` so the value is 5, the form (using superRefine and + * custom issues) will tell you that the `min` should be lower than the default. + * You update the `default` so the new value is 5.5. Without this hook, the + * field `min` will not revalidate. With this hook, if you give the field name, + * it will. + * + * @example + * // Get the form + * const form = useFormContext(); + * + * // Subscribe to fields validation + * // If `default` changes, `min`, `default` and `max` will validate and trigger + * // error if any. + * useWatchToTrigger({ form, names: ['min', 'default', 'max']}) + */ +export const useWatchToTrigger = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>(params: { + form: Pick, 'watch' | 'trigger'>; + names: Array; +}) => { + const { watch, trigger } = params.form; + useEffect(() => { + const subscription = watch((_, { name }) => { + if (name && params.names.includes(name as TName)) { + trigger(params.names); + } + }); + return () => subscription.unsubscribe(); + }, [watch, trigger, params.names]); +};