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