form-provider
v0.1.2The single place `react-hook-form` is imported directly in this repo. Everything else should import form state/control from here instead of `react-hook-form` — that keeps the underlying library swappable from one spot later.
registry/behivetech/forms/form-provider
form-provider.tsx
1import { type ReactNode } from "react";2import {3 FormProvider as RHFFormProvider,4 type FieldValues,5 type UseFormReturn,6} from "react-hook-form";78export interface FormProviderProps<9 TFieldValues extends FieldValues = FieldValues,10> {11 /** The return value of `useForm()` (re-exported from this package) */12 methods: UseFormReturn<TFieldValues>;13 /** Descendants that will consume the form context via `useFormContext()` */14 children: ReactNode;15}1617/**18 * Thin wrapper around react-hook-form's `FormProvider`. Import this (and19 * `useForm`, `Controller`, etc.) from this package instead of importing20 * `react-hook-form` directly anywhere else — this is the one place that21 * knows about the underlying form library, so it can be swapped later22 * without touching every consumer.23 */24export const FormProvider = <TFieldValues extends FieldValues = FieldValues>({25 methods,26 children,27}: FormProviderProps<TFieldValues>) => {28 return <RHFFormProvider {...methods}>{children}</RHFFormProvider>;29};
form.tsx
1import { type FormHTMLAttributes, type ReactNode } from "react";2import {3 type FieldValues,4 type SubmitHandler,5 type UseFormReturn,6} from "react-hook-form";7import { FormProvider } from "./form-provider.js";89export interface FormProps<10 TFieldValues extends FieldValues = FieldValues,11> extends Omit<FormHTMLAttributes<HTMLFormElement>, "onSubmit"> {12 /** The return value of `useForm()` */13 methods: UseFormReturn<TFieldValues>;14 /** Called with the validated values when the form is submitted */15 onSubmit: SubmitHandler<TFieldValues>;16 /** Form content, typically FormField-wrapped controls */17 children: ReactNode;18}1920/**21 * Convenience wrapper for the common case: a `<form>` whose submit is wired22 * to react-hook-form's `handleSubmit`, with `FormProvider` already applied23 * so descendants can use `useFormContext()`. For editing UIs with no submit24 * moment (live/instant field-by-field updates), use the bare atoms +25 * `FormField` instead — this component assumes a real submit action exists.26 */27export const Form = <TFieldValues extends FieldValues = FieldValues>({28 methods,29 onSubmit,30 children,31 ...rest32}: FormProps<TFieldValues>) => {33 return (34 <FormProvider methods={methods}>35 <form {...rest} onSubmit={methods.handleSubmit(onSubmit)}>36 {children}37 </form>38 </FormProvider>39 );40};
index.ts
1export { FormProvider } from "./form-provider.js";2export type { FormProviderProps } from "./form-provider.js";34export { Form } from "./form.js";5export type { FormProps } from "./form.js";67export {8 useForm,9 useFormContext,10 useController,11 useFormState,12 useWatch,13 Controller,14} from "react-hook-form";15export type {16 UseFormReturn,17 UseFormProps,18 FieldValues,19 SubmitHandler,20 Control,21 ControllerProps,22 ControllerRenderProps,23 ControllerFieldState,24 FieldPath,25 RegisterOptions,26 FieldErrors,27} from "react-hook-form";
form-provider.composition.tsx
1import { FormField } from "@behivetech/forms.form-field";2import { TextField } from "@behivetech/atoms.text-field";3import { Button } from "@behivetech/atoms.button";4import { Form, FormProvider, useForm } from "./index.js";56interface DemoValues {7 email: string;8}910export const BasicFormProvider = () => {11 const methods = useForm<DemoValues>({ defaultValues: { email: "" } });12 const { register, handleSubmit } = methods;1314 return (15 <FormProvider methods={methods}>16 <form onSubmit={handleSubmit((values) => console.log(values))}>17 <FormField label="Email" htmlFor="email">18 <TextField id="email" {...register("email")} />19 </FormField>20 </form>21 </FormProvider>22 );23};2425export const BasicForm = () => {26 const methods = useForm<DemoValues>({ defaultValues: { email: "" } });27 const { register } = methods;2829 return (30 <Form methods={methods} onSubmit={(values) => console.log(values)}>31 <FormField label="Email" htmlFor="email-form">32 <TextField id="email-form" {...register("email")} />33 </FormField>34 <Button type="submit" variant="primary" size="sm">35 Save36 </Button>37 </Form>38 );39};
form-provider.spec.tsx
1import { render, screen, renderHook } from "@testing-library/react";2import { FormProvider, useForm, useFormContext } from "./index.js";34function Consumer() {5 const { register } = useFormContext();6 return <input {...register("email")} placeholder="email" />;7}89describe("FormProvider", () => {10 it("renders children", () => {11 const { result } = renderHook(() => useForm());12 render(13 <FormProvider methods={result.current}>14 <span>Hello</span>15 </FormProvider>,16 );17 expect(screen.getByText("Hello")).toBeTruthy();18 });1920 it("makes form methods available to descendants via useFormContext", () => {21 const { result } = renderHook(() => useForm());22 render(23 <FormProvider methods={result.current}>24 <Consumer />25 </FormProvider>,26 );27 expect(screen.getByPlaceholderText("email")).toBeTruthy();28 });29});
form.spec.tsx
1import { render, screen, renderHook } from "@testing-library/react";2import userEvent from "@testing-library/user-event";3import { Form, useForm, useFormContext } from "./index.js";45function Consumer() {6 const { register } = useFormContext();7 return <input {...register("email")} placeholder="email" />;8}910describe("Form", () => {11 it("renders children", () => {12 const { result } = renderHook(() => useForm());13 render(14 <Form methods={result.current} onSubmit={() => {}}>15 <span>Hello</span>16 </Form>,17 );18 expect(screen.getByText("Hello")).toBeTruthy();19 });2021 it("makes form methods available to descendants via FormProvider", () => {22 const { result } = renderHook(() => useForm());23 render(24 <Form methods={result.current} onSubmit={() => {}}>25 <Consumer />26 </Form>,27 );28 expect(screen.getByPlaceholderText("email")).toBeTruthy();29 });3031 it("calls onSubmit with the form values when submitted", async () => {32 const handleSubmit = vi.fn();33 const { result } = renderHook(() =>34 useForm({ defaultValues: { email: "a@b.com" } }),35 );36 render(37 <Form methods={result.current} onSubmit={handleSubmit}>38 <Consumer />39 <button type="submit">Save</button>40 </Form>,41 );42 await userEvent.click(screen.getByRole("button", { name: "Save" }));43 expect(handleSubmit).toHaveBeenCalledWith(44 { email: "a@b.com" },45 expect.anything(),46 );47 });48});