@classytic/formkit 1.0.2 → 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 -0
- package/README.md +660 -76
- 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 +56 -30
- package/dist/index.cjs +0 -169
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -288
- package/dist/index.d.ts +0 -288
- package/dist/index.js +0 -164
- package/dist/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,44 +1,82 @@
|
|
|
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)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
7
8
|
|
|
8
|
-
##
|
|
9
|
+
## Features
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
13
24
|
|
|
14
|
-
##
|
|
25
|
+
## Requirements
|
|
15
26
|
|
|
16
|
-
**
|
|
27
|
+
- **React 19.0+** (React 18 is not supported)
|
|
28
|
+
- **React Hook Form 7.54.0+**
|
|
17
29
|
|
|
18
|
-
|
|
30
|
+
## Installation
|
|
19
31
|
|
|
20
32
|
```bash
|
|
21
|
-
|
|
33
|
+
npm install @classytic/formkit react-hook-form
|
|
34
|
+
# or
|
|
35
|
+
pnpm add @classytic/formkit react-hook-form
|
|
36
|
+
# or
|
|
37
|
+
yarn add @classytic/formkit react-hook-form
|
|
22
38
|
```
|
|
23
39
|
|
|
24
|
-
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
### 1. Create Field Components
|
|
43
|
+
|
|
44
|
+
Each field component receives `FieldComponentProps` including `error`, `fieldId`, and the full `field` config:
|
|
25
45
|
|
|
26
46
|
```tsx
|
|
27
|
-
// components/form-input.tsx
|
|
47
|
+
// components/form/form-input.tsx
|
|
48
|
+
"use client";
|
|
49
|
+
|
|
28
50
|
import { Controller } from "react-hook-form";
|
|
51
|
+
import type { FieldComponentProps } from "@classytic/formkit";
|
|
29
52
|
import { Input } from "@/components/ui/input";
|
|
30
53
|
import { Label } from "@/components/ui/label";
|
|
31
54
|
|
|
32
|
-
export function FormInput({
|
|
55
|
+
export function FormInput({
|
|
56
|
+
control,
|
|
57
|
+
field,
|
|
58
|
+
label,
|
|
59
|
+
placeholder,
|
|
60
|
+
required,
|
|
61
|
+
error,
|
|
62
|
+
fieldId,
|
|
63
|
+
}: FieldComponentProps) {
|
|
33
64
|
return (
|
|
34
65
|
<Controller
|
|
35
|
-
name={name}
|
|
66
|
+
name={field.name}
|
|
36
67
|
control={control}
|
|
37
|
-
render={({ field
|
|
68
|
+
render={({ field: rhfField }) => (
|
|
38
69
|
<div className="space-y-2">
|
|
39
|
-
{label &&
|
|
40
|
-
|
|
41
|
-
|
|
70
|
+
{label && (
|
|
71
|
+
<Label htmlFor={fieldId}>
|
|
72
|
+
{label}
|
|
73
|
+
{required && <span className="text-red-500 ml-1">*</span>}
|
|
74
|
+
</Label>
|
|
75
|
+
)}
|
|
76
|
+
<Input {...rhfField} id={fieldId} placeholder={placeholder} />
|
|
77
|
+
{error && (
|
|
78
|
+
<p className="text-sm text-red-500">{error.message}</p>
|
|
79
|
+
)}
|
|
42
80
|
</div>
|
|
43
81
|
)}
|
|
44
82
|
/>
|
|
@@ -46,110 +84,655 @@ export function FormInput({ control, name, label, placeholder, required, ...prop
|
|
|
46
84
|
}
|
|
47
85
|
```
|
|
48
86
|
|
|
49
|
-
###
|
|
87
|
+
### 2. Create Form Adapter
|
|
88
|
+
|
|
89
|
+
Register your components and layouts:
|
|
50
90
|
|
|
51
91
|
```tsx
|
|
52
92
|
// lib/form-adapter.tsx
|
|
53
|
-
|
|
54
|
-
import { FormInput } from "@/components/form-input";
|
|
93
|
+
"use client";
|
|
55
94
|
|
|
56
|
-
|
|
95
|
+
import {
|
|
96
|
+
FormSystemProvider,
|
|
97
|
+
type ComponentRegistry,
|
|
98
|
+
type LayoutRegistry,
|
|
99
|
+
} from "@classytic/formkit";
|
|
100
|
+
import { FormInput } from "@/components/form/form-input";
|
|
101
|
+
|
|
102
|
+
const components: ComponentRegistry = {
|
|
57
103
|
text: FormInput,
|
|
58
104
|
email: FormInput,
|
|
105
|
+
password: FormInput,
|
|
106
|
+
// Add more field types...
|
|
59
107
|
};
|
|
60
108
|
|
|
61
|
-
const layouts = {
|
|
62
|
-
section: ({ title, children }) =>
|
|
63
|
-
|
|
109
|
+
const layouts: LayoutRegistry = {
|
|
110
|
+
section: ({ title, description, children }) => (
|
|
111
|
+
<div className="space-y-4">
|
|
112
|
+
{title && <h3 className="text-lg font-semibold">{title}</h3>}
|
|
113
|
+
{description && <p className="text-muted-foreground">{description}</p>}
|
|
114
|
+
{children}
|
|
115
|
+
</div>
|
|
116
|
+
),
|
|
117
|
+
grid: ({ children, cols = 1 }) => (
|
|
118
|
+
<div className={`grid grid-cols-${cols} gap-4`}>{children}</div>
|
|
119
|
+
),
|
|
64
120
|
};
|
|
65
121
|
|
|
66
|
-
export function FormProvider({ children }) {
|
|
67
|
-
return
|
|
122
|
+
export function FormProvider({ children }: { children: React.ReactNode }) {
|
|
123
|
+
return (
|
|
124
|
+
<FormSystemProvider components={components} layouts={layouts}>
|
|
125
|
+
{children}
|
|
126
|
+
</FormSystemProvider>
|
|
127
|
+
);
|
|
68
128
|
}
|
|
69
129
|
```
|
|
70
130
|
|
|
71
|
-
###
|
|
131
|
+
### 3. Use FormGenerator
|
|
72
132
|
|
|
73
133
|
```tsx
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
134
|
+
// app/signup/page.tsx
|
|
135
|
+
"use client";
|
|
136
|
+
|
|
137
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
138
|
+
import { z } from "zod";
|
|
139
|
+
import { FormGenerator, useFormKit, type FormSchema } from "@classytic/formkit";
|
|
140
|
+
import { FormProvider } from "@/lib/form-adapter";
|
|
141
|
+
|
|
142
|
+
const signupSchema = z.object({
|
|
143
|
+
firstName: z.string().min(2),
|
|
144
|
+
lastName: z.string().min(2),
|
|
145
|
+
email: z.string().email(),
|
|
146
|
+
password: z.string().min(8),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
type SignupData = z.infer<typeof signupSchema>;
|
|
150
|
+
|
|
151
|
+
const formSchema: FormSchema<SignupData> = {
|
|
152
|
+
sections: [
|
|
153
|
+
{
|
|
154
|
+
title: "Personal Information",
|
|
155
|
+
cols: 2,
|
|
156
|
+
fields: [
|
|
157
|
+
{ name: "firstName", type: "text", label: "First Name", required: true, defaultValue: "" },
|
|
158
|
+
{ name: "lastName", type: "text", label: "Last Name", required: true, defaultValue: "" },
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
title: "Account",
|
|
163
|
+
fields: [
|
|
164
|
+
{ name: "email", type: "email", label: "Email", required: true, defaultValue: "" },
|
|
165
|
+
{ name: "password", type: "password", label: "Password", required: true, defaultValue: "" },
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
};
|
|
77
170
|
|
|
78
|
-
export function
|
|
79
|
-
const
|
|
171
|
+
export default function SignupPage() {
|
|
172
|
+
const { handleSubmit, generatorProps } = useFormKit({
|
|
173
|
+
schema: formSchema,
|
|
174
|
+
resolver: zodResolver(signupSchema),
|
|
175
|
+
});
|
|
80
176
|
|
|
81
177
|
return (
|
|
82
178
|
<FormProvider>
|
|
83
|
-
<form onSubmit={
|
|
84
|
-
<FormGenerator
|
|
85
|
-
|
|
86
|
-
sections: [{
|
|
87
|
-
fields: [
|
|
88
|
-
{ name: "email", type: "email", label: "Email", required: true },
|
|
89
|
-
{ name: "password", type: "text", label: "Password" },
|
|
90
|
-
]
|
|
91
|
-
}]
|
|
92
|
-
}}
|
|
93
|
-
control={form.control}
|
|
94
|
-
/>
|
|
95
|
-
<button type="submit">Submit</button>
|
|
179
|
+
<form onSubmit={handleSubmit(console.log)} className="space-y-8">
|
|
180
|
+
<FormGenerator {...generatorProps} />
|
|
181
|
+
<button type="submit">Sign Up</button>
|
|
96
182
|
</form>
|
|
97
183
|
</FormProvider>
|
|
98
184
|
);
|
|
99
185
|
}
|
|
100
186
|
```
|
|
101
187
|
|
|
102
|
-
##
|
|
188
|
+
## API Reference
|
|
103
189
|
|
|
104
|
-
|
|
105
|
-
- FormInput, FormSelect, FormCheckbox
|
|
106
|
-
- Validation with Zod
|
|
107
|
-
- Conditional fields
|
|
108
|
-
- Error handling
|
|
190
|
+
### useFormKit
|
|
109
191
|
|
|
110
|
-
|
|
192
|
+
Convenience hook that combines schema default extraction with react-hook-form setup. Returns all `useForm` methods plus ready-to-spread `generatorProps`.
|
|
111
193
|
|
|
112
|
-
**You MUST use Controller:**
|
|
113
194
|
```tsx
|
|
114
|
-
import {
|
|
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
|
+
|
|
217
|
+
### FormGenerator
|
|
115
218
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
)
|
|
219
|
+
The main component that renders forms from a schema. Supports React 19 `ref` as a regular prop.
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
<FormGenerator
|
|
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)
|
|
122
229
|
/>
|
|
123
230
|
```
|
|
124
231
|
|
|
125
|
-
|
|
232
|
+
### FormSchema
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
interface FormSchema<T extends FieldValues = FieldValues> {
|
|
236
|
+
sections: Section<T>[];
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Section
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
interface Section<T> {
|
|
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
|
|
254
|
+
defaultCollapsed?: boolean;
|
|
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;
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### BaseField
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
interface BaseField<T> {
|
|
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
|
+
|
|
282
|
+
// Conditional rendering
|
|
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
|
+
|
|
293
|
+
// For select/radio/checkbox
|
|
294
|
+
options?: FieldOption[];
|
|
295
|
+
|
|
296
|
+
// HTML input attributes
|
|
297
|
+
min?: number | string;
|
|
298
|
+
max?: number | string;
|
|
299
|
+
step?: number;
|
|
300
|
+
pattern?: string;
|
|
301
|
+
minLength?: number;
|
|
302
|
+
maxLength?: number;
|
|
303
|
+
rows?: number;
|
|
304
|
+
multiple?: boolean;
|
|
305
|
+
accept?: string;
|
|
306
|
+
autoComplete?: string;
|
|
307
|
+
autoFocus?: boolean;
|
|
308
|
+
|
|
309
|
+
// Custom render override
|
|
310
|
+
render?: (props: FieldComponentProps<T>) => ReactNode;
|
|
311
|
+
|
|
312
|
+
// Arbitrary extra props for custom components
|
|
313
|
+
customProps?: Record<string, unknown>;
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### FieldComponentProps
|
|
318
|
+
|
|
319
|
+
Props passed to your field components:
|
|
320
|
+
|
|
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")
|
|
337
|
+
}
|
|
338
|
+
```
|
|
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
|
+
|
|
371
|
+
### ComponentRegistry
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
const components: ComponentRegistry = {
|
|
375
|
+
// Simple mapping
|
|
376
|
+
text: FormInput,
|
|
377
|
+
select: FormSelect,
|
|
378
|
+
|
|
379
|
+
// Variant-specific components
|
|
380
|
+
compact: {
|
|
381
|
+
text: CompactInput,
|
|
382
|
+
select: CompactSelect,
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### LayoutRegistry
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
const layouts: LayoutRegistry = {
|
|
391
|
+
section: SectionLayout,
|
|
392
|
+
grid: GridLayout,
|
|
393
|
+
|
|
394
|
+
// Variant-specific layouts
|
|
395
|
+
compact: {
|
|
396
|
+
section: CompactSection,
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
```
|
|
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
|
+
|
|
467
|
+
## Advanced Features
|
|
468
|
+
|
|
469
|
+
### Conditional Fields (Function)
|
|
470
|
+
|
|
471
|
+
```ts
|
|
472
|
+
{
|
|
473
|
+
name: "companyName",
|
|
474
|
+
type: "text",
|
|
475
|
+
label: "Company Name",
|
|
476
|
+
condition: (values) => values.accountType === "business",
|
|
477
|
+
}
|
|
478
|
+
```
|
|
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
|
+
|
|
547
|
+
### Variants
|
|
548
|
+
|
|
549
|
+
Apply different styles based on context:
|
|
550
|
+
|
|
126
551
|
```tsx
|
|
552
|
+
// Register variant-specific components
|
|
553
|
+
const components = {
|
|
554
|
+
text: DefaultInput,
|
|
555
|
+
compact: {
|
|
556
|
+
text: CompactInput,
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
// Use variant on the whole form
|
|
561
|
+
<FormGenerator schema={schema} variant="compact" />
|
|
562
|
+
|
|
563
|
+
// Or per-section
|
|
564
|
+
{ variant: "compact", fields: [...] }
|
|
565
|
+
|
|
566
|
+
// Or per-field
|
|
567
|
+
{ name: "notes", type: "text", variant: "compact" }
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### Dynamic Options Loading
|
|
571
|
+
|
|
572
|
+
```ts
|
|
127
573
|
{
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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,
|
|
133
582
|
}
|
|
134
583
|
```
|
|
135
584
|
|
|
136
|
-
|
|
585
|
+
### Custom Section Render
|
|
137
586
|
|
|
138
|
-
|
|
587
|
+
```ts
|
|
588
|
+
{
|
|
589
|
+
title: "Payment",
|
|
590
|
+
render: ({ control, disabled }) => (
|
|
591
|
+
<StripeElements>
|
|
592
|
+
<CardElement />
|
|
593
|
+
<FormInput name="billingName" control={control} />
|
|
594
|
+
</StripeElements>
|
|
595
|
+
),
|
|
596
|
+
}
|
|
597
|
+
```
|
|
139
598
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
```
|
|
146
626
|
|
|
147
|
-
|
|
627
|
+
Access in your component:
|
|
148
628
|
|
|
149
629
|
```tsx
|
|
150
|
-
|
|
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
|
|
639
|
+
{
|
|
640
|
+
name: "country",
|
|
641
|
+
type: "select",
|
|
642
|
+
options: [
|
|
643
|
+
{
|
|
644
|
+
label: "North America",
|
|
645
|
+
options: [
|
|
646
|
+
{ value: "us", label: "United States" },
|
|
647
|
+
{ value: "ca", label: "Canada" },
|
|
648
|
+
],
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
label: "Europe",
|
|
652
|
+
options: [
|
|
653
|
+
{ value: "uk", label: "United Kingdom" },
|
|
654
|
+
{ value: "de", label: "Germany" },
|
|
655
|
+
],
|
|
656
|
+
},
|
|
657
|
+
],
|
|
658
|
+
}
|
|
151
659
|
```
|
|
152
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
|
+
|
|
686
|
+
## Type Exports
|
|
687
|
+
|
|
688
|
+
```ts
|
|
689
|
+
import type {
|
|
690
|
+
// Core
|
|
691
|
+
FormSchema,
|
|
692
|
+
FormGeneratorProps,
|
|
693
|
+
BaseField,
|
|
694
|
+
Section,
|
|
695
|
+
|
|
696
|
+
// Components
|
|
697
|
+
FieldComponentProps,
|
|
698
|
+
FieldComponent,
|
|
699
|
+
ComponentRegistry,
|
|
700
|
+
|
|
701
|
+
// Layouts
|
|
702
|
+
SectionLayoutProps,
|
|
703
|
+
GridLayoutProps,
|
|
704
|
+
LayoutComponent,
|
|
705
|
+
LayoutRegistry,
|
|
706
|
+
|
|
707
|
+
// Options
|
|
708
|
+
FieldOption,
|
|
709
|
+
FieldOptionGroup,
|
|
710
|
+
|
|
711
|
+
// Conditions
|
|
712
|
+
ConditionRule,
|
|
713
|
+
ConditionConfig,
|
|
714
|
+
Condition,
|
|
715
|
+
|
|
716
|
+
// Hook types
|
|
717
|
+
UseFormKitOptions,
|
|
718
|
+
UseFormKitReturn,
|
|
719
|
+
|
|
720
|
+
// Utility types
|
|
721
|
+
FieldType,
|
|
722
|
+
LayoutType,
|
|
723
|
+
Variant,
|
|
724
|
+
DefineField,
|
|
725
|
+
InferSchemaValues,
|
|
726
|
+
SchemaFieldNames,
|
|
727
|
+
FormElement,
|
|
728
|
+
} from "@classytic/formkit";
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
## Browser Support
|
|
732
|
+
|
|
733
|
+
- React 19.0+
|
|
734
|
+
- All modern browsers
|
|
735
|
+
|
|
153
736
|
## License
|
|
154
737
|
|
|
155
738
|
MIT © [Classytic](https://github.com/classytic)
|
|
@@ -157,5 +740,6 @@ MIT © [Classytic](https://github.com/classytic)
|
|
|
157
740
|
## Links
|
|
158
741
|
|
|
159
742
|
- [GitHub](https://github.com/classytic/formkit)
|
|
743
|
+
- [npm](https://www.npmjs.com/package/@classytic/formkit)
|
|
160
744
|
- [Examples](https://github.com/classytic/formkit/tree/main/example/shadcn)
|
|
161
745
|
- [Issues](https://github.com/classytic/formkit/issues)
|