Skip to content

Commit

Permalink
feat: add watch to trigger hook (#526)
Browse files Browse the repository at this point in the history
* feat: add watch to trigger hook

* docs: improve comment by adding example
  • Loading branch information
yoannfleurydev authored Sep 12, 2024
1 parent 1c429f6 commit 76deb85
Show file tree
Hide file tree
Showing 2 changed files with 181 additions and 0 deletions.
134 changes: 134 additions & 0 deletions src/lib/form/useWatchToTrigger/docs.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<ReturnType<typeof formSchema>>;
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<FormType>('min'),
message: 'The min should be <= to default',
});
}

if (val.default > val.max) {
ctx.addIssue({
code: 'custom',
path: getFieldPath<FormType>('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 (
<Form {...form}>
<Stack>
<FormField>
<FormFieldLabel>Min</FormFieldLabel>
<FormFieldController
control={form.control}
name="min"
type="number"
/>
</FormField>
<FormField>
<FormFieldLabel>Default</FormFieldLabel>
<FormFieldController
control={form.control}
name="default"
type="number"
/>
</FormField>
<FormField>
<FormFieldLabel>Max</FormFieldLabel>
<FormFieldController
control={form.control}
name="max"
type="number"
/>
</FormField>
<Button type="submit" variant="@primary">
Submit
</Button>
</Stack>
</Form>
);
};

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 (
<Form {...form}>
<Stack>
<FormField>
<FormFieldLabel>Min</FormFieldLabel>
<FormFieldController
control={form.control}
name="min"
type="number"
/>
</FormField>
<FormField>
<FormFieldLabel>Default</FormFieldLabel>
<FormFieldController
control={form.control}
name="default"
type="number"
/>
</FormField>
<FormField>
<FormFieldLabel>Max</FormFieldLabel>
<FormFieldController
control={form.control}
name="max"
type="number"
/>
</FormField>
<Button type="submit" variant="@primary">
Submit
</Button>
</Stack>
</Form>
);
};
47 changes: 47 additions & 0 deletions src/lib/form/useWatchToTrigger/index.ts
Original file line number Diff line number Diff line change
@@ -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<FormType>();
*
* // 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<TFieldValues> = FieldPath<TFieldValues>,
>(params: {
form: Pick<UseFormReturn<TFieldValues>, 'watch' | 'trigger'>;
names: Array<TName>;
}) => {
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]);
};

0 comments on commit 76deb85

Please sign in to comment.