@beignet/react-hook-form 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.md +286 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -0
- package/src/index.ts +130 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# @beignet/react-hook-form
|
|
2
|
+
|
|
3
|
+
> React Hook Form integration for Beignet
|
|
4
|
+
|
|
5
|
+
This package provides automatic form validation using your contract's body schema. Works with any [Standard Schema](https://github.com/standard-schema/standard-schema) library (Zod, Valibot, ArkType, etc.).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @beignet/react-hook-form @beignet/core react-hook-form @hookform/resolvers react
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## TypeScript requirements
|
|
15
|
+
|
|
16
|
+
This package requires TypeScript 5.0 or higher for proper type inference.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
### Basic form
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { createReactHookForm } from "@beignet/react-hook-form";
|
|
24
|
+
import { createTodo } from "@/features/todos/contracts";
|
|
25
|
+
|
|
26
|
+
const rhf = createReactHookForm();
|
|
27
|
+
|
|
28
|
+
function CreateTodoForm() {
|
|
29
|
+
const { useForm } = rhf(createTodo);
|
|
30
|
+
const form = useForm({
|
|
31
|
+
defaultValues: {
|
|
32
|
+
title: "",
|
|
33
|
+
completed: false,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const onSubmit = form.handleSubmit((values) => {
|
|
38
|
+
// values is typed as: { title: string; completed?: boolean }
|
|
39
|
+
console.log("Creating todo:", values);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<form onSubmit={onSubmit}>
|
|
44
|
+
<input
|
|
45
|
+
{...form.register("title")}
|
|
46
|
+
placeholder="What needs to be done?"
|
|
47
|
+
/>
|
|
48
|
+
{form.formState.errors.title && (
|
|
49
|
+
<p className="error">{form.formState.errors.title.message}</p>
|
|
50
|
+
)}
|
|
51
|
+
|
|
52
|
+
<label>
|
|
53
|
+
<input type="checkbox" {...form.register("completed")} />
|
|
54
|
+
Completed
|
|
55
|
+
</label>
|
|
56
|
+
|
|
57
|
+
<button type="submit" disabled={form.formState.isSubmitting}>
|
|
58
|
+
Create Todo
|
|
59
|
+
</button>
|
|
60
|
+
</form>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### With React Query mutation
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { createReactHookForm } from "@beignet/react-hook-form";
|
|
69
|
+
import { useMutation } from "@tanstack/react-query";
|
|
70
|
+
import { rq } from "@/client/rq";
|
|
71
|
+
import { createTodo } from "@/features/todos/contracts";
|
|
72
|
+
|
|
73
|
+
const rhf = createReactHookForm();
|
|
74
|
+
|
|
75
|
+
function CreateTodoForm() {
|
|
76
|
+
const { useForm } = rhf(createTodo);
|
|
77
|
+
const form = useForm({
|
|
78
|
+
defaultValues: { title: "" },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const mutation = useMutation(
|
|
82
|
+
rq(createTodo).mutationOptions()
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const onSubmit = form.handleSubmit((values) => {
|
|
86
|
+
mutation.mutate({ body: values });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<form onSubmit={onSubmit}>
|
|
91
|
+
<input {...form.register("title")} placeholder="Title" />
|
|
92
|
+
{form.formState.errors.title && (
|
|
93
|
+
<p>{form.formState.errors.title.message}</p>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
<button type="submit" disabled={mutation.isPending}>
|
|
97
|
+
{mutation.isPending ? "Creating..." : "Create"}
|
|
98
|
+
</button>
|
|
99
|
+
|
|
100
|
+
{mutation.isError && (
|
|
101
|
+
<p className="error">{mutation.error.message}</p>
|
|
102
|
+
)}
|
|
103
|
+
</form>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Disabling validation
|
|
109
|
+
|
|
110
|
+
If you need to disable the schema resolver (e.g., for partial form handling):
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
const { useForm } = rhf(createTodo);
|
|
114
|
+
const form = useForm({
|
|
115
|
+
resolverEnabled: false, // Disable schema validation
|
|
116
|
+
defaultValues: { title: "" },
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Using contract config directly
|
|
121
|
+
|
|
122
|
+
You can pass either a contract builder or its config:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
import { createReactHookForm } from "@beignet/react-hook-form";
|
|
126
|
+
import { createTodo } from "@/features/todos/contracts";
|
|
127
|
+
|
|
128
|
+
const rhf = createReactHookForm();
|
|
129
|
+
|
|
130
|
+
// Using ContractBuilder directly
|
|
131
|
+
const { useForm } = rhf(createTodo);
|
|
132
|
+
|
|
133
|
+
// Or using the contract config
|
|
134
|
+
const { useForm } = rhf(createTodo.config);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## API reference
|
|
138
|
+
|
|
139
|
+
### `createReactHookForm()`
|
|
140
|
+
|
|
141
|
+
Creates a React Hook Form adapter factory.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const rhf = createReactHookForm();
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### `rhf(contract)`
|
|
148
|
+
|
|
149
|
+
Creates a React Hook Form adapter for a contract.
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
const adapter = rhf(createTodo);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### `adapter.useForm(props?)`
|
|
156
|
+
|
|
157
|
+
Returns a React Hook Form `useForm` result with the contract's body schema as resolver.
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
const form = adapter.useForm({
|
|
161
|
+
defaultValues?: { ... },
|
|
162
|
+
resolverEnabled?: boolean, // default: true
|
|
163
|
+
// ...other React Hook Form options
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Type inference
|
|
168
|
+
|
|
169
|
+
Form values are automatically typed based on the contract's body schema:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
// Contract definition
|
|
173
|
+
const createTodo = todos
|
|
174
|
+
.post("/api/todos")
|
|
175
|
+
.body(z.object({
|
|
176
|
+
title: z.string().min(1),
|
|
177
|
+
description: z.string().optional(),
|
|
178
|
+
completed: z.boolean().optional(),
|
|
179
|
+
}))
|
|
180
|
+
.responses({ 201: TodoSchema });
|
|
181
|
+
|
|
182
|
+
// Form values are inferred
|
|
183
|
+
const rhf = createReactHookForm();
|
|
184
|
+
const form = rhf(createTodo).useForm();
|
|
185
|
+
form.register("title"); // ✓ Valid
|
|
186
|
+
form.register("description"); // ✓ Valid
|
|
187
|
+
form.register("invalid"); // ✗ Type error
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Standard Schema support
|
|
191
|
+
|
|
192
|
+
This package uses the `@hookform/resolvers/standard-schema` resolver, which works with any Standard Schema compatible library:
|
|
193
|
+
|
|
194
|
+
- **Zod** - `z.object({ ... })`
|
|
195
|
+
- **Valibot** - `v.object({ ... })`
|
|
196
|
+
- **ArkType** - `type({ ... })`
|
|
197
|
+
|
|
198
|
+
## Validation behavior
|
|
199
|
+
|
|
200
|
+
The resolver validates:
|
|
201
|
+
|
|
202
|
+
1. **On blur** - When a field loses focus
|
|
203
|
+
2. **On change** - After first submission attempt
|
|
204
|
+
3. **On submit** - Before calling your submit handler
|
|
205
|
+
|
|
206
|
+
Validation errors are available via `form.formState.errors`:
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
{form.formState.errors.title && (
|
|
210
|
+
<span className="error">
|
|
211
|
+
{form.formState.errors.title.message}
|
|
212
|
+
</span>
|
|
213
|
+
)}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Complete example
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
import { createReactHookForm } from "@beignet/react-hook-form";
|
|
220
|
+
import { useMutation } from "@tanstack/react-query";
|
|
221
|
+
import { rq } from "@/client/rq";
|
|
222
|
+
import { updateProfile } from "@/features/profile/contracts";
|
|
223
|
+
|
|
224
|
+
const rhf = createReactHookForm();
|
|
225
|
+
|
|
226
|
+
function ProfileForm({ profile }) {
|
|
227
|
+
const { useForm } = rhf(updateProfile);
|
|
228
|
+
const form = useForm({
|
|
229
|
+
defaultValues: {
|
|
230
|
+
name: profile.name,
|
|
231
|
+
email: profile.email,
|
|
232
|
+
bio: profile.bio ?? "",
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const mutation = useMutation(
|
|
237
|
+
rq(updateProfile).mutationOptions({
|
|
238
|
+
onSuccess: () => {
|
|
239
|
+
toast.success("Profile updated!");
|
|
240
|
+
},
|
|
241
|
+
})
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const onSubmit = form.handleSubmit((values) => {
|
|
245
|
+
mutation.mutate({ body: values });
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const { errors, isDirty, isSubmitting } = form.formState;
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<form onSubmit={onSubmit}>
|
|
252
|
+
<div>
|
|
253
|
+
<label htmlFor="name">Name</label>
|
|
254
|
+
<input id="name" {...form.register("name")} />
|
|
255
|
+
{errors.name && <span className="error">{errors.name.message}</span>}
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<div>
|
|
259
|
+
<label htmlFor="email">Email</label>
|
|
260
|
+
<input id="email" type="email" {...form.register("email")} />
|
|
261
|
+
{errors.email && <span className="error">{errors.email.message}</span>}
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<div>
|
|
265
|
+
<label htmlFor="bio">Bio</label>
|
|
266
|
+
<textarea id="bio" {...form.register("bio")} />
|
|
267
|
+
{errors.bio && <span className="error">{errors.bio.message}</span>}
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<button type="submit" disabled={!isDirty || isSubmitting}>
|
|
271
|
+
{isSubmitting ? "Saving..." : "Save Changes"}
|
|
272
|
+
</button>
|
|
273
|
+
</form>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Related packages
|
|
279
|
+
|
|
280
|
+
- [`@beignet/core/contracts`](https://beignet.dev/contracts) - Core contract definitions
|
|
281
|
+
- [`@beignet/react-query`](https://beignet.dev/react-query) - TanStack Query integration
|
|
282
|
+
- [`@beignet/core/client`](https://beignet.dev/client) - HTTP client
|
|
283
|
+
|
|
284
|
+
## License
|
|
285
|
+
|
|
286
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type ContractLike, type HttpContractConfig, type InferOutput, type ResolveContract, type StandardSchemaV1 } from "@beignet/core/contracts";
|
|
2
|
+
import { type FieldValues, type UseFormProps, type UseFormReturn } from "react-hook-form";
|
|
3
|
+
/**
|
|
4
|
+
* Infer form values from contract body
|
|
5
|
+
*/
|
|
6
|
+
type InferBody<TContract extends HttpContractConfig> = TContract["body"] extends StandardSchemaV1 ? InferOutput<TContract["body"]> & FieldValues : FieldValues;
|
|
7
|
+
type FormValues<TContract extends HttpContractConfig> = InferBody<TContract>;
|
|
8
|
+
/**
|
|
9
|
+
* Options for React Hook Form with contract support
|
|
10
|
+
*/
|
|
11
|
+
type RhfFormOptions<TContract extends HttpContractConfig> = Omit<UseFormProps<FormValues<TContract>>, "resolver"> & {
|
|
12
|
+
/**
|
|
13
|
+
* Optional override for the resolver. If supplied, this is used instead of
|
|
14
|
+
* the default contract-based resolver.
|
|
15
|
+
*/
|
|
16
|
+
resolver?: UseFormProps<FormValues<TContract>>["resolver"];
|
|
17
|
+
/**
|
|
18
|
+
* Enables or disables automatic resolver generation from the contract body schema.
|
|
19
|
+
* Defaults to true.
|
|
20
|
+
*/
|
|
21
|
+
resolverEnabled?: boolean;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* React Hook Form adapter for a contract
|
|
25
|
+
*/
|
|
26
|
+
export type ReactHookFormContractAdapter<TContract extends HttpContractConfig> = {
|
|
27
|
+
/**
|
|
28
|
+
* Generate UseFormProps<FormValues<TContract>> for any RHF usage.
|
|
29
|
+
*/
|
|
30
|
+
formOptions: (props?: RhfFormOptions<TContract>) => UseFormProps<FormValues<TContract>>;
|
|
31
|
+
/**
|
|
32
|
+
* Convenience wrapper around useForm(formOptions(props)).
|
|
33
|
+
*/
|
|
34
|
+
useForm: (props?: RhfFormOptions<TContract>) => UseFormReturn<FormValues<TContract>>;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Create a React Hook Form adapter factory.
|
|
38
|
+
*
|
|
39
|
+
* Mirrors the React integration pattern used by `createReactQuery()` and `createNuqs()`:
|
|
40
|
+
* create the adapter once, then bind individual contracts with `rhf(contract)`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createReactHookForm(): <TContractLike extends ContractLike>(contract: TContractLike) => ReactHookFormContractAdapter<ResolveContract<TContractLike>>;
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAChB,KAAK,eAAe,EAEpB,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,KAAK,WAAW,EAEhB,KAAK,YAAY,EACjB,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAEzB;;GAEG;AACH,KAAK,SAAS,CAAC,SAAS,SAAS,kBAAkB,IACjD,SAAS,CAAC,MAAM,CAAC,SAAS,gBAAgB,GACtC,WAAW,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,WAAW,GAC5C,WAAW,CAAC;AAElB,KAAK,UAAU,CAAC,SAAS,SAAS,kBAAkB,IAAI,SAAS,CAAC,SAAS,CAAC,CAAC;AAE7E;;GAEG;AACH,KAAK,cAAc,CAAC,SAAS,SAAS,kBAAkB,IAAI,IAAI,CAC9D,YAAY,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,EACnC,UAAU,CACX,GAAG;IACF;;;OAGG;IACH,QAAQ,CAAC,EAAE,YAAY,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;IAE3D;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,4BAA4B,CAAC,SAAS,SAAS,kBAAkB,IAC3E;IACE;;OAEG;IACH,WAAW,EAAE,CACX,KAAK,CAAC,EAAE,cAAc,CAAC,SAAS,CAAC,KAC9B,YAAY,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;IAEzC;;OAEG;IACH,OAAO,EAAE,CACP,KAAK,CAAC,EAAE,cAAc,CAAC,SAAS,CAAC,KAC9B,aAAa,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;CAC3C,CAAC;AAqDJ;;;;;GAKG;AACH,wBAAgB,mBAAmB,KACb,aAAa,SAAS,YAAY,EACpD,UAAU,aAAa,KACtB,4BAA4B,CAAC,eAAe,CAAC,aAAa,CAAC,CAAC,CAGhE"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { resolveContract, } from "@beignet/core/contracts";
|
|
2
|
+
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
|
|
3
|
+
import { useForm as rhfUseForm, } from "react-hook-form";
|
|
4
|
+
function createReactHookFormAdapter(contract) {
|
|
5
|
+
const resolvedContract = resolveContract(contract);
|
|
6
|
+
if (!resolvedContract.body) {
|
|
7
|
+
throw new Error(`rhf(${resolvedContract.name}): Contract has no body schema. ` +
|
|
8
|
+
"React Hook Form requires a body schema to generate form fields and validation. " +
|
|
9
|
+
"Use .body(schema) on the contract builder to define the request body shape.");
|
|
10
|
+
}
|
|
11
|
+
function formOptions(props) {
|
|
12
|
+
const bodySchema = resolvedContract.body;
|
|
13
|
+
const { resolverEnabled = true, resolver: resolverOverride, ...rest } = props ?? {};
|
|
14
|
+
const resolver = resolverOverride ??
|
|
15
|
+
(bodySchema && resolverEnabled
|
|
16
|
+
? standardSchemaResolver(bodySchema)
|
|
17
|
+
: undefined);
|
|
18
|
+
return {
|
|
19
|
+
...rest,
|
|
20
|
+
resolver,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function useForm(props) {
|
|
24
|
+
const options = formOptions(props);
|
|
25
|
+
return rhfUseForm(options);
|
|
26
|
+
}
|
|
27
|
+
return { formOptions, useForm };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create a React Hook Form adapter factory.
|
|
31
|
+
*
|
|
32
|
+
* Mirrors the React integration pattern used by `createReactQuery()` and `createNuqs()`:
|
|
33
|
+
* create the adapter once, then bind individual contracts with `rhf(contract)`.
|
|
34
|
+
*/
|
|
35
|
+
export function createReactHookForm() {
|
|
36
|
+
return function rhf(contract) {
|
|
37
|
+
return createReactHookFormAdapter(contract);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,eAAe,GAEhB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAC7E,OAAO,EAEL,OAAO,IAAI,UAAU,GAGtB,MAAM,iBAAiB,CAAC;AAoDzB,SAAS,0BAA0B,CACjC,QAAuB;IAEvB,MAAM,gBAAgB,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;IAEnD,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CACb,OAAO,gBAAgB,CAAC,IAAI,kCAAkC;YAC5D,iFAAiF;YACjF,6EAA6E,CAChF,CAAC;IACJ,CAAC;IAID,SAAS,WAAW,CAClB,KAAsD;QAEtD,MAAM,UAAU,GAAG,gBAAgB,CAAC,IAGvB,CAAC;QAEd,MAAM,EACJ,eAAe,GAAG,IAAI,EACtB,QAAQ,EAAE,gBAAgB,EAC1B,GAAG,IAAI,EACR,GAAG,KAAK,IAAI,EAAE,CAAC;QAEhB,MAAM,QAAQ,GACZ,gBAAgB;YAChB,CAAC,UAAU,IAAI,eAAe;gBAC5B,CAAC,CAAC,sBAAsB,CAAC,UAAU,CAAC;gBACpC,CAAC,CAAC,SAAS,CAAC,CAAC;QAEjB,OAAO;YACL,GAAI,IAA6B;YACjC,QAAQ;SACT,CAAC;IACJ,CAAC;IAED,SAAS,OAAO,CACd,KAAsD;QAEtD,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;QACnC,OAAO,UAAU,CAAS,OAAO,CAAC,CAAC;IACrC,CAAC;IAED,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC;AAClC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAO,SAAS,GAAG,CACjB,QAAuB;QAEvB,OAAO,0BAA0B,CAAC,QAAQ,CAAC,CAAC;IAC9C,CAAC,CAAC;AACJ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@beignet/react-hook-form",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "React Hook Form integration for Beignet with Standard Schema support",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src",
|
|
17
|
+
"!src/**/*.test.ts",
|
|
18
|
+
"!src/**/*.test.tsx",
|
|
19
|
+
"!src/**/*.test-d.ts",
|
|
20
|
+
"README.md",
|
|
21
|
+
"CHANGELOG.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"dev": "tsc --watch",
|
|
26
|
+
"clean": "rm -rf dist coverage .turbo",
|
|
27
|
+
"test": "bun test",
|
|
28
|
+
"test:coverage": "bun test --coverage",
|
|
29
|
+
"lint": "biome check ."
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"contract",
|
|
33
|
+
"api",
|
|
34
|
+
"typescript",
|
|
35
|
+
"react-hook-form",
|
|
36
|
+
"standard-schema"
|
|
37
|
+
],
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/taylorbryant/beignet.git",
|
|
42
|
+
"directory": "packages/react-hook-form"
|
|
43
|
+
},
|
|
44
|
+
"author": "Taylor Bryant",
|
|
45
|
+
"homepage": "https://github.com/taylorbryant/beignet#readme",
|
|
46
|
+
"bugs": "https://github.com/taylorbryant/beignet/issues",
|
|
47
|
+
"sideEffects": false,
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18.0.0"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@hookform/resolvers": "^5.0.0",
|
|
56
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
57
|
+
"react-hook-form": "^7.0.0"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@beignet/core": "*"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@hookform/resolvers": "^5.2.2",
|
|
64
|
+
"@testing-library/dom": "^9.3.4",
|
|
65
|
+
"@testing-library/react": "^14.3.1",
|
|
66
|
+
"@types/bun": "^1.3.13",
|
|
67
|
+
"@types/node": "^20.10.0",
|
|
68
|
+
"@types/react": "^18.2.0",
|
|
69
|
+
"happy-dom": "^20.0.11",
|
|
70
|
+
"react": "^18.2.0",
|
|
71
|
+
"react-hook-form": "^7.48.0",
|
|
72
|
+
"typescript": "^5.3.0",
|
|
73
|
+
"zod": "^4.0.0"
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ContractLike,
|
|
3
|
+
type HttpContractConfig,
|
|
4
|
+
type InferOutput,
|
|
5
|
+
type ResolveContract,
|
|
6
|
+
resolveContract,
|
|
7
|
+
type StandardSchemaV1,
|
|
8
|
+
} from "@beignet/core/contracts";
|
|
9
|
+
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
|
|
10
|
+
import {
|
|
11
|
+
type FieldValues,
|
|
12
|
+
useForm as rhfUseForm,
|
|
13
|
+
type UseFormProps,
|
|
14
|
+
type UseFormReturn,
|
|
15
|
+
} from "react-hook-form";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Infer form values from contract body
|
|
19
|
+
*/
|
|
20
|
+
type InferBody<TContract extends HttpContractConfig> =
|
|
21
|
+
TContract["body"] extends StandardSchemaV1
|
|
22
|
+
? InferOutput<TContract["body"]> & FieldValues
|
|
23
|
+
: FieldValues;
|
|
24
|
+
|
|
25
|
+
type FormValues<TContract extends HttpContractConfig> = InferBody<TContract>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Options for React Hook Form with contract support
|
|
29
|
+
*/
|
|
30
|
+
type RhfFormOptions<TContract extends HttpContractConfig> = Omit<
|
|
31
|
+
UseFormProps<FormValues<TContract>>,
|
|
32
|
+
"resolver"
|
|
33
|
+
> & {
|
|
34
|
+
/**
|
|
35
|
+
* Optional override for the resolver. If supplied, this is used instead of
|
|
36
|
+
* the default contract-based resolver.
|
|
37
|
+
*/
|
|
38
|
+
resolver?: UseFormProps<FormValues<TContract>>["resolver"];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Enables or disables automatic resolver generation from the contract body schema.
|
|
42
|
+
* Defaults to true.
|
|
43
|
+
*/
|
|
44
|
+
resolverEnabled?: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* React Hook Form adapter for a contract
|
|
49
|
+
*/
|
|
50
|
+
export type ReactHookFormContractAdapter<TContract extends HttpContractConfig> =
|
|
51
|
+
{
|
|
52
|
+
/**
|
|
53
|
+
* Generate UseFormProps<FormValues<TContract>> for any RHF usage.
|
|
54
|
+
*/
|
|
55
|
+
formOptions: (
|
|
56
|
+
props?: RhfFormOptions<TContract>,
|
|
57
|
+
) => UseFormProps<FormValues<TContract>>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Convenience wrapper around useForm(formOptions(props)).
|
|
61
|
+
*/
|
|
62
|
+
useForm: (
|
|
63
|
+
props?: RhfFormOptions<TContract>,
|
|
64
|
+
) => UseFormReturn<FormValues<TContract>>;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function createReactHookFormAdapter<TContractLike extends ContractLike>(
|
|
68
|
+
contract: TContractLike,
|
|
69
|
+
): ReactHookFormContractAdapter<ResolveContract<TContractLike>> {
|
|
70
|
+
const resolvedContract = resolveContract(contract);
|
|
71
|
+
|
|
72
|
+
if (!resolvedContract.body) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`rhf(${resolvedContract.name}): Contract has no body schema. ` +
|
|
75
|
+
"React Hook Form requires a body schema to generate form fields and validation. " +
|
|
76
|
+
"Use .body(schema) on the contract builder to define the request body shape.",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type Values = FormValues<ResolveContract<TContractLike>>;
|
|
81
|
+
|
|
82
|
+
function formOptions(
|
|
83
|
+
props?: RhfFormOptions<ResolveContract<TContractLike>>,
|
|
84
|
+
): UseFormProps<Values> {
|
|
85
|
+
const bodySchema = resolvedContract.body as
|
|
86
|
+
| StandardSchemaV1<FieldValues>
|
|
87
|
+
| null
|
|
88
|
+
| undefined;
|
|
89
|
+
|
|
90
|
+
const {
|
|
91
|
+
resolverEnabled = true,
|
|
92
|
+
resolver: resolverOverride,
|
|
93
|
+
...rest
|
|
94
|
+
} = props ?? {};
|
|
95
|
+
|
|
96
|
+
const resolver =
|
|
97
|
+
resolverOverride ??
|
|
98
|
+
(bodySchema && resolverEnabled
|
|
99
|
+
? standardSchemaResolver(bodySchema)
|
|
100
|
+
: undefined);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
...(rest as UseFormProps<Values>),
|
|
104
|
+
resolver,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function useForm(
|
|
109
|
+
props?: RhfFormOptions<ResolveContract<TContractLike>>,
|
|
110
|
+
): UseFormReturn<Values> {
|
|
111
|
+
const options = formOptions(props);
|
|
112
|
+
return rhfUseForm<Values>(options);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { formOptions, useForm };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Create a React Hook Form adapter factory.
|
|
120
|
+
*
|
|
121
|
+
* Mirrors the React integration pattern used by `createReactQuery()` and `createNuqs()`:
|
|
122
|
+
* create the adapter once, then bind individual contracts with `rhf(contract)`.
|
|
123
|
+
*/
|
|
124
|
+
export function createReactHookForm() {
|
|
125
|
+
return function rhf<TContractLike extends ContractLike>(
|
|
126
|
+
contract: TContractLike,
|
|
127
|
+
): ReactHookFormContractAdapter<ResolveContract<TContractLike>> {
|
|
128
|
+
return createReactHookFormAdapter(contract);
|
|
129
|
+
};
|
|
130
|
+
}
|