@beignet/react-hook-form 0.0.3 → 0.0.5
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 +35 -0
- package/README.md +104 -19
- package/dist/index.d.ts +46 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -5
- package/src/index.ts +77 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# @beignet/react-hook-form
|
|
2
2
|
|
|
3
|
+
## 0.0.5
|
|
4
|
+
|
|
5
|
+
## 0.0.4
|
|
6
|
+
|
|
7
|
+
### Patch Changes
|
|
8
|
+
|
|
9
|
+
- 8bcb31f: Mark package READMEs with Beignet's experimental alpha status and 0.0.x stability expectations.
|
|
10
|
+
- c1a834d: Generate an app-owned client error helper and align form error handling docs with
|
|
11
|
+
the canonical React Query and React Hook Form mutation path.
|
|
12
|
+
- d137044: Declare `@beignet/core` as a peer dependency with a lockstep version range in
|
|
13
|
+
every integration and provider package instead of a regular `"*"` dependency.
|
|
14
|
+
Installs now always resolve a single shared copy of core, so `instanceof`
|
|
15
|
+
checks such as `isContractError` and upload error identity keep working, and
|
|
16
|
+
mixed Beignet versions fail loudly at install time instead of at runtime.
|
|
17
|
+
|
|
18
|
+
If your package manager does not install peer dependencies automatically, add
|
|
19
|
+
`@beignet/core` to your app alongside these packages. `@beignet/nuqs` now also
|
|
20
|
+
declares `@beignet/react-query` as a peer dependency, and
|
|
21
|
+
`@beignet/provider-storage-s3` now expects you to install
|
|
22
|
+
`@aws-sdk/client-s3` and `@aws-sdk/s3-request-presigner` yourself, matching
|
|
23
|
+
how other providers treat their SDKs.
|
|
24
|
+
|
|
25
|
+
- 1a79090: Emit Node-compatible ESM: all relative imports in published packages now carry explicit .js extensions, fixing ERR_MODULE_NOT_FOUND when running the CLI or importing package dist files under plain Node.
|
|
26
|
+
- 9d1bf0b: Clarify React Hook Form submit-error handling and align generated form examples with Beignet client error semantics.
|
|
27
|
+
- 89390fe: Type form values with the contract body schema's input/output split. Live field
|
|
28
|
+
values (`register`, `watch`, `setValue`, `getValues`, `defaultValues`) now use
|
|
29
|
+
the schema input, and `handleSubmit` callbacks receive the parsed schema
|
|
30
|
+
output, so coercing, transforming, and defaulting body schemas type correctly
|
|
31
|
+
end to end.
|
|
32
|
+
- d6ad8bb: Standardize generated client helpers around `client/index.ts`, add a typed
|
|
33
|
+
React Query invalidation helper, and align package docs with the canonical app
|
|
34
|
+
client entrypoint.
|
|
35
|
+
- 8063d38: Rename the contract front door to `defineContract`/`defineContractGroup`, rename operational commands to tasks (`@beignet/core/tasks`, `defineTasks`, `runTask`, `beignet task run`, `beignet make task`, `server/tasks.ts`, `features/<feature>/tasks/`, `paths.tasks`), and standardize context binding: context-free declarations stay top-level (`defineEvent`), while context-bound definitions come from per-capability factories (`createListeners`, `createJobs`, `createSchedules`, `createNotifications`, `createTasks`) called once in `lib/`. Top-level context-generic `defineListener`, `defineJob`, `defineSchedule`, and `defineNotification` are removed.
|
|
36
|
+
- 192c6ad: Typed clients now attach idempotency keys automatically from contract metadata (override with `idempotencyKey`), React Query mutations keep the key stable across retry attempts, and the shared client error helpers `contractErrorMessage` and `rootFormError` are now exported by @beignet/core/client and @beignet/react-hook-form.
|
|
37
|
+
|
|
3
38
|
## 0.0.3
|
|
4
39
|
|
|
5
40
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
> React Hook Form integration for Beignet
|
|
4
4
|
|
|
5
|
+
> [!CAUTION]
|
|
6
|
+
> Beignet is experimental alpha software. The `0.0.x` package line is for early
|
|
7
|
+
> evaluation, and APIs may change between releases while the framework settles.
|
|
8
|
+
|
|
5
9
|
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
10
|
|
|
7
11
|
## Installation
|
|
@@ -10,7 +14,6 @@ This package provides automatic form validation using your contract's body schem
|
|
|
10
14
|
npm install @beignet/react-hook-form @beignet/core react-hook-form @hookform/resolvers react
|
|
11
15
|
```
|
|
12
16
|
|
|
13
|
-
|
|
14
17
|
## TypeScript requirements
|
|
15
18
|
|
|
16
19
|
This package requires TypeScript 5.0 or higher for proper type inference.
|
|
@@ -35,7 +38,7 @@ function CreateTodoForm() {
|
|
|
35
38
|
});
|
|
36
39
|
|
|
37
40
|
const onSubmit = form.handleSubmit((values) => {
|
|
38
|
-
// values is
|
|
41
|
+
// values is the parsed schema output: { title: string; completed?: boolean }
|
|
39
42
|
console.log("Creating todo:", values);
|
|
40
43
|
});
|
|
41
44
|
|
|
@@ -65,9 +68,9 @@ function CreateTodoForm() {
|
|
|
65
68
|
### With React Query mutation
|
|
66
69
|
|
|
67
70
|
```tsx
|
|
68
|
-
import { createReactHookForm } from "@beignet/react-hook-form";
|
|
71
|
+
import { createReactHookForm, rootFormError } from "@beignet/react-hook-form";
|
|
69
72
|
import { useMutation } from "@tanstack/react-query";
|
|
70
|
-
import { rq } from "@/client
|
|
73
|
+
import { rq } from "@/client";
|
|
71
74
|
import { createTodo } from "@/features/todos/contracts";
|
|
72
75
|
|
|
73
76
|
const rhf = createReactHookForm();
|
|
@@ -79,10 +82,16 @@ function CreateTodoForm() {
|
|
|
79
82
|
});
|
|
80
83
|
|
|
81
84
|
const mutation = useMutation(
|
|
82
|
-
rq(createTodo).mutationOptions(
|
|
85
|
+
rq(createTodo).mutationOptions({
|
|
86
|
+
onSuccess: () => form.reset(),
|
|
87
|
+
onError: (error) => {
|
|
88
|
+
form.setError("root", rootFormError(error, "Could not create the todo."));
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
83
91
|
);
|
|
84
92
|
|
|
85
93
|
const onSubmit = form.handleSubmit((values) => {
|
|
94
|
+
form.clearErrors("root");
|
|
86
95
|
mutation.mutate({ body: values });
|
|
87
96
|
});
|
|
88
97
|
|
|
@@ -92,19 +101,25 @@ function CreateTodoForm() {
|
|
|
92
101
|
{form.formState.errors.title && (
|
|
93
102
|
<p>{form.formState.errors.title.message}</p>
|
|
94
103
|
)}
|
|
104
|
+
{form.formState.errors.root && (
|
|
105
|
+
<p>{form.formState.errors.root.message}</p>
|
|
106
|
+
)}
|
|
95
107
|
|
|
96
108
|
<button type="submit" disabled={mutation.isPending}>
|
|
97
109
|
{mutation.isPending ? "Creating..." : "Create"}
|
|
98
110
|
</button>
|
|
99
|
-
|
|
100
|
-
{mutation.isError && (
|
|
101
|
-
<p className="error">{mutation.error.message}</p>
|
|
102
|
-
)}
|
|
103
111
|
</form>
|
|
104
112
|
);
|
|
105
113
|
}
|
|
106
114
|
```
|
|
107
115
|
|
|
116
|
+
React Hook Form only owns request body fields. Pass path params, query params,
|
|
117
|
+
headers, and auth-derived values to the endpoint call or mutation variables.
|
|
118
|
+
Submit failures come back as Beignet client errors, so map route-owned catalog
|
|
119
|
+
errors, framework validation errors, network failures, and contract drift
|
|
120
|
+
through `rootFormError(...)`, then set `form.setError("root", ...)` or a
|
|
121
|
+
specific field when your server response identifies one.
|
|
122
|
+
|
|
108
123
|
### Disabling validation
|
|
109
124
|
|
|
110
125
|
If you need to disable the schema resolver (e.g., for partial form handling):
|
|
@@ -164,9 +179,33 @@ const form = adapter.useForm({
|
|
|
164
179
|
});
|
|
165
180
|
```
|
|
166
181
|
|
|
182
|
+
### `rootFormError(error, fallback, overrides?)`
|
|
183
|
+
|
|
184
|
+
Maps a failed endpoint call or mutation error to the
|
|
185
|
+
`form.setError("root", ...)` shape. Wraps `contractErrorMessage` from
|
|
186
|
+
`@beignet/core/client`: non-contract errors return the fallback copy,
|
|
187
|
+
client-side input validation failures return a generic "check the highlighted
|
|
188
|
+
fields" message, and catalog codes can override copy per form.
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
form.setError(
|
|
192
|
+
"root",
|
|
193
|
+
rootFormError(error, "Could not update profile.", {
|
|
194
|
+
HANDLE_UNAVAILABLE: "That handle is already taken.",
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
```
|
|
198
|
+
|
|
167
199
|
## Type inference
|
|
168
200
|
|
|
169
|
-
Form
|
|
201
|
+
Form types come from the contract's body schema and follow React Hook Form's
|
|
202
|
+
input/output split:
|
|
203
|
+
|
|
204
|
+
- Live field values — `register`, `watch`, `setValue`, `getValues`, and
|
|
205
|
+
`defaultValues` — use the schema **input**: what the user edits before
|
|
206
|
+
validation runs.
|
|
207
|
+
- `handleSubmit` callbacks receive the schema **output**: the parsed values
|
|
208
|
+
after coercion, transforms, and defaults run.
|
|
170
209
|
|
|
171
210
|
```ts
|
|
172
211
|
// Contract definition
|
|
@@ -187,6 +226,45 @@ form.register("description"); // ✓ Valid
|
|
|
187
226
|
form.register("invalid"); // ✗ Type error
|
|
188
227
|
```
|
|
189
228
|
|
|
229
|
+
For plain schemas like the one above, input and output are identical. For
|
|
230
|
+
coercing or transforming schemas they differ:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
const createPayment = payments
|
|
234
|
+
.post("/api/payments")
|
|
235
|
+
.body(z.object({
|
|
236
|
+
amount: z.string().transform(Number),
|
|
237
|
+
note: z.string().optional(),
|
|
238
|
+
}))
|
|
239
|
+
.responses({ 201: PaymentSchema });
|
|
240
|
+
|
|
241
|
+
const form = rhf(createPayment).useForm({
|
|
242
|
+
defaultValues: { amount: "" }, // input: string
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
form.watch("amount"); // string (input)
|
|
246
|
+
|
|
247
|
+
form.handleSubmit((values) => {
|
|
248
|
+
values.amount; // number (output)
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Submitting transforming schemas
|
|
253
|
+
|
|
254
|
+
The typed client posts the schema input — the server validates and transforms
|
|
255
|
+
the body when it receives the request. Parsed output from `handleSubmit` is
|
|
256
|
+
still valid input for plain, defaulted, and coerced schemas, so
|
|
257
|
+
`mutation.mutate({ body: values })` keeps working for those. When a transform
|
|
258
|
+
changes a field's type, the parsed output no longer matches the contract body
|
|
259
|
+
and TypeScript rejects it. Send the raw field values instead — validation has
|
|
260
|
+
already passed by the time the submit handler runs:
|
|
261
|
+
|
|
262
|
+
```tsx
|
|
263
|
+
const onSubmit = form.handleSubmit(() => {
|
|
264
|
+
mutation.mutate({ body: form.getValues() });
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
190
268
|
## Standard Schema support
|
|
191
269
|
|
|
192
270
|
This package uses the `@hookform/resolvers/standard-schema` resolver, which works with any Standard Schema compatible library:
|
|
@@ -197,11 +275,11 @@ This package uses the `@hookform/resolvers/standard-schema` resolver, which work
|
|
|
197
275
|
|
|
198
276
|
## Validation behavior
|
|
199
277
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
278
|
+
React Hook Form controls when the generated resolver runs. With the default
|
|
279
|
+
React Hook Form settings, the resolver validates before submit and then
|
|
280
|
+
revalidates changed fields after a failed submit. Pass normal React Hook Form
|
|
281
|
+
options such as `mode: "onBlur"` or `reValidateMode: "onChange"` when a form
|
|
282
|
+
needs different timing.
|
|
205
283
|
|
|
206
284
|
Validation errors are available via `form.formState.errors`:
|
|
207
285
|
|
|
@@ -218,7 +296,8 @@ Validation errors are available via `form.formState.errors`:
|
|
|
218
296
|
```tsx
|
|
219
297
|
import { createReactHookForm } from "@beignet/react-hook-form";
|
|
220
298
|
import { useMutation } from "@tanstack/react-query";
|
|
221
|
-
import { rq } from "@/client
|
|
299
|
+
import { rq } from "@/client";
|
|
300
|
+
import { rootFormError } from "@/client/errors";
|
|
222
301
|
import { updateProfile } from "@/features/profile/contracts";
|
|
223
302
|
|
|
224
303
|
const rhf = createReactHookForm();
|
|
@@ -238,10 +317,14 @@ function ProfileForm({ profile }) {
|
|
|
238
317
|
onSuccess: () => {
|
|
239
318
|
toast.success("Profile updated!");
|
|
240
319
|
},
|
|
320
|
+
onError: (error) => {
|
|
321
|
+
form.setError("root", rootFormError(error, "Could not update the profile."));
|
|
322
|
+
},
|
|
241
323
|
})
|
|
242
324
|
);
|
|
243
325
|
|
|
244
326
|
const onSubmit = form.handleSubmit((values) => {
|
|
327
|
+
form.clearErrors("root");
|
|
245
328
|
mutation.mutate({ body: values });
|
|
246
329
|
});
|
|
247
330
|
|
|
@@ -270,6 +353,8 @@ function ProfileForm({ profile }) {
|
|
|
270
353
|
<button type="submit" disabled={!isDirty || isSubmitting}>
|
|
271
354
|
{isSubmitting ? "Saving..." : "Save Changes"}
|
|
272
355
|
</button>
|
|
356
|
+
|
|
357
|
+
{errors.root && <span className="error">{errors.root.message}</span>}
|
|
273
358
|
</form>
|
|
274
359
|
);
|
|
275
360
|
}
|
|
@@ -277,9 +362,9 @@ function ProfileForm({ profile }) {
|
|
|
277
362
|
|
|
278
363
|
## Related packages
|
|
279
364
|
|
|
280
|
-
- [`@beignet/core/contracts`](https://
|
|
281
|
-
- [`@beignet/react-query`](https://
|
|
282
|
-
- [`@beignet/core/client`](https://
|
|
365
|
+
- [`@beignet/core/contracts`](https://beignetjs.com/contracts) - Core contract definitions
|
|
366
|
+
- [`@beignet/react-query`](https://beignetjs.com/react-query) - TanStack Query integration
|
|
367
|
+
- [`@beignet/core/client`](https://beignetjs.com/client) - HTTP client
|
|
283
368
|
|
|
284
369
|
## License
|
|
285
370
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ErrorMessageOverrides } from "@beignet/core/client";
|
|
2
|
+
import { type ContractLike, type HttpContractConfig, type InferInput, type InferOutput, type ResolveContract, type StandardSchemaV1 } from "@beignet/core/contracts";
|
|
2
3
|
import { type FieldValues, type UseFormProps, type UseFormReturn } from "react-hook-form";
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
+
* Constrain a schema input type to React Hook Form field values without
|
|
6
|
+
* widening field name inference for object inputs.
|
|
5
7
|
*/
|
|
6
|
-
type
|
|
7
|
-
|
|
8
|
+
type AsFieldValues<T> = T extends FieldValues ? T : T & FieldValues;
|
|
9
|
+
/**
|
|
10
|
+
* Live form field values inferred from the contract body schema input.
|
|
11
|
+
*
|
|
12
|
+
* React Hook Form field values (`register`, `watch`, `setValue`,
|
|
13
|
+
* `defaultValues`) hold pre-validation input, so coercing or transforming body
|
|
14
|
+
* schemas type fields as what the user edits, not what the schema produces.
|
|
15
|
+
*/
|
|
16
|
+
type FormInput<TContract extends HttpContractConfig> = TContract["body"] extends StandardSchemaV1 ? AsFieldValues<InferInput<TContract["body"]>> : FieldValues;
|
|
17
|
+
/**
|
|
18
|
+
* Validated submit values inferred from the contract body schema output.
|
|
19
|
+
*
|
|
20
|
+
* `handleSubmit` callbacks receive the resolver-parsed output.
|
|
21
|
+
*/
|
|
22
|
+
type FormOutput<TContract extends HttpContractConfig> = TContract["body"] extends StandardSchemaV1 ? InferOutput<TContract["body"]> : FieldValues;
|
|
8
23
|
/**
|
|
9
24
|
* React Hook Form options accepted by the Beignet adapter.
|
|
10
25
|
*/
|
|
11
|
-
type RhfFormOptions<TContract extends HttpContractConfig> = Omit<UseFormProps<
|
|
26
|
+
type RhfFormOptions<TContract extends HttpContractConfig> = Omit<UseFormProps<FormInput<TContract>, unknown, FormOutput<TContract>>, "resolver"> & {
|
|
12
27
|
/**
|
|
13
28
|
* Optional override for the resolver. If supplied, this is used instead of
|
|
14
29
|
* the default contract-based resolver.
|
|
15
30
|
*/
|
|
16
|
-
resolver?: UseFormProps<
|
|
31
|
+
resolver?: UseFormProps<FormInput<TContract>, unknown, FormOutput<TContract>>["resolver"];
|
|
17
32
|
/**
|
|
18
33
|
* Enables or disables automatic resolver generation from the contract body schema.
|
|
19
34
|
* Defaults to true.
|
|
@@ -27,11 +42,11 @@ export type ReactHookFormContractAdapter<TContract extends HttpContractConfig> =
|
|
|
27
42
|
/**
|
|
28
43
|
* Generate `UseFormProps` for any React Hook Form usage.
|
|
29
44
|
*/
|
|
30
|
-
formOptions: (props?: RhfFormOptions<TContract>) => UseFormProps<
|
|
45
|
+
formOptions: (props?: RhfFormOptions<TContract>) => UseFormProps<FormInput<TContract>, unknown, FormOutput<TContract>>;
|
|
31
46
|
/**
|
|
32
47
|
* Convenience wrapper around `useForm(formOptions(props))`.
|
|
33
48
|
*/
|
|
34
|
-
useForm: (props?: RhfFormOptions<TContract>) => UseFormReturn<
|
|
49
|
+
useForm: (props?: RhfFormOptions<TContract>) => UseFormReturn<FormInput<TContract>, unknown, FormOutput<TContract>>;
|
|
35
50
|
};
|
|
36
51
|
/**
|
|
37
52
|
* Create a React Hook Form adapter factory.
|
|
@@ -42,5 +57,28 @@ export type ReactHookFormContractAdapter<TContract extends HttpContractConfig> =
|
|
|
42
57
|
* disable the generated resolver through `formOptions(...)` or `useForm(...)`.
|
|
43
58
|
*/
|
|
44
59
|
export declare function createReactHookForm(): <TContractLike extends ContractLike>(contract: TContractLike) => ReactHookFormContractAdapter<ResolveContract<TContractLike>>;
|
|
60
|
+
/**
|
|
61
|
+
* Root-level form error for `form.setError("root", ...)`.
|
|
62
|
+
*/
|
|
63
|
+
export type RootFormError = {
|
|
64
|
+
type: "server";
|
|
65
|
+
message: string;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Map a failed endpoint call or mutation error to a root-level form error.
|
|
69
|
+
*
|
|
70
|
+
* Wraps `contractErrorMessage` from `@beignet/core/client`, so non-contract
|
|
71
|
+
* errors get the fallback copy and catalog codes can be overridden per form:
|
|
72
|
+
*
|
|
73
|
+
* ```ts
|
|
74
|
+
* form.setError(
|
|
75
|
+
* "root",
|
|
76
|
+
* rootFormError(error, "Could not update profile.", {
|
|
77
|
+
* HANDLE_UNAVAILABLE: "That handle is already taken.",
|
|
78
|
+
* }),
|
|
79
|
+
* );
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export declare function rootFormError(error: unknown, fallback: string, overrides?: ErrorMessageOverrides): RootFormError;
|
|
45
83
|
export {};
|
|
46
84
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,UAAU,EACf,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;;;GAGG;AACH,KAAK,aAAa,CAAC,CAAC,IAAI,CAAC,SAAS,WAAW,GAAG,CAAC,GAAG,CAAC,GAAG,WAAW,CAAC;AAEpE;;;;;;GAMG;AACH,KAAK,SAAS,CAAC,SAAS,SAAS,kBAAkB,IACjD,SAAS,CAAC,MAAM,CAAC,SAAS,gBAAgB,GACtC,aAAa,CAAC,UAAU,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,GAC5C,WAAW,CAAC;AAElB;;;;GAIG;AACH,KAAK,UAAU,CAAC,SAAS,SAAS,kBAAkB,IAClD,SAAS,CAAC,MAAM,CAAC,SAAS,gBAAgB,GACtC,WAAW,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,GAC9B,WAAW,CAAC;AAElB;;GAEG;AACH,KAAK,cAAc,CAAC,SAAS,SAAS,kBAAkB,IAAI,IAAI,CAC9D,YAAY,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC,EAClE,UAAU,CACX,GAAG;IACF;;;OAGG;IACH,QAAQ,CAAC,EAAE,YAAY,CACrB,SAAS,CAAC,SAAS,CAAC,EACpB,OAAO,EACP,UAAU,CAAC,SAAS,CAAC,CACtB,CAAC,UAAU,CAAC,CAAC;IAEd;;;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,SAAS,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;IAExE;;OAEG;IACH,OAAO,EAAE,CACP,KAAK,CAAC,EAAE,cAAc,CAAC,SAAS,CAAC,KAC9B,aAAa,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;CAC1E,CAAC;AAsDJ;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,KACb,aAAa,SAAS,YAAY,EACpD,UAAU,aAAa,KACtB,4BAA4B,CAAC,eAAe,CAAC,aAAa,CAAC,CAAC,CAGhE;AAED;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,qBAAqB,GAChC,aAAa,CAKf"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { contractErrorMessage, } from "@beignet/core/client";
|
|
1
2
|
import { resolveContract, } from "@beignet/core/contracts";
|
|
2
3
|
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
|
|
3
4
|
import { useForm as rhfUseForm, } from "react-hook-form";
|
|
@@ -39,4 +40,25 @@ export function createReactHookForm() {
|
|
|
39
40
|
return createReactHookFormAdapter(contract);
|
|
40
41
|
};
|
|
41
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Map a failed endpoint call or mutation error to a root-level form error.
|
|
45
|
+
*
|
|
46
|
+
* Wraps `contractErrorMessage` from `@beignet/core/client`, so non-contract
|
|
47
|
+
* errors get the fallback copy and catalog codes can be overridden per form:
|
|
48
|
+
*
|
|
49
|
+
* ```ts
|
|
50
|
+
* form.setError(
|
|
51
|
+
* "root",
|
|
52
|
+
* rootFormError(error, "Could not update profile.", {
|
|
53
|
+
* HANDLE_UNAVAILABLE: "That handle is already taken.",
|
|
54
|
+
* }),
|
|
55
|
+
* );
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function rootFormError(error, fallback, overrides) {
|
|
59
|
+
return {
|
|
60
|
+
type: "server",
|
|
61
|
+
message: contractErrorMessage(error, fallback, overrides),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
42
64
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,GAErB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAML,eAAe,GAEhB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAC7E,OAAO,EAEL,OAAO,IAAI,UAAU,GAGtB,MAAM,iBAAiB,CAAC;AA0EzB,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;IAKD,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,CAAyB,UAAU,CAAC;gBAC5D,CAAC,CAAC,SAAS,CAAC,CAAC;QAEjB,OAAO;YACL,GAAI,IAA6C;YACjD,QAAQ;SACT,CAAC;IACJ,CAAC;IAED,SAAS,OAAO,CACd,KAAsD;QAEtD,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;QACnC,OAAO,UAAU,CAAyB,OAAO,CAAC,CAAC;IACrD,CAAC;IAED,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC;AAClC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAO,SAAS,GAAG,CACjB,QAAuB;QAEvB,OAAO,0BAA0B,CAAC,QAAQ,CAAC,CAAC;IAC9C,CAAC,CAAC;AACJ,CAAC;AAUD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,aAAa,CAC3B,KAAc,EACd,QAAgB,EAChB,SAAiC;IAEjC,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,OAAO,EAAE,oBAAoB,CAAC,KAAK,EAAE,QAAQ,EAAE,SAAS,CAAC;KAC1D,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beignet/react-hook-form",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "React Hook Form integration for Beignet with Standard Schema support",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
"build": "tsc",
|
|
25
25
|
"dev": "tsc --watch",
|
|
26
26
|
"clean": "rm -rf dist coverage .turbo",
|
|
27
|
-
"test": "bun test",
|
|
27
|
+
"test": "bun test && bun run typecheck:types",
|
|
28
28
|
"test:coverage": "bun test --coverage",
|
|
29
|
+
"typecheck:types": "tsc -p tsconfig.type-tests.json",
|
|
29
30
|
"lint": "biome check ."
|
|
30
31
|
},
|
|
31
32
|
"keywords": [
|
|
@@ -52,13 +53,11 @@
|
|
|
52
53
|
"node": ">=18.0.0"
|
|
53
54
|
},
|
|
54
55
|
"peerDependencies": {
|
|
56
|
+
"@beignet/core": ">=0.0.3 <1.0.0",
|
|
55
57
|
"@hookform/resolvers": "^5.0.0",
|
|
56
58
|
"react": "^18.0.0 || ^19.0.0",
|
|
57
59
|
"react-hook-form": "^7.0.0"
|
|
58
60
|
},
|
|
59
|
-
"dependencies": {
|
|
60
|
-
"@beignet/core": "*"
|
|
61
|
-
},
|
|
62
61
|
"devDependencies": {
|
|
63
62
|
"@hookform/resolvers": "^5.2.2",
|
|
64
63
|
"@testing-library/dom": "^9.3.4",
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
contractErrorMessage,
|
|
3
|
+
type ErrorMessageOverrides,
|
|
4
|
+
} from "@beignet/core/client";
|
|
1
5
|
import {
|
|
2
6
|
type ContractLike,
|
|
3
7
|
type HttpContractConfig,
|
|
8
|
+
type InferInput,
|
|
4
9
|
type InferOutput,
|
|
5
10
|
type ResolveContract,
|
|
6
11
|
resolveContract,
|
|
@@ -15,27 +20,49 @@ import {
|
|
|
15
20
|
} from "react-hook-form";
|
|
16
21
|
|
|
17
22
|
/**
|
|
18
|
-
*
|
|
23
|
+
* Constrain a schema input type to React Hook Form field values without
|
|
24
|
+
* widening field name inference for object inputs.
|
|
25
|
+
*/
|
|
26
|
+
type AsFieldValues<T> = T extends FieldValues ? T : T & FieldValues;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Live form field values inferred from the contract body schema input.
|
|
30
|
+
*
|
|
31
|
+
* React Hook Form field values (`register`, `watch`, `setValue`,
|
|
32
|
+
* `defaultValues`) hold pre-validation input, so coercing or transforming body
|
|
33
|
+
* schemas type fields as what the user edits, not what the schema produces.
|
|
19
34
|
*/
|
|
20
|
-
type
|
|
35
|
+
type FormInput<TContract extends HttpContractConfig> =
|
|
21
36
|
TContract["body"] extends StandardSchemaV1
|
|
22
|
-
?
|
|
37
|
+
? AsFieldValues<InferInput<TContract["body"]>>
|
|
23
38
|
: FieldValues;
|
|
24
39
|
|
|
25
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Validated submit values inferred from the contract body schema output.
|
|
42
|
+
*
|
|
43
|
+
* `handleSubmit` callbacks receive the resolver-parsed output.
|
|
44
|
+
*/
|
|
45
|
+
type FormOutput<TContract extends HttpContractConfig> =
|
|
46
|
+
TContract["body"] extends StandardSchemaV1
|
|
47
|
+
? InferOutput<TContract["body"]>
|
|
48
|
+
: FieldValues;
|
|
26
49
|
|
|
27
50
|
/**
|
|
28
51
|
* React Hook Form options accepted by the Beignet adapter.
|
|
29
52
|
*/
|
|
30
53
|
type RhfFormOptions<TContract extends HttpContractConfig> = Omit<
|
|
31
|
-
UseFormProps<
|
|
54
|
+
UseFormProps<FormInput<TContract>, unknown, FormOutput<TContract>>,
|
|
32
55
|
"resolver"
|
|
33
56
|
> & {
|
|
34
57
|
/**
|
|
35
58
|
* Optional override for the resolver. If supplied, this is used instead of
|
|
36
59
|
* the default contract-based resolver.
|
|
37
60
|
*/
|
|
38
|
-
resolver?: UseFormProps<
|
|
61
|
+
resolver?: UseFormProps<
|
|
62
|
+
FormInput<TContract>,
|
|
63
|
+
unknown,
|
|
64
|
+
FormOutput<TContract>
|
|
65
|
+
>["resolver"];
|
|
39
66
|
|
|
40
67
|
/**
|
|
41
68
|
* Enables or disables automatic resolver generation from the contract body schema.
|
|
@@ -54,14 +81,14 @@ export type ReactHookFormContractAdapter<TContract extends HttpContractConfig> =
|
|
|
54
81
|
*/
|
|
55
82
|
formOptions: (
|
|
56
83
|
props?: RhfFormOptions<TContract>,
|
|
57
|
-
) => UseFormProps<
|
|
84
|
+
) => UseFormProps<FormInput<TContract>, unknown, FormOutput<TContract>>;
|
|
58
85
|
|
|
59
86
|
/**
|
|
60
87
|
* Convenience wrapper around `useForm(formOptions(props))`.
|
|
61
88
|
*/
|
|
62
89
|
useForm: (
|
|
63
90
|
props?: RhfFormOptions<TContract>,
|
|
64
|
-
) => UseFormReturn<
|
|
91
|
+
) => UseFormReturn<FormInput<TContract>, unknown, FormOutput<TContract>>;
|
|
65
92
|
};
|
|
66
93
|
|
|
67
94
|
function createReactHookFormAdapter<TContractLike extends ContractLike>(
|
|
@@ -77,13 +104,14 @@ function createReactHookFormAdapter<TContractLike extends ContractLike>(
|
|
|
77
104
|
);
|
|
78
105
|
}
|
|
79
106
|
|
|
80
|
-
type
|
|
107
|
+
type Input = FormInput<ResolveContract<TContractLike>>;
|
|
108
|
+
type Output = FormOutput<ResolveContract<TContractLike>>;
|
|
81
109
|
|
|
82
110
|
function formOptions(
|
|
83
111
|
props?: RhfFormOptions<ResolveContract<TContractLike>>,
|
|
84
|
-
): UseFormProps<
|
|
112
|
+
): UseFormProps<Input, unknown, Output> {
|
|
85
113
|
const bodySchema = resolvedContract.body as
|
|
86
|
-
| StandardSchemaV1<
|
|
114
|
+
| StandardSchemaV1<Input, Output>
|
|
87
115
|
| null
|
|
88
116
|
| undefined;
|
|
89
117
|
|
|
@@ -96,20 +124,20 @@ function createReactHookFormAdapter<TContractLike extends ContractLike>(
|
|
|
96
124
|
const resolver =
|
|
97
125
|
resolverOverride ??
|
|
98
126
|
(bodySchema && resolverEnabled
|
|
99
|
-
? standardSchemaResolver(bodySchema)
|
|
127
|
+
? standardSchemaResolver<Input, unknown, Output>(bodySchema)
|
|
100
128
|
: undefined);
|
|
101
129
|
|
|
102
130
|
return {
|
|
103
|
-
...(rest as UseFormProps<
|
|
131
|
+
...(rest as UseFormProps<Input, unknown, Output>),
|
|
104
132
|
resolver,
|
|
105
133
|
};
|
|
106
134
|
}
|
|
107
135
|
|
|
108
136
|
function useForm(
|
|
109
137
|
props?: RhfFormOptions<ResolveContract<TContractLike>>,
|
|
110
|
-
): UseFormReturn<
|
|
138
|
+
): UseFormReturn<Input, unknown, Output> {
|
|
111
139
|
const options = formOptions(props);
|
|
112
|
-
return rhfUseForm<
|
|
140
|
+
return rhfUseForm<Input, unknown, Output>(options);
|
|
113
141
|
}
|
|
114
142
|
|
|
115
143
|
return { formOptions, useForm };
|
|
@@ -130,3 +158,37 @@ export function createReactHookForm() {
|
|
|
130
158
|
return createReactHookFormAdapter(contract);
|
|
131
159
|
};
|
|
132
160
|
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Root-level form error for `form.setError("root", ...)`.
|
|
164
|
+
*/
|
|
165
|
+
export type RootFormError = {
|
|
166
|
+
type: "server";
|
|
167
|
+
message: string;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Map a failed endpoint call or mutation error to a root-level form error.
|
|
172
|
+
*
|
|
173
|
+
* Wraps `contractErrorMessage` from `@beignet/core/client`, so non-contract
|
|
174
|
+
* errors get the fallback copy and catalog codes can be overridden per form:
|
|
175
|
+
*
|
|
176
|
+
* ```ts
|
|
177
|
+
* form.setError(
|
|
178
|
+
* "root",
|
|
179
|
+
* rootFormError(error, "Could not update profile.", {
|
|
180
|
+
* HANDLE_UNAVAILABLE: "That handle is already taken.",
|
|
181
|
+
* }),
|
|
182
|
+
* );
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
export function rootFormError(
|
|
186
|
+
error: unknown,
|
|
187
|
+
fallback: string,
|
|
188
|
+
overrides?: ErrorMessageOverrides,
|
|
189
|
+
): RootFormError {
|
|
190
|
+
return {
|
|
191
|
+
type: "server",
|
|
192
|
+
message: contractErrorMessage(error, fallback, overrides),
|
|
193
|
+
};
|
|
194
|
+
}
|