@classytic/formkit 1.3.1 → 1.5.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 +73 -0
- package/README.md +166 -20
- package/dist/index.d.mts +577 -164
- package/dist/index.mjs +1131 -294
- package/dist/server.d.mts +472 -151
- package/dist/server.mjs +593 -98
- package/package.json +116 -113
package/dist/index.d.mts
CHANGED
|
@@ -8,7 +8,7 @@ import { ClassValue } from "clsx";
|
|
|
8
8
|
* Field type identifier.
|
|
9
9
|
* Can be built-in types or custom string identifiers.
|
|
10
10
|
*/
|
|
11
|
-
type FieldType = "text" | "email" | "password" | "number" | "tel" | "url" | "textarea" | "select" | "checkbox" | "radio" | "switch" | "date" | "time" | "datetime" | "file" | "hidden" | "group" | "array" | "custom" | (string & {});
|
|
11
|
+
type FieldType = "text" | "email" | "password" | "number" | "tel" | "phone" | "url" | "textarea" | "slug" | "select" | "combobox" | "multiselect" | "dependentSelect" | "checkbox" | "radio" | "switch" | "date" | "time" | "datetime" | "rich-text" | "color" | "rating" | "tags" | "json" | "file" | "hidden" | "group" | "array" | "custom" | (string & {});
|
|
12
12
|
/**
|
|
13
13
|
* Layout type identifier.
|
|
14
14
|
*/
|
|
@@ -70,12 +70,55 @@ interface FieldOptionGroup<TValue = string> {
|
|
|
70
70
|
/** Whether group is disabled */
|
|
71
71
|
disabled?: boolean;
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Object form for length/range validation rules with a custom message.
|
|
75
|
+
*/
|
|
76
|
+
interface ValidationRuleObject<TValue = number> {
|
|
77
|
+
value: TValue;
|
|
78
|
+
message: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Object form for pattern validation with a custom message.
|
|
82
|
+
*/
|
|
83
|
+
interface PatternRuleObject {
|
|
84
|
+
/** Regex string (will be compiled with `new RegExp(regex)`) */
|
|
85
|
+
regex: string;
|
|
86
|
+
/** Custom error message */
|
|
87
|
+
message: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* AI-friendly metadata attached to any field.
|
|
91
|
+
* Consumed by LLM coding assistants, documentation generators, and
|
|
92
|
+
* schema-introspection tools — never by the form renderer itself.
|
|
93
|
+
*/
|
|
94
|
+
interface FieldMeta {
|
|
95
|
+
/** Human-readable description of what this field collects */
|
|
96
|
+
description?: string;
|
|
97
|
+
/** Representative example value (for documentation / AI context) */
|
|
98
|
+
example?: unknown;
|
|
99
|
+
/** Logical category for grouping fields in documentation or tooling */
|
|
100
|
+
category?: string;
|
|
101
|
+
/** Arbitrary tags for filtering, search, or feature-flagging */
|
|
102
|
+
tags?: string[];
|
|
103
|
+
/** Whether this field is considered PII */
|
|
104
|
+
pii?: boolean;
|
|
105
|
+
/** Arbitrary extension point — anything extra you want to carry */
|
|
106
|
+
[key: string]: unknown;
|
|
107
|
+
}
|
|
73
108
|
/**
|
|
74
109
|
* Base field configuration shared by all field types.
|
|
75
110
|
* @template TFieldValues - Form field values type for type-safe field names
|
|
76
111
|
*/
|
|
77
112
|
interface BaseField<TFieldValues extends FieldValues = FieldValues> {
|
|
78
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* Field name — a path into TFieldValues (e.g. `"email"`, `"address.street"`).
|
|
115
|
+
*
|
|
116
|
+
* Accepts any string so that:
|
|
117
|
+
* - Namespaced sections can use relative names (`"street"` → prefixed to `"address.street"`)
|
|
118
|
+
* - Group/array `itemFields` can use relative names (`"email"` → prefixed at render time)
|
|
119
|
+
*
|
|
120
|
+
* Use `field.for<T>()` builder for call-site enforcement that names are valid paths.
|
|
121
|
+
*/
|
|
79
122
|
name: Path<TFieldValues> | (string & {});
|
|
80
123
|
/** Field type identifier */
|
|
81
124
|
type: FieldType;
|
|
@@ -95,6 +138,13 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
|
|
|
95
138
|
variant?: Variant;
|
|
96
139
|
/** Whether field should span full width in grid */
|
|
97
140
|
fullWidth?: boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Span N columns of the section grid (e.g. `2` of a 3-col section). `1` (or
|
|
143
|
+
* unset) is the default single cell; `fullWidth` still means "span the whole
|
|
144
|
+
* row" and takes precedence. Applied as an inline `grid-column: span N` so it
|
|
145
|
+
* works regardless of the host's Tailwind content scan.
|
|
146
|
+
*/
|
|
147
|
+
colSpan?: number;
|
|
98
148
|
/** Custom CSS class name */
|
|
99
149
|
className?: string;
|
|
100
150
|
/**
|
|
@@ -106,18 +156,41 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
|
|
|
106
156
|
defaultValue?: unknown;
|
|
107
157
|
/** Options for select/radio/checkbox fields */
|
|
108
158
|
options?: (FieldOption | FieldOptionGroup)[];
|
|
109
|
-
/**
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Minimum value (for number/date fields).
|
|
161
|
+
* Pass an object `{ value, message }` to provide a custom error message.
|
|
162
|
+
*/
|
|
163
|
+
min?: number | string | ValidationRuleObject<number | string>;
|
|
164
|
+
/**
|
|
165
|
+
* Maximum value (for number/date fields).
|
|
166
|
+
* Pass an object `{ value, message }` to provide a custom error message.
|
|
167
|
+
*/
|
|
168
|
+
max?: number | string | ValidationRuleObject<number | string>;
|
|
113
169
|
/** Step value (for number fields) */
|
|
114
170
|
step?: number;
|
|
115
|
-
/**
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Pattern for validation.
|
|
173
|
+
* Pass a regex string or `{ regex, message }` for a custom error message.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```ts
|
|
177
|
+
* // simple
|
|
178
|
+
* pattern: "^[a-z]+$"
|
|
179
|
+
* // with custom message
|
|
180
|
+
* pattern: { regex: "^[a-z]+$", message: "Only lowercase letters allowed" }
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
pattern?: string | PatternRuleObject;
|
|
184
|
+
/**
|
|
185
|
+
* Minimum length.
|
|
186
|
+
* Pass an object `{ value, message }` to provide a custom error message.
|
|
187
|
+
*/
|
|
188
|
+
minLength?: number | ValidationRuleObject<number>;
|
|
189
|
+
/**
|
|
190
|
+
* Maximum length.
|
|
191
|
+
* Pass an object `{ value, message }` to provide a custom error message.
|
|
192
|
+
*/
|
|
193
|
+
maxLength?: number | ValidationRuleObject<number>;
|
|
121
194
|
/** Number of rows (for textarea) */
|
|
122
195
|
rows?: number;
|
|
123
196
|
/** Multiple selection (for select/file) */
|
|
@@ -133,18 +206,41 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
|
|
|
133
206
|
/**
|
|
134
207
|
* Dynamic options loaded based on current form values.
|
|
135
208
|
* Useful for dependent selects (e.g., state depends on country).
|
|
209
|
+
*
|
|
210
|
+
* Receives an `AbortSignal` in the second arg — forward it to `fetch` (or
|
|
211
|
+
* abort your own request on it) so a superseded / unmounted load is cancelled
|
|
212
|
+
* instead of racing to completion:
|
|
213
|
+
*
|
|
214
|
+
* ```ts
|
|
215
|
+
* loadOptions: (values, { signal }) =>
|
|
216
|
+
* fetch(`/api/cities?country=${values.country}`, { signal }).then(r => r.json())
|
|
217
|
+
* ```
|
|
136
218
|
*/
|
|
137
|
-
loadOptions?: (formValues: Partial<TFieldValues
|
|
219
|
+
loadOptions?: (formValues: Partial<TFieldValues>, options?: {
|
|
220
|
+
signal: AbortSignal;
|
|
221
|
+
}) => Promise<(FieldOption | FieldOptionGroup)[]> | (FieldOption | FieldOptionGroup)[];
|
|
138
222
|
/**
|
|
139
223
|
* Error callback for loadOptions failures.
|
|
140
224
|
* Called when loadOptions rejects. Defaults to console.error.
|
|
141
225
|
*/
|
|
142
226
|
onLoadError?: (error: unknown) => void;
|
|
143
227
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
228
|
+
* Memoize async `loadOptions` results per set of watched values, so toggling
|
|
229
|
+
* a dependency back and forth (e.g. re-selecting a country) reuses the last
|
|
230
|
+
* fetch instead of hitting the API again. Off by default — enable only when
|
|
231
|
+
* the options are stable for a given input, since the cache lives for the
|
|
232
|
+
* field's lifetime and won't see server-side changes. Bounded (LRU-ish) so it
|
|
233
|
+
* can't grow without limit.
|
|
234
|
+
*/
|
|
235
|
+
cacheOptions?: boolean;
|
|
236
|
+
/**
|
|
237
|
+
* Sub-fields for `group` and `array` field types.
|
|
238
|
+
*
|
|
239
|
+
* These fields use **relative** names (`"street"`, `"email"`) — FormGenerator
|
|
240
|
+
* prefixes them with the parent field name at render time (`"address.street"`).
|
|
241
|
+
* They are intentionally untyped to TFieldValues for this reason.
|
|
146
242
|
*/
|
|
147
|
-
itemFields?: BaseField
|
|
243
|
+
itemFields?: BaseField[];
|
|
148
244
|
/**
|
|
149
245
|
* Custom render function to override the component registry for this specific field.
|
|
150
246
|
* Completely bypasses the globally registered FieldComponent for this type.
|
|
@@ -153,15 +249,22 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
|
|
|
153
249
|
/**
|
|
154
250
|
* Cross-field validation function.
|
|
155
251
|
* Receives the field value and all form values for cross-field checks.
|
|
156
|
-
* Return `true` for valid,
|
|
252
|
+
* Return `true` for valid, a string error message for invalid, or a Promise of either
|
|
253
|
+
* for async validation (e.g., checking server-side uniqueness).
|
|
157
254
|
*
|
|
158
255
|
* @example
|
|
159
256
|
* ```ts
|
|
160
257
|
* validate: (value, formValues) =>
|
|
161
258
|
* value > formValues.minPrice || "Must be greater than min price"
|
|
259
|
+
*
|
|
260
|
+
* // Async example
|
|
261
|
+
* validate: async (value) => {
|
|
262
|
+
* const taken = await checkUsernameAvailability(value as string);
|
|
263
|
+
* return taken ? "Username already taken" : true;
|
|
264
|
+
* }
|
|
162
265
|
* ```
|
|
163
266
|
*/
|
|
164
|
-
validate?: (value: unknown, formValues: Partial<TFieldValues>) => string | true
|
|
267
|
+
validate?: (value: unknown, formValues: Partial<TFieldValues>) => string | true | Promise<string | true>;
|
|
165
268
|
/**
|
|
166
269
|
* Dependencies for optimizing conditionally rendered fields.
|
|
167
270
|
* Allows specifying specific field names to watch, preventing full form re-renders.
|
|
@@ -169,6 +272,32 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
|
|
|
169
272
|
watchNames?: Path<TFieldValues> | Path<TFieldValues>[];
|
|
170
273
|
/** Additional field-specific props for custom components */
|
|
171
274
|
customProps?: Record<string, unknown>;
|
|
275
|
+
/**
|
|
276
|
+
* AI / agent-friendly metadata for this field.
|
|
277
|
+
*
|
|
278
|
+
* Provides hints that help LLM coding assistants generate correct field
|
|
279
|
+
* configs without needing to inspect the schema at runtime.
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* ```ts
|
|
283
|
+
* field.text("companyName", "Company", {
|
|
284
|
+
* meta: {
|
|
285
|
+
* description: "Legal entity name of the organization",
|
|
286
|
+
* example: "Acme Corp",
|
|
287
|
+
* category: "identity",
|
|
288
|
+
* tags: ["crm", "required-for-billing"],
|
|
289
|
+
* }
|
|
290
|
+
* })
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
meta?: FieldMeta;
|
|
294
|
+
/**
|
|
295
|
+
* Escape hatch for adapter-specific or custom props.
|
|
296
|
+
* Allows passing arbitrary props directly on the field object so they
|
|
297
|
+
* flow through the adapter spread (`{...field}`) without needing `customProps`.
|
|
298
|
+
* Intentionally broad to support diverse UI component libraries.
|
|
299
|
+
*/
|
|
300
|
+
[key: string]: unknown;
|
|
172
301
|
}
|
|
173
302
|
/**
|
|
174
303
|
* Props passed to field components.
|
|
@@ -214,13 +343,68 @@ interface FieldComponentProps<TFieldValues extends FieldValues = FieldValues> ex
|
|
|
214
343
|
invalid: boolean;
|
|
215
344
|
isDirty: boolean;
|
|
216
345
|
isTouched: boolean;
|
|
217
|
-
isValidating: boolean;
|
|
346
|
+
isValidating: boolean; /** True after the enclosing form's submit handler has been called at least once. */
|
|
347
|
+
isSubmitted: boolean;
|
|
218
348
|
error?: FieldError;
|
|
219
349
|
};
|
|
220
|
-
/**
|
|
350
|
+
/**
|
|
351
|
+
* Generated field ID for label-input association (e.g. `formkit-field-email`).
|
|
352
|
+
* Use as `id` on the input element and `htmlFor` on the `<label>`.
|
|
353
|
+
*/
|
|
221
354
|
fieldId: string;
|
|
355
|
+
/**
|
|
356
|
+
* Generated error container ID for ARIA association (e.g. `formkit-field-email-error`).
|
|
357
|
+
* Use as `id` on the error message element and as the value for `aria-errormessage`
|
|
358
|
+
* (preferred) or `aria-describedby` (broader support) on the input.
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* ```tsx
|
|
362
|
+
* <input
|
|
363
|
+
* id={fieldId}
|
|
364
|
+
* aria-invalid={shouldShowError || undefined}
|
|
365
|
+
* aria-errormessage={shouldShowError ? errorId : undefined}
|
|
366
|
+
* />
|
|
367
|
+
* <p id={errorId} role="alert" aria-live="polite">
|
|
368
|
+
* {shouldShowError ? error?.message : null}
|
|
369
|
+
* </p>
|
|
370
|
+
* ```
|
|
371
|
+
*/
|
|
372
|
+
errorId: string;
|
|
373
|
+
/**
|
|
374
|
+
* Whether to display the field error right now.
|
|
375
|
+
*
|
|
376
|
+
* Aligns with the CSS `:user-invalid` timing model: `true` only after the
|
|
377
|
+
* user has interacted with the field (blur) **or** the form has been
|
|
378
|
+
* submitted. This prevents premature "required" errors on untouched fields.
|
|
379
|
+
*
|
|
380
|
+
* Use this — not `!!error` — to drive `aria-invalid`, error message
|
|
381
|
+
* visibility, and destructive ring/border styles.
|
|
382
|
+
*
|
|
383
|
+
* @example
|
|
384
|
+
* ```tsx
|
|
385
|
+
* <input aria-invalid={shouldShowError || undefined} />
|
|
386
|
+
* {shouldShowError && <p id={errorId} role="alert">{error?.message}</p>}
|
|
387
|
+
* ```
|
|
388
|
+
*/
|
|
389
|
+
shouldShowError: boolean;
|
|
222
390
|
/** Whether dynamic options are currently loading */
|
|
223
391
|
isLoading?: boolean;
|
|
392
|
+
/**
|
|
393
|
+
* Pre-computed react-hook-form validation rules for this field.
|
|
394
|
+
* Equivalent to calling `buildValidationRules(field)` — provided here so
|
|
395
|
+
* adapter components can pass `rules={rules}` directly to `<Controller>`
|
|
396
|
+
* without importing or calling `buildValidationRules` themselves.
|
|
397
|
+
*
|
|
398
|
+
* @example
|
|
399
|
+
* ```tsx
|
|
400
|
+
* function TextInput({ field, control, rules }: FieldComponentProps) {
|
|
401
|
+
* return (
|
|
402
|
+
* <Controller name={field.name} control={control} rules={rules} render={...} />
|
|
403
|
+
* );
|
|
404
|
+
* }
|
|
405
|
+
* ```
|
|
406
|
+
*/
|
|
407
|
+
rules: ValidationRules;
|
|
224
408
|
}
|
|
225
409
|
/**
|
|
226
410
|
* Field component type.
|
|
@@ -271,6 +455,17 @@ interface Section<TFieldValues extends FieldValues = FieldValues> {
|
|
|
271
455
|
collapsible?: boolean;
|
|
272
456
|
/** Default collapsed state */
|
|
273
457
|
defaultCollapsed?: boolean;
|
|
458
|
+
/**
|
|
459
|
+
* Defer rendering of this section until it scrolls near the viewport.
|
|
460
|
+
* Applies `content-visibility: auto` + `contain-intrinsic-size` to the
|
|
461
|
+
* section container, skipping layout/paint work while off-screen.
|
|
462
|
+
*
|
|
463
|
+
* **Only use for sections that are below the initial fold.** Applying this
|
|
464
|
+
* to above-fold sections has no benefit and slightly increases overhead.
|
|
465
|
+
*
|
|
466
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/content-visibility
|
|
467
|
+
*/
|
|
468
|
+
deferRender?: boolean;
|
|
274
469
|
}
|
|
275
470
|
/**
|
|
276
471
|
* Props passed to section render function.
|
|
@@ -298,6 +493,12 @@ interface SectionLayoutProps {
|
|
|
298
493
|
collapsible?: boolean;
|
|
299
494
|
/** Default collapsed state */
|
|
300
495
|
defaultCollapsed?: boolean;
|
|
496
|
+
/**
|
|
497
|
+
* When true the section is below the initial fold and should defer
|
|
498
|
+
* browser layout/paint work until it nears the viewport.
|
|
499
|
+
* Layout components may apply `content-visibility: auto` here.
|
|
500
|
+
*/
|
|
501
|
+
deferRender?: boolean;
|
|
301
502
|
/** Children content */
|
|
302
503
|
children: ReactNode;
|
|
303
504
|
}
|
|
@@ -336,6 +537,12 @@ type LayoutComponent<TProps extends LayoutComponentProps = LayoutComponentProps>
|
|
|
336
537
|
* @template TFieldValues - Form field values type for type-safe schemas
|
|
337
538
|
*/
|
|
338
539
|
interface FormSchema<TFieldValues extends FieldValues = FieldValues> {
|
|
540
|
+
/** Optional schema identifier — useful for serialization, analytics, and AI context */
|
|
541
|
+
id?: string;
|
|
542
|
+
/** Human-readable form title (AI / documentation use) */
|
|
543
|
+
title?: string;
|
|
544
|
+
/** Human-readable description of the form's purpose */
|
|
545
|
+
description?: string;
|
|
339
546
|
/** Form sections */
|
|
340
547
|
sections: Section<TFieldValues>[];
|
|
341
548
|
}
|
|
@@ -373,6 +580,8 @@ interface FormSystemContextValue {
|
|
|
373
580
|
components: ComponentRegistry;
|
|
374
581
|
/** Registered layout components */
|
|
375
582
|
layouts: LayoutRegistry;
|
|
583
|
+
/** Notified when a field type has no registered component (see provider). */
|
|
584
|
+
onMissingComponent?: (type: string, variant?: string) => void;
|
|
376
585
|
}
|
|
377
586
|
/**
|
|
378
587
|
* Form system provider props.
|
|
@@ -382,6 +591,13 @@ interface FormSystemProviderProps {
|
|
|
382
591
|
components?: ComponentRegistry;
|
|
383
592
|
/** Layout component registry */
|
|
384
593
|
layouts?: LayoutRegistry;
|
|
594
|
+
/**
|
|
595
|
+
* Called (once per type) when a field's `type` has no registered component
|
|
596
|
+
* and the visible placeholder is shown instead. Use it to log/report the
|
|
597
|
+
* missing registration to your telemetry — the form still renders a
|
|
598
|
+
* placeholder rather than silently dropping the field.
|
|
599
|
+
*/
|
|
600
|
+
onMissingComponent?: (type: string, variant?: string) => void;
|
|
385
601
|
/** Children content */
|
|
386
602
|
children: ReactNode;
|
|
387
603
|
}
|
|
@@ -462,10 +678,12 @@ interface SectionRendererProps<TFieldValues extends FieldValues = FieldValues> {
|
|
|
462
678
|
disabled?: boolean;
|
|
463
679
|
variant?: Variant;
|
|
464
680
|
}
|
|
681
|
+
declare function SectionRendererImpl<TFieldValues extends FieldValues = FieldValues>(props: SectionRendererProps<TFieldValues>): FormElement;
|
|
465
682
|
/**
|
|
466
|
-
*
|
|
683
|
+
* Memoized section renderer. Re-renders only when section config or
|
|
684
|
+
* disabled/variant context changes — not on every form value change.
|
|
467
685
|
*/
|
|
468
|
-
declare
|
|
686
|
+
declare const SectionRenderer: typeof SectionRendererImpl;
|
|
469
687
|
interface GridRendererProps<TFieldValues extends FieldValues = FieldValues> {
|
|
470
688
|
fields?: BaseField<TFieldValues>[];
|
|
471
689
|
cols?: number;
|
|
@@ -474,10 +692,7 @@ interface GridRendererProps<TFieldValues extends FieldValues = FieldValues> {
|
|
|
474
692
|
disabled?: boolean;
|
|
475
693
|
variant?: Variant;
|
|
476
694
|
}
|
|
477
|
-
|
|
478
|
-
* Renders a grid of fields with specified column layout.
|
|
479
|
-
*/
|
|
480
|
-
declare function GridRenderer<TFieldValues extends FieldValues = FieldValues>({
|
|
695
|
+
declare function GridRendererImpl<TFieldValues extends FieldValues = FieldValues>({
|
|
481
696
|
fields,
|
|
482
697
|
cols,
|
|
483
698
|
gap,
|
|
@@ -485,18 +700,22 @@ declare function GridRenderer<TFieldValues extends FieldValues = FieldValues>({
|
|
|
485
700
|
disabled,
|
|
486
701
|
variant
|
|
487
702
|
}: GridRendererProps<TFieldValues>): FormElement;
|
|
703
|
+
/**
|
|
704
|
+
* Memoized grid renderer.
|
|
705
|
+
*/
|
|
706
|
+
declare const GridRenderer: typeof GridRendererImpl;
|
|
488
707
|
interface FieldWrapperProps<TFieldValues extends FieldValues = FieldValues> {
|
|
489
708
|
field: BaseField<TFieldValues>;
|
|
490
709
|
control?: Control<TFieldValues>;
|
|
491
710
|
disabled?: boolean;
|
|
492
711
|
variant?: Variant;
|
|
493
712
|
}
|
|
713
|
+
declare function FieldWrapperImpl<TFieldValues extends FieldValues = FieldValues>(props: FieldWrapperProps<TFieldValues>): FormElement;
|
|
494
714
|
/**
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
* Otherwise, it uses the Static wrapper, vastly improving performance by skipping `useWatch`.
|
|
715
|
+
* Memoized field wrapper — the dispatch layer between schema fields and
|
|
716
|
+
* their renderer. Re-renders only when the field config itself changes.
|
|
498
717
|
*/
|
|
499
|
-
declare
|
|
718
|
+
declare const FieldWrapper: typeof FieldWrapperImpl;
|
|
500
719
|
//#endregion
|
|
501
720
|
//#region src/FormSystemContext.d.ts
|
|
502
721
|
/**
|
|
@@ -535,6 +754,7 @@ declare function FieldWrapper<TFieldValues extends FieldValues = FieldValues>(pr
|
|
|
535
754
|
declare function FormSystemProvider({
|
|
536
755
|
components,
|
|
537
756
|
layouts,
|
|
757
|
+
onMissingComponent,
|
|
538
758
|
children
|
|
539
759
|
}: FormSystemProviderProps): FormElement;
|
|
540
760
|
/**
|
|
@@ -556,11 +776,16 @@ declare function useFormSystem(): FormSystemContextValue;
|
|
|
556
776
|
* 1. Variant-specific component: `components[variant][type]`
|
|
557
777
|
* 2. Type-specific component: `components[type]`
|
|
558
778
|
* 3. Default component: `components["default"]`
|
|
559
|
-
*
|
|
779
|
+
*
|
|
780
|
+
* There is intentionally NO "text" fallback: rendering a text input for an
|
|
781
|
+
* unregistered `date`/`switch`/etc. is a silent wrong-widget bug. A missing
|
|
782
|
+
* registration instead renders a visible placeholder (`MissingFieldComponent`)
|
|
783
|
+
* — in production too — and notifies `onMissingComponent`, so the gap is
|
|
784
|
+
* observable rather than a mystery empty box.
|
|
560
785
|
*
|
|
561
786
|
* @param type - Field type identifier
|
|
562
787
|
* @param variant - Optional variant name
|
|
563
|
-
* @returns Field component or
|
|
788
|
+
* @returns Field component or the visible placeholder
|
|
564
789
|
*
|
|
565
790
|
* @internal
|
|
566
791
|
*/
|
|
@@ -645,6 +870,7 @@ declare function defineSection<TFieldValues extends FieldValues = FieldValues>(s
|
|
|
645
870
|
/**
|
|
646
871
|
* Extracts default values from a form schema.
|
|
647
872
|
* Walks all sections and fields, respecting nameSpace prefixes and group nesting.
|
|
873
|
+
* Array fields default to `[]` when no explicit `defaultValue` is provided.
|
|
648
874
|
*
|
|
649
875
|
* @example
|
|
650
876
|
* ```ts
|
|
@@ -653,11 +879,78 @@ declare function defineSection<TFieldValues extends FieldValues = FieldValues>(s
|
|
|
653
879
|
* ```
|
|
654
880
|
*/
|
|
655
881
|
declare function extractDefaultValues<TFieldValues extends FieldValues = FieldValues>(schema: FormSchema<TFieldValues>): Partial<TFieldValues>;
|
|
882
|
+
interface SchemaIssue {
|
|
883
|
+
/** Where the issue is, e.g. `sections[0].fields[2]` or `sections[1]`. */
|
|
884
|
+
path: string;
|
|
885
|
+
code: "missing-name" | "missing-type" | "duplicate-name" | "itemfields-on-noncontainer" | "empty-container" | "unknown-operator";
|
|
886
|
+
/** `error` = will misbehave at runtime; `warning` = suspicious but tolerated. */
|
|
887
|
+
severity: "error" | "warning";
|
|
888
|
+
message: string;
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Structurally validate a form schema and return a list of issues (empty ⇒ OK).
|
|
892
|
+
* Server-safe (no hooks/DOM), so you can run it when a schema is loaded from a
|
|
893
|
+
* DB, in a test, or in a dev boot check. It validates SHAPE, not your component
|
|
894
|
+
* registry — an unknown field `type` is a registry concern, not a schema error.
|
|
895
|
+
*
|
|
896
|
+
* Checks: missing `name`/`type`, duplicate names (namespace-aware), `itemFields`
|
|
897
|
+
* on a non-container type, containers with no `itemFields`, and unknown DSL
|
|
898
|
+
* condition operators.
|
|
899
|
+
*/
|
|
900
|
+
declare function validateSchema(schema: FormSchema): SchemaIssue[];
|
|
901
|
+
/**
|
|
902
|
+
* Build a default-value object for a flat list of fields — used to seed a new
|
|
903
|
+
* array item (or a group) from its `itemFields`.
|
|
904
|
+
*
|
|
905
|
+
* Recurses into nested `group` children (→ nested object) and seeds nested
|
|
906
|
+
* `array` children as `[]`, so appending an item never leaves a deep sub-field
|
|
907
|
+
* `undefined`. That matters because a missing deep field can trip a resolver
|
|
908
|
+
* (zod et al.) into a spurious "required" error the moment the row is added.
|
|
909
|
+
* Leaf fields without an explicit `defaultValue` seed to `""` (a controlled
|
|
910
|
+
* empty value RHF is happy with).
|
|
911
|
+
*/
|
|
912
|
+
declare function buildFieldDefaults(fields: BaseField[] | undefined): Record<string, unknown>;
|
|
913
|
+
/**
|
|
914
|
+
* Deep-merge `override` onto `base` with default-values semantics: nested
|
|
915
|
+
* plain objects merge recursively; arrays, primitives, and class instances
|
|
916
|
+
* (Date, File, …) from `override` replace wholesale.
|
|
917
|
+
*
|
|
918
|
+
* This is the merge `useFormKit` applies between schema-extracted defaults and
|
|
919
|
+
* caller-provided `defaultValues`. Exported so wrappers that re-seed a form at
|
|
920
|
+
* runtime (e.g. an edit sheet swapping entities) can reproduce the exact same
|
|
921
|
+
* merge for `form.reset(...)`:
|
|
922
|
+
*
|
|
923
|
+
* @example
|
|
924
|
+
* ```ts
|
|
925
|
+
* const merged = mergeDefaultValues(extractDefaultValues(schema), entity);
|
|
926
|
+
* form.reset(merged);
|
|
927
|
+
* ```
|
|
928
|
+
*/
|
|
929
|
+
declare function mergeDefaultValues(base: Record<string, unknown>, override: Record<string, unknown>): Record<string, unknown>;
|
|
930
|
+
/**
|
|
931
|
+
* Async version of `extractDefaultValues` for large schemas (50+ fields).
|
|
932
|
+
* Yields to the main thread after each section so that the browser can
|
|
933
|
+
* handle input events between chunks — keeping INP scores low.
|
|
934
|
+
*
|
|
935
|
+
* Use in `getDefaultValues` passed to `useForm` when the schema is known
|
|
936
|
+
* to be large:
|
|
937
|
+
*
|
|
938
|
+
* @example
|
|
939
|
+
* ```ts
|
|
940
|
+
* const form = useForm({
|
|
941
|
+
* defaultValues: async () => extractDefaultValuesAsync(schema),
|
|
942
|
+
* });
|
|
943
|
+
* ```
|
|
944
|
+
*/
|
|
945
|
+
declare function extractDefaultValuesAsync<TFieldValues extends FieldValues = FieldValues>(schema: FormSchema<TFieldValues>): Promise<Partial<TFieldValues>>;
|
|
656
946
|
/**
|
|
657
947
|
* Generates react-hook-form `RegisterOptions`-compatible validation rules
|
|
658
948
|
* from a field's schema props. Maps `required`, `min`, `max`, `minLength`,
|
|
659
949
|
* `maxLength`, `pattern`, and `validate` to RHF rules.
|
|
660
950
|
*
|
|
951
|
+
* Supports both shorthand scalars and `{ value, message }` objects for all
|
|
952
|
+
* numeric/length rules, and `{ regex, message }` for pattern.
|
|
953
|
+
*
|
|
661
954
|
* @example
|
|
662
955
|
* ```tsx
|
|
663
956
|
* import { buildValidationRules } from '@classytic/formkit';
|
|
@@ -669,155 +962,174 @@ declare function extractDefaultValues<TFieldValues extends FieldValues = FieldVa
|
|
|
669
962
|
* ```
|
|
670
963
|
*/
|
|
671
964
|
declare function buildValidationRules<TFieldValues extends FieldValues = FieldValues>(field: BaseField<TFieldValues>): ValidationRules;
|
|
965
|
+
/** Returns true for fields that carry an `options` array (select, radio, etc.) */
|
|
966
|
+
declare function isChoiceField(field: BaseField): boolean;
|
|
967
|
+
/** Returns true for free-text input fields */
|
|
968
|
+
declare function isTextField(field: BaseField): boolean;
|
|
969
|
+
/** Returns true for numeric input fields */
|
|
970
|
+
declare function isNumericField(field: BaseField): boolean;
|
|
971
|
+
/** Returns true for date / time fields */
|
|
972
|
+
declare function isDateField(field: BaseField): boolean;
|
|
973
|
+
/** Returns true for structural fields that contain sub-fields (`itemFields`) */
|
|
974
|
+
declare function isContainerField(field: BaseField): boolean;
|
|
975
|
+
/** Returns true for array fields that render a repeatable list */
|
|
976
|
+
declare function isArrayField(field: BaseField): boolean;
|
|
977
|
+
/** Returns true for fields that load options asynchronously */
|
|
978
|
+
declare function isDynamicField(field: BaseField): boolean;
|
|
979
|
+
/** Returns true for fields with conditional rendering */
|
|
980
|
+
declare function isConditionalField(field: BaseField): boolean;
|
|
981
|
+
/**
|
|
982
|
+
* Merge two or more schemas into one, concatenating their sections.
|
|
983
|
+
*
|
|
984
|
+
* @example
|
|
985
|
+
* ```ts
|
|
986
|
+
* const full = mergeSchemas(personalSchema, addressSchema, billingSchema);
|
|
987
|
+
* ```
|
|
988
|
+
*/
|
|
989
|
+
declare function mergeSchemas<TFieldValues extends FieldValues = FieldValues>(...schemas: FormSchema<TFieldValues>[]): FormSchema<TFieldValues>;
|
|
990
|
+
/**
|
|
991
|
+
* Add fields to a section identified by `sectionId`.
|
|
992
|
+
* Returns a new schema — the original is not mutated.
|
|
993
|
+
*
|
|
994
|
+
* @example
|
|
995
|
+
* ```ts
|
|
996
|
+
* const extended = extendSection(schema, "personal", [
|
|
997
|
+
* field.text("middleName", "Middle Name"),
|
|
998
|
+
* ]);
|
|
999
|
+
* ```
|
|
1000
|
+
*/
|
|
1001
|
+
declare function extendSection<TFieldValues extends FieldValues = FieldValues>(schema: FormSchema<TFieldValues>, sectionId: string, fields: BaseField<TFieldValues>[], position?: "start" | "end"): FormSchema<TFieldValues>;
|
|
1002
|
+
/**
|
|
1003
|
+
* Create a new schema that includes only the named fields.
|
|
1004
|
+
*
|
|
1005
|
+
* @example
|
|
1006
|
+
* ```ts
|
|
1007
|
+
* const slim = pickFields(schema, ["email", "password"]);
|
|
1008
|
+
* ```
|
|
1009
|
+
*/
|
|
1010
|
+
declare function pickFields<TFieldValues extends FieldValues = FieldValues>(schema: FormSchema<TFieldValues>, names: string[]): FormSchema<TFieldValues>;
|
|
1011
|
+
/**
|
|
1012
|
+
* Create a new schema that excludes the named fields.
|
|
1013
|
+
*
|
|
1014
|
+
* @example
|
|
1015
|
+
* ```ts
|
|
1016
|
+
* const withoutInternal = omitFields(schema, ["__id", "__createdAt"]);
|
|
1017
|
+
* ```
|
|
1018
|
+
*/
|
|
1019
|
+
declare function omitFields<TFieldValues extends FieldValues = FieldValues>(schema: FormSchema<TFieldValues>, names: string[]): FormSchema<TFieldValues>;
|
|
1020
|
+
/**
|
|
1021
|
+
* Collect every field from every section into a flat array.
|
|
1022
|
+
* Useful for validation, documentation, and AI schema introspection.
|
|
1023
|
+
*
|
|
1024
|
+
* @example
|
|
1025
|
+
* ```ts
|
|
1026
|
+
* const allFields = flattenSchema(schema);
|
|
1027
|
+
* const required = allFields.filter(f => f.required);
|
|
1028
|
+
* ```
|
|
1029
|
+
*/
|
|
1030
|
+
declare function flattenSchema<TFieldValues extends FieldValues = FieldValues>(schema: FormSchema<TFieldValues>): BaseField<TFieldValues>[];
|
|
1031
|
+
/**
|
|
1032
|
+
* Maps a server error response to react-hook-form field errors.
|
|
1033
|
+
*
|
|
1034
|
+
* Call this in your `onError` / `catch` handler after a failed API submission
|
|
1035
|
+
* to surface per-field server-side errors using the same UX as client validation.
|
|
1036
|
+
*
|
|
1037
|
+
* @param form - The `useForm` return value
|
|
1038
|
+
* @param errors - Map of field path → error message (dot-notation paths supported)
|
|
1039
|
+
*
|
|
1040
|
+
* @example
|
|
1041
|
+
* ```ts
|
|
1042
|
+
* async function onSubmit(data: FormValues) {
|
|
1043
|
+
* try {
|
|
1044
|
+
* await api.save(data);
|
|
1045
|
+
* } catch (err) {
|
|
1046
|
+
* if (err.fieldErrors) {
|
|
1047
|
+
* applyServerErrors(form, err.fieldErrors);
|
|
1048
|
+
* // { email: "Already taken", "address.zip": "Invalid ZIP" }
|
|
1049
|
+
* }
|
|
1050
|
+
* }
|
|
1051
|
+
* }
|
|
1052
|
+
* ```
|
|
1053
|
+
*/
|
|
1054
|
+
declare function applyServerErrors<TFieldValues extends FieldValues = FieldValues>(form: UseFormReturn<TFieldValues>, errors: Record<string, string>): void;
|
|
672
1055
|
//#endregion
|
|
673
1056
|
//#region src/builders.d.ts
|
|
674
1057
|
/**
|
|
675
|
-
* Additional field props
|
|
1058
|
+
* Additional field props accepted by builder helpers.
|
|
676
1059
|
* Accepts all BaseField properties except `name`, `type`, and `label`
|
|
677
|
-
* which are
|
|
1060
|
+
* which are provided by the builder method directly.
|
|
678
1061
|
*/
|
|
679
|
-
type FieldProps<TFieldValues extends FieldValues = FieldValues> = Omit<BaseField<TFieldValues>, "name" | "type" | "label"
|
|
680
|
-
/** Grid column class (e.g., "col-span-2") */gridColumn?: string; /** Icon for the left side of input */
|
|
681
|
-
iconLeft?: ReactNode; /** Icon for the right side of input */
|
|
682
|
-
iconRight?: ReactNode; /** Additional custom props */
|
|
683
|
-
[key: string]: unknown;
|
|
684
|
-
};
|
|
1062
|
+
type FieldProps<TFieldValues extends FieldValues = FieldValues> = Omit<BaseField<TFieldValues>, "name" | "type" | "label">;
|
|
685
1063
|
/**
|
|
686
1064
|
* Section configuration props.
|
|
687
1065
|
*/
|
|
688
1066
|
interface SectionProps<TFieldValues extends FieldValues = FieldValues> extends Omit<Section<TFieldValues>, "id" | "title" | "fields" | "cols"> {
|
|
689
1067
|
cols?: number;
|
|
690
1068
|
}
|
|
691
|
-
/**
|
|
692
|
-
* Render function for custom field types.
|
|
693
|
-
*/
|
|
694
|
-
type CustomRenderFn = (props: {
|
|
695
|
-
control: Control<FieldValues>;
|
|
696
|
-
disabled?: boolean;
|
|
697
|
-
error?: FieldError;
|
|
698
|
-
}) => ReactNode;
|
|
699
1069
|
/**
|
|
700
1070
|
* Type-safe field builder helpers for schema-driven forms.
|
|
701
1071
|
*
|
|
702
|
-
*
|
|
703
|
-
*
|
|
1072
|
+
* All methods are generic over TFieldValues, defaulting to FieldValues (any string)
|
|
1073
|
+
* when no type argument is provided. Specify the generic to enforce that field
|
|
1074
|
+
* names are valid paths in your form values type.
|
|
1075
|
+
*
|
|
1076
|
+
* For fully-typed schemas where every field name is checked, prefer
|
|
1077
|
+
* `field.for<MyForm>()` which fixes the generic once for the whole schema:
|
|
704
1078
|
*
|
|
705
1079
|
* @example
|
|
706
1080
|
* ```ts
|
|
707
|
-
*
|
|
708
|
-
*
|
|
709
|
-
*
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
* ]),
|
|
718
|
-
* ], { cols: 2 }),
|
|
719
|
-
* ],
|
|
720
|
-
* };
|
|
1081
|
+
* // Untyped — any string accepted (backwards compatible)
|
|
1082
|
+
* field.text("email", "Email")
|
|
1083
|
+
*
|
|
1084
|
+
* // Per-call generic — name is checked against MyForm
|
|
1085
|
+
* field.text<MyForm>("email", "Email")
|
|
1086
|
+
*
|
|
1087
|
+
* // Typed factory — name checked on every call without repeating the generic
|
|
1088
|
+
* const f = field.for<MyForm>()
|
|
1089
|
+
* f.text("email", "Email") // ✓
|
|
1090
|
+
* f.text("typo", "Email") // ✗ TypeScript error
|
|
721
1091
|
* ```
|
|
722
1092
|
*/
|
|
723
1093
|
declare const field: {
|
|
724
|
-
/**
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
/**
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
/**
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
/**
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
/**
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
number: (name: string, label: string, props?: FieldProps) => BaseField;
|
|
748
|
-
/**
|
|
749
|
-
* Textarea field with default 3 rows.
|
|
750
|
-
*/
|
|
751
|
-
textarea: (name: string, label: string, props?: FieldProps) => BaseField;
|
|
752
|
-
/**
|
|
753
|
-
* Select dropdown field.
|
|
754
|
-
*/
|
|
755
|
-
select: (name: string, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps) => BaseField;
|
|
756
|
-
/**
|
|
757
|
-
* Searchable combobox field.
|
|
758
|
-
*/
|
|
759
|
-
combobox: (name: string, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps) => BaseField;
|
|
760
|
-
/**
|
|
761
|
-
* Multi-select field (tag choice).
|
|
762
|
-
*/
|
|
763
|
-
multiselect: (name: string, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps) => BaseField;
|
|
764
|
-
/**
|
|
765
|
-
* Dependent select field that reacts to parent field changes.
|
|
766
|
-
*/
|
|
767
|
-
dependentSelect: (name: string, label: string, props?: FieldProps) => BaseField;
|
|
768
|
-
/**
|
|
769
|
-
* Switch/toggle field.
|
|
770
|
-
*/
|
|
771
|
-
switch: (name: string, label: string, props?: FieldProps) => BaseField;
|
|
772
|
-
/**
|
|
773
|
-
* Boolean field (alias for switch).
|
|
774
|
-
*/
|
|
775
|
-
boolean: (name: string, label: string, props?: FieldProps) => BaseField;
|
|
776
|
-
/**
|
|
777
|
-
* Checkbox field.
|
|
778
|
-
*/
|
|
779
|
-
checkbox: (name: string, label: string, props?: FieldProps) => BaseField;
|
|
780
|
-
/**
|
|
781
|
-
* Radio button group field.
|
|
782
|
-
*/
|
|
783
|
-
radio: (name: string, label: string, options: FieldOption[], props?: FieldProps) => BaseField;
|
|
784
|
-
/**
|
|
785
|
-
* Date picker field.
|
|
786
|
-
*/
|
|
787
|
-
date: (name: string, label: string, props?: FieldProps) => BaseField;
|
|
788
|
-
/**
|
|
789
|
-
* Tag input field with default placeholder.
|
|
790
|
-
*/
|
|
791
|
-
tags: (name: string, label: string, props?: FieldProps) => BaseField;
|
|
792
|
-
/**
|
|
793
|
-
* Slug field with auto-generation from source value.
|
|
794
|
-
*/
|
|
795
|
-
slug: (name: string, label: string, props?: FieldProps) => BaseField;
|
|
796
|
-
/**
|
|
797
|
-
* File upload field.
|
|
798
|
-
*/
|
|
799
|
-
file: (name: string, label: string, props?: FieldProps) => BaseField;
|
|
800
|
-
/**
|
|
801
|
-
* Hidden field (no UI).
|
|
802
|
-
*/
|
|
803
|
-
hidden: (name: string, props?: FieldProps) => BaseField;
|
|
1094
|
+
/** Text input field. */text: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Email input field with default placeholder. */
|
|
1095
|
+
email: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** URL input field with default placeholder. */
|
|
1096
|
+
url: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Phone/tel input field with default placeholder. */
|
|
1097
|
+
tel: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Password input field. */
|
|
1098
|
+
password: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
|
|
1099
|
+
/** Number input field. No implicit `min` — pass `{ min }` to add one, so the
|
|
1100
|
+
* builder never injects validation the author didn't write (signed
|
|
1101
|
+
* quantities like deltas / temperatures stay valid). */
|
|
1102
|
+
number: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Textarea field with default 3 rows. */
|
|
1103
|
+
textarea: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Select dropdown field. */
|
|
1104
|
+
select: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>; /** Searchable combobox field. */
|
|
1105
|
+
combobox: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>; /** Multi-select field. */
|
|
1106
|
+
multiselect: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>; /** Dependent select field that reacts to parent field changes. */
|
|
1107
|
+
dependentSelect: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Switch/toggle field. */
|
|
1108
|
+
switch: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Boolean field (alias for switch). */
|
|
1109
|
+
boolean: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Checkbox field. */
|
|
1110
|
+
checkbox: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Radio button group field. */
|
|
1111
|
+
radio: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, options: FieldOption[], props?: FieldProps<T>) => BaseField<T>; /** Date picker field. */
|
|
1112
|
+
date: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Tag input field. */
|
|
1113
|
+
tags: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Slug field. */
|
|
1114
|
+
slug: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** File upload field. */
|
|
1115
|
+
file: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Hidden field (no UI). */
|
|
1116
|
+
hidden: <T extends FieldValues = FieldValues>(name: Path<T>, props?: FieldProps<T>) => BaseField<T>;
|
|
804
1117
|
/**
|
|
805
1118
|
* Group field for nested objects.
|
|
806
|
-
* Renders itemFields as a sub-grid
|
|
1119
|
+
* Renders itemFields as a sub-grid. Child names are relative (e.g. "street"),
|
|
1120
|
+
* FormGenerator prefixes them with the group name at render time.
|
|
807
1121
|
*
|
|
808
1122
|
* @example
|
|
809
1123
|
* ```ts
|
|
810
1124
|
* field.group("address", "Address", [
|
|
811
1125
|
* field.text("street", "Street"),
|
|
812
1126
|
* field.text("city", "City"),
|
|
813
|
-
*
|
|
814
|
-
* ], { cols: 3 })
|
|
1127
|
+
* ], { cols: 2 })
|
|
815
1128
|
* ```
|
|
816
1129
|
*/
|
|
817
|
-
group: (name:
|
|
1130
|
+
group: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, itemFields: BaseField[], props?: FieldProps<T>) => BaseField<T>;
|
|
818
1131
|
/**
|
|
819
|
-
* Array/repeatable field.
|
|
820
|
-
* Renders a dynamic list of sub-forms using react-hook-form's useFieldArray.
|
|
1132
|
+
* Array/repeatable field backed by react-hook-form's useFieldArray.
|
|
821
1133
|
*
|
|
822
1134
|
* @example
|
|
823
1135
|
* ```ts
|
|
@@ -827,42 +1139,109 @@ declare const field: {
|
|
|
827
1139
|
* ])
|
|
828
1140
|
* ```
|
|
829
1141
|
*/
|
|
830
|
-
array: (name:
|
|
1142
|
+
array: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, itemFields: BaseField[], props?: FieldProps<T>) => BaseField<T>;
|
|
831
1143
|
/**
|
|
832
1144
|
* Custom field with a render function.
|
|
833
|
-
* Bypasses the component registry
|
|
1145
|
+
* Bypasses the component registry — full control over rendering.
|
|
1146
|
+
*
|
|
1147
|
+
* The render callback receives the complete `FieldComponentProps` including
|
|
1148
|
+
* `fieldId`, `errorId`, `shouldShowError`, `error`, `rules`, and `control`.
|
|
1149
|
+
*
|
|
1150
|
+
* Use `shouldShowError` (not `!!error`) to drive `aria-invalid` and error
|
|
1151
|
+
* visibility so timing mirrors the CSS `:user-invalid` pseudo-class.
|
|
834
1152
|
*
|
|
835
1153
|
* @example
|
|
836
|
-
* ```
|
|
837
|
-
* field.custom("skills", "Skills", ({ control,
|
|
838
|
-
* <
|
|
1154
|
+
* ```tsx
|
|
1155
|
+
* field.custom("skills", "Skills", ({ control, shouldShowError, errorId, error, fieldId }) => (
|
|
1156
|
+
* <div>
|
|
1157
|
+
* <SkillSelector
|
|
1158
|
+
* id={fieldId}
|
|
1159
|
+
* control={control}
|
|
1160
|
+
* aria-invalid={shouldShowError || undefined}
|
|
1161
|
+
* aria-errormessage={shouldShowError ? errorId : undefined}
|
|
1162
|
+
* />
|
|
1163
|
+
* {shouldShowError && (
|
|
1164
|
+
* <p id={errorId} role="alert" className="text-sm text-destructive">
|
|
1165
|
+
* {error?.message}
|
|
1166
|
+
* </p>
|
|
1167
|
+
* )}
|
|
1168
|
+
* </div>
|
|
839
1169
|
* ))
|
|
840
1170
|
* ```
|
|
841
1171
|
*/
|
|
842
|
-
custom: (name:
|
|
1172
|
+
custom: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, render: (props: FieldComponentProps<T>) => ReactNode, props?: FieldProps<T>) => BaseField<T>;
|
|
1173
|
+
/**
|
|
1174
|
+
* Returns a typed field builder with `TFieldValues` fixed.
|
|
1175
|
+
* Every field name is validated against `Path<TFieldValues>` at the call site —
|
|
1176
|
+
* no need to repeat the generic on each individual builder call.
|
|
1177
|
+
*
|
|
1178
|
+
* @example
|
|
1179
|
+
* ```ts
|
|
1180
|
+
* interface ContactForm {
|
|
1181
|
+
* firstName: string;
|
|
1182
|
+
* email: string;
|
|
1183
|
+
* address: { street: string; city: string };
|
|
1184
|
+
* }
|
|
1185
|
+
*
|
|
1186
|
+
* const f = field.for<ContactForm>()
|
|
1187
|
+
*
|
|
1188
|
+
* const schema = defineSchema<ContactForm>({
|
|
1189
|
+
* sections: [{
|
|
1190
|
+
* fields: [
|
|
1191
|
+
* f.text("firstName", "First Name"), // ✓
|
|
1192
|
+
* f.email("email", "Email"), // ✓
|
|
1193
|
+
* f.text("typo", "Label"), // ✗ TypeScript error
|
|
1194
|
+
* ],
|
|
1195
|
+
* }],
|
|
1196
|
+
* })
|
|
1197
|
+
* ```
|
|
1198
|
+
*/
|
|
1199
|
+
for: <T extends FieldValues>() => {
|
|
1200
|
+
text: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
|
|
1201
|
+
email: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
|
|
1202
|
+
url: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
|
|
1203
|
+
tel: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
|
|
1204
|
+
password: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
|
|
1205
|
+
number: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
|
|
1206
|
+
textarea: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
|
|
1207
|
+
select: (name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>;
|
|
1208
|
+
combobox: (name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>;
|
|
1209
|
+
multiselect: (name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>;
|
|
1210
|
+
dependentSelect: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
|
|
1211
|
+
switch: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
|
|
1212
|
+
boolean: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
|
|
1213
|
+
checkbox: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
|
|
1214
|
+
radio: (name: Path<T>, label: string, options: FieldOption[], props?: FieldProps<T>) => BaseField<T>;
|
|
1215
|
+
date: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
|
|
1216
|
+
tags: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
|
|
1217
|
+
slug: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
|
|
1218
|
+
file: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
|
|
1219
|
+
hidden: (name: Path<T>, props?: FieldProps<T>) => BaseField<T>;
|
|
1220
|
+
group: (name: Path<T>, label: string, itemFields: BaseField[], props?: FieldProps<T>) => BaseField<T>;
|
|
1221
|
+
array: (name: Path<T>, label: string, itemFields: BaseField[], props?: FieldProps<T>) => BaseField<T>;
|
|
1222
|
+
custom: (name: Path<T>, label: string, render: (props: FieldComponentProps<T>) => ReactNode, builderProps?: FieldProps<T>) => BaseField<T>;
|
|
1223
|
+
};
|
|
843
1224
|
};
|
|
844
1225
|
/**
|
|
845
1226
|
* Create a section definition with sensible defaults.
|
|
846
1227
|
*
|
|
847
|
-
* @param id - Unique section identifier
|
|
848
|
-
* @param title - Section title
|
|
849
|
-
* @param fields - Array of field definitions
|
|
850
|
-
* @param props - Additional section configuration
|
|
851
|
-
*
|
|
852
1228
|
* @example
|
|
853
1229
|
* ```ts
|
|
854
1230
|
* section("personal", "Personal Info", [
|
|
855
1231
|
* field.text("name", "Name", { required: true }),
|
|
856
1232
|
* field.email("email", "Email"),
|
|
857
|
-
* ], { cols: 2
|
|
1233
|
+
* ], { cols: 2 })
|
|
858
1234
|
* ```
|
|
859
1235
|
*/
|
|
860
1236
|
declare function section<TFieldValues extends FieldValues = FieldValues>(id: string, title: string, fields: BaseField<TFieldValues>[], props?: SectionProps<TFieldValues>): Section<TFieldValues>;
|
|
861
1237
|
/**
|
|
862
1238
|
* Create a section without a title (transparent section).
|
|
863
1239
|
* Useful for grouping fields without visual separation.
|
|
1240
|
+
*
|
|
1241
|
+
* Accepts `BaseField[]` (no generic) so mixed-type field arrays don't trigger
|
|
1242
|
+
* conflicting type inference across different field name generics.
|
|
864
1243
|
*/
|
|
865
|
-
declare function sectionUntitled
|
|
1244
|
+
declare function sectionUntitled(fields: BaseField[], props?: Omit<SectionProps, "variant">): Section;
|
|
866
1245
|
//#endregion
|
|
867
1246
|
//#region src/useFormKit.d.ts
|
|
868
1247
|
/**
|
|
@@ -880,6 +1259,14 @@ interface UseFormKitOptions<TFieldValues extends FieldValues = FieldValues> exte
|
|
|
880
1259
|
variant?: Variant;
|
|
881
1260
|
/** Root element className */
|
|
882
1261
|
className?: string;
|
|
1262
|
+
/**
|
|
1263
|
+
* Re-seed the form with the schema's freshly-extracted defaults whenever the
|
|
1264
|
+
* `schema` identity changes (e.g. a schema fetched from a DB is swapped in).
|
|
1265
|
+
* Off by default because a reset discards in-progress edits — enable it only
|
|
1266
|
+
* when a schema swap should intentionally re-initialise the form. Skipped when
|
|
1267
|
+
* `defaultValues` is an async factory (the caller owns that reset).
|
|
1268
|
+
*/
|
|
1269
|
+
resetOnSchemaChange?: boolean;
|
|
883
1270
|
}
|
|
884
1271
|
/**
|
|
885
1272
|
* Return value from useFormKit.
|
|
@@ -910,4 +1297,30 @@ interface UseFormKitReturn<TFieldValues extends FieldValues = FieldValues> exten
|
|
|
910
1297
|
*/
|
|
911
1298
|
declare function useFormKit<TFieldValues extends FieldValues = FieldValues>(options: UseFormKitOptions<TFieldValues>): UseFormKitReturn<TFieldValues>;
|
|
912
1299
|
//#endregion
|
|
913
|
-
|
|
1300
|
+
//#region src/focus.d.ts
|
|
1301
|
+
/**
|
|
1302
|
+
* Focus (and scroll to) the first field currently shown as invalid.
|
|
1303
|
+
*
|
|
1304
|
+
* It queries the rendered `[data-formkit-field][data-invalid]` wrapper — which
|
|
1305
|
+
* `FormGenerator` marks once errors are visible (after touch / submit) — so it
|
|
1306
|
+
* targets the first error in **visual order**, not react-hook-form's error-key
|
|
1307
|
+
* order (which isn't guaranteed to match the layout). Wire it into your submit's
|
|
1308
|
+
* invalid handler:
|
|
1309
|
+
*
|
|
1310
|
+
* ```ts
|
|
1311
|
+
* form.handleSubmit(onValid, () => focusFirstError());
|
|
1312
|
+
* ```
|
|
1313
|
+
*
|
|
1314
|
+
* **Timing:** react-hook-form calls the invalid handler *before* React commits
|
|
1315
|
+
* the re-render that sets `data-invalid` on the wrappers, so on the very first
|
|
1316
|
+
* invalid submit a synchronous query finds nothing. When the immediate attempt
|
|
1317
|
+
* misses, this retries on the next two animation frames (the commit lands
|
|
1318
|
+
* before the next paint), so the plain wiring above Just Works.
|
|
1319
|
+
*
|
|
1320
|
+
* @param root Optional container to search within (defaults to `document`).
|
|
1321
|
+
* @returns `true` if a field was focused immediately; `false` if not found yet
|
|
1322
|
+
* (a deferred retry may still focus it on the next frame).
|
|
1323
|
+
*/
|
|
1324
|
+
declare function focusFirstError(root?: HTMLElement | Document | null): boolean;
|
|
1325
|
+
//#endregion
|
|
1326
|
+
export { type BaseField, type ClassValue, type ComponentRegistry, type Condition, type ConditionConfig, type ConditionRule, type DefaultLayoutProps, type DefineField, type FieldComponent, type FieldComponentProps, type FieldMeta, type FieldOption, type FieldOptionGroup, type FieldType, FieldWrapper, type FieldWrapperProps, type FormElement, FormGenerator, type FormGeneratorProps, type FormSchema, type FormSystemContextValue, FormSystemProvider, type FormSystemProviderProps, type GridLayoutProps, GridRenderer, type GridRendererProps, type InferSchemaValues, type LayoutComponent, type LayoutComponentProps, type LayoutRegistry, type LayoutType, type PatternRuleObject, type SchemaFieldNames, type SchemaIssue, type Section, type SectionLayoutProps, type SectionRenderProps, SectionRenderer, type SectionRendererProps, type UseFormKitOptions, type UseFormKitReturn, type ValidationRuleObject, type ValidationRules, type Variant, applyServerErrors, buildFieldDefaults, buildValidationRules, cn, defineField, defineSchema, defineSection, evaluateCondition, extendSection, extractDefaultValues, extractDefaultValuesAsync, extractWatchNames, field, flattenSchema, focusFirstError, isArrayField, isChoiceField, isConditionalField, isContainerField, isDateField, isDynamicField, isNumericField, isTextField, mergeDefaultValues, mergeSchemas, omitFields, pickFields, section, sectionUntitled, shallowEqual, useFieldComponent, useFormKit, useFormSystem, useLayoutComponent, validateSchema };
|