@beignet/react-hook-form 0.0.57 → 0.0.59
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 +21 -0
- package/README.md +96 -78
- package/dist/index.d.ts +117 -59
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -17
- package/dist/index.js.map +1 -1
- package/dist/input-snapshot.d.ts +7 -0
- package/dist/input-snapshot.d.ts.map +1 -0
- package/dist/input-snapshot.js +46 -0
- package/dist/input-snapshot.js.map +1 -0
- package/package.json +4 -4
- package/skills/forms/SKILL.md +45 -18
- package/src/index.ts +323 -103
- package/src/input-snapshot.ts +42 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot data containers without manufacturing invalid copies of opaque
|
|
3
|
+
* instances (which may depend on private fields or native internal slots).
|
|
4
|
+
* Opaque instances and functions remain references and must be treated as immutable.
|
|
5
|
+
*/
|
|
6
|
+
export function snapshotInput(input) {
|
|
7
|
+
const seen = new WeakMap();
|
|
8
|
+
function copy(value) {
|
|
9
|
+
if (value === null || typeof value !== "object")
|
|
10
|
+
return value;
|
|
11
|
+
const previous = seen.get(value);
|
|
12
|
+
if (previous)
|
|
13
|
+
return previous;
|
|
14
|
+
const prototype = Object.getPrototypeOf(value);
|
|
15
|
+
let snapshot;
|
|
16
|
+
if (prototype === Date.prototype) {
|
|
17
|
+
snapshot = new Date(Date.prototype.getTime.call(value));
|
|
18
|
+
}
|
|
19
|
+
else if (Array.isArray(value) && prototype === Array.prototype) {
|
|
20
|
+
snapshot = new Array(value.length);
|
|
21
|
+
}
|
|
22
|
+
else if (prototype === Object.prototype || prototype === null) {
|
|
23
|
+
snapshot = Object.create(prototype);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
seen.set(value, snapshot);
|
|
29
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
30
|
+
if (Array.isArray(value) && key === "length")
|
|
31
|
+
continue;
|
|
32
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
33
|
+
if (!descriptor)
|
|
34
|
+
continue;
|
|
35
|
+
Object.defineProperty(snapshot, key, {
|
|
36
|
+
value: copy(Reflect.get(value, key)),
|
|
37
|
+
enumerable: descriptor.enumerable,
|
|
38
|
+
configurable: true,
|
|
39
|
+
writable: true,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return snapshot;
|
|
43
|
+
}
|
|
44
|
+
return copy(input);
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=input-snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input-snapshot.js","sourceRoot":"","sources":["../src/input-snapshot.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAI,KAAQ;IACvC,MAAM,IAAI,GAAG,IAAI,OAAO,EAAkB,CAAC;IAE3C,SAAS,IAAI,CAAC,KAAc;QAC1B,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAC9D,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACjC,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAE9B,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;QAC/C,IAAI,QAAgB,CAAC;QACrB,IAAI,SAAS,KAAK,IAAI,CAAC,SAAS,EAAE,CAAC;YACjC,QAAQ,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1D,CAAC;aAAM,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,SAAS,KAAK,KAAK,CAAC,SAAS,EAAE,CAAC;YACjE,QAAQ,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACrC,CAAC;aAAM,IAAI,SAAS,KAAK,MAAM,CAAC,SAAS,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;YAChE,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACtC,CAAC;aAAM,CAAC;YACN,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QAE1B,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACzC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,GAAG,KAAK,QAAQ;gBAAE,SAAS;YACvD,MAAM,UAAU,GAAG,MAAM,CAAC,wBAAwB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;YAC/D,IAAI,CAAC,UAAU;gBAAE,SAAS;YAC1B,MAAM,CAAC,cAAc,CAAC,QAAQ,EAAE,GAAG,EAAE;gBACnC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;gBACpC,UAAU,EAAE,UAAU,CAAC,UAAU;gBACjC,YAAY,EAAE,IAAI;gBAClB,QAAQ,EAAE,IAAI;aACf,CAAC,CAAC;QACL,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,OAAO,IAAI,CAAC,KAAK,CAAM,CAAC;AAC1B,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.59",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "React Hook Form integration for Beignet with Standard Schema support",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"clean": "rm -rf dist coverage .turbo",
|
|
29
29
|
"test": "bun test && bun run typecheck:types",
|
|
30
30
|
"test:coverage": "bun test --coverage",
|
|
31
|
-
"typecheck:types": "tsc -p tsconfig.type-tests.json",
|
|
31
|
+
"typecheck:types": "bun run build && tsc -p tsconfig.type-tests.json && tsc -p tsconfig.consumer-tests.json",
|
|
32
32
|
"lint": "biome check ."
|
|
33
33
|
},
|
|
34
34
|
"keywords": [
|
|
@@ -69,10 +69,10 @@
|
|
|
69
69
|
"@types/bun": "^1.4.0",
|
|
70
70
|
"@types/node": "^22.0.0",
|
|
71
71
|
"@types/react": "^18.2.0",
|
|
72
|
-
"happy-dom": "^
|
|
72
|
+
"happy-dom": "^21.0.4",
|
|
73
73
|
"react": "^18.2.0",
|
|
74
74
|
"react-hook-form": "^7.48.0",
|
|
75
75
|
"typescript": "^5.3.0",
|
|
76
|
-
"zod": "^4.
|
|
76
|
+
"zod": "^4.6.6"
|
|
77
77
|
}
|
|
78
78
|
}
|
package/skills/forms/SKILL.md
CHANGED
|
@@ -26,14 +26,15 @@ routes, providers, infra adapters, `server/`, or `app-context.ts`.
|
|
|
26
26
|
|
|
27
27
|
## Contract Requirements
|
|
28
28
|
|
|
29
|
-
React Hook Form binds to the contract body schema
|
|
29
|
+
React Hook Form binds to the contract body schema by default. Pass
|
|
30
|
+
`rhf(contract, { schema })` for a separate form schema whose parsed output is
|
|
31
|
+
assignable to the contract body input. TypeScript rejects contracts
|
|
30
32
|
without `.body(...)` because there are no form fields or body validation to
|
|
31
33
|
infer; untyped JavaScript calls receive the same failure at runtime.
|
|
32
34
|
|
|
33
|
-
The
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
backed by a form.
|
|
35
|
+
The selected form schema input must be an object with named fields. Scalar or
|
|
36
|
+
array request bodies need an explicit object-input form schema that transforms
|
|
37
|
+
named fields into the required body shape.
|
|
37
38
|
|
|
38
39
|
Keep shared form DTOs in `features/<feature>/schemas.ts`, then reuse them from
|
|
39
40
|
contracts, use cases, tests, and components.
|
|
@@ -86,17 +87,36 @@ export function CreateTodoForm() {
|
|
|
86
87
|
```
|
|
87
88
|
|
|
88
89
|
Use `formOptions(...)` when a component or wrapper wants options without
|
|
89
|
-
calling the hook directly.
|
|
90
|
+
calling the hook directly. Returned options fit native `UseFormProps`, `useForm`,
|
|
91
|
+
and `createFormControl` with `exactOptionalPropertyTypes` enabled.
|
|
90
92
|
|
|
91
93
|
## Input And Output Types
|
|
92
94
|
|
|
93
|
-
React Hook Form field state uses
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
`
|
|
95
|
+
React Hook Form field state uses schema input; default `handleSubmit` values use
|
|
96
|
+
parsed output, including transforms, defaults, and removal of unknown keys.
|
|
97
|
+
Use `submitValues: "raw"` only when original validated input is needed. Use
|
|
98
|
+
`defaultValues` for visible defaults; parsing does not replace live field state.
|
|
99
|
+
|
|
100
|
+
Prefer numeric HTTP contracts (`z.number()`) and numeric field bindings
|
|
101
|
+
(`valueAsNumber`) when the API accepts numbers. Use a separate form schema when
|
|
102
|
+
UI values need conversion: `rhf(contract, { schema: formSchema })`. The form
|
|
103
|
+
schema owns validation; derive it from shared request validators to preserve
|
|
104
|
+
constraints. Its output must fit the contract's input. Keep schemas in feature
|
|
105
|
+
`schemas.ts` and reusable bindings in feature `client/forms.ts`.
|
|
106
|
+
|
|
107
|
+
Shared coercing schemas are appropriate when the API intentionally accepts
|
|
108
|
+
those inputs. Reuse transforms only when parsed output is accepted as input and
|
|
109
|
+
parsing again preserves its meaning. One-way conversions belong in a separate
|
|
110
|
+
form schema, or use raw mode to submit the original contract input. Raw mode
|
|
111
|
+
with a separate form schema may still need mapping into contract input.
|
|
112
|
+
|
|
113
|
+
Generated resolvers snapshot plain objects, arrays, and dates. Opaque instances
|
|
114
|
+
retain identity and must stay immutable during validation and submission.
|
|
115
|
+
Disabled resolvers submit input; dynamic enable flags return an input/output
|
|
116
|
+
union. Explicit parsed mode requires validation. Custom resolvers own output
|
|
117
|
+
and cannot combine with `submitValues`. Prefer adapter `useForm` for inference;
|
|
118
|
+
direct `useForm(formOptions(...))` may need explicit input, context, and output
|
|
119
|
+
generics with partial defaults.
|
|
100
120
|
|
|
101
121
|
## Error Mapping
|
|
102
122
|
|
|
@@ -122,11 +142,9 @@ codes declared on the contract with `.errors(...)`.
|
|
|
122
142
|
params, headers, tenant IDs, and auth-derived values belong in mutation
|
|
123
143
|
variables or app context, not form fields.
|
|
124
144
|
- Contracts without `.body(...)` are not form contracts.
|
|
125
|
-
- Scalar and array request bodies
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
types, submit `form.getValues()` after validation when the contract client
|
|
129
|
-
expects body input.
|
|
145
|
+
- Scalar and array request bodies need a separate object-input form schema.
|
|
146
|
+
- `handleSubmit` returns parsed output by default. Submit callback values
|
|
147
|
+
directly rather than rereading `form.getValues()` after asynchronous validation.
|
|
130
148
|
- Keep the root form helper in `client/forms.ts` unless the app already uses a
|
|
131
149
|
different local convention.
|
|
132
150
|
- Do not import use cases or server modules to reuse validation. Move shared
|
|
@@ -138,6 +156,15 @@ Leave the generated Standard Schema resolver enabled by default. Use
|
|
|
138
156
|
`resolverEnabled: false` only for deliberate partial or multi-step flows, and
|
|
139
157
|
keep final validation in use cases.
|
|
140
158
|
|
|
159
|
+
Forwarded `UseFormProps` and optional resolver settings are supported. A missing
|
|
160
|
+
custom resolver uses generated validation unless disabled; submit types include
|
|
161
|
+
the custom output and any possible parsed or unparsed fallback.
|
|
162
|
+
Wrappers may forward an `undefined` options object; this uses generated validation
|
|
163
|
+
and parsed output, which is included in the inferred submit union.
|
|
164
|
+
Returned props remain optional when the whole options object may be absent.
|
|
165
|
+
Conditional configurations can select a custom resolver or a generated submit
|
|
166
|
+
mode, with output inferred per branch. Do not set both in the same configuration.
|
|
167
|
+
|
|
141
168
|
After form changes, run from the app root:
|
|
142
169
|
|
|
143
170
|
```bash
|
package/src/index.ts
CHANGED
|
@@ -16,10 +16,12 @@ import {
|
|
|
16
16
|
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
|
|
17
17
|
import {
|
|
18
18
|
type FieldValues,
|
|
19
|
+
type Resolver,
|
|
19
20
|
useForm as rhfUseForm,
|
|
20
21
|
type UseFormProps,
|
|
21
22
|
type UseFormReturn,
|
|
22
23
|
} from "react-hook-form";
|
|
24
|
+
import { snapshotInput } from "./input-snapshot.js";
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* Keep only object-shaped React Hook Form field values. Arrays are objects in
|
|
@@ -32,95 +34,269 @@ type ObjectFieldValues<T> = [T] extends [FieldValues]
|
|
|
32
34
|
: never
|
|
33
35
|
: never;
|
|
34
36
|
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
*
|
|
38
|
-
* React Hook Form field values (`register`, `watch`, `setValue`,
|
|
39
|
-
* `defaultValues`) hold pre-validation input, so coercing or transforming body
|
|
40
|
-
* schemas type fields as what the user edits, not what the schema produces.
|
|
41
|
-
*/
|
|
42
|
-
type FormInput<TContract extends HttpContractConfig> =
|
|
43
|
-
TContract["body"] extends StandardSchemaV1
|
|
44
|
-
? ObjectFieldValues<InferInput<TContract["body"]>>
|
|
45
|
-
: never;
|
|
37
|
+
/** Live fields and defaults use the selected schema's input. */
|
|
38
|
+
type FormInput<S extends StandardSchemaV1> = ObjectFieldValues<InferInput<S>>;
|
|
46
39
|
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
*
|
|
50
|
-
* `handleSubmit` callbacks receive the resolver-parsed output.
|
|
51
|
-
*/
|
|
52
|
-
type FormOutput<TContract extends HttpContractConfig> =
|
|
53
|
-
TContract["body"] extends StandardSchemaV1
|
|
54
|
-
? InferOutput<TContract["body"]>
|
|
55
|
-
: never;
|
|
40
|
+
/** Parsed submissions use the selected schema's output. */
|
|
41
|
+
type FormOutput<S extends StandardSchemaV1> = InferOutput<S>;
|
|
56
42
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
43
|
+
type BodySchema<C extends HttpContractConfig> = Extract<
|
|
44
|
+
C["body"],
|
|
45
|
+
StandardSchemaV1
|
|
46
|
+
>;
|
|
47
|
+
|
|
48
|
+
type ReactHookFormContract<T extends ContractLike> = [
|
|
49
|
+
FormInput<BodySchema<ResolveContract<T>>>,
|
|
62
50
|
] extends [never]
|
|
63
51
|
? never
|
|
64
|
-
:
|
|
52
|
+
: T;
|
|
65
53
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
54
|
+
/** A form-specific schema produces the request input accepted by the contract. */
|
|
55
|
+
type CompatibleFormSchema<
|
|
56
|
+
C extends HttpContractConfig,
|
|
57
|
+
S extends StandardSchemaV1,
|
|
58
|
+
> = [BodySchema<C>] extends [never]
|
|
59
|
+
? never
|
|
60
|
+
: [FormInput<S>] extends [never]
|
|
61
|
+
? never
|
|
62
|
+
: [FormOutput<S>] extends [InferInput<BodySchema<C>>]
|
|
63
|
+
? S
|
|
64
|
+
: never;
|
|
65
|
+
|
|
66
|
+
type RhfFormOptions<S extends StandardSchemaV1, Output> = Omit<
|
|
67
|
+
UseFormProps<FormInput<S>, unknown, Output>,
|
|
71
68
|
"resolver"
|
|
69
|
+
>;
|
|
70
|
+
|
|
71
|
+
type ParsedFormOptions<S extends StandardSchemaV1> = RhfFormOptions<
|
|
72
|
+
S,
|
|
73
|
+
FormOutput<S>
|
|
72
74
|
> & {
|
|
73
|
-
/**
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
75
|
+
/** Submit parsed schema output. Default: parsed. Requires the generated resolver. */
|
|
76
|
+
submitValues?: "parsed";
|
|
77
|
+
resolver?: undefined;
|
|
78
|
+
/** Generate the schema resolver. Default: true. */
|
|
79
|
+
resolverEnabled?: true;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
type RawFormOptions<S extends StandardSchemaV1> = RhfFormOptions<
|
|
83
|
+
S,
|
|
84
|
+
FormInput<S>
|
|
85
|
+
> & {
|
|
86
|
+
/** Validate with the selected schema and submit original input. */
|
|
87
|
+
submitValues: "raw";
|
|
88
|
+
resolver?: undefined;
|
|
87
89
|
resolverEnabled?: boolean;
|
|
88
90
|
};
|
|
89
91
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
92
|
+
type DisabledFormOptions<S extends StandardSchemaV1> = RhfFormOptions<
|
|
93
|
+
S,
|
|
94
|
+
FormInput<S>
|
|
95
|
+
> & {
|
|
96
|
+
resolverEnabled: false;
|
|
97
|
+
resolver?: undefined;
|
|
98
|
+
submitValues?: "raw";
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
type VariableFormOptions<S extends StandardSchemaV1> = RhfFormOptions<
|
|
102
|
+
S,
|
|
103
|
+
FormInput<S> | FormOutput<S>
|
|
104
|
+
> & { resolver?: undefined } & (
|
|
105
|
+
| { submitValues?: "raw" | "parsed" | undefined; resolverEnabled?: true }
|
|
106
|
+
| { submitValues?: "raw"; resolverEnabled?: boolean | undefined }
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
type CustomFormOptions<S extends StandardSchemaV1, Output> = RhfFormOptions<
|
|
110
|
+
S,
|
|
111
|
+
Output
|
|
112
|
+
> & {
|
|
113
|
+
/** A custom resolver owns its submit output, independently of the schema. */
|
|
114
|
+
resolver: Resolver<FormInput<S>, unknown, Output>;
|
|
115
|
+
resolverEnabled?: boolean;
|
|
116
|
+
submitValues?: never;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
type OptionalResolverFormOptions<
|
|
120
|
+
S extends StandardSchemaV1,
|
|
121
|
+
Output,
|
|
122
|
+
> = RhfFormOptions<S, Output> & {
|
|
123
|
+
resolver?: Resolver<FormInput<S>, unknown, Output> | undefined;
|
|
124
|
+
resolverEnabled?: boolean | undefined;
|
|
125
|
+
submitValues?: never;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
type SuppliedResolverFormOptions<
|
|
129
|
+
S extends StandardSchemaV1,
|
|
130
|
+
Output,
|
|
131
|
+
> = OptionalResolverFormOptions<S, Output> & {
|
|
132
|
+
resolver: Resolver<FormInput<S>, unknown, Output> | undefined;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** An absent override falls back to schema parsing unless generation is disabled. */
|
|
136
|
+
type ResolverFallbackOutput<S extends StandardSchemaV1, Props> = Props extends {
|
|
137
|
+
resolverEnabled: false;
|
|
138
|
+
}
|
|
139
|
+
? FormInput<S>
|
|
140
|
+
: Props extends { resolverEnabled?: infer Enabled }
|
|
141
|
+
? false extends Enabled
|
|
142
|
+
? FormInput<S> | FormOutput<S>
|
|
143
|
+
: FormOutput<S>
|
|
144
|
+
: FormOutput<S>;
|
|
145
|
+
|
|
146
|
+
type ResolverSubmitOutput<
|
|
147
|
+
S extends StandardSchemaV1,
|
|
148
|
+
Output,
|
|
149
|
+
Props,
|
|
150
|
+
> = Props extends { resolver: undefined }
|
|
151
|
+
? ResolverFallbackOutput<S, Props>
|
|
152
|
+
: Props extends { resolver: Resolver<FormInput<S>, unknown, Output> }
|
|
153
|
+
? Output
|
|
154
|
+
: Output | ResolverFallbackOutput<S, Props>;
|
|
155
|
+
|
|
156
|
+
type ConditionalFormOptions<S extends StandardSchemaV1> =
|
|
157
|
+
| ParsedFormOptions<S>
|
|
158
|
+
| RawFormOptions<S>
|
|
159
|
+
| DisabledFormOptions<S>
|
|
160
|
+
| VariableFormOptions<S>
|
|
161
|
+
| OptionalResolverFormOptions<S, unknown>;
|
|
162
|
+
|
|
163
|
+
type GeneratedSubmitOutput<S extends StandardSchemaV1, Props> = Props extends
|
|
164
|
+
| { submitValues: "raw" }
|
|
165
|
+
| { resolverEnabled: false }
|
|
166
|
+
? FormInput<S>
|
|
167
|
+
: Props extends { submitValues?: infer Mode }
|
|
168
|
+
? "raw" extends Mode
|
|
169
|
+
? FormInput<S> | ResolverFallbackOutput<S, Props>
|
|
170
|
+
: ResolverFallbackOutput<S, Props>
|
|
171
|
+
: ResolverFallbackOutput<S, Props>;
|
|
172
|
+
|
|
173
|
+
type CustomSubmitOutput<S extends StandardSchemaV1, R> =
|
|
174
|
+
R extends Resolver<FormInput<S>, unknown, infer Output> ? Output : never;
|
|
175
|
+
|
|
176
|
+
/** Distribute over configurations so each branch contributes only its own output. */
|
|
177
|
+
type ConditionalSubmitOutput<
|
|
178
|
+
S extends StandardSchemaV1,
|
|
179
|
+
Props,
|
|
180
|
+
> = Props extends undefined
|
|
181
|
+
? FormOutput<S>
|
|
182
|
+
: Props extends { resolver: infer R }
|
|
183
|
+
? undefined extends R
|
|
184
|
+
? CustomSubmitOutput<S, R> | GeneratedSubmitOutput<S, Props>
|
|
185
|
+
: CustomSubmitOutput<S, R>
|
|
186
|
+
: Props extends { resolver?: infer R }
|
|
187
|
+
? CustomSubmitOutput<S, R> | GeneratedSubmitOutput<S, Props>
|
|
188
|
+
: GeneratedSubmitOutput<S, Props>;
|
|
189
|
+
|
|
190
|
+
type SuppliedFormProps<Props> = undefined extends Props
|
|
191
|
+
? Partial<Exclude<Props, undefined>>
|
|
192
|
+
: Props;
|
|
193
|
+
|
|
194
|
+
// Keep only supplied props so unused React Hook Form controls do not leak across peers.
|
|
195
|
+
type FormOptions<Input extends FieldValues, Output, Props> = Omit<
|
|
196
|
+
Props,
|
|
197
|
+
"resolver" | "resolverEnabled" | "submitValues"
|
|
198
|
+
> &
|
|
199
|
+
Pick<UseFormProps<Input, unknown, Output>, "resolver">;
|
|
200
|
+
|
|
201
|
+
/** React Hook Form methods for the selected form schema. */
|
|
202
|
+
type ReactHookFormContractAdapterShape<S extends StandardSchemaV1> = {
|
|
203
|
+
/** Generate options for React Hook Form or a wrapper. */
|
|
204
|
+
formOptions: {
|
|
205
|
+
(
|
|
206
|
+
props?: undefined,
|
|
207
|
+
): FormOptions<FormInput<S>, FormOutput<S>, Record<never, never>>;
|
|
208
|
+
<Output, Props extends SuppliedResolverFormOptions<S, Output>>(
|
|
209
|
+
props: SuppliedResolverFormOptions<S, Output> & Props,
|
|
210
|
+
): FormOptions<FormInput<S>, ResolverSubmitOutput<S, Output, Props>, Props>;
|
|
211
|
+
<Props extends RawFormOptions<S> | DisabledFormOptions<S>>(
|
|
212
|
+
props: Props,
|
|
213
|
+
): FormOptions<FormInput<S>, FormInput<S>, Props>;
|
|
214
|
+
<Props extends ParsedFormOptions<S>>(
|
|
215
|
+
props: Props,
|
|
216
|
+
): FormOptions<FormInput<S>, FormOutput<S>, Props>;
|
|
217
|
+
<Props extends VariableFormOptions<S>>(
|
|
218
|
+
props: Props,
|
|
219
|
+
): FormOptions<FormInput<S>, FormInput<S> | FormOutput<S>, Props>;
|
|
220
|
+
<Output, Props extends OptionalResolverFormOptions<S, Output>>(
|
|
221
|
+
props: OptionalResolverFormOptions<S, Output> & Props,
|
|
222
|
+
): FormOptions<
|
|
223
|
+
FormInput<S>,
|
|
224
|
+
Output | ResolverFallbackOutput<S, Props>,
|
|
225
|
+
Props
|
|
226
|
+
>;
|
|
227
|
+
<Props extends ParsedFormOptions<S>>(
|
|
228
|
+
props?: Props,
|
|
229
|
+
): FormOptions<FormInput<S>, FormOutput<S>, Partial<Props>>;
|
|
230
|
+
<Props extends VariableFormOptions<S>>(
|
|
231
|
+
props: Props | undefined,
|
|
232
|
+
): FormOptions<FormInput<S>, FormInput<S> | FormOutput<S>, Partial<Props>>;
|
|
233
|
+
<Output, Props extends OptionalResolverFormOptions<S, Output>>(
|
|
234
|
+
props: (OptionalResolverFormOptions<S, Output> & Props) | undefined,
|
|
235
|
+
): FormOptions<
|
|
236
|
+
FormInput<S>,
|
|
237
|
+
ResolverSubmitOutput<S, Output, Props> | FormOutput<S>,
|
|
238
|
+
Partial<Props>
|
|
239
|
+
>;
|
|
240
|
+
<Props extends ConditionalFormOptions<S> | undefined>(
|
|
241
|
+
props: Props,
|
|
242
|
+
): FormOptions<
|
|
243
|
+
FormInput<S>,
|
|
244
|
+
ConditionalSubmitOutput<S, Props>,
|
|
245
|
+
SuppliedFormProps<Props>
|
|
246
|
+
>;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/** Convenience wrapper around useForm(formOptions(props)). */
|
|
250
|
+
useForm: {
|
|
251
|
+
<Output>(
|
|
252
|
+
props: CustomFormOptions<S, Output>,
|
|
253
|
+
): UseFormReturn<FormInput<S>, unknown, Output>;
|
|
254
|
+
(
|
|
255
|
+
props: RawFormOptions<S> | DisabledFormOptions<S>,
|
|
256
|
+
): UseFormReturn<FormInput<S>, unknown, FormInput<S>>;
|
|
257
|
+
(
|
|
258
|
+
props?: ParsedFormOptions<S>,
|
|
259
|
+
): UseFormReturn<FormInput<S>, unknown, FormOutput<S>>;
|
|
260
|
+
(
|
|
261
|
+
props?: VariableFormOptions<S>,
|
|
262
|
+
): UseFormReturn<FormInput<S>, unknown, FormInput<S> | FormOutput<S>>;
|
|
263
|
+
<Output, Props extends OptionalResolverFormOptions<S, Output>>(
|
|
264
|
+
props: OptionalResolverFormOptions<S, Output> & Props,
|
|
265
|
+
): UseFormReturn<
|
|
266
|
+
FormInput<S>,
|
|
267
|
+
unknown,
|
|
268
|
+
Output | ResolverFallbackOutput<S, Props>
|
|
269
|
+
>;
|
|
270
|
+
<Output, Props extends OptionalResolverFormOptions<S, Output>>(
|
|
271
|
+
props: (OptionalResolverFormOptions<S, Output> & Props) | undefined,
|
|
272
|
+
): UseFormReturn<
|
|
273
|
+
FormInput<S>,
|
|
274
|
+
unknown,
|
|
275
|
+
ResolverSubmitOutput<S, Output, Props> | FormOutput<S>
|
|
276
|
+
>;
|
|
277
|
+
<Props extends ConditionalFormOptions<S> | undefined>(
|
|
278
|
+
props: Props,
|
|
279
|
+
): UseFormReturn<FormInput<S>, unknown, ConditionalSubmitOutput<S, Props>>;
|
|
280
|
+
};
|
|
107
281
|
};
|
|
108
282
|
|
|
109
283
|
/**
|
|
110
|
-
*
|
|
111
|
-
* object-shaped.
|
|
284
|
+
* Adapter for a contract body, optionally using a separate form schema.
|
|
285
|
+
* The selected schema must have object-shaped input for named form fields.
|
|
112
286
|
*/
|
|
113
|
-
export type ReactHookFormContractAdapter<
|
|
114
|
-
|
|
287
|
+
export type ReactHookFormContractAdapter<
|
|
288
|
+
TContract extends HttpContractConfig,
|
|
289
|
+
TSchema extends StandardSchemaV1 = BodySchema<TContract>,
|
|
290
|
+
> = [BodySchema<TContract>] extends [never]
|
|
291
|
+
? never
|
|
292
|
+
: [FormInput<TSchema>] extends [never]
|
|
115
293
|
? never
|
|
116
|
-
: ReactHookFormContractAdapterShape<
|
|
117
|
-
|
|
118
|
-
function createReactHookFormAdapter
|
|
119
|
-
contract:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
contract: ReactHookFormContract<TContractLike>,
|
|
123
|
-
): ReactHookFormContractAdapterShape<ResolveContract<TContractLike>> {
|
|
294
|
+
: ReactHookFormContractAdapterShape<TSchema>;
|
|
295
|
+
|
|
296
|
+
function createReactHookFormAdapter(
|
|
297
|
+
contract: ContractLike,
|
|
298
|
+
schema?: StandardSchemaV1,
|
|
299
|
+
): ReactHookFormContractAdapterShape<StandardSchemaV1<FieldValues, unknown>> {
|
|
124
300
|
const resolvedContract = resolveContract(contract);
|
|
125
301
|
|
|
126
302
|
if (!resolvedContract.body) {
|
|
@@ -131,57 +307,101 @@ function createReactHookFormAdapter<TContractLike extends ContractLike>(
|
|
|
131
307
|
);
|
|
132
308
|
}
|
|
133
309
|
|
|
134
|
-
type Input =
|
|
135
|
-
type
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
310
|
+
type Input = FieldValues;
|
|
311
|
+
type Schema = StandardSchemaV1<Input, unknown>;
|
|
312
|
+
type Options = RhfFormOptions<Schema, unknown> & {
|
|
313
|
+
resolver?: Resolver<Input, unknown, unknown>;
|
|
314
|
+
resolverEnabled?: boolean;
|
|
315
|
+
submitValues?: "raw" | "parsed";
|
|
316
|
+
};
|
|
317
|
+
const formSchema = (schema ?? resolvedContract.body) as Schema;
|
|
141
318
|
|
|
319
|
+
function formOptions(props?: Options): UseFormProps<Input, unknown, unknown> {
|
|
142
320
|
const {
|
|
143
321
|
resolverEnabled = true,
|
|
144
322
|
resolver: resolverOverride,
|
|
323
|
+
submitValues = "parsed",
|
|
145
324
|
...rest
|
|
146
325
|
} = props ?? {};
|
|
147
326
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
327
|
+
if (resolverOverride && props?.submitValues !== undefined) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
`rhf(${resolvedContract.name}): A custom resolver owns its submit values; omit submitValues.`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
if (
|
|
333
|
+
props?.submitValues === "parsed" &&
|
|
334
|
+
!resolverEnabled &&
|
|
335
|
+
!resolverOverride
|
|
336
|
+
) {
|
|
337
|
+
throw new Error(
|
|
338
|
+
`rhf(${resolvedContract.name}): Parsed submit values require the generated resolver.`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let resolver: Resolver<Input, unknown, unknown> | undefined =
|
|
343
|
+
resolverOverride;
|
|
344
|
+
if (!resolver && resolverEnabled) {
|
|
345
|
+
const generated =
|
|
346
|
+
submitValues === "parsed"
|
|
347
|
+
? standardSchemaResolver<Input, unknown, unknown>(formSchema)
|
|
348
|
+
: standardSchemaResolver<Input, unknown, unknown>(
|
|
349
|
+
formSchema,
|
|
350
|
+
undefined,
|
|
351
|
+
{ raw: true },
|
|
352
|
+
);
|
|
353
|
+
resolver = async (values, context, options) => {
|
|
354
|
+
// Capture data containers before asynchronous validation.
|
|
355
|
+
const snapshot = snapshotInput(values);
|
|
356
|
+
const result = await generated(
|
|
357
|
+
submitValues === "raw" ? snapshotInput(snapshot) : snapshot,
|
|
358
|
+
context,
|
|
359
|
+
options,
|
|
360
|
+
);
|
|
361
|
+
return submitValues === "raw" && Object.keys(result.errors).length === 0
|
|
362
|
+
? { errors: {}, values: snapshot }
|
|
363
|
+
: result;
|
|
364
|
+
};
|
|
365
|
+
}
|
|
153
366
|
|
|
154
367
|
return {
|
|
155
|
-
...
|
|
368
|
+
...rest,
|
|
369
|
+
// RHF merges options on rerender; an explicit undefined clears an old resolver.
|
|
156
370
|
resolver,
|
|
157
371
|
};
|
|
158
372
|
}
|
|
159
373
|
|
|
160
|
-
function useForm(
|
|
161
|
-
props?: RhfFormOptions<ResolveContract<TContractLike>>,
|
|
162
|
-
): UseFormReturn<Input, unknown, Output> {
|
|
374
|
+
function useForm(props?: Options): UseFormReturn<Input, unknown, unknown> {
|
|
163
375
|
const options = formOptions(props);
|
|
164
|
-
return rhfUseForm<Input, unknown,
|
|
376
|
+
return rhfUseForm<Input, unknown, unknown>(options);
|
|
165
377
|
}
|
|
166
378
|
|
|
167
|
-
return { formOptions, useForm }
|
|
379
|
+
return { formOptions, useForm } as ReactHookFormContractAdapterShape<Schema>;
|
|
168
380
|
}
|
|
169
381
|
|
|
170
382
|
/**
|
|
171
|
-
* Create a React Hook Form adapter factory.
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* Contracts must declare a body schema whose input is object-shaped. After
|
|
176
|
-
* binding, callers may override or disable the generated resolver through
|
|
177
|
-
* `formOptions(...)` or `useForm(...)`.
|
|
383
|
+
* Create a React Hook Form adapter factory. Bind a contract with rhf(contract),
|
|
384
|
+
* or pass { schema } when the form's input differs from the request body.
|
|
385
|
+
* A separate schema must have object-shaped input and output assignable to the
|
|
386
|
+
* contract body input. Parsed submissions are the default; raw mode is opt-in.
|
|
178
387
|
*/
|
|
179
388
|
export function createReactHookForm() {
|
|
180
|
-
|
|
389
|
+
function rhf<TContractLike extends ContractLike>(
|
|
181
390
|
contract: ReactHookFormContract<TContractLike>,
|
|
182
|
-
): ReactHookFormContractAdapter<ResolveContract<TContractLike
|
|
183
|
-
|
|
184
|
-
|
|
391
|
+
): ReactHookFormContractAdapter<ResolveContract<TContractLike>>;
|
|
392
|
+
function rhf<
|
|
393
|
+
TContractLike extends ContractLike,
|
|
394
|
+
TSchema extends StandardSchemaV1,
|
|
395
|
+
>(
|
|
396
|
+
contract: TContractLike,
|
|
397
|
+
options: {
|
|
398
|
+
schema: CompatibleFormSchema<ResolveContract<TContractLike>, TSchema>;
|
|
399
|
+
},
|
|
400
|
+
): ReactHookFormContractAdapter<ResolveContract<TContractLike>, TSchema>;
|
|
401
|
+
function rhf(contract: ContractLike, options?: { schema: StandardSchemaV1 }) {
|
|
402
|
+
return createReactHookFormAdapter(contract, options?.schema);
|
|
403
|
+
}
|
|
404
|
+
return rhf;
|
|
185
405
|
}
|
|
186
406
|
|
|
187
407
|
/**
|