@kitr/forms 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +14 -2
- package/dist/index.d.ts +96 -0
- package/dist/index.js +183 -0
- package/dist/kitr.d.ts +28 -0
- package/dist/kitr.js +49 -0
- package/docs/api.md +77 -0
- package/docs/index.md +841 -0
- package/docs/overview.md +95 -0
- package/package.json +68 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Platon Podkopov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @kitr/forms
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Forms.** Build forms with validation, and show server errors next to the right field.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pnpm kitr add forms
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`kitr add` installs the package and wires it into your app: the provider in `kitrrc.ts`, its config file and its `AGENTS.md` section. Import it from `@kitr/forms`.
|
|
10
|
+
|
|
11
|
+
Built on react-hook-form.
|
|
12
|
+
|
|
13
|
+
Read the guide at https://kitr.dev/packages/forms.
|
|
14
|
+
|
|
15
|
+
Part of [kitr](https://kitr.dev), the TypeScript framework where everything is an add-on. MIT licensed.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { FieldPath, FieldValues, Form as RhfForm, FormProps as RhfFormProps, UseFormProps, UseFormReturn } from "react-hook-form";
|
|
2
|
+
import { StandardSchemaV1 } from "@kitr/core";
|
|
3
|
+
import { ComponentProps, ReactNode } from "react";
|
|
4
|
+
export * from "react-hook-form";
|
|
5
|
+
/** Props of `<Form>`. Every other prop is passed to the `<form>` element. */
|
|
6
|
+
interface FormProps<TIn extends FieldValues, TOut extends FieldValues> extends Omit<ComponentProps<'form'>, 'onSubmit' | 'onError'> {
|
|
7
|
+
/** The object returned by `useSchemaForm` or React Hook Form's `useForm`. */
|
|
8
|
+
form: UseFormReturn<TIn, unknown, TOut>;
|
|
9
|
+
/** Runs with validated values. A thrown KitrError with `fieldErrors` lands on fields. */
|
|
10
|
+
onSubmit: (values: TOut) => unknown | Promise<unknown>;
|
|
11
|
+
/** Runs after `onSubmit` resolves, with its return value and the submitted values. */
|
|
12
|
+
onSuccess?: (result: unknown, values: TOut) => void;
|
|
13
|
+
/** Runs after a thrown error has been mapped onto the fields; receives it as a `KitrError`. */
|
|
14
|
+
onError?: (error: unknown) => void;
|
|
15
|
+
/** Reset to default values after a successful submit. */
|
|
16
|
+
resetOnSuccess?: boolean;
|
|
17
|
+
/** The fields, buttons and anything else inside the form. */
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The kitr glue around React Hook Form: submit handling with pending state,
|
|
22
|
+
* server validation errors mapped onto fields, and a root error for everything else.
|
|
23
|
+
*/
|
|
24
|
+
export declare function Form<TIn extends FieldValues, TOut extends FieldValues>({ form, onSubmit, onSuccess, onError, resetOnSuccess, children, ...rest }: FormProps<TIn, TOut>): import("react").JSX.Element;
|
|
25
|
+
/** `pending` is true while `onSubmit` runs. Use it on the submit button. */
|
|
26
|
+
export declare function useFormStatus(): {
|
|
27
|
+
pending: boolean;
|
|
28
|
+
isValid: boolean;
|
|
29
|
+
};
|
|
30
|
+
/** The form-level error from the server (anything not mapped to a field). */
|
|
31
|
+
export declare function useRootError(): string | undefined;
|
|
32
|
+
/** Props of `<FormField>`: which field it wraps and the text around the input. */
|
|
33
|
+
interface FormFieldProps<T extends FieldValues> {
|
|
34
|
+
/** The field path, dot-separated for nested fields (`'lines.0.quantity'`). */
|
|
35
|
+
name: FieldPath<T>;
|
|
36
|
+
/** Content of the `<label>`, which points at the input's generated id. */
|
|
37
|
+
label?: ReactNode;
|
|
38
|
+
/** Help text under the input, linked to it with `aria-describedby`. */
|
|
39
|
+
description?: ReactNode;
|
|
40
|
+
/** The input, or a function that receives `{ id, name, invalid, describedBy }` and renders it. */
|
|
41
|
+
children: ReactNode | ((field: {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
invalid: boolean;
|
|
45
|
+
describedBy: string;
|
|
46
|
+
}) => ReactNode);
|
|
47
|
+
/** Class for the wrapper `<div data-kitr="field">`. */
|
|
48
|
+
className?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Headless field wrapper: generates ids, wires `aria-describedby` and
|
|
52
|
+
* `aria-invalid`, shows the field's error. Style it with your own classes
|
|
53
|
+
* or pull the registry's `form` component for a styled version.
|
|
54
|
+
*/
|
|
55
|
+
export declare function FormField<T extends FieldValues>({ name, label, description, children, className }: FormFieldProps<T>): import("react").JSX.Element;
|
|
56
|
+
/** Props to spread on the input inside a `<FormField>`: `{...useFieldProps()}` plus `form.register(name)`. */
|
|
57
|
+
export declare function useFieldProps(): {
|
|
58
|
+
onChange: import("react-hook-form").ChangeHandler;
|
|
59
|
+
onBlur: import("react-hook-form").ChangeHandler;
|
|
60
|
+
ref: import("react-hook-form").RefCallBack;
|
|
61
|
+
name: string;
|
|
62
|
+
min?: string | number;
|
|
63
|
+
max?: string | number;
|
|
64
|
+
maxLength?: number;
|
|
65
|
+
minLength?: number;
|
|
66
|
+
pattern?: string;
|
|
67
|
+
required?: boolean;
|
|
68
|
+
disabled?: boolean;
|
|
69
|
+
id: string;
|
|
70
|
+
'aria-invalid': boolean | undefined;
|
|
71
|
+
'aria-describedby': string | undefined;
|
|
72
|
+
};
|
|
73
|
+
/** Convenience: an `<input>` already registered to the surrounding field. */
|
|
74
|
+
export declare function FieldInput(props: Omit<ComponentProps<'input'>, 'name' | 'id'>): import("react").JSX.Element;
|
|
75
|
+
/**
|
|
76
|
+
* A `type="submit"` button that is disabled and `aria-busy` while the form submits, and shows
|
|
77
|
+
* `pendingText` in that time. A `disabled` prop is combined with the pending state, never replaced.
|
|
78
|
+
*/
|
|
79
|
+
export declare function SubmitButton({ children, pendingText, disabled, ...props }: ComponentProps<'button'> & {
|
|
80
|
+
pendingText?: ReactNode;
|
|
81
|
+
}): import("react").JSX.Element;
|
|
82
|
+
/**
|
|
83
|
+
* Map kitr's `fieldErrors` onto React Hook Form fields. Unknown fields and
|
|
84
|
+
* `_root` go to `root.server`. Returns true if the error was a validation error.
|
|
85
|
+
*/
|
|
86
|
+
export declare function applyServerErrors<T extends FieldValues>(form: UseFormReturn<T>, error: unknown): boolean;
|
|
87
|
+
/** Every React Hook Form `useForm` option except `resolver`, which `useSchemaForm` sets from the schema. */
|
|
88
|
+
export type SchemaFormOptions<TIn extends FieldValues, TOut> = Omit<UseFormProps<TIn, unknown, TOut>, 'resolver'>;
|
|
89
|
+
/**
|
|
90
|
+
* `useForm` with a Standard Schema resolver (zod, valibot, arktype). The full
|
|
91
|
+
* React Hook Form API is on the returned object; nothing is wrapped.
|
|
92
|
+
*/
|
|
93
|
+
export declare function useSchemaForm<TIn extends FieldValues, TOut extends FieldValues = TIn>(schema: StandardSchemaV1<TIn, TOut>, options?: SchemaFormOptions<TIn, TOut>): UseFormReturn<TIn, unknown, TOut>;
|
|
94
|
+
/** Alias kept for people who think in zod. */
|
|
95
|
+
export declare const useZodForm: typeof useSchemaForm;
|
|
96
|
+
export { type FormFieldProps, type FormProps, RhfForm, type RhfFormProps };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { Form as RhfForm, FormProvider, useForm, useFormContext } from "react-hook-form";
|
|
2
|
+
import { isKitrError, toKitrError } from "@kitr/core";
|
|
3
|
+
import { createContext, useContext, useId, useState } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
|
|
6
|
+
export * from "react-hook-form";
|
|
7
|
+
// src/server-errors.ts
|
|
8
|
+
/**
|
|
9
|
+
* Map kitr's `fieldErrors` onto React Hook Form fields. Unknown fields and
|
|
10
|
+
* `_root` go to `root.server`. Returns true if the error was a validation error.
|
|
11
|
+
*/
|
|
12
|
+
function applyServerErrors(form, error) {
|
|
13
|
+
const err = toKitrError(error);
|
|
14
|
+
const fieldErrors = err.fieldErrors;
|
|
15
|
+
const known = new Set(Object.keys(form.getValues() ?? {}));
|
|
16
|
+
let applied = false;
|
|
17
|
+
if (fieldErrors) for (const [field, messages] of Object.entries(fieldErrors)) {
|
|
18
|
+
const first = messages[0] ?? "Invalid";
|
|
19
|
+
const top = field.split(".")[0] ?? field;
|
|
20
|
+
if (field !== "_root" && (known.has(top) || known.size === 0)) {
|
|
21
|
+
form.setError(field, {
|
|
22
|
+
type: "server",
|
|
23
|
+
message: first
|
|
24
|
+
});
|
|
25
|
+
applied = true;
|
|
26
|
+
} else form.setError("root.server", {
|
|
27
|
+
type: "server",
|
|
28
|
+
message: first
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (!applied && !fieldErrors) form.setError("root.server", {
|
|
32
|
+
type: "server",
|
|
33
|
+
message: err.message
|
|
34
|
+
});
|
|
35
|
+
return isKitrError(error) && err.code === "VALIDATION";
|
|
36
|
+
}
|
|
37
|
+
// src/form.tsx
|
|
38
|
+
/**
|
|
39
|
+
* The kitr glue around React Hook Form: submit handling with pending state,
|
|
40
|
+
* server validation errors mapped onto fields, and a root error for everything else.
|
|
41
|
+
*/
|
|
42
|
+
function Form({ form, onSubmit, onSuccess, onError, resetOnSuccess, children, ...rest }) {
|
|
43
|
+
const [pending, setPending] = useState(false);
|
|
44
|
+
const submit = form.handleSubmit(async (values) => {
|
|
45
|
+
setPending(true);
|
|
46
|
+
form.clearErrors("root.server");
|
|
47
|
+
try {
|
|
48
|
+
const result = await onSubmit(values);
|
|
49
|
+
onSuccess?.(result, values);
|
|
50
|
+
if (resetOnSuccess) form.reset();
|
|
51
|
+
} catch (error) {
|
|
52
|
+
applyServerErrors(form, error);
|
|
53
|
+
onError?.(toKitrError(error));
|
|
54
|
+
} finally {
|
|
55
|
+
setPending(false);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return /* @__PURE__ */ jsx(FormProvider, {
|
|
59
|
+
...form,
|
|
60
|
+
children: /* @__PURE__ */ jsx(FormStatusContext.Provider, {
|
|
61
|
+
value: { pending },
|
|
62
|
+
children: /* @__PURE__ */ jsx("form", {
|
|
63
|
+
noValidate: true,
|
|
64
|
+
...rest,
|
|
65
|
+
onSubmit: submit,
|
|
66
|
+
children
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const FormStatusContext = createContext({ pending: false });
|
|
72
|
+
/** `pending` is true while `onSubmit` runs. Use it on the submit button. */
|
|
73
|
+
function useFormStatus() {
|
|
74
|
+
const { pending } = useContext(FormStatusContext);
|
|
75
|
+
const form = useFormContext();
|
|
76
|
+
return {
|
|
77
|
+
pending: pending || form.formState.isSubmitting,
|
|
78
|
+
isValid: form.formState.isValid
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** The form-level error from the server (anything not mapped to a field). */
|
|
82
|
+
function useRootError() {
|
|
83
|
+
return useFormContext().formState.errors?.root?.server?.message;
|
|
84
|
+
}
|
|
85
|
+
const FieldContext = createContext(null);
|
|
86
|
+
/**
|
|
87
|
+
* Headless field wrapper: generates ids, wires `aria-describedby` and
|
|
88
|
+
* `aria-invalid`, shows the field's error. Style it with your own classes
|
|
89
|
+
* or pull the registry's `form` component for a styled version.
|
|
90
|
+
*/
|
|
91
|
+
function FormField({ name, label, description, children, className }) {
|
|
92
|
+
const form = useFormContext();
|
|
93
|
+
const id = useId();
|
|
94
|
+
const error = form.getFieldState(name, form.formState).error?.message ?? void 0;
|
|
95
|
+
const describedBy = [description ? `${id}-description` : null, error ? `${id}-error` : null].filter(Boolean).join(" ");
|
|
96
|
+
const value = {
|
|
97
|
+
name,
|
|
98
|
+
id,
|
|
99
|
+
error,
|
|
100
|
+
describedBy
|
|
101
|
+
};
|
|
102
|
+
return /* @__PURE__ */ jsx(FieldContext.Provider, {
|
|
103
|
+
value,
|
|
104
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
105
|
+
className,
|
|
106
|
+
"data-kitr": "field",
|
|
107
|
+
"data-invalid": error ? "" : void 0,
|
|
108
|
+
children: [
|
|
109
|
+
label ? /* @__PURE__ */ jsx("label", {
|
|
110
|
+
htmlFor: id,
|
|
111
|
+
"data-kitr": "label",
|
|
112
|
+
children: label
|
|
113
|
+
}) : null,
|
|
114
|
+
typeof children === "function" ? children({
|
|
115
|
+
id,
|
|
116
|
+
name,
|
|
117
|
+
invalid: !!error,
|
|
118
|
+
describedBy
|
|
119
|
+
}) : children,
|
|
120
|
+
description ? /* @__PURE__ */ jsx("p", {
|
|
121
|
+
id: `${id}-description`,
|
|
122
|
+
"data-kitr": "description",
|
|
123
|
+
children: description
|
|
124
|
+
}) : null,
|
|
125
|
+
error ? /* @__PURE__ */ jsx("p", {
|
|
126
|
+
id: `${id}-error`,
|
|
127
|
+
role: "alert",
|
|
128
|
+
"data-kitr": "field-error",
|
|
129
|
+
children: error
|
|
130
|
+
}) : null
|
|
131
|
+
]
|
|
132
|
+
})
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/** Props to spread on the input inside a `<FormField>`: `{...useFieldProps()}` plus `form.register(name)`. */
|
|
136
|
+
function useFieldProps() {
|
|
137
|
+
const ctx = useContext(FieldContext);
|
|
138
|
+
if (!ctx) throw new Error("[@kitr/forms] useFieldProps() must be used inside <FormField>.");
|
|
139
|
+
const form = useFormContext();
|
|
140
|
+
return {
|
|
141
|
+
id: ctx.id,
|
|
142
|
+
"aria-invalid": ctx.error ? true : void 0,
|
|
143
|
+
"aria-describedby": ctx.describedBy || void 0,
|
|
144
|
+
...form.register(ctx.name)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/** Convenience: an `<input>` already registered to the surrounding field. */
|
|
148
|
+
function FieldInput(props) {
|
|
149
|
+
const fieldProps = useFieldProps();
|
|
150
|
+
return /* @__PURE__ */ jsx("input", {
|
|
151
|
+
...props,
|
|
152
|
+
...fieldProps
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* A `type="submit"` button that is disabled and `aria-busy` while the form submits, and shows
|
|
157
|
+
* `pendingText` in that time. A `disabled` prop is combined with the pending state, never replaced.
|
|
158
|
+
*/
|
|
159
|
+
function SubmitButton({ children, pendingText, disabled, ...props }) {
|
|
160
|
+
const { pending } = useFormStatus();
|
|
161
|
+
return /* @__PURE__ */ jsx("button", {
|
|
162
|
+
type: "submit",
|
|
163
|
+
...props,
|
|
164
|
+
disabled: pending || disabled,
|
|
165
|
+
"aria-busy": pending,
|
|
166
|
+
children: pending && pendingText ? pendingText : children
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
// src/use-schema-form.ts
|
|
170
|
+
/**
|
|
171
|
+
* `useForm` with a Standard Schema resolver (zod, valibot, arktype). The full
|
|
172
|
+
* React Hook Form API is on the returned object; nothing is wrapped.
|
|
173
|
+
*/
|
|
174
|
+
function useSchemaForm(schema, options = {}) {
|
|
175
|
+
return useForm({
|
|
176
|
+
mode: "onTouched",
|
|
177
|
+
...options,
|
|
178
|
+
resolver: standardSchemaResolver(schema)
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/** Alias kept for people who think in zod. */
|
|
182
|
+
const useZodForm = useSchemaForm;
|
|
183
|
+
export { FieldInput, Form, FormField, RhfForm, SubmitButton, applyServerErrors, useFieldProps, useFormStatus, useRootError, useSchemaForm, useZodForm };
|
package/dist/kitr.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** `kitr add forms`: React Hook Form with Standard Schema resolvers, `<Form>` with server-error mapping. */
|
|
2
|
+
declare const _default: {
|
|
3
|
+
readonly name: "forms";
|
|
4
|
+
readonly client: true;
|
|
5
|
+
readonly add: (ctx: import("@kitr/core/package").AddContext) => void;
|
|
6
|
+
readonly remove: (ctx: import("@kitr/core/package").AddContext) => void;
|
|
7
|
+
readonly generators: {
|
|
8
|
+
readonly form: {
|
|
9
|
+
readonly description: "Create a form component in app/components/forms";
|
|
10
|
+
readonly definition: "A component that owns one schema-validated form and its submit.";
|
|
11
|
+
readonly target: (name: string, project: import("@kitr/core/package").ProjectInfo) => {
|
|
12
|
+
file: string;
|
|
13
|
+
vars: Record<string, string>;
|
|
14
|
+
};
|
|
15
|
+
readonly stub: ({ name }: import("@kitr/core/package").GeneratorVars) => string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
readonly wraps: {
|
|
19
|
+
readonly 'react-hook-form': "@kitr/forms";
|
|
20
|
+
};
|
|
21
|
+
readonly eject: {
|
|
22
|
+
readonly '@kitr/forms': "react-hook-form + @hookform/resolvers";
|
|
23
|
+
};
|
|
24
|
+
readonly vite: {
|
|
25
|
+
readonly optimizeDeps: ["react-hook-form", "@hookform/resolvers/standard-schema"];
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
export { _default as default };
|
package/dist/kitr.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { definePackage, generatorFile, naming } from "@kitr/core/package";
|
|
2
|
+
// src/kitr.ts
|
|
3
|
+
/** `kitr add forms`: React Hook Form with Standard Schema resolvers, `<Form>` with server-error mapping. */
|
|
4
|
+
var kitr_default = definePackage({
|
|
5
|
+
name: "forms",
|
|
6
|
+
client: true,
|
|
7
|
+
add(ctx) {
|
|
8
|
+
ctx.deps.add("react-hook-form", "@hookform/resolvers", "zod");
|
|
9
|
+
ctx.agents.add("forms", `- Forms: \`const form = useSchemaForm(schema, { defaultValues })\` then \`<Form form={form} onSubmit={...}>\` from \`@kitr/forms\` (or the styled copy in \`app/components/ui/form.tsx\`).\n- Throw or reject with a KitrError carrying \`fieldErrors\` and the fields show the messages; anything else lands in \`useRootError()\`.`);
|
|
10
|
+
},
|
|
11
|
+
remove(ctx) {
|
|
12
|
+
ctx.deps.remove("react-hook-form", "@hookform/resolvers");
|
|
13
|
+
ctx.agents.remove("forms");
|
|
14
|
+
},
|
|
15
|
+
generators: { form: {
|
|
16
|
+
description: "Create a form component in app/components/forms",
|
|
17
|
+
definition: "A component that owns one schema-validated form and its submit.",
|
|
18
|
+
target: (name, project) => generatorFile(project, "components", "forms", name, {
|
|
19
|
+
suffix: "-form",
|
|
20
|
+
ext: ".tsx"
|
|
21
|
+
}),
|
|
22
|
+
stub: ({ name }) => `import { useSchemaForm } from '@kitr/forms'
|
|
23
|
+
import { z } from 'zod'
|
|
24
|
+
import { FieldInput, Form, FormField, FormMessage, SubmitButton } from '#components/ui/form'
|
|
25
|
+
|
|
26
|
+
export const ${naming.camel(name)}Schema = z.object({
|
|
27
|
+
name: z.string().min(2, 'Name must be at least 2 characters'),
|
|
28
|
+
})
|
|
29
|
+
export type ${naming.pascal(name)}Values = z.infer<typeof ${naming.camel(name)}Schema>
|
|
30
|
+
|
|
31
|
+
export function ${naming.pascal(name)}Form({ defaultValues, onSubmit }: { defaultValues?: Partial<${naming.pascal(name)}Values>; onSubmit: (values: ${naming.pascal(name)}Values) => Promise<unknown> }) {
|
|
32
|
+
const form = useSchemaForm(${naming.camel(name)}Schema, { defaultValues: { name: '', ...defaultValues } })
|
|
33
|
+
return (
|
|
34
|
+
<Form form={form} onSubmit={onSubmit} className="grid gap-4">
|
|
35
|
+
<FormMessage />
|
|
36
|
+
<FormField name="name" label="Name">
|
|
37
|
+
<FieldInput autoComplete="off" />
|
|
38
|
+
</FormField>
|
|
39
|
+
<SubmitButton>Save</SubmitButton>
|
|
40
|
+
</Form>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
`
|
|
44
|
+
} },
|
|
45
|
+
wraps: { "react-hook-form": "@kitr/forms" },
|
|
46
|
+
eject: { "@kitr/forms": "react-hook-form + @hookform/resolvers" },
|
|
47
|
+
vite: { optimizeDeps: ["react-hook-form", "@hookform/resolvers/standard-schema"] }
|
|
48
|
+
});
|
|
49
|
+
export { kitr_default as default };
|
package/docs/api.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: API reference
|
|
3
|
+
description: Every export of @kitr/forms.
|
|
4
|
+
order: 2
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# API reference
|
|
8
|
+
|
|
9
|
+
`@kitr/forms` re-exports everything from `react-hook-form` (its `Form` as `RhfForm`) and adds the following.
|
|
10
|
+
|
|
11
|
+
## useSchemaForm
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
function useSchemaForm<TIn, TOut = TIn>(
|
|
15
|
+
schema: StandardSchemaV1<TIn, TOut>,
|
|
16
|
+
options?: Omit<UseFormProps<TIn, unknown, TOut>, 'resolver'>,
|
|
17
|
+
): UseFormReturn<TIn, unknown, TOut>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`useForm` with a Standard Schema resolver and `mode: 'onTouched'`. Every React Hook Form option is accepted. `useZodForm` is an alias.
|
|
21
|
+
|
|
22
|
+
## Form
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
<Form form={form} onSubmit={fn} onSuccess? onError? resetOnSuccess? {...formAttributes}>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| Prop | Type | Description |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| `form` | `UseFormReturn` | From `useSchemaForm` or `useForm`. |
|
|
31
|
+
| `onSubmit` | `(values: TOut) => unknown` | Runs with validated values. Throw or reject to report errors. |
|
|
32
|
+
| `onSuccess` | `(result, values) => void` | Runs after `onSubmit` resolves. |
|
|
33
|
+
| `onError` | `(error: KitrError) => void` | Runs after errors are applied to fields. |
|
|
34
|
+
| `resetOnSuccess` | `boolean` | Reset to default values after success. |
|
|
35
|
+
|
|
36
|
+
Any other prop goes to the `<form>` element, which always has `noValidate`.
|
|
37
|
+
|
|
38
|
+
## FormField
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
<FormField name="email" label="Email" description="We never share it." className="...">
|
|
42
|
+
{children | ({ id, name, invalid, describedBy }) => ReactNode}
|
|
43
|
+
</FormField>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Renders a wrapper with `data-kitr="field"` (and `data-invalid` when it has an error), a `<label>`, the description and the error message with `role="alert"`. It is headless: style it with your own classes or the `data-kitr` attributes.
|
|
47
|
+
|
|
48
|
+
## useFieldProps and FieldInput
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
useFieldProps(): { id, 'aria-invalid', 'aria-describedby', name, onChange, onBlur, ref }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Props for the input inside the nearest `<FormField>`, including `form.register(name)`. `<FieldInput {...inputProps} />` is an `<input>` with these applied.
|
|
55
|
+
|
|
56
|
+
## SubmitButton
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
<SubmitButton pendingText="Saving…">Save</SubmitButton>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
A `type="submit"` button, disabled with `aria-busy` while the form submits.
|
|
63
|
+
|
|
64
|
+
## useFormStatus and useRootError
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
useFormStatus(): { pending: boolean; isValid: boolean }
|
|
68
|
+
useRootError(): string | undefined
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## applyServerErrors
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
function applyServerErrors(form: UseFormReturn, error: unknown): boolean
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Maps a `KitrError`'s `fieldErrors` onto fields and everything else to `root.server`. `<Form>` calls it for you; use it when submitting another way. Returns `true` for validation errors.
|