@classytic/formkit 1.0.3 â 1.2.2
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 +88 -56
- package/README.md +465 -113
- package/dist/index.d.mts +914 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1044 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server.d.mts +625 -0
- package/dist/server.d.mts.map +1 -0
- package/dist/server.mjs +418 -0
- package/dist/server.mjs.map +1 -0
- package/package.json +24 -31
- package/dist/index.cjs +0 -233
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -511
- package/dist/index.d.ts +0 -511
- package/dist/index.js +0 -223
- package/dist/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @classytic/formkit
|
|
2
2
|
|
|
3
|
-
Headless, type-safe form generation engine for React
|
|
3
|
+
Headless, type-safe form generation engine for React 19. Schema-driven with full TypeScript support.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@classytic/formkit)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -8,14 +8,24 @@ Headless, type-safe form generation engine for React 18/19. Schema-driven with f
|
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
11
|
+
- **Minimal boilerplate** - `useFormKit` hook: 5 lines to set up a complete form
|
|
12
|
+
- **Headless** - Bring your own UI components (Shadcn, MUI, Chakra, etc.)
|
|
13
|
+
- **Schema-driven** - Define forms with JSON/TypeScript schemas, defaults extracted automatically
|
|
14
|
+
- **Type-safe** - Full TypeScript support with generics
|
|
15
|
+
- **React Hook Form** - Built on top of the best form library, referentially stable return values
|
|
16
|
+
- **React 19** - Uses modern React 19 patterns (Context as provider, ref as prop)
|
|
17
|
+
- **Server Components** - Dedicated `@classytic/formkit/server` entry point for RSC
|
|
18
|
+
- **Variants** - Support for multiple component variants
|
|
19
|
+
- **Conditional fields** - Show/hide fields based on form values (function, DSL rules, AND/OR logic)
|
|
20
|
+
- **Responsive layouts** - Multi-column grid layouts
|
|
21
|
+
- **Accessibility** - Auto-generated `fieldId`, `error`, and `fieldState` props
|
|
22
|
+
- **Validation helpers** - `buildValidationRules` generates RHF rules from schema props
|
|
23
|
+
- **Lightweight** - ~7KB gzipped, tree-shakeable
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
- **React 19.0+** (React 18 is not supported)
|
|
28
|
+
- **React Hook Form 7.55.0+**
|
|
19
29
|
|
|
20
30
|
## Installation
|
|
21
31
|
|
|
@@ -31,7 +41,7 @@ yarn add @classytic/formkit react-hook-form
|
|
|
31
41
|
|
|
32
42
|
### 1. Create Field Components
|
|
33
43
|
|
|
34
|
-
Each field component
|
|
44
|
+
Each field component receives `FieldComponentProps` including `error`, `fieldId`, and the full `field` config:
|
|
35
45
|
|
|
36
46
|
```tsx
|
|
37
47
|
// components/form/form-input.tsx
|
|
@@ -42,23 +52,29 @@ import type { FieldComponentProps } from "@classytic/formkit";
|
|
|
42
52
|
import { Input } from "@/components/ui/input";
|
|
43
53
|
import { Label } from "@/components/ui/label";
|
|
44
54
|
|
|
45
|
-
export function FormInput({
|
|
55
|
+
export function FormInput({
|
|
56
|
+
control,
|
|
57
|
+
field,
|
|
58
|
+
label,
|
|
59
|
+
placeholder,
|
|
60
|
+
required,
|
|
61
|
+
error,
|
|
62
|
+
fieldId,
|
|
63
|
+
}: FieldComponentProps) {
|
|
46
64
|
return (
|
|
47
65
|
<Controller
|
|
48
|
-
name={name}
|
|
66
|
+
name={field.name}
|
|
49
67
|
control={control}
|
|
50
|
-
render={({ field
|
|
68
|
+
render={({ field: rhfField }) => (
|
|
51
69
|
<div className="space-y-2">
|
|
52
70
|
{label && (
|
|
53
|
-
<Label htmlFor={
|
|
71
|
+
<Label htmlFor={fieldId}>
|
|
54
72
|
{label}
|
|
55
73
|
{required && <span className="text-red-500 ml-1">*</span>}
|
|
56
74
|
</Label>
|
|
57
75
|
)}
|
|
58
|
-
<Input {...
|
|
59
|
-
{
|
|
60
|
-
<p className="text-sm text-red-500">{fieldState.error.message}</p>
|
|
61
|
-
)}
|
|
76
|
+
<Input {...rhfField} id={fieldId} placeholder={placeholder} />
|
|
77
|
+
{error && <p className="text-sm text-red-500">{error.message}</p>}
|
|
62
78
|
</div>
|
|
63
79
|
)}
|
|
64
80
|
/>
|
|
@@ -74,7 +90,11 @@ Register your components and layouts:
|
|
|
74
90
|
// lib/form-adapter.tsx
|
|
75
91
|
"use client";
|
|
76
92
|
|
|
77
|
-
import {
|
|
93
|
+
import {
|
|
94
|
+
FormSystemProvider,
|
|
95
|
+
type ComponentRegistry,
|
|
96
|
+
type LayoutRegistry,
|
|
97
|
+
} from "@classytic/formkit";
|
|
78
98
|
import { FormInput } from "@/components/form/form-input";
|
|
79
99
|
|
|
80
100
|
const components: ComponentRegistry = {
|
|
@@ -112,13 +132,11 @@ export function FormProvider({ children }: { children: React.ReactNode }) {
|
|
|
112
132
|
// app/signup/page.tsx
|
|
113
133
|
"use client";
|
|
114
134
|
|
|
115
|
-
import { useForm } from "react-hook-form";
|
|
116
135
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
117
136
|
import { z } from "zod";
|
|
118
|
-
import { FormGenerator, type FormSchema } from "@classytic/formkit";
|
|
137
|
+
import { FormGenerator, useFormKit, type FormSchema } from "@classytic/formkit";
|
|
119
138
|
import { FormProvider } from "@/lib/form-adapter";
|
|
120
139
|
|
|
121
|
-
// Validation schema
|
|
122
140
|
const signupSchema = z.object({
|
|
123
141
|
firstName: z.string().min(2),
|
|
124
142
|
lastName: z.string().min(2),
|
|
@@ -128,36 +146,60 @@ const signupSchema = z.object({
|
|
|
128
146
|
|
|
129
147
|
type SignupData = z.infer<typeof signupSchema>;
|
|
130
148
|
|
|
131
|
-
// Form schema (type-safe!)
|
|
132
149
|
const formSchema: FormSchema<SignupData> = {
|
|
133
150
|
sections: [
|
|
134
151
|
{
|
|
135
152
|
title: "Personal Information",
|
|
136
153
|
cols: 2,
|
|
137
154
|
fields: [
|
|
138
|
-
{
|
|
139
|
-
|
|
155
|
+
{
|
|
156
|
+
name: "firstName",
|
|
157
|
+
type: "text",
|
|
158
|
+
label: "First Name",
|
|
159
|
+
required: true,
|
|
160
|
+
defaultValue: "",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "lastName",
|
|
164
|
+
type: "text",
|
|
165
|
+
label: "Last Name",
|
|
166
|
+
required: true,
|
|
167
|
+
defaultValue: "",
|
|
168
|
+
},
|
|
140
169
|
],
|
|
141
170
|
},
|
|
142
171
|
{
|
|
143
172
|
title: "Account",
|
|
144
173
|
fields: [
|
|
145
|
-
{
|
|
146
|
-
|
|
174
|
+
{
|
|
175
|
+
name: "email",
|
|
176
|
+
type: "email",
|
|
177
|
+
label: "Email",
|
|
178
|
+
required: true,
|
|
179
|
+
defaultValue: "",
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: "password",
|
|
183
|
+
type: "password",
|
|
184
|
+
label: "Password",
|
|
185
|
+
required: true,
|
|
186
|
+
defaultValue: "",
|
|
187
|
+
},
|
|
147
188
|
],
|
|
148
189
|
},
|
|
149
190
|
],
|
|
150
191
|
};
|
|
151
192
|
|
|
152
193
|
export default function SignupPage() {
|
|
153
|
-
const
|
|
194
|
+
const { handleSubmit, generatorProps } = useFormKit({
|
|
195
|
+
schema: formSchema,
|
|
154
196
|
resolver: zodResolver(signupSchema),
|
|
155
197
|
});
|
|
156
198
|
|
|
157
199
|
return (
|
|
158
200
|
<FormProvider>
|
|
159
|
-
<form onSubmit={
|
|
160
|
-
<FormGenerator
|
|
201
|
+
<form onSubmit={handleSubmit(console.log)} className="space-y-8">
|
|
202
|
+
<FormGenerator {...generatorProps} />
|
|
161
203
|
<button type="submit">Sign Up</button>
|
|
162
204
|
</form>
|
|
163
205
|
</FormProvider>
|
|
@@ -167,67 +209,125 @@ export default function SignupPage() {
|
|
|
167
209
|
|
|
168
210
|
## API Reference
|
|
169
211
|
|
|
212
|
+
### useFormKit
|
|
213
|
+
|
|
214
|
+
Convenience hook that combines schema default extraction with react-hook-form setup. Returns all `useForm` methods plus ready-to-spread `generatorProps`.
|
|
215
|
+
|
|
216
|
+
**Referentially stable** â the return value preserves the original `useForm` object identity across re-renders, so it's safe to use in `useEffect` dependency arrays.
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
import { useFormKit, FormGenerator } from "@classytic/formkit";
|
|
220
|
+
|
|
221
|
+
const form = useFormKit({
|
|
222
|
+
schema: formSchema,
|
|
223
|
+
resolver: zodResolver(validationSchema), // optional
|
|
224
|
+
defaultValues: { email: "pre@fill.com" }, // optional overrides
|
|
225
|
+
disabled: false, // optional
|
|
226
|
+
variant: "compact", // optional
|
|
227
|
+
className: "my-form", // optional
|
|
228
|
+
mode: "onBlur", // any useForm option
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const { handleSubmit, generatorProps } = form;
|
|
232
|
+
|
|
233
|
+
// Safe to use in useEffect deps â form is referentially stable
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
if (open) form.reset(defaults);
|
|
236
|
+
}, [open, form]);
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
240
|
+
<FormGenerator {...generatorProps} />
|
|
241
|
+
<button type="submit">Submit</button>
|
|
242
|
+
</form>
|
|
243
|
+
);
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Schema `defaultValue` fields are automatically extracted and merged with any explicit `defaultValues` you provide (explicit values take priority).
|
|
247
|
+
|
|
248
|
+
`generatorProps` is memoized â it only recomputes when `schema`, `control`, `disabled`, `variant`, or `className` change.
|
|
249
|
+
|
|
170
250
|
### FormGenerator
|
|
171
251
|
|
|
172
|
-
The main component that renders forms from a schema.
|
|
252
|
+
The main component that renders forms from a schema. Supports React 19 `ref` as a regular prop.
|
|
173
253
|
|
|
174
254
|
```tsx
|
|
175
255
|
<FormGenerator
|
|
176
|
-
schema={formSchema}
|
|
177
|
-
control={form.control}
|
|
178
|
-
disabled={false}
|
|
179
|
-
variant="default"
|
|
180
|
-
className="my-form"
|
|
256
|
+
schema={formSchema} // Required: Form schema
|
|
257
|
+
control={form.control} // Optional: React Hook Form control (or wrap in <FormProvider>)
|
|
258
|
+
disabled={false} // Optional: Disable all fields
|
|
259
|
+
variant="default" // Optional: Global variant
|
|
260
|
+
className="my-form" // Optional: Root element class
|
|
261
|
+
ref={formRef} // Optional: Ref to the root <div> (React 19 ref-as-prop)
|
|
181
262
|
/>
|
|
182
263
|
```
|
|
183
264
|
|
|
184
265
|
### FormSchema
|
|
185
266
|
|
|
186
|
-
```
|
|
267
|
+
```ts
|
|
187
268
|
interface FormSchema<T extends FieldValues = FieldValues> {
|
|
188
269
|
sections: Section<T>[];
|
|
189
270
|
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Section
|
|
190
274
|
|
|
275
|
+
```ts
|
|
191
276
|
interface Section<T> {
|
|
192
|
-
id?: string;
|
|
193
|
-
title?: string;
|
|
194
|
-
description?: string;
|
|
195
|
-
icon?: ReactNode;
|
|
196
|
-
fields?: BaseField<T>[];
|
|
197
|
-
cols?: number;
|
|
198
|
-
gap?: number;
|
|
199
|
-
variant?: string;
|
|
200
|
-
className?: string;
|
|
201
|
-
collapsible?: boolean;
|
|
277
|
+
id?: string; // Unique identifier
|
|
278
|
+
title?: string; // Section title
|
|
279
|
+
description?: string; // Section description
|
|
280
|
+
icon?: ReactNode; // Section icon
|
|
281
|
+
fields?: BaseField<T>[]; // Fields in this section
|
|
282
|
+
cols?: number; // Grid columns (1-6)
|
|
283
|
+
gap?: number; // Grid gap
|
|
284
|
+
variant?: string; // Section variant
|
|
285
|
+
className?: string; // Custom class
|
|
286
|
+
collapsible?: boolean; // Make section collapsible
|
|
202
287
|
defaultCollapsed?: boolean;
|
|
203
|
-
|
|
204
|
-
|
|
288
|
+
nameSpace?: string; // Prefix for nested object fields (e.g. "address")
|
|
289
|
+
|
|
290
|
+
// Conditional rendering (function, DSL rule, or ConditionConfig)
|
|
291
|
+
condition?: Condition<T>;
|
|
292
|
+
|
|
293
|
+
// Custom render function (bypasses grid layout)
|
|
294
|
+
render?: (props: SectionRenderProps<T>) => ReactNode;
|
|
205
295
|
}
|
|
206
296
|
```
|
|
207
297
|
|
|
208
298
|
### BaseField
|
|
209
299
|
|
|
210
|
-
```
|
|
300
|
+
```ts
|
|
211
301
|
interface BaseField<T> {
|
|
212
|
-
name: string;
|
|
213
|
-
type: FieldType;
|
|
214
|
-
label?: string;
|
|
215
|
-
placeholder?: string;
|
|
216
|
-
helperText?: string;
|
|
217
|
-
disabled?: boolean;
|
|
218
|
-
required?: boolean;
|
|
219
|
-
readOnly?: boolean;
|
|
220
|
-
variant?: string;
|
|
221
|
-
fullWidth?: boolean;
|
|
222
|
-
className?: string;
|
|
223
|
-
defaultValue?: unknown;
|
|
224
|
-
|
|
302
|
+
name: string; // Field name (required)
|
|
303
|
+
type: FieldType; // Field type (required)
|
|
304
|
+
label?: string; // Field label
|
|
305
|
+
placeholder?: string; // Placeholder text
|
|
306
|
+
helperText?: string; // Helper text below field
|
|
307
|
+
disabled?: boolean; // Disable field
|
|
308
|
+
required?: boolean; // Mark as required
|
|
309
|
+
readOnly?: boolean; // Read-only field
|
|
310
|
+
variant?: string; // Field variant
|
|
311
|
+
fullWidth?: boolean; // Span full grid width
|
|
312
|
+
className?: string; // Custom class
|
|
313
|
+
defaultValue?: unknown; // Default value
|
|
314
|
+
|
|
225
315
|
// Conditional rendering
|
|
226
|
-
condition?:
|
|
227
|
-
|
|
316
|
+
condition?: Condition<T>;
|
|
317
|
+
watchNames?: string | string[]; // Optimize useWatch performance
|
|
318
|
+
|
|
319
|
+
// Dynamic options loading
|
|
320
|
+
loadOptions?: (
|
|
321
|
+
formValues: Partial<T>,
|
|
322
|
+
) => Promise<FieldOption[]> | FieldOption[];
|
|
323
|
+
debounceMs?: number;
|
|
324
|
+
|
|
325
|
+
// For array/grouped types
|
|
326
|
+
itemFields?: BaseField<T>[];
|
|
327
|
+
|
|
228
328
|
// For select/radio/checkbox
|
|
229
329
|
options?: FieldOption[];
|
|
230
|
-
|
|
330
|
+
|
|
231
331
|
// HTML input attributes
|
|
232
332
|
min?: number | string;
|
|
233
333
|
max?: number | string;
|
|
@@ -235,14 +335,17 @@ interface BaseField<T> {
|
|
|
235
335
|
pattern?: string;
|
|
236
336
|
minLength?: number;
|
|
237
337
|
maxLength?: number;
|
|
238
|
-
rows?: number;
|
|
239
|
-
multiple?: boolean;
|
|
240
|
-
accept?: string;
|
|
338
|
+
rows?: number;
|
|
339
|
+
multiple?: boolean;
|
|
340
|
+
accept?: string;
|
|
241
341
|
autoComplete?: string;
|
|
242
342
|
autoFocus?: boolean;
|
|
243
|
-
|
|
244
|
-
// Custom
|
|
245
|
-
|
|
343
|
+
|
|
344
|
+
// Custom render override
|
|
345
|
+
render?: (props: FieldComponentProps<T>) => ReactNode;
|
|
346
|
+
|
|
347
|
+
// Arbitrary extra props for custom components
|
|
348
|
+
customProps?: Record<string, unknown>;
|
|
246
349
|
}
|
|
247
350
|
```
|
|
248
351
|
|
|
@@ -250,23 +353,66 @@ interface BaseField<T> {
|
|
|
250
353
|
|
|
251
354
|
Props passed to your field components:
|
|
252
355
|
|
|
253
|
-
```
|
|
254
|
-
interface FieldComponentProps<
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
356
|
+
```ts
|
|
357
|
+
interface FieldComponentProps<
|
|
358
|
+
T extends FieldValues = FieldValues,
|
|
359
|
+
> extends BaseField<T> {
|
|
360
|
+
field: BaseField<T>; // Full field config
|
|
361
|
+
control: Control<T>; // React Hook Form control
|
|
362
|
+
disabled?: boolean; // Merged disabled state
|
|
363
|
+
variant?: string; // Active variant
|
|
364
|
+
error?: FieldError; // Field error from react-hook-form
|
|
365
|
+
fieldState?: {
|
|
366
|
+
// Field state metadata
|
|
367
|
+
invalid: boolean;
|
|
368
|
+
isDirty: boolean;
|
|
369
|
+
isTouched: boolean;
|
|
370
|
+
isValidating: boolean;
|
|
371
|
+
error?: FieldError;
|
|
372
|
+
};
|
|
373
|
+
fieldId: string; // Generated ID for label-input association (e.g. "formkit-field-email")
|
|
259
374
|
}
|
|
260
375
|
```
|
|
261
376
|
|
|
377
|
+
### Condition Types
|
|
378
|
+
|
|
379
|
+
Conditions can be a function, a DSL rule, an array of rules (AND), or a `ConditionConfig` (AND/OR):
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
// Function condition
|
|
383
|
+
condition: (values) => values.accountType === "business"
|
|
384
|
+
|
|
385
|
+
// Single DSL rule
|
|
386
|
+
condition: { watch: "country", operator: "===", value: "US" }
|
|
387
|
+
|
|
388
|
+
// Array of rules (AND - all must match)
|
|
389
|
+
condition: [
|
|
390
|
+
{ watch: "country", operator: "===", value: "US" },
|
|
391
|
+
{ watch: "age", operator: "truthy" },
|
|
392
|
+
]
|
|
393
|
+
|
|
394
|
+
// ConditionConfig with OR logic
|
|
395
|
+
condition: {
|
|
396
|
+
rules: [
|
|
397
|
+
{ watch: "country", operator: "===", value: "US" },
|
|
398
|
+
{ watch: "country", operator: "===", value: "CA" },
|
|
399
|
+
],
|
|
400
|
+
logic: "or",
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Supported operators:** `===`, `!==`, `in`, `not-in`, `truthy`, `falsy`
|
|
405
|
+
|
|
406
|
+
**Nested paths:** DSL rules support dot-notation paths like `"address.city"` for nested form values.
|
|
407
|
+
|
|
262
408
|
### ComponentRegistry
|
|
263
409
|
|
|
264
|
-
```
|
|
410
|
+
```ts
|
|
265
411
|
const components: ComponentRegistry = {
|
|
266
412
|
// Simple mapping
|
|
267
413
|
text: FormInput,
|
|
268
414
|
select: FormSelect,
|
|
269
|
-
|
|
415
|
+
|
|
270
416
|
// Variant-specific components
|
|
271
417
|
compact: {
|
|
272
418
|
text: CompactInput,
|
|
@@ -277,11 +423,11 @@ const components: ComponentRegistry = {
|
|
|
277
423
|
|
|
278
424
|
### LayoutRegistry
|
|
279
425
|
|
|
280
|
-
```
|
|
426
|
+
```ts
|
|
281
427
|
const layouts: LayoutRegistry = {
|
|
282
428
|
section: SectionLayout,
|
|
283
429
|
grid: GridLayout,
|
|
284
|
-
|
|
430
|
+
|
|
285
431
|
// Variant-specific layouts
|
|
286
432
|
compact: {
|
|
287
433
|
section: CompactSection,
|
|
@@ -289,11 +435,77 @@ const layouts: LayoutRegistry = {
|
|
|
289
435
|
};
|
|
290
436
|
```
|
|
291
437
|
|
|
438
|
+
### extractDefaultValues
|
|
439
|
+
|
|
440
|
+
Extracts default values from a schema. Server-safe (no hooks).
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
import { extractDefaultValues } from "@classytic/formkit"; // or /server
|
|
444
|
+
|
|
445
|
+
const defaults = extractDefaultValues(formSchema);
|
|
446
|
+
// { firstName: "", lastName: "", email: "", password: "" }
|
|
447
|
+
|
|
448
|
+
// Use with react-hook-form
|
|
449
|
+
const form = useForm({ defaultValues: defaults });
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Respects `nameSpace` prefixes and group `itemFields` defaults.
|
|
453
|
+
|
|
454
|
+
### buildValidationRules
|
|
455
|
+
|
|
456
|
+
Generates react-hook-form validation rules from a field's schema props. Server-safe (no hooks).
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
import { buildValidationRules } from "@classytic/formkit"; // or /server
|
|
460
|
+
|
|
461
|
+
function FormInput({ field, control, error, fieldId }: FieldComponentProps) {
|
|
462
|
+
const rules = buildValidationRules(field);
|
|
463
|
+
return (
|
|
464
|
+
<Controller
|
|
465
|
+
name={field.name}
|
|
466
|
+
control={control}
|
|
467
|
+
rules={rules}
|
|
468
|
+
render={({ field: rhf }) => <input {...rhf} id={fieldId} />}
|
|
469
|
+
/>
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Maps `required`, `min`, `max`, `minLength`, `maxLength`, and `pattern` from the field schema to RHF-compatible rules with auto-generated error messages.
|
|
475
|
+
|
|
476
|
+
## Server Components
|
|
477
|
+
|
|
478
|
+
The `@classytic/formkit/server` entry point exports server-safe utilities with no React hooks or client-side code:
|
|
479
|
+
|
|
480
|
+
```ts
|
|
481
|
+
import {
|
|
482
|
+
cn,
|
|
483
|
+
defineSchema,
|
|
484
|
+
defineField,
|
|
485
|
+
defineSection,
|
|
486
|
+
evaluateCondition,
|
|
487
|
+
extractWatchNames,
|
|
488
|
+
extractDefaultValues,
|
|
489
|
+
buildValidationRules,
|
|
490
|
+
} from "@classytic/formkit/server";
|
|
491
|
+
|
|
492
|
+
// Type-only imports also available
|
|
493
|
+
import type {
|
|
494
|
+
FormSchema,
|
|
495
|
+
BaseField,
|
|
496
|
+
Section,
|
|
497
|
+
ConditionRule,
|
|
498
|
+
ConditionConfig,
|
|
499
|
+
} from "@classytic/formkit/server";
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Use this entry point in React Server Components to define schemas, evaluate conditions, or use `cn` without pulling in client-side code.
|
|
503
|
+
|
|
292
504
|
## Advanced Features
|
|
293
505
|
|
|
294
|
-
### Conditional Fields
|
|
506
|
+
### Conditional Fields (Function)
|
|
295
507
|
|
|
296
|
-
```
|
|
508
|
+
```ts
|
|
297
509
|
{
|
|
298
510
|
name: "companyName",
|
|
299
511
|
type: "text",
|
|
@@ -302,6 +514,73 @@ const layouts: LayoutRegistry = {
|
|
|
302
514
|
}
|
|
303
515
|
```
|
|
304
516
|
|
|
517
|
+
### Conditional Fields (DSL Rules)
|
|
518
|
+
|
|
519
|
+
```ts
|
|
520
|
+
{
|
|
521
|
+
name: "stateField",
|
|
522
|
+
type: "select",
|
|
523
|
+
label: "State",
|
|
524
|
+
condition: { watch: "country", operator: "===", value: "US" },
|
|
525
|
+
watchNames: ["country"], // Optimizes re-renders
|
|
526
|
+
}
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### Conditional Sections
|
|
530
|
+
|
|
531
|
+
```ts
|
|
532
|
+
{
|
|
533
|
+
title: "Business Details",
|
|
534
|
+
condition: (values) => values.accountType === "business",
|
|
535
|
+
fields: [
|
|
536
|
+
{ name: "companyName", type: "text", label: "Company" },
|
|
537
|
+
{ name: "taxId", type: "text", label: "Tax ID" },
|
|
538
|
+
],
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### OR Conditions
|
|
543
|
+
|
|
544
|
+
```ts
|
|
545
|
+
{
|
|
546
|
+
name: "taxField",
|
|
547
|
+
type: "text",
|
|
548
|
+
condition: {
|
|
549
|
+
rules: [
|
|
550
|
+
{ watch: "country", operator: "===", value: "US" },
|
|
551
|
+
{ watch: "country", operator: "===", value: "CA" },
|
|
552
|
+
],
|
|
553
|
+
logic: "or",
|
|
554
|
+
},
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### Nested Path Conditions
|
|
559
|
+
|
|
560
|
+
DSL rules resolve dot-notation paths for nested form values:
|
|
561
|
+
|
|
562
|
+
```ts
|
|
563
|
+
{
|
|
564
|
+
name: "zipCode",
|
|
565
|
+
type: "text",
|
|
566
|
+
condition: { watch: "address.country", operator: "===", value: "US" },
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### Namespace Support
|
|
571
|
+
|
|
572
|
+
Prefix all field names in a section with a namespace for nested objects:
|
|
573
|
+
|
|
574
|
+
```ts
|
|
575
|
+
{
|
|
576
|
+
nameSpace: "address",
|
|
577
|
+
fields: [
|
|
578
|
+
{ name: "street", type: "text" }, // Becomes "address.street"
|
|
579
|
+
{ name: "city", type: "text" }, // Becomes "address.city"
|
|
580
|
+
],
|
|
581
|
+
}
|
|
582
|
+
```
|
|
583
|
+
|
|
305
584
|
### Variants
|
|
306
585
|
|
|
307
586
|
Apply different styles based on context:
|
|
@@ -315,25 +594,34 @@ const components = {
|
|
|
315
594
|
},
|
|
316
595
|
};
|
|
317
596
|
|
|
318
|
-
// Use variant
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}]
|
|
324
|
-
};
|
|
597
|
+
// Use variant on the whole form
|
|
598
|
+
<FormGenerator schema={schema} variant="compact" />
|
|
599
|
+
|
|
600
|
+
// Or per-section
|
|
601
|
+
{ variant: "compact", fields: [...] }
|
|
325
602
|
|
|
326
603
|
// Or per-field
|
|
604
|
+
{ name: "notes", type: "text", variant: "compact" }
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### Dynamic Options Loading
|
|
608
|
+
|
|
609
|
+
```ts
|
|
327
610
|
{
|
|
328
|
-
name: "
|
|
329
|
-
type: "
|
|
330
|
-
|
|
611
|
+
name: "city",
|
|
612
|
+
type: "select",
|
|
613
|
+
watchNames: ["country"],
|
|
614
|
+
loadOptions: async (values) => {
|
|
615
|
+
const cities = await fetchCities(values.country);
|
|
616
|
+
return cities.map(c => ({ label: c.name, value: c.id }));
|
|
617
|
+
},
|
|
618
|
+
debounceMs: 300,
|
|
331
619
|
}
|
|
332
620
|
```
|
|
333
621
|
|
|
334
622
|
### Custom Section Render
|
|
335
623
|
|
|
336
|
-
```
|
|
624
|
+
```ts
|
|
337
625
|
{
|
|
338
626
|
title: "Payment",
|
|
339
627
|
render: ({ control, disabled }) => (
|
|
@@ -345,9 +633,46 @@ const schema = {
|
|
|
345
633
|
}
|
|
346
634
|
```
|
|
347
635
|
|
|
348
|
-
###
|
|
636
|
+
### Custom Field Render
|
|
637
|
+
|
|
638
|
+
```ts
|
|
639
|
+
{
|
|
640
|
+
name: "avatar",
|
|
641
|
+
type: "file",
|
|
642
|
+
render: ({ field, control, error, fieldId }) => (
|
|
643
|
+
<AvatarUploader fieldId={fieldId} error={error} />
|
|
644
|
+
),
|
|
645
|
+
}
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### Custom Props
|
|
649
|
+
|
|
650
|
+
Pass arbitrary props to your field components via `customProps`:
|
|
651
|
+
|
|
652
|
+
```ts
|
|
653
|
+
{
|
|
654
|
+
name: "bio",
|
|
655
|
+
type: "textarea",
|
|
656
|
+
label: "Biography",
|
|
657
|
+
customProps: {
|
|
658
|
+
maxCharacters: 500,
|
|
659
|
+
showCounter: true,
|
|
660
|
+
},
|
|
661
|
+
}
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
Access in your component:
|
|
349
665
|
|
|
350
666
|
```tsx
|
|
667
|
+
function FormTextarea({ field, customProps, ...props }: FieldComponentProps) {
|
|
668
|
+
const maxChars = customProps?.maxCharacters as number;
|
|
669
|
+
// ...
|
|
670
|
+
}
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### Grouped Select Options
|
|
674
|
+
|
|
675
|
+
```ts
|
|
351
676
|
{
|
|
352
677
|
name: "country",
|
|
353
678
|
type: "select",
|
|
@@ -370,31 +695,69 @@ const schema = {
|
|
|
370
695
|
}
|
|
371
696
|
```
|
|
372
697
|
|
|
698
|
+
### Schema Builder Utilities
|
|
699
|
+
|
|
700
|
+
Type-safe helpers for defining schemas outside of components:
|
|
701
|
+
|
|
702
|
+
```ts
|
|
703
|
+
import {
|
|
704
|
+
defineSchema,
|
|
705
|
+
defineField,
|
|
706
|
+
defineSection,
|
|
707
|
+
} from "@classytic/formkit/server";
|
|
708
|
+
|
|
709
|
+
const emailField = defineField<MyFormData>({
|
|
710
|
+
name: "email",
|
|
711
|
+
type: "email",
|
|
712
|
+
label: "Email Address",
|
|
713
|
+
required: true,
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
const personalSection = defineSection<MyFormData>({
|
|
717
|
+
title: "Personal Info",
|
|
718
|
+
cols: 2,
|
|
719
|
+
fields: [emailField],
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
const schema = defineSchema<MyFormData>({
|
|
723
|
+
sections: [personalSection],
|
|
724
|
+
});
|
|
725
|
+
```
|
|
726
|
+
|
|
373
727
|
## Type Exports
|
|
374
728
|
|
|
375
|
-
```
|
|
729
|
+
```ts
|
|
376
730
|
import type {
|
|
377
731
|
// Core
|
|
378
732
|
FormSchema,
|
|
379
733
|
FormGeneratorProps,
|
|
380
734
|
BaseField,
|
|
381
735
|
Section,
|
|
382
|
-
|
|
736
|
+
|
|
383
737
|
// Components
|
|
384
738
|
FieldComponentProps,
|
|
385
739
|
FieldComponent,
|
|
386
740
|
ComponentRegistry,
|
|
387
|
-
|
|
741
|
+
|
|
388
742
|
// Layouts
|
|
389
743
|
SectionLayoutProps,
|
|
390
744
|
GridLayoutProps,
|
|
391
745
|
LayoutComponent,
|
|
392
746
|
LayoutRegistry,
|
|
393
|
-
|
|
747
|
+
|
|
394
748
|
// Options
|
|
395
749
|
FieldOption,
|
|
396
750
|
FieldOptionGroup,
|
|
397
|
-
|
|
751
|
+
|
|
752
|
+
// Conditions
|
|
753
|
+
ConditionRule,
|
|
754
|
+
ConditionConfig,
|
|
755
|
+
Condition,
|
|
756
|
+
|
|
757
|
+
// Hook types
|
|
758
|
+
UseFormKitOptions,
|
|
759
|
+
UseFormKitReturn,
|
|
760
|
+
|
|
398
761
|
// Utility types
|
|
399
762
|
FieldType,
|
|
400
763
|
LayoutType,
|
|
@@ -402,23 +765,12 @@ import type {
|
|
|
402
765
|
DefineField,
|
|
403
766
|
InferSchemaValues,
|
|
404
767
|
SchemaFieldNames,
|
|
768
|
+
FormElement,
|
|
405
769
|
} from "@classytic/formkit";
|
|
406
770
|
```
|
|
407
771
|
|
|
408
|
-
## Examples
|
|
409
|
-
|
|
410
|
-
See the [`example/shadcn`](./example/shadcn) directory for complete working examples with:
|
|
411
|
-
|
|
412
|
-
- Form components (Input, Select, Checkbox)
|
|
413
|
-
- Full adapter configuration
|
|
414
|
-
- Zod validation
|
|
415
|
-
- Conditional fields
|
|
416
|
-
- Multi-column layouts
|
|
417
|
-
- TypeScript integration
|
|
418
|
-
|
|
419
772
|
## Browser Support
|
|
420
773
|
|
|
421
|
-
- React 18.0+
|
|
422
774
|
- React 19.0+
|
|
423
775
|
- All modern browsers
|
|
424
776
|
|