@classytic/formkit 1.4.0 → 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 +61 -0
- package/README.md +136 -9
- package/dist/index.d.mts +132 -5
- package/dist/index.mjs +513 -162
- package/dist/server.d.mts +81 -3
- package/dist/server.mjs +214 -12
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,67 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.5.0] - 2026-07-02
|
|
9
|
+
|
|
10
|
+
Stability / hardening release. Two intentional **behavioral changes** are called
|
|
11
|
+
out below — read them before upgrading.
|
|
12
|
+
|
|
13
|
+
### Behavioral changes
|
|
14
|
+
|
|
15
|
+
- **`field.number()` no longer injects `min: 0`.** The builder previously added
|
|
16
|
+
an implicit non-negative constraint; forms relying on it must now pass
|
|
17
|
+
`{ min: 0 }` explicitly. Signed quantities (deltas, temperatures) are valid by
|
|
18
|
+
default.
|
|
19
|
+
- **Missing components render a visible placeholder in production** (previously
|
|
20
|
+
a silent null). The wrong-widget `text` fallback for unregistered types was
|
|
21
|
+
removed entirely. Use the new `onMissingComponent` provider callback for
|
|
22
|
+
telemetry. Exception: `hidden` fields with no registered component render
|
|
23
|
+
nothing by design.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- `validateSchema(schema)` — structural schema linting (missing name/type,
|
|
28
|
+
duplicate names, `itemFields` on non-containers, unknown DSL operators),
|
|
29
|
+
exported from both entries and auto-run in dev by `FormGenerator`.
|
|
30
|
+
- `focusFirstError(root?)` — focuses the first visibly-invalid field in layout
|
|
31
|
+
order; retries across two animation frames so it works from
|
|
32
|
+
`handleSubmit(onValid, () => focusFirstError())` on the very first submit.
|
|
33
|
+
- `loadOptions` now receives `{ signal: AbortSignal }` as a second argument;
|
|
34
|
+
superseded/unmounted loads are aborted. New opt-in `cacheOptions` memoizes
|
|
35
|
+
results per watched-value signature (bounded).
|
|
36
|
+
- `onMissingComponent` callback on `FormSystemProvider`.
|
|
37
|
+
- `colSpan` field prop (surfaced as `data-col-span`; `fullWidth` now also emits
|
|
38
|
+
an inline `grid-column: 1 / -1` so full-width layout works without scanning
|
|
39
|
+
the package with Tailwind).
|
|
40
|
+
- `resetOnSchemaChange` option on `useFormKit` for DB-loaded schema swaps.
|
|
41
|
+
- Dev warning when `loadOptions` is used without `watchNames` (whole-form
|
|
42
|
+
refetch storms).
|
|
43
|
+
- `buildFieldDefaults` exported; array-item seeds are now type-aware (`false`
|
|
44
|
+
for checkbox/switch, `[]` for multiselect/tags, `undefined` for number).
|
|
45
|
+
- `mergeDefaultValues(base, override)` exported (both entries) — the exact
|
|
46
|
+
Date-safe deep merge `useFormKit` applies between schema defaults and caller
|
|
47
|
+
`defaultValues`, for wrappers that re-seed via `form.reset(...)`.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- **Rules-of-hooks crash**: `useFieldComponent` and the field-props memo are
|
|
52
|
+
hoisted above the group/array fallback returns, so a registry that changes
|
|
53
|
+
after mount (code-split adapters) can no longer change the hook count.
|
|
54
|
+
`eslint-plugin-react-hooks` is now enforced in CI-lint.
|
|
55
|
+
- **Date / class-instance defaults survive merging**: `useFormKit`'s deep-merge
|
|
56
|
+
now replaces non-plain objects (Date, File, class instances) wholesale
|
|
57
|
+
instead of spread-merging them into `{}`.
|
|
58
|
+
- Function conditions with `watchNames` receive properly **nested** watched
|
|
59
|
+
values (`(v) => v.address.city` works; previously flat `"address.city"` keys
|
|
60
|
+
made them throw and hide the field).
|
|
61
|
+
- `extractDefaultValues` recurses into nested groups (deep defaults were
|
|
62
|
+
dropped beyond one level); shares one walker with the async variant.
|
|
63
|
+
- Fallback UI (array items, error boundary, missing-component placeholder) uses
|
|
64
|
+
structural inline styles + `currentColor` instead of Tailwind classes that
|
|
65
|
+
don't exist in consumer builds; `formkit-*` classes remain as theming hooks.
|
|
66
|
+
- `process.env` reads are guarded (`isDev()` helper), so bundler-less ESM
|
|
67
|
+
environments no longer throw `ReferenceError`.
|
|
68
|
+
|
|
8
69
|
## [1.4.0] - 2026-05-26
|
|
9
70
|
|
|
10
71
|
### Added
|
package/README.md
CHANGED
|
@@ -8,8 +8,12 @@ Headless, type-safe form generation engine for React 19. Schema-driven with full
|
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
|
-
- **
|
|
12
|
-
-
|
|
11
|
+
- **Headless** - Bring your own UI components (Shadcn, MUI, Chakra, etc.) via a
|
|
12
|
+
one-time component/layout adapter. Already using shadcn? Skip the adapter
|
|
13
|
+
entirely with the prebuilt one in **[`@classytic/fluid/formkit`](https://www.npmjs.com/package/@classytic/fluid)** —
|
|
14
|
+
its `SchemaForm` is genuinely a schema + `onSubmit` away.
|
|
15
|
+
- **Minimal boilerplate** - once an adapter is registered, `useFormKit` wires a
|
|
16
|
+
full form (defaults + RHF + generator props) in a few lines
|
|
13
17
|
- **Schema-driven** - Define forms with JSON/TypeScript schemas, defaults extracted automatically
|
|
14
18
|
- **Type-safe** - Full TypeScript support with generics
|
|
15
19
|
- **React Hook Form** - Built on top of the best form library, referentially stable return values
|
|
@@ -20,7 +24,7 @@ Headless, type-safe form generation engine for React 19. Schema-driven with full
|
|
|
20
24
|
- **Responsive layouts** - Multi-column grid layouts
|
|
21
25
|
- **Accessibility** - Auto-generated `fieldId`, `error`, and `fieldState` props
|
|
22
26
|
- **Validation helpers** - `buildValidationRules` generates RHF rules from schema props
|
|
23
|
-
- **Lightweight** - ~
|
|
27
|
+
- **Lightweight** - ~17KB gzipped (peer deps excluded, everything included), tree-shakeable
|
|
24
28
|
|
|
25
29
|
## Requirements
|
|
26
30
|
|
|
@@ -127,9 +131,16 @@ const layouts: LayoutRegistry = {
|
|
|
127
131
|
{children}
|
|
128
132
|
</div>
|
|
129
133
|
),
|
|
130
|
-
grid: ({ children, cols = 1 }) =>
|
|
131
|
-
|
|
132
|
-
|
|
134
|
+
grid: ({ children, cols = 1 }) => {
|
|
135
|
+
// Map to STATIC class names. A template like `grid-cols-${cols}` is never
|
|
136
|
+
// generated by Tailwind's JIT (it only sees whole class strings in source).
|
|
137
|
+
const COLS: Record<number, string> = {
|
|
138
|
+
1: "grid-cols-1",
|
|
139
|
+
2: "grid-cols-1 md:grid-cols-2",
|
|
140
|
+
3: "grid-cols-1 md:grid-cols-2 lg:grid-cols-3",
|
|
141
|
+
};
|
|
142
|
+
return <div className={`grid gap-4 ${COLS[cols] ?? "grid-cols-1"}`}>{children}</div>;
|
|
143
|
+
},
|
|
133
144
|
};
|
|
134
145
|
|
|
135
146
|
export function FormProvider({ children }: { children: React.ReactNode }) {
|
|
@@ -198,6 +209,26 @@ export default function SignupPage() {
|
|
|
198
209
|
}
|
|
199
210
|
```
|
|
200
211
|
|
|
212
|
+
## Type safety
|
|
213
|
+
|
|
214
|
+
Field **names** are enforced against your form type at *authoring* time by the
|
|
215
|
+
builders. Use `field.for<T>()`, `defineField<T>()`, or `section<T>()` and a typo
|
|
216
|
+
is a compile error:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
const f = field.for<SignupData>();
|
|
220
|
+
f.text("emial", "Email"); // ✗ compile error — "emial" is not a key of SignupData
|
|
221
|
+
f.text("email", "Email"); // ✓
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The `<FormGenerator schema>` / `useFormKit({ schema })` props themselves accept
|
|
225
|
+
`FormSchema<any>` **by design**: a schema's condition functions (`(values) => …`)
|
|
226
|
+
make `FormSchema<T>` contravariant in `T`, so pinning the prop to the form's `T`
|
|
227
|
+
would force a cast at every call site. Author with the typed builders for name
|
|
228
|
+
safety; the generator stays permissive so any built schema drops in without
|
|
229
|
+
ceremony. `itemFields` (group/array children) use relative names and are
|
|
230
|
+
resolved at render time, so they are typed loosely on purpose.
|
|
231
|
+
|
|
201
232
|
## API Reference
|
|
202
233
|
|
|
203
234
|
### useFormKit
|
|
@@ -216,6 +247,7 @@ const form = useFormKit({
|
|
|
216
247
|
disabled: false, // optional
|
|
217
248
|
variant: "compact", // optional
|
|
218
249
|
className: "my-form", // optional
|
|
250
|
+
resetOnSchemaChange: true, // optional — re-seed on schema swap
|
|
219
251
|
mode: "onBlur", // any useForm option
|
|
220
252
|
});
|
|
221
253
|
|
|
@@ -234,7 +266,9 @@ return (
|
|
|
234
266
|
);
|
|
235
267
|
```
|
|
236
268
|
|
|
237
|
-
Schema `defaultValue` fields are automatically extracted and merged with any explicit `defaultValues` you provide (explicit values take priority).
|
|
269
|
+
Schema `defaultValue` fields are automatically extracted and merged with any explicit `defaultValues` you provide (explicit values take priority). Nested/namespaced defaults are extracted as a nested object (`{ address: { city } }`), so react-hook-form's dot-path resolution applies them correctly.
|
|
270
|
+
|
|
271
|
+
Pass `resetOnSchemaChange: true` to re-seed the form when the `schema` identity changes at runtime (e.g. a schema loaded from a DB is swapped in). It's off by default because a reset discards in-progress edits.
|
|
238
272
|
|
|
239
273
|
`generatorProps` is memoized — it only recomputes when `schema`, `control`, `disabled`, `variant`, or `className` change.
|
|
240
274
|
|
|
@@ -361,9 +395,14 @@ interface FieldComponentProps<T extends FieldValues = FieldValues>
|
|
|
361
395
|
isDirty: boolean;
|
|
362
396
|
isTouched: boolean;
|
|
363
397
|
isValidating: boolean;
|
|
398
|
+
isSubmitted: boolean;
|
|
364
399
|
error?: FieldError;
|
|
365
400
|
};
|
|
366
401
|
fieldId: string; // Generated ID for label-input association (e.g. "formkit-field-email")
|
|
402
|
+
errorId: string; // ID for the error node — set as aria-errormessage on the input
|
|
403
|
+
shouldShowError: boolean; // true only after touch/submit (mirrors :user-invalid timing)
|
|
404
|
+
rules: RegisterOptions; // Pre-computed RHF rules from schema (required/min/max/pattern…)
|
|
405
|
+
isLoading?: boolean; // true while async loadOptions is in flight
|
|
367
406
|
}
|
|
368
407
|
```
|
|
369
408
|
|
|
@@ -466,6 +505,64 @@ function FormInput({ field, control, error, fieldId }: FieldComponentProps) {
|
|
|
466
505
|
|
|
467
506
|
Maps `required`, `min`, `max`, `minLength`, `maxLength`, and `pattern` from the field schema to RHF-compatible rules with auto-generated error messages.
|
|
468
507
|
|
|
508
|
+
## Validation & helpers
|
|
509
|
+
|
|
510
|
+
### validateSchema
|
|
511
|
+
|
|
512
|
+
Structurally validate a schema (great for DB-loaded / `resetOnSchemaChange`
|
|
513
|
+
schemas). Server-safe; returns `SchemaIssue[]` (empty ⇒ OK). `FormGenerator`
|
|
514
|
+
also runs it automatically in dev and `console.warn`s any issues.
|
|
515
|
+
|
|
516
|
+
```ts
|
|
517
|
+
import { validateSchema } from "@classytic/formkit"; // or /server
|
|
518
|
+
|
|
519
|
+
for (const issue of validateSchema(schema)) {
|
|
520
|
+
console.warn(`${issue.severity} ${issue.path}: ${issue.message}`);
|
|
521
|
+
}
|
|
522
|
+
// Detects: missing name/type, duplicate names (namespace-aware),
|
|
523
|
+
// itemFields on a non-container, empty containers, unknown DSL operators.
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
It validates **shape**, not your component registry — an unregistered field
|
|
527
|
+
`type` is a rendering concern, surfaced separately (see `onMissingComponent`).
|
|
528
|
+
|
|
529
|
+
### focusFirstError
|
|
530
|
+
|
|
531
|
+
Focus + scroll to the first visibly-invalid field, in layout order. Wire it into
|
|
532
|
+
the submit's invalid handler:
|
|
533
|
+
|
|
534
|
+
```tsx
|
|
535
|
+
import { focusFirstError } from "@classytic/formkit";
|
|
536
|
+
|
|
537
|
+
<form onSubmit={handleSubmit(onValid, () => focusFirstError())}>
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
React Hook Form calls the invalid handler *before* React commits the re-render
|
|
541
|
+
that marks fields invalid, so if the immediate query finds nothing,
|
|
542
|
+
`focusFirstError` automatically retries on the next two animation frames — the
|
|
543
|
+
plain wiring above works on the very first submit. It returns `true` when a
|
|
544
|
+
field was focused immediately, `false` when the attempt was deferred.
|
|
545
|
+
|
|
546
|
+
### onMissingComponent
|
|
547
|
+
|
|
548
|
+
If a field's `type` has no registered component, the form renders a **visible
|
|
549
|
+
placeholder** (dev and prod — never a silent drop or a wrong-widget text input)
|
|
550
|
+
and notifies this callback so you can log the gap. `hidden` fields are the one
|
|
551
|
+
exception: with no registered component they render nothing by design (the
|
|
552
|
+
value still lives in form state); register a `hidden` component to override.
|
|
553
|
+
|
|
554
|
+
```tsx
|
|
555
|
+
<FormSystemProvider
|
|
556
|
+
components={components}
|
|
557
|
+
layouts={layouts}
|
|
558
|
+
onMissingComponent={(type) => reportToTelemetry("formkit_missing_component", { type })}
|
|
559
|
+
>
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
> **Security note:** `buildValidationRules` compiles a field's `pattern` into a
|
|
563
|
+
> `new RegExp(...)`. If your schemas are ever authored by untrusted users, treat
|
|
564
|
+
> `pattern` as a ReDoS surface — validate/limit it before persisting.
|
|
565
|
+
|
|
469
566
|
## Server Components
|
|
470
567
|
|
|
471
568
|
The `@classytic/formkit/server` entry point exports server-safe utilities with no React hooks or client-side code:
|
|
@@ -494,6 +591,24 @@ import type {
|
|
|
494
591
|
|
|
495
592
|
Use this entry point in React Server Components to define schemas, evaluate conditions, or use `cn` without pulling in client-side code.
|
|
496
593
|
|
|
594
|
+
### Passing a schema across the RSC → Client boundary
|
|
595
|
+
|
|
596
|
+
`FormGenerator` is a Client Component (it drives react-hook-form). If you build
|
|
597
|
+
a schema in a Server Component and pass it as a prop into the client, React
|
|
598
|
+
requires that prop to be **serializable** — so **function** members won't cross
|
|
599
|
+
the boundary:
|
|
600
|
+
|
|
601
|
+
- ❌ function `condition: (values) => …`, custom `render`, and `loadOptions`
|
|
602
|
+
(functions aren't serializable)
|
|
603
|
+
- ✅ **DSL** conditions (`{ watch, operator, value }`), and all the plain data
|
|
604
|
+
fields (`name`, `type`, `label`, `options`, `required`, `min`/`max`, …)
|
|
605
|
+
|
|
606
|
+
For fully static, server-defined schemas use DSL conditions. If a schema needs
|
|
607
|
+
function conditions / custom renders / async options, define it **inside the
|
|
608
|
+
Client Component** (e.g. the file that renders `FormGenerator`) rather than
|
|
609
|
+
passing it down from an RSC. This is a general Next.js constraint, not specific
|
|
610
|
+
to formkit.
|
|
611
|
+
|
|
497
612
|
## Advanced Features
|
|
498
613
|
|
|
499
614
|
### Conditional Fields (Function)
|
|
@@ -507,6 +622,12 @@ Use this entry point in React Server Components to define schemas, evaluate cond
|
|
|
507
622
|
}
|
|
508
623
|
```
|
|
509
624
|
|
|
625
|
+
> **Performance:** a function condition can't be statically analyzed, so the
|
|
626
|
+
> field subscribes to the **whole form** and re-evaluates on every change. For
|
|
627
|
+
> large forms prefer a DSL rule (below, whose watched paths are auto-scoped), or
|
|
628
|
+
> add `watchNames` to scope the subscription yourself:
|
|
629
|
+
> `{ condition: (v) => v.accountType === "business", watchNames: ["accountType"] }`.
|
|
630
|
+
|
|
510
631
|
### Conditional Fields (DSL Rules)
|
|
511
632
|
|
|
512
633
|
```ts
|
|
@@ -604,14 +725,20 @@ const components = {
|
|
|
604
725
|
name: "city",
|
|
605
726
|
type: "select",
|
|
606
727
|
watchNames: ["country"],
|
|
607
|
-
loadOptions: async (values) => {
|
|
608
|
-
|
|
728
|
+
loadOptions: async (values, { signal }) => {
|
|
729
|
+
// Forward `signal` so a superseded/unmounted load is cancelled.
|
|
730
|
+
const cities = await fetchCities(values.country, { signal });
|
|
609
731
|
return cities.map(c => ({ label: c.name, value: c.id }));
|
|
610
732
|
},
|
|
611
733
|
debounceMs: 300,
|
|
734
|
+
cacheOptions: true, // reuse the last fetch when a dependency is re-selected
|
|
612
735
|
}
|
|
613
736
|
```
|
|
614
737
|
|
|
738
|
+
> `cacheOptions` (default `false`) memoizes results per watched-value signature
|
|
739
|
+
> for the field's lifetime, so toggling `country` back and forth doesn't re-hit
|
|
740
|
+
> the API. Leave it off when the options can change server-side between loads.
|
|
741
|
+
|
|
615
742
|
### Custom Section Render
|
|
616
743
|
|
|
617
744
|
```ts
|
package/dist/index.d.mts
CHANGED
|
@@ -138,6 +138,13 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
|
|
|
138
138
|
variant?: Variant;
|
|
139
139
|
/** Whether field should span full width in grid */
|
|
140
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;
|
|
141
148
|
/** Custom CSS class name */
|
|
142
149
|
className?: string;
|
|
143
150
|
/**
|
|
@@ -199,13 +206,33 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
|
|
|
199
206
|
/**
|
|
200
207
|
* Dynamic options loaded based on current form values.
|
|
201
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
|
+
* ```
|
|
202
218
|
*/
|
|
203
|
-
loadOptions?: (formValues: Partial<TFieldValues
|
|
219
|
+
loadOptions?: (formValues: Partial<TFieldValues>, options?: {
|
|
220
|
+
signal: AbortSignal;
|
|
221
|
+
}) => Promise<(FieldOption | FieldOptionGroup)[]> | (FieldOption | FieldOptionGroup)[];
|
|
204
222
|
/**
|
|
205
223
|
* Error callback for loadOptions failures.
|
|
206
224
|
* Called when loadOptions rejects. Defaults to console.error.
|
|
207
225
|
*/
|
|
208
226
|
onLoadError?: (error: unknown) => void;
|
|
227
|
+
/**
|
|
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;
|
|
209
236
|
/**
|
|
210
237
|
* Sub-fields for `group` and `array` field types.
|
|
211
238
|
*
|
|
@@ -553,6 +580,8 @@ interface FormSystemContextValue {
|
|
|
553
580
|
components: ComponentRegistry;
|
|
554
581
|
/** Registered layout components */
|
|
555
582
|
layouts: LayoutRegistry;
|
|
583
|
+
/** Notified when a field type has no registered component (see provider). */
|
|
584
|
+
onMissingComponent?: (type: string, variant?: string) => void;
|
|
556
585
|
}
|
|
557
586
|
/**
|
|
558
587
|
* Form system provider props.
|
|
@@ -562,6 +591,13 @@ interface FormSystemProviderProps {
|
|
|
562
591
|
components?: ComponentRegistry;
|
|
563
592
|
/** Layout component registry */
|
|
564
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;
|
|
565
601
|
/** Children content */
|
|
566
602
|
children: ReactNode;
|
|
567
603
|
}
|
|
@@ -718,6 +754,7 @@ declare const FieldWrapper: typeof FieldWrapperImpl;
|
|
|
718
754
|
declare function FormSystemProvider({
|
|
719
755
|
components,
|
|
720
756
|
layouts,
|
|
757
|
+
onMissingComponent,
|
|
721
758
|
children
|
|
722
759
|
}: FormSystemProviderProps): FormElement;
|
|
723
760
|
/**
|
|
@@ -739,11 +776,16 @@ declare function useFormSystem(): FormSystemContextValue;
|
|
|
739
776
|
* 1. Variant-specific component: `components[variant][type]`
|
|
740
777
|
* 2. Type-specific component: `components[type]`
|
|
741
778
|
* 3. Default component: `components["default"]`
|
|
742
|
-
*
|
|
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.
|
|
743
785
|
*
|
|
744
786
|
* @param type - Field type identifier
|
|
745
787
|
* @param variant - Optional variant name
|
|
746
|
-
* @returns Field component or
|
|
788
|
+
* @returns Field component or the visible placeholder
|
|
747
789
|
*
|
|
748
790
|
* @internal
|
|
749
791
|
*/
|
|
@@ -837,6 +879,54 @@ declare function defineSection<TFieldValues extends FieldValues = FieldValues>(s
|
|
|
837
879
|
* ```
|
|
838
880
|
*/
|
|
839
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>;
|
|
840
930
|
/**
|
|
841
931
|
* Async version of `extractDefaultValues` for large schemas (50+ fields).
|
|
842
932
|
* Yields to the main thread after each section so that the browser can
|
|
@@ -1005,7 +1095,10 @@ declare const field: {
|
|
|
1005
1095
|
email: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** URL input field with default placeholder. */
|
|
1006
1096
|
url: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Phone/tel input field with default placeholder. */
|
|
1007
1097
|
tel: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Password input field. */
|
|
1008
|
-
password: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
|
|
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). */
|
|
1009
1102
|
number: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Textarea field with default 3 rows. */
|
|
1010
1103
|
textarea: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Select dropdown field. */
|
|
1011
1104
|
select: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>; /** Searchable combobox field. */
|
|
@@ -1166,6 +1259,14 @@ interface UseFormKitOptions<TFieldValues extends FieldValues = FieldValues> exte
|
|
|
1166
1259
|
variant?: Variant;
|
|
1167
1260
|
/** Root element className */
|
|
1168
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;
|
|
1169
1270
|
}
|
|
1170
1271
|
/**
|
|
1171
1272
|
* Return value from useFormKit.
|
|
@@ -1196,4 +1297,30 @@ interface UseFormKitReturn<TFieldValues extends FieldValues = FieldValues> exten
|
|
|
1196
1297
|
*/
|
|
1197
1298
|
declare function useFormKit<TFieldValues extends FieldValues = FieldValues>(options: UseFormKitOptions<TFieldValues>): UseFormKitReturn<TFieldValues>;
|
|
1198
1299
|
//#endregion
|
|
1199
|
-
|
|
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 };
|