@formdef/core 0.1.2 → 0.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/dist/index.cjs +118 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -4
- package/dist/index.d.ts +18 -4
- package/dist/index.js +119 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -2,6 +2,11 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
let _tanstack_react_form = require("@tanstack/react-form");
|
|
3
3
|
let react = require("react");
|
|
4
4
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
5
|
+
// src/widget/declares-empty-value.ts
|
|
6
|
+
/** Whether `widget` declares its empty value, the value it holds while nothing is entered. */
|
|
7
|
+
function declaresEmptyValue(widget) {
|
|
8
|
+
return Object.hasOwn(widget, "emptyValue");
|
|
9
|
+
}
|
|
5
10
|
// src/render/array-entry.tsx
|
|
6
11
|
const removeLabelOf = (removeLabel, item, index) => typeof removeLabel === "function" ? removeLabel(item, index) : String(removeLabel);
|
|
7
12
|
const itemControlsOf = (options, value) => {
|
|
@@ -103,6 +108,17 @@ const Derived = ({ form, entry, children }) => {
|
|
|
103
108
|
}
|
|
104
109
|
});
|
|
105
110
|
};
|
|
111
|
+
// src/render/path-pattern.ts
|
|
112
|
+
const ITEM_INDEX = /\[\d+\]/g;
|
|
113
|
+
const ANY_ITEM = "[]";
|
|
114
|
+
/** The path with every array index left open: `contacts[2].name` becomes `contacts[].name`. */
|
|
115
|
+
function pathPattern(path) {
|
|
116
|
+
return path.replaceAll(ITEM_INDEX, ANY_ITEM);
|
|
117
|
+
}
|
|
118
|
+
// src/render/required-paths.ts
|
|
119
|
+
const NO_REQUIRED_PATHS = /* @__PURE__ */ new Set();
|
|
120
|
+
/** The path patterns of the required fields of the level being rendered. */
|
|
121
|
+
const RequiredPaths = (0, react.createContext)(NO_REQUIRED_PATHS);
|
|
106
122
|
const OWN_ENTRY_KEY_SET = /* @__PURE__ */ new Set([
|
|
107
123
|
"widget",
|
|
108
124
|
"props",
|
|
@@ -161,13 +177,16 @@ const Item = ({ parts, form, entry, path, index, extra }) => {
|
|
|
161
177
|
};
|
|
162
178
|
const FieldContent = ({ parts, form, entry, path, props, extra }) => {
|
|
163
179
|
const { own, fieldOptions } = entry;
|
|
180
|
+
const requiredPaths = (0, react.use)(RequiredPaths);
|
|
164
181
|
if (typeof own.widget === "string") {
|
|
165
182
|
const Widget = byName(parts.widgets, own.widget);
|
|
166
183
|
if (Widget === void 0) throw new Error(`formdef: field "${path}" names the unregistered widget "${own.widget}".`);
|
|
184
|
+
const found = declaresEmptyValue(Widget) ? { required: requiredPaths.has(pathPattern(path)) } : void 0;
|
|
167
185
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(form.AppField, {
|
|
168
186
|
...fieldOptions,
|
|
169
187
|
name: path,
|
|
170
188
|
children: (field) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Widget, {
|
|
189
|
+
...found,
|
|
171
190
|
...props,
|
|
172
191
|
...extra,
|
|
173
192
|
field
|
|
@@ -333,15 +352,75 @@ const LevelEntries = ({ layout, ...level }) => {
|
|
|
333
352
|
* into sections and rows and places form components and inline entries. A leaf renders as its
|
|
334
353
|
* widget, an object as its own level, a custom entry through its `render`, an array through the
|
|
335
354
|
* registered `structure.arrayFrame` or the bare default. An object entry with TanStack options of
|
|
336
|
-
* its own also mounts a field at its path, so its validators run.
|
|
355
|
+
* its own also mounts a field at its path, so its validators run. A `Fields` nested in a form finds
|
|
356
|
+
* no required fields of its own, since its paths may be relative to a field group.
|
|
337
357
|
*/
|
|
338
|
-
const Fields = ({ parts, form, fields, layout }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
358
|
+
const Fields = ({ parts, form, fields, layout, requiredPaths }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RequiredPaths, {
|
|
359
|
+
value: requiredPaths ?? NO_REQUIRED_PATHS,
|
|
360
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LevelEntries, {
|
|
361
|
+
parts,
|
|
362
|
+
form,
|
|
363
|
+
fields,
|
|
364
|
+
layout,
|
|
365
|
+
path: void 0
|
|
366
|
+
})
|
|
344
367
|
});
|
|
368
|
+
// src/form/find-required-paths.ts
|
|
369
|
+
const NONE = { paths: /* @__PURE__ */ new Set() };
|
|
370
|
+
const ASYNC_SCHEMA = "formdef: the schema validates asynchronously, so no field is found to be required; pass required in props.";
|
|
371
|
+
const UNSETTLED_SCHEMA = "formdef: the schema did not validate empty values synchronously, so no field is found to be required; pass required in props.";
|
|
372
|
+
const THROWING_SCHEMA = "formdef: the schema threw on empty values, so no field is found to be required";
|
|
373
|
+
const FIRST_ITEM = 0;
|
|
374
|
+
const isStandardSchema = (schema) => typeof schema === "object" && schema !== null && "~standard" in schema;
|
|
375
|
+
const emptyLevel = (fields, path, empty) => Object.fromEntries(Object.entries(fields).map(([key, entry]) => [key, emptyEntry(entry, childPath(path, key), empty)]));
|
|
376
|
+
function emptyEntry(entry, path, empty) {
|
|
377
|
+
const { own } = splitEntry(entry);
|
|
378
|
+
if (typeof own.widget === "string") {
|
|
379
|
+
const widget = byName(empty.widgets, own.widget);
|
|
380
|
+
if (widget === void 0 || !declaresEmptyValue(widget)) return;
|
|
381
|
+
empty.declared.add(pathPattern(path));
|
|
382
|
+
return widget.emptyValue;
|
|
383
|
+
}
|
|
384
|
+
if (typeof own.item === "object" && own.item !== null) return [emptyEntry(own.item, childPath(path, FIRST_ITEM), empty)];
|
|
385
|
+
if (typeof own.fields === "object" && own.fields !== null) return emptyLevel(own.fields, path, empty);
|
|
386
|
+
}
|
|
387
|
+
const issuePath = (issue) => pathPattern((issue.path ?? []).reduce((parent, segment) => {
|
|
388
|
+
const key = typeof segment === "object" ? segment.key : segment;
|
|
389
|
+
return childPath(parent, typeof key === "number" ? key : String(key));
|
|
390
|
+
}, void 0) ?? "");
|
|
391
|
+
const parentOf = (path) => path.slice(0, Math.max(path.lastIndexOf("."), path.lastIndexOf("[")));
|
|
392
|
+
const leafOf = (path, leaves) => {
|
|
393
|
+
for (let candidate = path; candidate !== ""; candidate = parentOf(candidate)) if (leaves.has(candidate)) return candidate;
|
|
394
|
+
};
|
|
395
|
+
const ignoreRejection = () => void 0;
|
|
396
|
+
const errorText = (error) => error instanceof Error ? error.message : String(error);
|
|
397
|
+
/**
|
|
398
|
+
* Finds the required fields of a config: validates `schema` once, synchronously, with every leaf
|
|
399
|
+
* holding its widget's empty value and every array one such item. A leaf whose widget declares an
|
|
400
|
+
* empty value and whose path receives an issue is required. A schema the config validates
|
|
401
|
+
* asynchronously is not run, so its asynchronous checks send nothing.
|
|
402
|
+
*/
|
|
403
|
+
function findRequiredPaths({ schema, validatesAsync, fields, widgets }) {
|
|
404
|
+
if (!isStandardSchema(schema)) return NONE;
|
|
405
|
+
if (validatesAsync) return { problem: ASYNC_SCHEMA };
|
|
406
|
+
const empty = {
|
|
407
|
+
widgets,
|
|
408
|
+
declared: /* @__PURE__ */ new Set()
|
|
409
|
+
};
|
|
410
|
+
const values = emptyLevel(fields, void 0, empty);
|
|
411
|
+
let result;
|
|
412
|
+
try {
|
|
413
|
+
result = schema["~standard"].validate(values);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
return { problem: `${THROWING_SCHEMA}: ${errorText(error)}` };
|
|
416
|
+
}
|
|
417
|
+
if (result instanceof Promise) {
|
|
418
|
+
result.then(void 0, ignoreRejection);
|
|
419
|
+
return { problem: UNSETTLED_SCHEMA };
|
|
420
|
+
}
|
|
421
|
+
const leaves = (result.issues ?? []).map((issue) => leafOf(issuePath(issue), empty.declared));
|
|
422
|
+
return { paths: new Set(leaves.filter((leaf) => leaf !== void 0)) };
|
|
423
|
+
}
|
|
345
424
|
// src/form/blur-then-change.ts
|
|
346
425
|
const isSchemaDue = ({ event, form }) => {
|
|
347
426
|
if (event.type === "blur" || event.type === "submit") return true;
|
|
@@ -416,7 +495,7 @@ function formOptionsFor({ config, options }) {
|
|
|
416
495
|
}
|
|
417
496
|
// src/form/create-form.tsx
|
|
418
497
|
const MISSING_DEFAULT_VALUES = "formdef: the form has no default values; set defaultValues in its config or in its options.";
|
|
419
|
-
const ConfiguredForm = ({ binding, config, options, ...formProps }) => {
|
|
498
|
+
const ConfiguredForm = ({ binding, config, requiredPaths, options, ...formProps }) => {
|
|
420
499
|
const formOptions = formOptionsFor({
|
|
421
500
|
config,
|
|
422
501
|
options
|
|
@@ -428,7 +507,8 @@ const ConfiguredForm = ({ binding, config, options, ...formProps }) => {
|
|
|
428
507
|
parts: binding.parts,
|
|
429
508
|
form,
|
|
430
509
|
fields: config.fields,
|
|
431
|
-
layout: config.layout
|
|
510
|
+
layout: config.layout,
|
|
511
|
+
requiredPaths
|
|
432
512
|
});
|
|
433
513
|
const handleSubmit = (event) => {
|
|
434
514
|
event.preventDefault();
|
|
@@ -442,17 +522,36 @@ const ConfiguredForm = ({ binding, config, options, ...formProps }) => {
|
|
|
442
522
|
children: Body === void 0 ? content : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Body, { children: content })
|
|
443
523
|
}) });
|
|
444
524
|
};
|
|
525
|
+
const isAsyncValidation = (validation) => typeof validation === "object" && validation !== null && "async" in validation ? validation.async === true : false;
|
|
526
|
+
const requiredPathsOf = (binding, config) => {
|
|
527
|
+
const result = findRequiredPaths({
|
|
528
|
+
schema: config.schema,
|
|
529
|
+
validatesAsync: isAsyncValidation(config.validation),
|
|
530
|
+
fields: config.fields ?? {},
|
|
531
|
+
widgets: binding.parts.widgets
|
|
532
|
+
});
|
|
533
|
+
if ("problem" in result) {
|
|
534
|
+
warnOnce(config, result.problem);
|
|
535
|
+
return /* @__PURE__ */ new Set();
|
|
536
|
+
}
|
|
537
|
+
return result.paths;
|
|
538
|
+
};
|
|
445
539
|
/**
|
|
446
540
|
* Turns a form config into a form component bound to `binding`: TanStack's form with the merged
|
|
447
541
|
* options, a `<form>` element that submits through it, the registered body and the config's fields.
|
|
542
|
+
* When it first renders, it finds the config's required fields once.
|
|
448
543
|
*/
|
|
449
544
|
function createForm(binding, config) {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
binding,
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
545
|
+
let requiredPaths;
|
|
546
|
+
return (props) => {
|
|
547
|
+
requiredPaths ??= requiredPathsOf(binding, config);
|
|
548
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfiguredForm, {
|
|
549
|
+
...props,
|
|
550
|
+
binding,
|
|
551
|
+
config,
|
|
552
|
+
requiredPaths
|
|
553
|
+
});
|
|
554
|
+
};
|
|
456
555
|
}
|
|
457
556
|
// src/registry/create-registry.ts
|
|
458
557
|
function buildRegistry(parts) {
|
|
@@ -463,13 +562,14 @@ function buildRegistry(parts) {
|
|
|
463
562
|
formComponents: parts.formComponents
|
|
464
563
|
});
|
|
465
564
|
function extendRegistry(extension) {
|
|
565
|
+
const addedWidgets = extension.widgets ?? {};
|
|
466
566
|
const addedStructure = extension.structure ?? {};
|
|
467
567
|
return buildRegistry({
|
|
468
568
|
fieldContext: parts.fieldContext,
|
|
469
569
|
formContext: parts.formContext,
|
|
470
570
|
widgets: {
|
|
471
571
|
...parts.widgets,
|
|
472
|
-
...
|
|
572
|
+
...addedWidgets
|
|
473
573
|
},
|
|
474
574
|
formComponents: {
|
|
475
575
|
...parts.formComponents,
|
|
@@ -513,7 +613,8 @@ function buildRegistry(parts) {
|
|
|
513
613
|
* @param config - The contexts and what the registry holds.
|
|
514
614
|
* @param config.fieldContext - The field context from `createFormHookContexts()`.
|
|
515
615
|
* @param config.formContext - The form context from `createFormHookContexts()`.
|
|
516
|
-
* @param config.widgets - The widgets by name.
|
|
616
|
+
* @param config.widgets - The widgets by name. A widget's static `emptyValue`, the value it holds
|
|
617
|
+
* while nothing is entered, must be of the value type it serves.
|
|
517
618
|
* @param config.formComponents - The form components by name.
|
|
518
619
|
* @param config.structure - The components that render the structure slots.
|
|
519
620
|
* @defaultValue `config.formComponents` and `config.structure`: none registered.
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["useSelector","Fragment","useRef","getBy","revalidateLogic","createFormHook","createElement"],"sources":["../src/render/array-entry.tsx","../src/render/by-name.ts","../src/render/child-path.ts","../src/render/derived.tsx","../src/render/split-entry.ts","../src/render/warn-once.ts","../src/render/fields.tsx","../src/form/blur-then-change.ts","../src/form/form-options-for.ts","../src/form/create-form.tsx","../src/registry/create-registry.ts","../src/widget/errors-of.ts","../src/widget/field-id.ts","../src/widget/field-parts.ts"],"sourcesContent":["import { type AnyFieldApi, useSelector } from '@tanstack/react-form'\nimport { type ComponentType, Fragment, type ReactNode } from 'react'\n\n// biome-ignore lint/suspicious/noExplicitAny: a registered frame takes props of its own; the config types check them.\ntype AnyProps = any\n\n/** The keys of an array entry the renderer reads besides its item. */\ntype ArrayOptions = {\n newItem?: unknown\n addLabel?: unknown\n removeLabel?: unknown\n itemKey?: unknown\n}\n\ntype ArrayEntryProps = {\n /** The array field, mounted at the entry's path. */\n field: AnyFieldApi\n options: ArrayOptions\n /** The registered `structure.arrayFrame`, if any. */\n frame: ComponentType<AnyProps> | undefined\n /** The entry's props for the frame. */\n props: object | undefined\n renderItem: (index: number, extra?: object) => ReactNode\n}\n\ntype ItemFunction<TResult> = (item: unknown, index?: number) => TResult\n\n/** What the controls of an array entry show: one key and one remove label per item. */\ntype ItemControls = {\n keys: (string | number)[]\n removeLabels: string[]\n}\n\nconst removeLabelOf = (removeLabel: unknown, item: unknown, index: number): string =>\n typeof removeLabel === 'function'\n ? (removeLabel as ItemFunction<string>)(item, index)\n : String(removeLabel)\n\nconst itemControlsOf = (options: ArrayOptions, value: unknown): ItemControls => {\n const items: unknown[] = Array.isArray(value) ? value : []\n\n return {\n keys: items.map((item, index) =>\n typeof options.itemKey === 'function'\n ? (options.itemKey as ItemFunction<string | number>)(item)\n : index,\n ),\n removeLabels: items.map((item, index) => removeLabelOf(options.removeLabel, item, index)),\n }\n}\n\nconst isSameList = <TItem,>(first: readonly TItem[], second: readonly TItem[]): boolean =>\n first.length === second.length && first.every((item, index) => item === second[index])\n\nconst isSameControls = (first: ItemControls, second: ItemControls): boolean =>\n isSameList(first.keys, second.keys) && isSameList(first.removeLabels, second.removeLabels)\n\n/**\n * Renders an array entry: through the registered frame, which receives `ArrayFrameProps`, or as\n * the bare default, each item followed by its remove button and one add button at the end.\n */\nexport const ArrayEntry = ({\n field,\n options,\n frame: Frame,\n props,\n renderItem,\n}: ArrayEntryProps): ReactNode => {\n // The field renders in TanStack's array mode, so only a change to the list reaches it; keys and\n // labels that name an item follow the items through a selection of their own.\n const { keys, removeLabels } = useSelector(\n field.store,\n (state: { value: unknown }) => itemControlsOf(options, state.value),\n { compare: isSameControls },\n )\n const newItem = options.newItem as () => unknown\n const addLabel = String(options.addLabel)\n const add = (): void => field.pushValue(newItem())\n const remove = (index: number): void => field.removeValue(index)\n\n if (Frame === undefined) {\n return (\n <>\n {keys.map((key, index) => (\n <Fragment key={key}>\n {renderItem(index)}\n <button type=\"button\" data-formdef-remove=\"\" onClick={() => remove(index)}>\n {removeLabels[index]}\n </button>\n </Fragment>\n ))}\n <button type=\"button\" data-formdef-add=\"\" onClick={add}>\n {addLabel}\n </button>\n </>\n )\n }\n\n return (\n <Frame\n {...props}\n field={field}\n keys={keys}\n renderItem={renderItem}\n addLabel={addLabel}\n removeLabels={removeLabels}\n add={add}\n remove={remove}\n insert={(index: number, item?: unknown) =>\n field.insertValue(index, item === undefined ? newItem() : item)\n }\n move={(from: number, to: number) => field.moveValue(from, to)}\n swap={(first: number, second: number) => field.swapValues(first, second)}\n replace={(index: number, item: unknown) => field.replaceValue(index, item)}\n clear={() => field.clearValues()}\n />\n )\n}\n","/** What `map` holds under `name` itself; a name every object inherits finds nothing. */\nexport function byName<TValue>(\n map: Readonly<Record<string, TValue>>,\n name: string,\n): TValue | undefined {\n return Object.hasOwn(map, name) ? map[name] : undefined\n}\n","/**\n * The path of `key` below `parent`: a key of the root level by itself, a key of a nested level\n * after a dot, the index of an array item in brackets.\n */\nexport function childPath(parent: string | undefined, key: string | number): string {\n if (typeof key === 'number') {\n return `${parent ?? ''}[${key}]`\n }\n\n return parent === undefined ? key : `${parent}.${key}`\n}\n","import { getBy } from '@tanstack/react-form'\nimport { type ReactNode, useRef } from 'react'\n\nimport type { RenderForm } from './render-form'\n\n/** What an entry reads from the form values: its condition, its props and the paths they read. */\ntype DerivedEntry = {\n visibleWhen?: unknown\n props?: unknown\n dependsOn?: unknown\n}\n\ntype DerivedProps = {\n form: RenderForm\n entry: DerivedEntry\n /** Builds the entry from its props; called only while the entry is visible. */\n children: (props: object | undefined) => ReactNode\n}\n\ntype LastRead = {\n entry: DerivedEntry\n dependencies: unknown[]\n selected: unknown\n}\n\nconst HIDDEN = Symbol('hidden')\nconst VISIBLE = Symbol('visible')\n\nconst propsOf = (props: unknown): object | undefined =>\n typeof props === 'object' && props !== null ? props : undefined\n\nconst isStale = (\n last: LastRead | undefined,\n entry: DerivedEntry,\n dependencies: unknown[],\n): boolean =>\n last === undefined ||\n last.entry !== entry ||\n dependencies.some((value, index) => !Object.is(value, last.dependencies[index]))\n\n/**\n * Renders an entry from what it reads from the form values. An entry with a condition or derived\n * props subscribes to the values; the selected result alone decides whether it re-renders: whether\n * it is shown for a condition, the values for derived props. `dependsOn` narrows when the selector\n * reads again, never what it returns. A hidden entry is never built.\n */\nexport const Derived = ({ form, entry, children }: DerivedProps): ReactNode => {\n const lastRead = useRef<LastRead | undefined>(undefined)\n const { visibleWhen, props, dependsOn } = entry\n const isConditional = typeof visibleWhen === 'function'\n const isDerived = typeof props === 'function'\n\n if (!(isConditional || isDerived)) {\n return children(propsOf(props))\n }\n\n const selectFrom = (values: unknown): unknown => {\n if (isConditional && !visibleWhen(values)) {\n return HIDDEN\n }\n\n return isDerived ? values : VISIBLE\n }\n\n const select = ({ values }: { values: unknown }): unknown => {\n if (!Array.isArray(dependsOn)) {\n return selectFrom(values)\n }\n\n const dependencies = dependsOn.map((path: string) => getBy(values, path))\n if (isStale(lastRead.current, entry, dependencies)) {\n lastRead.current = { entry, dependencies, selected: selectFrom(values) }\n }\n\n return lastRead.current?.selected\n }\n\n return (\n <form.Subscribe selector={select}>\n {(selected) => {\n if (selected === HIDDEN) {\n return null\n }\n\n return children(propsOf(isDerived ? props(selected) : props))\n }}\n </form.Subscribe>\n )\n}\n","/**\n * The keys formdef reads from a field entry. Every other key is one of TanStack's field options and\n * goes to the field unchanged, so an option TanStack adds works without a formdef release.\n */\nexport const OWN_ENTRY_KEYS = [\n 'widget',\n 'props',\n 'fields',\n 'layout',\n 'item',\n 'newItem',\n 'addLabel',\n 'removeLabel',\n 'itemKey',\n 'render',\n 'visibleWhen',\n 'dependsOn',\n] as const\n\n/** A key formdef reads from a field entry. */\nexport type OwnEntryKey = (typeof OWN_ENTRY_KEYS)[number]\n\nconst OWN_ENTRY_KEY_SET: ReadonlySet<string> = new Set(OWN_ENTRY_KEYS)\n\n/** Splits a field entry into the keys formdef reads and TanStack's field options. */\nexport function splitEntry(entry: object): {\n own: Partial<Record<OwnEntryKey, unknown>>\n fieldOptions: Record<string, unknown>\n} {\n const entries = Object.entries(entry)\n\n // `fromEntries` defines each key as data, so a key named `__proto__` cannot set a prototype.\n return {\n own: Object.fromEntries(entries.filter(([key]) => OWN_ENTRY_KEY_SET.has(key))),\n fieldOptions: Object.fromEntries(entries.filter(([key]) => !OWN_ENTRY_KEY_SET.has(key))),\n }\n}\n","// Bundlers replace `process.env.NODE_ENV` in exactly this spelling; without one, `process` may not\n// exist at all.\n// biome-ignore lint/style/useNamingConvention: the name bundlers replace.\ndeclare const process: { env: { NODE_ENV?: string } } | undefined\n\nconst warnedOwners = new WeakSet<object>()\n\nconst isProduction = (): boolean =>\n typeof process !== 'undefined' && process.env.NODE_ENV === 'production'\n\n/** Warns once per `owner`, and never in production. */\nexport function warnOnce(owner: object, message: string): void {\n if (isProduction() || warnedOwners.has(owner)) {\n return\n }\n\n warnedOwners.add(owner)\n console.warn(message)\n}\n","import type { ComponentType, ReactNode } from 'react'\n\nimport { ArrayEntry } from './array-entry'\nimport { byName } from './by-name'\nimport { childPath } from './child-path'\nimport { Derived } from './derived'\nimport type { AppFieldProps, RenderForm } from './render-form'\nimport { splitEntry } from './split-entry'\nimport { warnOnce } from './warn-once'\n\n// biome-ignore lint/suspicious/noExplicitAny: registered components take props of their own; the config types check them.\ntype AnyProps = any\n\ntype ComponentMap = Readonly<Record<string, ComponentType<AnyProps>>>\n\n/** The maps of a registry the renderer looks names up in. */\ntype RenderParts = {\n widgets: ComponentMap\n formComponents: ComponentMap\n structure: {\n section?: ComponentType<AnyProps>\n row?: ComponentType<AnyProps>\n arrayFrame?: ComponentType<AnyProps>\n }\n}\n\ntype LevelFields = Readonly<Record<string, object>>\n\ntype LayoutNodeEntry = string | Readonly<Record<string, unknown>>\n\n/** The props of {@link Fields}. */\nexport type FieldsProps = {\n parts: RenderParts\n form: RenderForm\n fields: LevelFields\n layout?: readonly unknown[] | undefined\n}\n\ntype Level = Omit<FieldsProps, 'layout'> & {\n path: string | undefined\n /** Props an array frame passes the one widget of an item, through this level. */\n extra?: object | undefined\n}\n\ntype LevelProps = Level & {\n layout: readonly unknown[] | undefined\n}\n\ntype EntryProps = Omit<Level, 'fields'> & {\n entry: object\n path: string\n}\n\ntype FieldContentProps = Omit<EntryProps, 'entry'> & {\n entry: ReturnType<typeof splitEntry> & { source: object }\n props: object | undefined\n}\n\ntype ItemProps = Omit<EntryProps, 'extra'> & {\n index: number\n extra: object | undefined\n}\n\ntype LayoutNodeProps = {\n level: Level\n entry: LayoutNodeEntry\n}\n\ntype LayoutKindProps = {\n level: Level\n entry: Readonly<Record<string, unknown>>\n props: object | undefined\n}\n\nconst ARRAY_MODE = 'array'\n\nconst DROPPED_EXTRA = 'passes extra props to an item that has no single widget to receive them'\n\nconst renderNothing = (): ReactNode => null\n\nconst layoutOwner = (path: string | undefined): string =>\n path === undefined ? 'the form' : `\"${path}\"`\n\nconst isLeaf = (entry: unknown): boolean =>\n typeof entry === 'object' && entry !== null && 'widget' in entry\n\n// A frame's extra props have a single target only in a leaf, or an object level whose one field\n// is a leaf and which has no layout.\nconst takesExtra = (item: object): boolean => {\n if (isLeaf(item)) {\n return true\n }\n const { own } = splitEntry(item)\n const fields =\n typeof own.fields === 'object' && own.fields !== null ? Object.values(own.fields) : []\n\n return own.layout === undefined && fields.length === 1 && isLeaf(fields[0])\n}\n\nconst Item = ({ parts, form, entry, path, index, extra }: ItemProps): ReactNode => {\n const { own } = splitEntry(entry)\n const item = own.item as object\n const reachesWidget = extra !== undefined && takesExtra(item)\n if (extra !== undefined && !reachesWidget) {\n warnOnce(entry, `formdef: the array frame of \"${path}\" ${DROPPED_EXTRA}; they are dropped.`)\n }\n\n return (\n <Entry\n parts={parts}\n form={form}\n entry={item}\n path={childPath(path, index)}\n extra={reachesWidget ? extra : undefined}\n />\n )\n}\n\nconst FieldContent = ({ parts, form, entry, path, props, extra }: FieldContentProps): ReactNode => {\n const { own, fieldOptions } = entry\n\n if (typeof own.widget === 'string') {\n const Widget = byName(parts.widgets, own.widget)\n if (Widget === undefined) {\n throw new Error(`formdef: field \"${path}\" names the unregistered widget \"${own.widget}\".`)\n }\n\n return (\n <form.AppField {...fieldOptions} name={path}>\n {(field) => <Widget {...props} {...extra} field={field} />}\n </form.AppField>\n )\n }\n\n if (typeof own.render === 'function') {\n return (\n <form.AppField {...fieldOptions} name={path}>\n {own.render as AppFieldProps['children']}\n </form.AppField>\n )\n }\n\n if (typeof own.item === 'object' && own.item !== null) {\n return (\n <form.AppField {...fieldOptions} name={path} mode={ARRAY_MODE}>\n {(field) => (\n <ArrayEntry\n field={field}\n options={own}\n frame={parts.structure.arrayFrame}\n props={props}\n renderItem={(index, itemExtra) => (\n <Item\n parts={parts}\n form={form}\n entry={entry.source}\n path={path}\n index={index}\n extra={itemExtra}\n />\n )}\n />\n )}\n </form.AppField>\n )\n }\n\n if (typeof own.fields !== 'object' || own.fields === null) {\n return null\n }\n\n return (\n <>\n {Object.keys(fieldOptions).length > 0 && (\n <form.AppField {...fieldOptions} name={path}>\n {renderNothing}\n </form.AppField>\n )}\n <LevelEntries\n parts={parts}\n form={form}\n fields={own.fields as LevelFields}\n layout={Array.isArray(own.layout) ? own.layout : undefined}\n path={path}\n extra={extra}\n />\n </>\n )\n}\n\nconst Entry = ({ parts, form, entry, path, extra }: EntryProps): ReactNode => {\n const split = { ...splitEntry(entry), source: entry }\n\n return (\n <Derived form={form} entry={split.own}>\n {(props) => (\n <FieldContent\n parts={parts}\n form={form}\n entry={split}\n path={path}\n props={props}\n extra={extra}\n />\n )}\n </Derived>\n )\n}\n\nconst FieldNode = ({ level, name }: { level: Level; name: string }): ReactNode => {\n const entry = byName(level.fields, name)\n if (entry === undefined) {\n throw new Error(\n `formdef: the layout of ${layoutOwner(level.path)} names \"${name}\", which the level does not describe.`,\n )\n }\n\n return (\n <Entry\n parts={level.parts}\n form={level.form}\n entry={entry}\n path={childPath(level.path, name)}\n extra={level.extra}\n />\n )\n}\n\nconst LayoutNodes = ({ level, entries }: { level: Level; entries: unknown }): ReactNode =>\n (Array.isArray(entries) ? (entries as LayoutNodeEntry[]) : []).map((entry, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: a layout comes from the config and never reorders; a name may appear twice.\n <LayoutNode key={index} level={level} entry={entry} />\n ))\n\nconst SectionNode = ({ level, entry, props }: LayoutKindProps): ReactNode => {\n const children = <LayoutNodes level={level} entries={entry.layout} />\n const Section = level.parts.structure.section\n\n if (Section !== undefined) {\n return (\n <Section {...props} label={entry.section}>\n {children}\n </Section>\n )\n }\n\n return (\n <fieldset {...props}>\n <legend>{entry.section as string}</legend>\n {children}\n </fieldset>\n )\n}\n\nconst RowNode = ({ level, entry, props }: LayoutKindProps): ReactNode => {\n const children = <LayoutNodes level={level} entries={entry.row} />\n const Row = level.parts.structure.row\n\n if (Row !== undefined) {\n return <Row {...props}>{children}</Row>\n }\n\n return (\n <div {...props} data-formdef-row=\"\">\n {children}\n </div>\n )\n}\n\nconst ComponentNode = ({ level, entry, props }: LayoutKindProps): ReactNode => {\n const name = entry.component as string\n const Component = byName(level.parts.formComponents, name)\n if (Component === undefined) {\n throw new Error(\n `formdef: the layout of ${layoutOwner(level.path)} places the unregistered form component \"${name}\".`,\n )\n }\n\n return <Component {...props} />\n}\n\nconst LayoutKind = ({ level, entry, props }: LayoutKindProps): ReactNode => {\n if ('section' in entry) {\n return <SectionNode level={level} entry={entry} props={props} />\n }\n if ('row' in entry) {\n return <RowNode level={level} entry={entry} props={props} />\n }\n if (typeof entry.component === 'string') {\n return <ComponentNode level={level} entry={entry} props={props} />\n }\n if (typeof entry.render === 'function') {\n return (entry.render as (form: RenderForm) => ReactNode)(level.form)\n }\n\n return null\n}\n\nconst LayoutNode = ({ level, entry }: LayoutNodeProps): ReactNode => {\n if (typeof entry === 'string') {\n return <FieldNode level={level} name={entry} />\n }\n\n return (\n <Derived form={level.form} entry={entry}>\n {(props) => <LayoutKind level={level} entry={entry} props={props} />}\n </Derived>\n )\n}\n\nconst LevelEntries = ({ layout, ...level }: LevelProps): ReactNode => {\n if (layout !== undefined) {\n return <LayoutNodes level={level} entries={layout} />\n }\n\n return Object.keys(level.fields).map((name) => <FieldNode key={name} level={level} name={name} />)\n}\n\n/**\n * Renders the field entries of a level with the widgets of `parts`. Without a `layout` the entries\n * appear in the order `fields` declares them; with one, the layout decides the order, groups them\n * into sections and rows and places form components and inline entries. A leaf renders as its\n * widget, an object as its own level, a custom entry through its `render`, an array through the\n * registered `structure.arrayFrame` or the bare default. An object entry with TanStack options of\n * its own also mounts a field at its path, so its validators run.\n */\nexport const Fields = ({ parts, form, fields, layout }: FieldsProps): ReactNode => (\n <LevelEntries parts={parts} form={form} fields={fields} layout={layout} path={undefined} />\n)\n","import {\n defaultValidationLogic,\n type ValidationLogicFn,\n type ValidationLogicProps,\n} from '@tanstack/react-form'\n\ntype ScheduledValidators = Parameters<ValidationLogicProps['runValidation']>[0]['validators']\n\n// Gated on the form, not the changed field: the schema validates the whole form, and TanStack names\n// the changed field only on the field-level call, where the schema does not sit.\nconst isSchemaDue = ({ event, form }: ValidationLogicProps): boolean => {\n if (event.type === 'blur' || event.type === 'submit') {\n return true\n }\n\n return event.type === 'change' && !form.state.isValid\n}\n\n/**\n * The validation logic of the `blurThenChange` mode: TanStack's default logic, plus the schema in\n * `onDynamic` when a field is left, on submit, and on every change while the form is invalid.\n */\nexport const blurThenChange: ValidationLogicFn = (props) => {\n let defaults: ScheduledValidators = []\n defaultValidationLogic({\n ...props,\n runValidation: ({ validators }) => {\n defaults = validators\n },\n })\n\n const schema = props.event.async ? props.validators?.onDynamicAsync : props.validators?.onDynamic\n if (schema === undefined || !isSchemaDue(props)) {\n return props.runValidation({ validators: defaults, form: props.form })\n }\n\n return props.runValidation({\n validators: [...defaults, { fn: schema, cause: 'dynamic' }],\n form: props.form,\n })\n}\n","import { revalidateLogic, type ValidationLogicFn } from '@tanstack/react-form'\n\nimport type { Validation } from '../config/validation'\nimport { blurThenChange } from './blur-then-change'\n\ntype Mode = NonNullable<Validation['mode']>\ntype DynamicMode = 'revalidate' | 'blurThenChange'\ntype Slot = 'onDynamic' | 'onChange' | 'onBlur' | 'onSubmit'\n\ntype Options = Readonly<Record<string, unknown>>\n\ntype FormOptionsInput = {\n /** The form config: the schema, formdef's own keys and TanStack's form options. */\n config: Options\n /** The instance's TanStack form options, from the form component's `options` prop. */\n options: Options | undefined\n}\n\nconst DEFAULT_MODE: Mode = 'revalidate'\n\nconst isDynamic = (mode: Mode): mode is DynamicMode =>\n mode === 'revalidate' || mode === 'blurThenChange'\n\nconst logicFor = (mode: DynamicMode): ValidationLogicFn =>\n mode === 'revalidate' ? revalidateLogic() : blurThenChange\n\nconst slotFor = (mode: Mode, isAsync: boolean): Slot | `${Slot}Async` => {\n const slot: Slot = isDynamic(mode) ? 'onDynamic' : mode\n\n return isAsync ? `${slot}Async` : slot\n}\n\nconst recordOf = (value: unknown): Options =>\n typeof value === 'object' && value !== null ? (value as Options) : {}\n\n// An instance passing `undefined`, such as data still loading, leaves the config's value in place.\nconst setEntriesOf = (value: unknown): Options =>\n Object.fromEntries(Object.entries(recordOf(value)).filter(([, entry]) => entry !== undefined))\n\n/**\n * The TanStack form options of a form: the config's options and the instance's merged key by key,\n * the instance winning, except `validators` and `listeners`, which merge slot by slot; a key or\n * slot the instance sets to `undefined` counts as unset. The schema\n * goes into the slot `validation` picks unless a validator already fills it; a dynamic mode brings\n * its validation logic, an asynchronous schema its debounce unless the options set one.\n */\nexport function formOptionsFor({ config, options }: FormOptionsInput): Record<string, unknown> {\n const {\n schema,\n validation,\n fields: _renderedByFields,\n layout: _arrangedByFields,\n ...configOptions\n } = config\n const { mode = DEFAULT_MODE, ...timing } = recordOf(validation) as Validation\n const isAsync = timing.async === true\n const slot = slotFor(mode, isAsync)\n const instanceOptions = setEntriesOf(options)\n const validators = {\n ...recordOf(configOptions.validators),\n ...setEntriesOf(instanceOptions.validators),\n }\n\n return {\n ...(isDynamic(mode) && { validationLogic: logicFor(mode) }),\n ...(isAsync &&\n timing.asyncDebounceMs !== undefined && { asyncDebounceMs: timing.asyncDebounceMs }),\n ...configOptions,\n ...instanceOptions,\n validators: validators[slot] === undefined ? { ...validators, [slot]: schema } : validators,\n listeners: { ...recordOf(configOptions.listeners), ...setEntriesOf(instanceOptions.listeners) },\n }\n}\n","import type { ComponentType, FormEvent, ReactNode } from 'react'\n\nimport type { FormElementProps } from '../config/form-props'\nimport type { BodyProps } from '../config/structure'\nimport { Fields, type FieldsProps } from '../render/fields'\nimport type { RenderForm } from '../render/render-form'\nimport { formOptionsFor } from './form-options-for'\n\ntype Options = Readonly<Record<string, unknown>>\n\n/** The form `useAppForm` returns, as far as the shell uses it. */\ntype ShellForm = RenderForm & {\n AppForm: ComponentType<{ children?: ReactNode }>\n handleSubmit: () => Promise<void>\n}\n\n/** What a registry binds a form to: its form hook and the maps the renderer looks names up in. */\nexport type FormBinding = {\n useAppForm: (options: Options) => ShellForm\n parts: FieldsProps['parts'] & {\n structure: { body?: ComponentType<BodyProps> }\n }\n}\n\n/** The props of a form component: the instance's options and what a `<form>` element takes. */\ntype FormComponentProps = FormElementProps & {\n options?: Options\n}\n\n/** A form config: TanStack's form options plus formdef's own keys. */\ntype ConfigInput = Options & {\n fields?: unknown\n layout?: unknown\n}\n\ntype ConfiguredFormProps = FormComponentProps & {\n binding: FormBinding\n config: ConfigInput\n}\n\nconst MISSING_DEFAULT_VALUES =\n 'formdef: the form has no default values; set defaultValues in its config or in its options.'\n\nconst ConfiguredForm = ({\n binding,\n config,\n options,\n ...formProps\n}: ConfiguredFormProps): ReactNode => {\n const formOptions = formOptionsFor({ config, options })\n if (formOptions.defaultValues === undefined) {\n throw new Error(MISSING_DEFAULT_VALUES)\n }\n\n const form = binding.useAppForm(formOptions)\n const Body = binding.parts.structure.body\n const content = (\n <Fields\n parts={binding.parts}\n form={form}\n fields={config.fields as FieldsProps['fields']}\n layout={config.layout as FieldsProps['layout']}\n />\n )\n const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {\n event.preventDefault()\n event.stopPropagation()\n void form.handleSubmit()\n }\n\n return (\n <form.AppForm>\n <form noValidate {...formProps} onSubmit={handleSubmit}>\n {Body === undefined ? content : <Body>{content}</Body>}\n </form>\n </form.AppForm>\n )\n}\n\n/**\n * Turns a form config into a form component bound to `binding`: TanStack's form with the merged\n * options, a `<form>` element that submits through it, the registered body and the config's fields.\n */\nexport function createForm(\n binding: FormBinding,\n config: ConfigInput,\n): (props: FormComponentProps) => ReactNode {\n // biome-ignore lint/correctness/noComponentHookFactories: runs once per form at module level, like TanStack's createFormHook.\n const Form = (props: FormComponentProps): ReactNode => (\n <ConfiguredForm {...props} binding={binding} config={config} />\n )\n\n return Form\n}\n","import { type AnyFieldApi, type AnyFormApi, createFormHook } from '@tanstack/react-form'\nimport { type Context, createElement, type ReactNode } from 'react'\n\nimport { createForm, type FormBinding } from '../form/create-form'\nimport { Fields, type FieldsProps } from '../render/fields'\n\nimport type {\n CheckedStructure,\n Components,\n NewFormComponents,\n NewStructure,\n NewWidgets,\n Registry,\n Structure,\n} from './registry'\n\ntype Contexts = {\n fieldContext: Context<AnyFieldApi>\n formContext: Context<AnyFormApi>\n}\n\ntype RegistryParts<TWidgets, TFormComponents, TStructure> = Contexts & {\n widgets: TWidgets\n formComponents: TFormComponents\n structure: TStructure\n}\n\nfunction buildRegistry<\n TWidgets extends Components,\n TFormComponents extends Components,\n TStructure extends Structure,\n>(\n parts: RegistryParts<TWidgets, TFormComponents, TStructure>,\n): Registry<TWidgets, TFormComponents, TStructure> {\n const { extendForm: _replacedByExtendRegistry, ...formHook } = createFormHook({\n fieldContext: parts.fieldContext,\n formContext: parts.formContext,\n fieldComponents: parts.widgets,\n formComponents: parts.formComponents,\n })\n\n function extendRegistry<\n const TNewWidgets extends NewWidgets<TWidgets> = Record<never, never>,\n const TNewFormComponents extends NewFormComponents<TFormComponents> = Record<never, never>,\n const TNewStructure extends NewStructure<TStructure> = Record<never, never>,\n >(extension: {\n widgets?: TNewWidgets\n formComponents?: TNewFormComponents\n structure?: TNewStructure & CheckedStructure<TNewStructure>\n }): Registry<\n TWidgets & TNewWidgets,\n TFormComponents & TNewFormComponents,\n TStructure & TNewStructure\n > {\n const addedStructure: Structure = extension.structure ?? {}\n\n // The extension's maps are optional, and spreading an optional generic map drops its type.\n return buildRegistry({\n fieldContext: parts.fieldContext,\n formContext: parts.formContext,\n widgets: { ...parts.widgets, ...extension.widgets } as TWidgets & TNewWidgets,\n formComponents: { ...parts.formComponents, ...extension.formComponents } as TFormComponents &\n TNewFormComponents,\n structure: { ...parts.structure, ...addedStructure } as TStructure & TNewStructure,\n })\n }\n\n // TanStack's hook is generic over the form's types; the config's types are checked at the\n // registry's `createForm`, so the binding passes the merged options on untyped.\n const binding: FormBinding = {\n useAppForm: formHook.useAppForm as unknown as FormBinding['useAppForm'],\n parts: {\n widgets: parts.widgets,\n formComponents: parts.formComponents,\n structure: parts.structure,\n },\n }\n\n // biome-ignore lint/correctness/noComponentHookFactories: created once per registry, like TanStack's createFormHook.\n const BoundFields = (props: Omit<FieldsProps, 'parts'>): ReactNode =>\n createElement(Fields, { ...props, parts: binding.parts })\n\n return {\n formHook,\n createForm: (config) => createForm(binding, config),\n // The member's signature types the level by the form it is given; the renderer reads it untyped.\n Fields: BoundFields as unknown as Registry<TWidgets, TFormComponents, TStructure>['Fields'],\n extendRegistry,\n }\n}\n\n/**\n * Binds widgets, form components and structure slots to the app's TanStack contexts and returns a\n * registry, the value every form of the app is built with.\n *\n * Create the contexts with TanStack's `createFormHookContexts()` in a module of their own, so a\n * form component can read the form through `useFormContext()` without importing the registry.\n *\n * @typeParam TWidgets - The widgets by name; each takes its field as a prop typed `FieldOf<V>`.\n * @typeParam TFormComponents - The form components by name, placed in a layout by `{ component }`.\n * @typeParam TStructure - The structure slots: `body`, `section`, `row` and `arrayFrame`.\n * @param config - The contexts and what the registry holds.\n * @param config.fieldContext - The field context from `createFormHookContexts()`.\n * @param config.formContext - The form context from `createFormHookContexts()`.\n * @param config.widgets - The widgets by name.\n * @param config.formComponents - The form components by name.\n * @param config.structure - The components that render the structure slots.\n * @defaultValue `config.formComponents` and `config.structure`: none registered.\n * @returns The registry.\n *\n * @example\n * ```tsx\n * 'use client'\n * import { createRegistry } from '@formdef/core'\n * import { fieldContext, formContext } from './context'\n *\n * export const registry = createRegistry({\n * fieldContext,\n * formContext,\n * widgets: { text: TextField, checkbox: CheckboxField },\n * formComponents: { SubmitButton },\n * structure: { section: Section },\n * })\n * ```\n */\nexport function createRegistry<\n const TWidgets extends Components,\n const TFormComponents extends Components = Record<never, never>,\n const TStructure extends Structure = Record<never, never>,\n>(\n config: Contexts & {\n widgets: TWidgets\n formComponents?: TFormComponents\n structure?: TStructure & CheckedStructure<TStructure>\n },\n): Registry<TWidgets, TFormComponents, TStructure> {\n // Both maps fall back to their empty defaults exactly when they are left out.\n return buildRegistry({\n fieldContext: config.fieldContext,\n formContext: config.formContext,\n widgets: config.widgets,\n formComponents: config.formComponents ?? ({} as TFormComponents),\n structure: config.structure ?? ({} as TStructure),\n })\n}\n","import type { FieldApiOf } from './field-of'\n\nfunction textOf(error: unknown): string | undefined {\n if (typeof error === 'string') {\n return error\n }\n if (typeof error !== 'object' || error === null || !('message' in error)) {\n return undefined\n }\n\n return typeof error.message === 'string' ? error.message : undefined\n}\n\nfunction hasText(text: string | undefined): text is string {\n return text !== undefined && text !== ''\n}\n\n/**\n * Turns the errors of a field into the texts a widget renders.\n *\n * An error is either a string or an object with a `message`, such as an issue of a Standard\n * Schema. Errors without text are left out, and a text that several validators report appears\n * once, where it first occurs. A field's errors stay readable with TanStack's `disableErrorFlat`\n * option.\n *\n * @typeParam TValue - The value type of the field.\n * @param field - The field the widget renders.\n * @returns The distinct texts of the field's current errors, in order; empty when there are none.\n *\n * @example\n * ```tsx\n * const TextField = ({ field }: FieldOf<string>) => (\n * <>\n * <input value={field.state.value} onChange={(event) => field.handleChange(event.target.value)} />\n * <p>{errorsOf(field).join(' ')}</p>\n * </>\n * )\n * ```\n */\nexport function errorsOf<TValue>(field: FieldApiOf<TValue>): string[] {\n const texts = field.state.meta.errors.flat().map(textOf).filter(hasText)\n\n return [...new Set(texts)]\n}\n","const PATH_SEPARATORS = /[.[\\]]+/g\nconst SELECTOR_SAFE_SEPARATOR = '__'\n\n/**\n * Builds the DOM id that `fieldParts` gives the control of a field, for code that has no\n * field at hand — an error summary above the form that links to each invalid field, for example.\n *\n * The id is the form's id, a hyphen, and the field's path with every run of dots and brackets\n * replaced by `__`. Fields of two different forms therefore never share an id, and the id can be\n * used in a CSS selector without escaping.\n *\n * @param formId - The id of the form, as TanStack Form reports it in `form.formId`.\n * @param path - The path of the field, such as `email`, `address.zip` or `contacts[0].name`.\n * @returns The id of the field's control, such as `signup-address__zip`.\n *\n * @remarks\n * The replacement cannot be reversed: a field literally named `a__b` gets the same id as the\n * path `a.b`.\n *\n * @example\n * ```tsx\n * <a href={`#${fieldId(form.formId, 'address.zip')}`}>Postal code</a>\n * ```\n */\nexport function fieldId(formId: string, path: string): string {\n return `${formId}-${path.replace(PATH_SEPARATORS, SELECTOR_SAFE_SEPARATOR)}`\n}\n","import { errorsOf } from './errors-of'\nimport { fieldId } from './field-id'\nimport type { FieldApiOf } from './field-of'\n\nconst DESCRIPTION_SUFFIX = '-description'\nconst ERROR_SUFFIX = '-error'\nconst IDREF_SEPARATOR = ' '\n\n/**\n * What {@link fieldParts} returns: the wiring of a widget's control and what it needs to render\n * its description and its errors.\n */\nexport interface FieldParts {\n /** Props to spread onto the control; its value and change handler stay with the widget. */\n control: {\n /** The control's id, as {@link fieldId} builds it. */\n id: string\n /** The field's path, so the browser can autofill the control. */\n name: string\n /** Reports to the field that the control was left. */\n onBlur: () => void\n /** `true` while an error is due; absent otherwise, never `false`. */\n 'aria-invalid': true | undefined\n /** The ids of the description and of a due error with text, when there are any. */\n 'aria-describedby': string | undefined\n /** The id of the error element, while an error with text is due. */\n 'aria-errormessage': string | undefined\n }\n /** The id to give the element that renders the description. */\n descriptionId: string\n /** The id to give the element that renders the error texts. */\n errorId: string\n /**\n * Whether the widget shows the field's errors now: the field is invalid and was edited and\n * left, or the form was submitted.\n */\n isErrorDue: boolean\n /** The texts of the field's errors, as {@link errorsOf} returns them. */\n errors: string[]\n}\n\nfunction isErrorDueFor<TValue>(field: FieldApiOf<TValue>): boolean {\n const { isBlurred, isDirty, isValid } = field.state.meta\n const wasEditedAndLeft = isBlurred && isDirty\n const wasSubmitted = field.form.state.submissionAttempts > 0\n\n return !isValid && (wasEditedAndLeft || wasSubmitted)\n}\n\nfunction idrefsOf(ids: (string | undefined)[]): string | undefined {\n const present = ids.filter((id) => id !== undefined)\n\n return present.length > 0 ? present.join(IDREF_SEPARATOR) : undefined\n}\n\n/**\n * Wires a widget's control to its field: its id and name, the blur report, and the aria\n * attributes that tie it to its description and its errors.\n *\n * An error is due once the field is invalid and was edited and left, or once the form was\n * submitted. While one is due, `aria-invalid` is `true`; while one with text is due, the control\n * points at the error element through `aria-describedby` and `aria-errormessage`. An id appears in\n * them only when its element is rendered.\n *\n * @typeParam TValue - The value type of the field.\n * @param field - The field the widget renders.\n * @param options - What the widget renders besides the control.\n * @param options.description - The description the widget renders below its label, if any. Only\n * whether it is a non-empty string is read.\n * @defaultValue `options.description`: `undefined`, no description is rendered.\n * @returns The props for the control, the ids for the description and error elements, whether an\n * error is due, and the error texts.\n *\n * @example\n * ```tsx\n * const TextField = ({ field, label, description }: FieldOf<string> & { label: string; description?: string }) => {\n * const { control, descriptionId, errorId, isErrorDue, errors } = fieldParts(field, { description })\n *\n * return (\n * <div>\n * <label htmlFor={control.id}>{label}</label>\n * {description && <p id={descriptionId}>{description}</p>}\n * <input {...control} value={field.state.value} onChange={(event) => field.handleChange(event.target.value)} />\n * {isErrorDue && errors.length > 0 && <p id={errorId}>{errors.join(' ')}</p>}\n * </div>\n * )\n * }\n * ```\n */\nexport function fieldParts<TValue>(\n field: FieldApiOf<TValue>,\n options: { description?: string | undefined } = {},\n): FieldParts {\n const id = fieldId(field.form.formId, field.name)\n const descriptionId = `${id}${DESCRIPTION_SUFFIX}`\n const errorId = `${id}${ERROR_SUFFIX}`\n const isErrorDue = isErrorDueFor(field)\n const errors = errorsOf(field)\n const isErrorNamed = isErrorDue && errors.length > 0\n const hasDescription = options.description !== undefined && options.description !== ''\n\n return {\n control: {\n id,\n name: field.name,\n onBlur: field.handleBlur,\n 'aria-invalid': isErrorDue || undefined,\n 'aria-describedby': idrefsOf([\n hasDescription ? descriptionId : undefined,\n isErrorNamed ? errorId : undefined,\n ]),\n 'aria-errormessage': isErrorNamed ? errorId : undefined,\n },\n descriptionId,\n errorId,\n isErrorDue,\n errors,\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["useSelector","Fragment","useRef","getBy","createContext","use","revalidateLogic","createFormHook","createElement"],"sources":["../src/widget/declares-empty-value.ts","../src/render/array-entry.tsx","../src/render/by-name.ts","../src/render/child-path.ts","../src/render/derived.tsx","../src/render/path-pattern.ts","../src/render/required-paths.ts","../src/render/split-entry.ts","../src/render/warn-once.ts","../src/render/fields.tsx","../src/form/find-required-paths.ts","../src/form/blur-then-change.ts","../src/form/form-options-for.ts","../src/form/create-form.tsx","../src/registry/create-registry.ts","../src/widget/errors-of.ts","../src/widget/field-id.ts","../src/widget/field-parts.ts"],"sourcesContent":["/** Whether `widget` declares its empty value, the value it holds while nothing is entered. */\nexport function declaresEmptyValue(widget: object): widget is { emptyValue: unknown } {\n return Object.hasOwn(widget, 'emptyValue')\n}\n","import { type AnyFieldApi, useSelector } from '@tanstack/react-form'\nimport { type ComponentType, Fragment, type ReactNode } from 'react'\n\n// biome-ignore lint/suspicious/noExplicitAny: a registered frame takes props of its own; the config types check them.\ntype AnyProps = any\n\n/** The keys of an array entry the renderer reads besides its item. */\ntype ArrayOptions = {\n newItem?: unknown\n addLabel?: unknown\n removeLabel?: unknown\n itemKey?: unknown\n}\n\ntype ArrayEntryProps = {\n /** The array field, mounted at the entry's path. */\n field: AnyFieldApi\n options: ArrayOptions\n /** The registered `structure.arrayFrame`, if any. */\n frame: ComponentType<AnyProps> | undefined\n /** The entry's props for the frame. */\n props: object | undefined\n renderItem: (index: number, extra?: object) => ReactNode\n}\n\ntype ItemFunction<TResult> = (item: unknown, index?: number) => TResult\n\n/** What the controls of an array entry show: one key and one remove label per item. */\ntype ItemControls = {\n keys: (string | number)[]\n removeLabels: string[]\n}\n\nconst removeLabelOf = (removeLabel: unknown, item: unknown, index: number): string =>\n typeof removeLabel === 'function'\n ? (removeLabel as ItemFunction<string>)(item, index)\n : String(removeLabel)\n\nconst itemControlsOf = (options: ArrayOptions, value: unknown): ItemControls => {\n const items: unknown[] = Array.isArray(value) ? value : []\n\n return {\n keys: items.map((item, index) =>\n typeof options.itemKey === 'function'\n ? (options.itemKey as ItemFunction<string | number>)(item)\n : index,\n ),\n removeLabels: items.map((item, index) => removeLabelOf(options.removeLabel, item, index)),\n }\n}\n\nconst isSameList = <TItem,>(first: readonly TItem[], second: readonly TItem[]): boolean =>\n first.length === second.length && first.every((item, index) => item === second[index])\n\nconst isSameControls = (first: ItemControls, second: ItemControls): boolean =>\n isSameList(first.keys, second.keys) && isSameList(first.removeLabels, second.removeLabels)\n\n/**\n * Renders an array entry: through the registered frame, which receives `ArrayFrameProps`, or as\n * the bare default, each item followed by its remove button and one add button at the end.\n */\nexport const ArrayEntry = ({\n field,\n options,\n frame: Frame,\n props,\n renderItem,\n}: ArrayEntryProps): ReactNode => {\n // The field renders in TanStack's array mode, so only a change to the list reaches it; keys and\n // labels that name an item follow the items through a selection of their own.\n const { keys, removeLabels } = useSelector(\n field.store,\n (state: { value: unknown }) => itemControlsOf(options, state.value),\n { compare: isSameControls },\n )\n const newItem = options.newItem as () => unknown\n const addLabel = String(options.addLabel)\n const add = (): void => field.pushValue(newItem())\n const remove = (index: number): void => field.removeValue(index)\n\n if (Frame === undefined) {\n return (\n <>\n {keys.map((key, index) => (\n <Fragment key={key}>\n {renderItem(index)}\n <button type=\"button\" data-formdef-remove=\"\" onClick={() => remove(index)}>\n {removeLabels[index]}\n </button>\n </Fragment>\n ))}\n <button type=\"button\" data-formdef-add=\"\" onClick={add}>\n {addLabel}\n </button>\n </>\n )\n }\n\n return (\n <Frame\n {...props}\n field={field}\n keys={keys}\n renderItem={renderItem}\n addLabel={addLabel}\n removeLabels={removeLabels}\n add={add}\n remove={remove}\n insert={(index: number, item?: unknown) =>\n field.insertValue(index, item === undefined ? newItem() : item)\n }\n move={(from: number, to: number) => field.moveValue(from, to)}\n swap={(first: number, second: number) => field.swapValues(first, second)}\n replace={(index: number, item: unknown) => field.replaceValue(index, item)}\n clear={() => field.clearValues()}\n />\n )\n}\n","/** What `map` holds under `name` itself; a name every object inherits finds nothing. */\nexport function byName<TValue>(\n map: Readonly<Record<string, TValue>>,\n name: string,\n): TValue | undefined {\n return Object.hasOwn(map, name) ? map[name] : undefined\n}\n","/**\n * The path of `key` below `parent`: a key of the root level by itself, a key of a nested level\n * after a dot, the index of an array item in brackets.\n */\nexport function childPath(parent: string | undefined, key: string | number): string {\n if (typeof key === 'number') {\n return `${parent ?? ''}[${key}]`\n }\n\n return parent === undefined ? key : `${parent}.${key}`\n}\n","import { getBy } from '@tanstack/react-form'\nimport { type ReactNode, useRef } from 'react'\n\nimport type { RenderForm } from './render-form'\n\n/** What an entry reads from the form values: its condition, its props and the paths they read. */\ntype DerivedEntry = {\n visibleWhen?: unknown\n props?: unknown\n dependsOn?: unknown\n}\n\ntype DerivedProps = {\n form: RenderForm\n entry: DerivedEntry\n /** Builds the entry from its props; called only while the entry is visible. */\n children: (props: object | undefined) => ReactNode\n}\n\ntype LastRead = {\n entry: DerivedEntry\n dependencies: unknown[]\n selected: unknown\n}\n\nconst HIDDEN = Symbol('hidden')\nconst VISIBLE = Symbol('visible')\n\nconst propsOf = (props: unknown): object | undefined =>\n typeof props === 'object' && props !== null ? props : undefined\n\nconst isStale = (\n last: LastRead | undefined,\n entry: DerivedEntry,\n dependencies: unknown[],\n): boolean =>\n last === undefined ||\n last.entry !== entry ||\n dependencies.some((value, index) => !Object.is(value, last.dependencies[index]))\n\n/**\n * Renders an entry from what it reads from the form values. An entry with a condition or derived\n * props subscribes to the values; the selected result alone decides whether it re-renders: whether\n * it is shown for a condition, the values for derived props. `dependsOn` narrows when the selector\n * reads again, never what it returns. A hidden entry is never built.\n */\nexport const Derived = ({ form, entry, children }: DerivedProps): ReactNode => {\n const lastRead = useRef<LastRead | undefined>(undefined)\n const { visibleWhen, props, dependsOn } = entry\n const isConditional = typeof visibleWhen === 'function'\n const isDerived = typeof props === 'function'\n\n if (!(isConditional || isDerived)) {\n return children(propsOf(props))\n }\n\n const selectFrom = (values: unknown): unknown => {\n if (isConditional && !visibleWhen(values)) {\n return HIDDEN\n }\n\n return isDerived ? values : VISIBLE\n }\n\n const select = ({ values }: { values: unknown }): unknown => {\n if (!Array.isArray(dependsOn)) {\n return selectFrom(values)\n }\n\n const dependencies = dependsOn.map((path: string) => getBy(values, path))\n if (isStale(lastRead.current, entry, dependencies)) {\n lastRead.current = { entry, dependencies, selected: selectFrom(values) }\n }\n\n return lastRead.current?.selected\n }\n\n return (\n <form.Subscribe selector={select}>\n {(selected) => {\n if (selected === HIDDEN) {\n return null\n }\n\n return children(propsOf(isDerived ? props(selected) : props))\n }}\n </form.Subscribe>\n )\n}\n","const ITEM_INDEX = /\\[\\d+\\]/g\nconst ANY_ITEM = '[]'\n\n/** The path with every array index left open: `contacts[2].name` becomes `contacts[].name`. */\nexport function pathPattern(path: string): string {\n return path.replaceAll(ITEM_INDEX, ANY_ITEM)\n}\n","import { type Context, createContext } from 'react'\n\nexport const NO_REQUIRED_PATHS: ReadonlySet<string> = new Set()\n\n/** The path patterns of the required fields of the level being rendered. */\nexport const RequiredPaths: Context<ReadonlySet<string>> = createContext(NO_REQUIRED_PATHS)\n","/**\n * The keys formdef reads from a field entry. Every other key is one of TanStack's field options and\n * goes to the field unchanged, so an option TanStack adds works without a formdef release.\n */\nexport const OWN_ENTRY_KEYS = [\n 'widget',\n 'props',\n 'fields',\n 'layout',\n 'item',\n 'newItem',\n 'addLabel',\n 'removeLabel',\n 'itemKey',\n 'render',\n 'visibleWhen',\n 'dependsOn',\n] as const\n\n/** A key formdef reads from a field entry. */\nexport type OwnEntryKey = (typeof OWN_ENTRY_KEYS)[number]\n\nconst OWN_ENTRY_KEY_SET: ReadonlySet<string> = new Set(OWN_ENTRY_KEYS)\n\n/** Splits a field entry into the keys formdef reads and TanStack's field options. */\nexport function splitEntry(entry: object): {\n own: Partial<Record<OwnEntryKey, unknown>>\n fieldOptions: Record<string, unknown>\n} {\n const entries = Object.entries(entry)\n\n // `fromEntries` defines each key as data, so a key named `__proto__` cannot set a prototype.\n return {\n own: Object.fromEntries(entries.filter(([key]) => OWN_ENTRY_KEY_SET.has(key))),\n fieldOptions: Object.fromEntries(entries.filter(([key]) => !OWN_ENTRY_KEY_SET.has(key))),\n }\n}\n","// Bundlers replace `process.env.NODE_ENV` in exactly this spelling; without one, `process` may not\n// exist at all.\n// biome-ignore lint/style/useNamingConvention: the name bundlers replace.\ndeclare const process: { env: { NODE_ENV?: string } } | undefined\n\nconst warnedOwners = new WeakSet<object>()\n\nconst isProduction = (): boolean =>\n typeof process !== 'undefined' && process.env.NODE_ENV === 'production'\n\n/** Warns once per `owner`, and never in production. */\nexport function warnOnce(owner: object, message: string): void {\n if (isProduction() || warnedOwners.has(owner)) {\n return\n }\n\n warnedOwners.add(owner)\n console.warn(message)\n}\n","import { type ComponentType, type ReactNode, use } from 'react'\n\nimport { declaresEmptyValue } from '../widget/declares-empty-value'\n\nimport { ArrayEntry } from './array-entry'\nimport { byName } from './by-name'\nimport { childPath } from './child-path'\nimport { Derived } from './derived'\nimport { pathPattern } from './path-pattern'\nimport type { AppFieldProps, RenderForm } from './render-form'\nimport { NO_REQUIRED_PATHS, RequiredPaths } from './required-paths'\nimport { splitEntry } from './split-entry'\nimport { warnOnce } from './warn-once'\n\n// biome-ignore lint/suspicious/noExplicitAny: registered components take props of their own; the config types check them.\ntype AnyProps = any\n\ntype ComponentMap = Readonly<Record<string, ComponentType<AnyProps>>>\n\n/** The maps of a registry the renderer looks names up in. */\ntype RenderParts = {\n widgets: ComponentMap\n formComponents: ComponentMap\n structure: {\n section?: ComponentType<AnyProps>\n row?: ComponentType<AnyProps>\n arrayFrame?: ComponentType<AnyProps>\n }\n}\n\ntype LevelFields = Readonly<Record<string, object>>\n\ntype LayoutNodeEntry = string | Readonly<Record<string, unknown>>\n\n/** The props of {@link Fields}. */\nexport type FieldsProps = {\n parts: RenderParts\n form: RenderForm\n fields: LevelFields\n layout?: readonly unknown[] | undefined\n /** The path patterns of the required fields; none unless `createForm` renders the level. */\n requiredPaths?: ReadonlySet<string> | undefined\n}\n\ntype Level = Omit<FieldsProps, 'layout'> & {\n path: string | undefined\n /** Props an array frame passes the one widget of an item, through this level. */\n extra?: object | undefined\n}\n\ntype LevelProps = Level & {\n layout: readonly unknown[] | undefined\n}\n\ntype EntryProps = Omit<Level, 'fields'> & {\n entry: object\n path: string\n}\n\ntype FieldContentProps = Omit<EntryProps, 'entry'> & {\n entry: ReturnType<typeof splitEntry> & { source: object }\n props: object | undefined\n}\n\ntype ItemProps = Omit<EntryProps, 'extra'> & {\n index: number\n extra: object | undefined\n}\n\ntype LayoutNodeProps = {\n level: Level\n entry: LayoutNodeEntry\n}\n\ntype LayoutKindProps = {\n level: Level\n entry: Readonly<Record<string, unknown>>\n props: object | undefined\n}\n\nconst ARRAY_MODE = 'array'\n\nconst DROPPED_EXTRA = 'passes extra props to an item that has no single widget to receive them'\n\nconst renderNothing = (): ReactNode => null\n\nconst layoutOwner = (path: string | undefined): string =>\n path === undefined ? 'the form' : `\"${path}\"`\n\nconst isLeaf = (entry: unknown): boolean =>\n typeof entry === 'object' && entry !== null && 'widget' in entry\n\n// A frame's extra props have a single target only in a leaf, or an object level whose one field\n// is a leaf and which has no layout.\nconst takesExtra = (item: object): boolean => {\n if (isLeaf(item)) {\n return true\n }\n const { own } = splitEntry(item)\n const fields =\n typeof own.fields === 'object' && own.fields !== null ? Object.values(own.fields) : []\n\n return own.layout === undefined && fields.length === 1 && isLeaf(fields[0])\n}\n\nconst Item = ({ parts, form, entry, path, index, extra }: ItemProps): ReactNode => {\n const { own } = splitEntry(entry)\n const item = own.item as object\n const reachesWidget = extra !== undefined && takesExtra(item)\n if (extra !== undefined && !reachesWidget) {\n warnOnce(entry, `formdef: the array frame of \"${path}\" ${DROPPED_EXTRA}; they are dropped.`)\n }\n\n return (\n <Entry\n parts={parts}\n form={form}\n entry={item}\n path={childPath(path, index)}\n extra={reachesWidget ? extra : undefined}\n />\n )\n}\n\nconst FieldContent = ({ parts, form, entry, path, props, extra }: FieldContentProps): ReactNode => {\n const { own, fieldOptions } = entry\n const requiredPaths = use(RequiredPaths)\n\n if (typeof own.widget === 'string') {\n const Widget = byName(parts.widgets, own.widget)\n if (Widget === undefined) {\n throw new Error(`formdef: field \"${path}\" names the unregistered widget \"${own.widget}\".`)\n }\n\n // The config's `required` comes after the found one, so it wins.\n const found = declaresEmptyValue(Widget)\n ? { required: requiredPaths.has(pathPattern(path)) }\n : undefined\n\n return (\n <form.AppField {...fieldOptions} name={path}>\n {(field) => <Widget {...found} {...props} {...extra} field={field} />}\n </form.AppField>\n )\n }\n\n if (typeof own.render === 'function') {\n return (\n <form.AppField {...fieldOptions} name={path}>\n {own.render as AppFieldProps['children']}\n </form.AppField>\n )\n }\n\n if (typeof own.item === 'object' && own.item !== null) {\n return (\n <form.AppField {...fieldOptions} name={path} mode={ARRAY_MODE}>\n {(field) => (\n <ArrayEntry\n field={field}\n options={own}\n frame={parts.structure.arrayFrame}\n props={props}\n renderItem={(index, itemExtra) => (\n <Item\n parts={parts}\n form={form}\n entry={entry.source}\n path={path}\n index={index}\n extra={itemExtra}\n />\n )}\n />\n )}\n </form.AppField>\n )\n }\n\n if (typeof own.fields !== 'object' || own.fields === null) {\n return null\n }\n\n return (\n <>\n {Object.keys(fieldOptions).length > 0 && (\n <form.AppField {...fieldOptions} name={path}>\n {renderNothing}\n </form.AppField>\n )}\n <LevelEntries\n parts={parts}\n form={form}\n fields={own.fields as LevelFields}\n layout={Array.isArray(own.layout) ? own.layout : undefined}\n path={path}\n extra={extra}\n />\n </>\n )\n}\n\nconst Entry = ({ parts, form, entry, path, extra }: EntryProps): ReactNode => {\n const split = { ...splitEntry(entry), source: entry }\n\n return (\n <Derived form={form} entry={split.own}>\n {(props) => (\n <FieldContent\n parts={parts}\n form={form}\n entry={split}\n path={path}\n props={props}\n extra={extra}\n />\n )}\n </Derived>\n )\n}\n\nconst FieldNode = ({ level, name }: { level: Level; name: string }): ReactNode => {\n const entry = byName(level.fields, name)\n if (entry === undefined) {\n throw new Error(\n `formdef: the layout of ${layoutOwner(level.path)} names \"${name}\", which the level does not describe.`,\n )\n }\n\n return (\n <Entry\n parts={level.parts}\n form={level.form}\n entry={entry}\n path={childPath(level.path, name)}\n extra={level.extra}\n />\n )\n}\n\nconst LayoutNodes = ({ level, entries }: { level: Level; entries: unknown }): ReactNode =>\n (Array.isArray(entries) ? (entries as LayoutNodeEntry[]) : []).map((entry, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: a layout comes from the config and never reorders; a name may appear twice.\n <LayoutNode key={index} level={level} entry={entry} />\n ))\n\nconst SectionNode = ({ level, entry, props }: LayoutKindProps): ReactNode => {\n const children = <LayoutNodes level={level} entries={entry.layout} />\n const Section = level.parts.structure.section\n\n if (Section !== undefined) {\n return (\n <Section {...props} label={entry.section}>\n {children}\n </Section>\n )\n }\n\n return (\n <fieldset {...props}>\n <legend>{entry.section as string}</legend>\n {children}\n </fieldset>\n )\n}\n\nconst RowNode = ({ level, entry, props }: LayoutKindProps): ReactNode => {\n const children = <LayoutNodes level={level} entries={entry.row} />\n const Row = level.parts.structure.row\n\n if (Row !== undefined) {\n return <Row {...props}>{children}</Row>\n }\n\n return (\n <div {...props} data-formdef-row=\"\">\n {children}\n </div>\n )\n}\n\nconst ComponentNode = ({ level, entry, props }: LayoutKindProps): ReactNode => {\n const name = entry.component as string\n const Component = byName(level.parts.formComponents, name)\n if (Component === undefined) {\n throw new Error(\n `formdef: the layout of ${layoutOwner(level.path)} places the unregistered form component \"${name}\".`,\n )\n }\n\n return <Component {...props} />\n}\n\nconst LayoutKind = ({ level, entry, props }: LayoutKindProps): ReactNode => {\n if ('section' in entry) {\n return <SectionNode level={level} entry={entry} props={props} />\n }\n if ('row' in entry) {\n return <RowNode level={level} entry={entry} props={props} />\n }\n if (typeof entry.component === 'string') {\n return <ComponentNode level={level} entry={entry} props={props} />\n }\n if (typeof entry.render === 'function') {\n return (entry.render as (form: RenderForm) => ReactNode)(level.form)\n }\n\n return null\n}\n\nconst LayoutNode = ({ level, entry }: LayoutNodeProps): ReactNode => {\n if (typeof entry === 'string') {\n return <FieldNode level={level} name={entry} />\n }\n\n return (\n <Derived form={level.form} entry={entry}>\n {(props) => <LayoutKind level={level} entry={entry} props={props} />}\n </Derived>\n )\n}\n\nconst LevelEntries = ({ layout, ...level }: LevelProps): ReactNode => {\n if (layout !== undefined) {\n return <LayoutNodes level={level} entries={layout} />\n }\n\n return Object.keys(level.fields).map((name) => <FieldNode key={name} level={level} name={name} />)\n}\n\n/**\n * Renders the field entries of a level with the widgets of `parts`. Without a `layout` the entries\n * appear in the order `fields` declares them; with one, the layout decides the order, groups them\n * into sections and rows and places form components and inline entries. A leaf renders as its\n * widget, an object as its own level, a custom entry through its `render`, an array through the\n * registered `structure.arrayFrame` or the bare default. An object entry with TanStack options of\n * its own also mounts a field at its path, so its validators run. A `Fields` nested in a form finds\n * no required fields of its own, since its paths may be relative to a field group.\n */\nexport const Fields = ({ parts, form, fields, layout, requiredPaths }: FieldsProps): ReactNode => (\n <RequiredPaths value={requiredPaths ?? NO_REQUIRED_PATHS}>\n <LevelEntries parts={parts} form={form} fields={fields} layout={layout} path={undefined} />\n </RequiredPaths>\n)\n","import type { StandardSchemaV1 } from '@tanstack/react-form'\n\nimport { byName } from '../render/by-name'\nimport { childPath } from '../render/child-path'\nimport { pathPattern } from '../render/path-pattern'\nimport { splitEntry } from '../render/split-entry'\nimport { declaresEmptyValue } from '../widget/declares-empty-value'\n\ntype LevelFields = Readonly<Record<string, object>>\n\ntype Widgets = Readonly<Record<string, object>>\n\ntype RequiredPathsInput = {\n schema: unknown\n /** Whether the config validates the schema asynchronously (`validation.async`). */\n validatesAsync: boolean\n fields: LevelFields\n widgets: Widgets\n}\n\n/** The path patterns of the required fields, or why none could be found. */\ntype RequiredPathsResult = { paths: ReadonlySet<string> } | { problem: string }\n\ntype EmptyValues = {\n widgets: Widgets\n /** The path patterns of the leaves whose widget declares an empty value. */\n declared: Set<string>\n}\n\ntype Validation = ReturnType<StandardSchemaV1['~standard']['validate']>\ntype Issue = NonNullable<Exclude<Validation, Promise<unknown>>['issues']>[number]\n\nconst NONE: RequiredPathsResult = { paths: new Set() }\n\nconst ASYNC_SCHEMA =\n 'formdef: the schema validates asynchronously, so no field is found to be required; pass required in props.'\nconst UNSETTLED_SCHEMA =\n 'formdef: the schema did not validate empty values synchronously, so no field is found to be required; pass required in props.'\nconst THROWING_SCHEMA =\n 'formdef: the schema threw on empty values, so no field is found to be required'\n\nconst FIRST_ITEM = 0\n\nconst isStandardSchema = (schema: unknown): schema is StandardSchemaV1 =>\n typeof schema === 'object' && schema !== null && '~standard' in schema\n\nconst emptyLevel = (fields: LevelFields, path: string | undefined, empty: EmptyValues): object =>\n Object.fromEntries(\n Object.entries(fields).map(([key, entry]) => [\n key,\n emptyEntry(entry, childPath(path, key), empty),\n ]),\n )\n\nfunction emptyEntry(entry: object, path: string, empty: EmptyValues): unknown {\n const { own } = splitEntry(entry)\n\n if (typeof own.widget === 'string') {\n const widget = byName(empty.widgets, own.widget)\n if (widget === undefined || !declaresEmptyValue(widget)) {\n return undefined\n }\n empty.declared.add(pathPattern(path))\n\n return widget.emptyValue\n }\n if (typeof own.item === 'object' && own.item !== null) {\n return [emptyEntry(own.item, childPath(path, FIRST_ITEM), empty)]\n }\n if (typeof own.fields === 'object' && own.fields !== null) {\n return emptyLevel(own.fields as LevelFields, path, empty)\n }\n\n return undefined\n}\n\nconst issuePath = (issue: Issue): string =>\n pathPattern(\n (issue.path ?? []).reduce<string | undefined>((parent, segment) => {\n const key = typeof segment === 'object' ? segment.key : segment\n\n return childPath(parent, typeof key === 'number' ? key : String(key))\n }, undefined) ?? '',\n )\n\nconst parentOf = (path: string): string =>\n path.slice(0, Math.max(path.lastIndexOf('.'), path.lastIndexOf('[')))\n\n// A leaf's value may be an object or a list, so an issue below a leaf's path is the leaf's.\nconst leafOf = (path: string, leaves: ReadonlySet<string>): string | undefined => {\n for (let candidate = path; candidate !== ''; candidate = parentOf(candidate)) {\n if (leaves.has(candidate)) {\n return candidate\n }\n }\n\n return undefined\n}\n\n// The promise is dropped unread; a rejection is the schema failing on empty values, not the form.\nconst ignoreRejection = (): undefined => undefined\n\nconst errorText = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\n/**\n * Finds the required fields of a config: validates `schema` once, synchronously, with every leaf\n * holding its widget's empty value and every array one such item. A leaf whose widget declares an\n * empty value and whose path receives an issue is required. A schema the config validates\n * asynchronously is not run, so its asynchronous checks send nothing.\n */\nexport function findRequiredPaths({\n schema,\n validatesAsync,\n fields,\n widgets,\n}: RequiredPathsInput): RequiredPathsResult {\n if (!isStandardSchema(schema)) {\n return NONE\n }\n if (validatesAsync) {\n return { problem: ASYNC_SCHEMA }\n }\n\n const empty: EmptyValues = { widgets, declared: new Set() }\n const values = emptyLevel(fields, undefined, empty)\n let result: Validation\n try {\n result = schema['~standard'].validate(values)\n } catch (error) {\n return { problem: `${THROWING_SCHEMA}: ${errorText(error)}` }\n }\n if (result instanceof Promise) {\n result.then(undefined, ignoreRejection)\n\n return { problem: UNSETTLED_SCHEMA }\n }\n\n const leaves = (result.issues ?? []).map((issue) => leafOf(issuePath(issue), empty.declared))\n\n return { paths: new Set(leaves.filter((leaf) => leaf !== undefined)) }\n}\n","import {\n defaultValidationLogic,\n type ValidationLogicFn,\n type ValidationLogicProps,\n} from '@tanstack/react-form'\n\ntype ScheduledValidators = Parameters<ValidationLogicProps['runValidation']>[0]['validators']\n\n// Gated on the form, not the changed field: the schema validates the whole form, and TanStack names\n// the changed field only on the field-level call, where the schema does not sit.\nconst isSchemaDue = ({ event, form }: ValidationLogicProps): boolean => {\n if (event.type === 'blur' || event.type === 'submit') {\n return true\n }\n\n return event.type === 'change' && !form.state.isValid\n}\n\n/**\n * The validation logic of the `blurThenChange` mode: TanStack's default logic, plus the schema in\n * `onDynamic` when a field is left, on submit, and on every change while the form is invalid.\n */\nexport const blurThenChange: ValidationLogicFn = (props) => {\n let defaults: ScheduledValidators = []\n defaultValidationLogic({\n ...props,\n runValidation: ({ validators }) => {\n defaults = validators\n },\n })\n\n const schema = props.event.async ? props.validators?.onDynamicAsync : props.validators?.onDynamic\n if (schema === undefined || !isSchemaDue(props)) {\n return props.runValidation({ validators: defaults, form: props.form })\n }\n\n return props.runValidation({\n validators: [...defaults, { fn: schema, cause: 'dynamic' }],\n form: props.form,\n })\n}\n","import { revalidateLogic, type ValidationLogicFn } from '@tanstack/react-form'\n\nimport type { Validation } from '../config/validation'\nimport { blurThenChange } from './blur-then-change'\n\ntype Mode = NonNullable<Validation['mode']>\ntype DynamicMode = 'revalidate' | 'blurThenChange'\ntype Slot = 'onDynamic' | 'onChange' | 'onBlur' | 'onSubmit'\n\ntype Options = Readonly<Record<string, unknown>>\n\ntype FormOptionsInput = {\n /** The form config: the schema, formdef's own keys and TanStack's form options. */\n config: Options\n /** The instance's TanStack form options, from the form component's `options` prop. */\n options: Options | undefined\n}\n\nconst DEFAULT_MODE: Mode = 'revalidate'\n\nconst isDynamic = (mode: Mode): mode is DynamicMode =>\n mode === 'revalidate' || mode === 'blurThenChange'\n\nconst logicFor = (mode: DynamicMode): ValidationLogicFn =>\n mode === 'revalidate' ? revalidateLogic() : blurThenChange\n\nconst slotFor = (mode: Mode, isAsync: boolean): Slot | `${Slot}Async` => {\n const slot: Slot = isDynamic(mode) ? 'onDynamic' : mode\n\n return isAsync ? `${slot}Async` : slot\n}\n\nconst recordOf = (value: unknown): Options =>\n typeof value === 'object' && value !== null ? (value as Options) : {}\n\n// An instance passing `undefined`, such as data still loading, leaves the config's value in place.\nconst setEntriesOf = (value: unknown): Options =>\n Object.fromEntries(Object.entries(recordOf(value)).filter(([, entry]) => entry !== undefined))\n\n/**\n * The TanStack form options of a form: the config's options and the instance's merged key by key,\n * the instance winning, except `validators` and `listeners`, which merge slot by slot; a key or\n * slot the instance sets to `undefined` counts as unset. The schema\n * goes into the slot `validation` picks unless a validator already fills it; a dynamic mode brings\n * its validation logic, an asynchronous schema its debounce unless the options set one.\n */\nexport function formOptionsFor({ config, options }: FormOptionsInput): Record<string, unknown> {\n const {\n schema,\n validation,\n fields: _renderedByFields,\n layout: _arrangedByFields,\n ...configOptions\n } = config\n const { mode = DEFAULT_MODE, ...timing } = recordOf(validation) as Validation\n const isAsync = timing.async === true\n const slot = slotFor(mode, isAsync)\n const instanceOptions = setEntriesOf(options)\n const validators = {\n ...recordOf(configOptions.validators),\n ...setEntriesOf(instanceOptions.validators),\n }\n\n return {\n ...(isDynamic(mode) && { validationLogic: logicFor(mode) }),\n ...(isAsync &&\n timing.asyncDebounceMs !== undefined && { asyncDebounceMs: timing.asyncDebounceMs }),\n ...configOptions,\n ...instanceOptions,\n validators: validators[slot] === undefined ? { ...validators, [slot]: schema } : validators,\n listeners: { ...recordOf(configOptions.listeners), ...setEntriesOf(instanceOptions.listeners) },\n }\n}\n","import type { ComponentType, FormEvent, ReactNode } from 'react'\n\nimport type { FormElementProps } from '../config/form-props'\nimport type { BodyProps } from '../config/structure'\nimport { Fields, type FieldsProps } from '../render/fields'\nimport type { RenderForm } from '../render/render-form'\nimport { warnOnce } from '../render/warn-once'\nimport { findRequiredPaths } from './find-required-paths'\nimport { formOptionsFor } from './form-options-for'\n\ntype Options = Readonly<Record<string, unknown>>\n\n/** The form `useAppForm` returns, as far as the shell uses it. */\ntype ShellForm = RenderForm & {\n AppForm: ComponentType<{ children?: ReactNode }>\n handleSubmit: () => Promise<void>\n}\n\n/** What a registry binds a form to: its form hook and the maps the renderer looks names up in. */\nexport type FormBinding = {\n useAppForm: (options: Options) => ShellForm\n parts: FieldsProps['parts'] & {\n structure: { body?: ComponentType<BodyProps> }\n }\n}\n\n/** The props of a form component: the instance's options and what a `<form>` element takes. */\ntype FormComponentProps = FormElementProps & {\n options?: Options\n}\n\n/** A form config: TanStack's form options plus formdef's own keys. */\ntype ConfigInput = Options & {\n fields?: unknown\n layout?: unknown\n}\n\ntype ConfiguredFormProps = FormComponentProps & {\n binding: FormBinding\n config: ConfigInput\n requiredPaths: ReadonlySet<string>\n}\n\nconst MISSING_DEFAULT_VALUES =\n 'formdef: the form has no default values; set defaultValues in its config or in its options.'\n\nconst ConfiguredForm = ({\n binding,\n config,\n requiredPaths,\n options,\n ...formProps\n}: ConfiguredFormProps): ReactNode => {\n const formOptions = formOptionsFor({ config, options })\n if (formOptions.defaultValues === undefined) {\n throw new Error(MISSING_DEFAULT_VALUES)\n }\n\n const form = binding.useAppForm(formOptions)\n const Body = binding.parts.structure.body\n const content = (\n <Fields\n parts={binding.parts}\n form={form}\n fields={config.fields as FieldsProps['fields']}\n layout={config.layout as FieldsProps['layout']}\n requiredPaths={requiredPaths}\n />\n )\n const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {\n event.preventDefault()\n event.stopPropagation()\n void form.handleSubmit()\n }\n\n return (\n <form.AppForm>\n <form noValidate {...formProps} onSubmit={handleSubmit}>\n {Body === undefined ? content : <Body>{content}</Body>}\n </form>\n </form.AppForm>\n )\n}\n\nconst isAsyncValidation = (validation: unknown): boolean =>\n typeof validation === 'object' && validation !== null && 'async' in validation\n ? validation.async === true\n : false\n\nconst requiredPathsOf = (binding: FormBinding, config: ConfigInput): ReadonlySet<string> => {\n const result = findRequiredPaths({\n schema: config.schema,\n validatesAsync: isAsyncValidation(config.validation),\n fields: (config.fields ?? {}) as Readonly<Record<string, object>>,\n widgets: binding.parts.widgets,\n })\n if ('problem' in result) {\n warnOnce(config, result.problem)\n\n return new Set()\n }\n\n return result.paths\n}\n\n/**\n * Turns a form config into a form component bound to `binding`: TanStack's form with the merged\n * options, a `<form>` element that submits through it, the registered body and the config's fields.\n * When it first renders, it finds the config's required fields once.\n */\nexport function createForm(\n binding: FormBinding,\n config: ConfigInput,\n): (props: FormComponentProps) => ReactNode {\n let requiredPaths: ReadonlySet<string> | undefined\n\n return (props: FormComponentProps): ReactNode => {\n requiredPaths ??= requiredPathsOf(binding, config)\n\n return (\n <ConfiguredForm {...props} binding={binding} config={config} requiredPaths={requiredPaths} />\n )\n }\n}\n","import { type AnyFieldApi, type AnyFormApi, createFormHook } from '@tanstack/react-form'\nimport { type Context, createElement, type ReactNode } from 'react'\n\nimport { createForm, type FormBinding } from '../form/create-form'\nimport { Fields, type FieldsProps } from '../render/fields'\n\nimport type {\n CheckedStructure,\n CheckedWidgets,\n Components,\n NewFormComponents,\n NewStructure,\n NewWidgets,\n Registry,\n Structure,\n} from './registry'\n\ntype Contexts = {\n fieldContext: Context<AnyFieldApi>\n formContext: Context<AnyFormApi>\n}\n\ntype RegistryParts<TWidgets, TFormComponents, TStructure> = Contexts & {\n widgets: TWidgets\n formComponents: TFormComponents\n structure: TStructure\n}\n\nfunction buildRegistry<\n TWidgets extends Components,\n TFormComponents extends Components,\n TStructure extends Structure,\n>(\n parts: RegistryParts<TWidgets, TFormComponents, TStructure>,\n): Registry<TWidgets, TFormComponents, TStructure> {\n const { extendForm: _replacedByExtendRegistry, ...formHook } = createFormHook({\n fieldContext: parts.fieldContext,\n formContext: parts.formContext,\n fieldComponents: parts.widgets,\n formComponents: parts.formComponents,\n })\n\n function extendRegistry<\n const TNewWidgets extends NewWidgets<TWidgets> = Record<never, never>,\n const TNewFormComponents extends NewFormComponents<TFormComponents> = Record<never, never>,\n const TNewStructure extends NewStructure<TStructure> = Record<never, never>,\n >(extension: {\n widgets?: TNewWidgets\n formComponents?: TNewFormComponents\n structure?: TNewStructure & CheckedStructure<TNewStructure>\n }): Registry<\n TWidgets & TNewWidgets,\n TFormComponents & TNewFormComponents,\n TStructure & TNewStructure\n > {\n const addedWidgets: Components = extension.widgets ?? {}\n const addedStructure: Structure = extension.structure ?? {}\n\n // The extension's maps are optional, and spreading an optional generic map drops its type.\n return buildRegistry({\n fieldContext: parts.fieldContext,\n formContext: parts.formContext,\n widgets: { ...parts.widgets, ...addedWidgets } as TWidgets & TNewWidgets,\n formComponents: { ...parts.formComponents, ...extension.formComponents } as TFormComponents &\n TNewFormComponents,\n structure: { ...parts.structure, ...addedStructure } as TStructure & TNewStructure,\n })\n }\n\n // TanStack's hook is generic over the form's types; the config's types are checked at the\n // registry's `createForm`, so the binding passes the merged options on untyped.\n const binding: FormBinding = {\n useAppForm: formHook.useAppForm as unknown as FormBinding['useAppForm'],\n parts: {\n widgets: parts.widgets,\n formComponents: parts.formComponents,\n structure: parts.structure,\n },\n }\n\n // biome-ignore lint/correctness/noComponentHookFactories: created once per registry, like TanStack's createFormHook.\n const BoundFields = (props: Omit<FieldsProps, 'parts'>): ReactNode =>\n createElement(Fields, { ...props, parts: binding.parts })\n\n return {\n formHook,\n createForm: (config) => createForm(binding, config),\n // The member's signature types the level by the form it is given; the renderer reads it untyped.\n Fields: BoundFields as unknown as Registry<TWidgets, TFormComponents, TStructure>['Fields'],\n // The member's signature checks the widgets' empty values; the implementation merges any map.\n extendRegistry: extendRegistry as Registry<\n TWidgets,\n TFormComponents,\n TStructure\n >['extendRegistry'],\n }\n}\n\n/**\n * Binds widgets, form components and structure slots to the app's TanStack contexts and returns a\n * registry, the value every form of the app is built with.\n *\n * Create the contexts with TanStack's `createFormHookContexts()` in a module of their own, so a\n * form component can read the form through `useFormContext()` without importing the registry.\n *\n * @typeParam TWidgets - The widgets by name; each takes its field as a prop typed `FieldOf<V>`.\n * @typeParam TFormComponents - The form components by name, placed in a layout by `{ component }`.\n * @typeParam TStructure - The structure slots: `body`, `section`, `row` and `arrayFrame`.\n * @param config - The contexts and what the registry holds.\n * @param config.fieldContext - The field context from `createFormHookContexts()`.\n * @param config.formContext - The form context from `createFormHookContexts()`.\n * @param config.widgets - The widgets by name. A widget's static `emptyValue`, the value it holds\n * while nothing is entered, must be of the value type it serves.\n * @param config.formComponents - The form components by name.\n * @param config.structure - The components that render the structure slots.\n * @defaultValue `config.formComponents` and `config.structure`: none registered.\n * @returns The registry.\n *\n * @example\n * ```tsx\n * 'use client'\n * import { createRegistry } from '@formdef/core'\n * import { fieldContext, formContext } from './context'\n *\n * export const registry = createRegistry({\n * fieldContext,\n * formContext,\n * widgets: { text: TextField, checkbox: CheckboxField },\n * formComponents: { SubmitButton },\n * structure: { section: Section },\n * })\n * ```\n */\nexport function createRegistry<\n const TWidgets extends Components,\n const TFormComponents extends Components = Record<never, never>,\n const TStructure extends Structure = Record<never, never>,\n>(\n config: Contexts & {\n widgets: TWidgets & CheckedWidgets<TWidgets>\n formComponents?: TFormComponents\n structure?: TStructure & CheckedStructure<TStructure>\n },\n): Registry<TWidgets, TFormComponents, TStructure> {\n // Both maps fall back to their empty defaults exactly when they are left out.\n return buildRegistry<TWidgets, TFormComponents, TStructure>({\n fieldContext: config.fieldContext,\n formContext: config.formContext,\n widgets: config.widgets,\n formComponents: config.formComponents ?? ({} as TFormComponents),\n structure: config.structure ?? ({} as TStructure),\n })\n}\n","import type { FieldApiOf } from './field-of'\n\nfunction textOf(error: unknown): string | undefined {\n if (typeof error === 'string') {\n return error\n }\n if (typeof error !== 'object' || error === null || !('message' in error)) {\n return undefined\n }\n\n return typeof error.message === 'string' ? error.message : undefined\n}\n\nfunction hasText(text: string | undefined): text is string {\n return text !== undefined && text !== ''\n}\n\n/**\n * Turns the errors of a field into the texts a widget renders.\n *\n * An error is either a string or an object with a `message`, such as an issue of a Standard\n * Schema. Errors without text are left out, and a text that several validators report appears\n * once, where it first occurs. A field's errors stay readable with TanStack's `disableErrorFlat`\n * option.\n *\n * @typeParam TValue - The value type of the field.\n * @param field - The field the widget renders.\n * @returns The distinct texts of the field's current errors, in order; empty when there are none.\n *\n * @example\n * ```tsx\n * const TextField = ({ field }: FieldOf<string>) => (\n * <>\n * <input value={field.state.value} onChange={(event) => field.handleChange(event.target.value)} />\n * <p>{errorsOf(field).join(' ')}</p>\n * </>\n * )\n * ```\n */\nexport function errorsOf<TValue>(field: FieldApiOf<TValue>): string[] {\n const texts = field.state.meta.errors.flat().map(textOf).filter(hasText)\n\n return [...new Set(texts)]\n}\n","const PATH_SEPARATORS = /[.[\\]]+/g\nconst SELECTOR_SAFE_SEPARATOR = '__'\n\n/**\n * Builds the DOM id that `fieldParts` gives the control of a field, for code that has no\n * field at hand — an error summary above the form that links to each invalid field, for example.\n *\n * The id is the form's id, a hyphen, and the field's path with every run of dots and brackets\n * replaced by `__`. Fields of two different forms therefore never share an id, and the id can be\n * used in a CSS selector without escaping.\n *\n * @param formId - The id of the form, as TanStack Form reports it in `form.formId`.\n * @param path - The path of the field, such as `email`, `address.zip` or `contacts[0].name`.\n * @returns The id of the field's control, such as `signup-address__zip`.\n *\n * @remarks\n * The replacement cannot be reversed: a field literally named `a__b` gets the same id as the\n * path `a.b`.\n *\n * @example\n * ```tsx\n * <a href={`#${fieldId(form.formId, 'address.zip')}`}>Postal code</a>\n * ```\n */\nexport function fieldId(formId: string, path: string): string {\n return `${formId}-${path.replace(PATH_SEPARATORS, SELECTOR_SAFE_SEPARATOR)}`\n}\n","import { errorsOf } from './errors-of'\nimport { fieldId } from './field-id'\nimport type { FieldApiOf } from './field-of'\n\nconst DESCRIPTION_SUFFIX = '-description'\nconst ERROR_SUFFIX = '-error'\nconst IDREF_SEPARATOR = ' '\n\n/**\n * What {@link fieldParts} returns: the wiring of a widget's control and what it needs to render\n * its description and its errors.\n */\nexport interface FieldParts {\n /** Props to spread onto the control; its value and change handler stay with the widget. */\n control: {\n /** The control's id, as {@link fieldId} builds it. */\n id: string\n /** The field's path, so the browser can autofill the control. */\n name: string\n /** Reports to the field that the control was left. */\n onBlur: () => void\n /** `true` while an error is due; absent otherwise, never `false`. */\n 'aria-invalid': true | undefined\n /** The ids of the description and of a due error with text, when there are any. */\n 'aria-describedby': string | undefined\n /** The id of the error element, while an error with text is due. */\n 'aria-errormessage': string | undefined\n }\n /** The id to give the element that renders the description. */\n descriptionId: string\n /** The id to give the element that renders the error texts. */\n errorId: string\n /**\n * Whether the widget shows the field's errors now: the field is invalid and was edited and\n * left, or the form was submitted.\n */\n isErrorDue: boolean\n /** The texts of the field's errors, as {@link errorsOf} returns them. */\n errors: string[]\n}\n\nfunction isErrorDueFor<TValue>(field: FieldApiOf<TValue>): boolean {\n const { isBlurred, isDirty, isValid } = field.state.meta\n const wasEditedAndLeft = isBlurred && isDirty\n const wasSubmitted = field.form.state.submissionAttempts > 0\n\n return !isValid && (wasEditedAndLeft || wasSubmitted)\n}\n\nfunction idrefsOf(ids: (string | undefined)[]): string | undefined {\n const present = ids.filter((id) => id !== undefined)\n\n return present.length > 0 ? present.join(IDREF_SEPARATOR) : undefined\n}\n\n/**\n * Wires a widget's control to its field: its id and name, the blur report, and the aria\n * attributes that tie it to its description and its errors.\n *\n * An error is due once the field is invalid and was edited and left, or once the form was\n * submitted. While one is due, `aria-invalid` is `true`; while one with text is due, the control\n * points at the error element through `aria-describedby` and `aria-errormessage`. An id appears in\n * them only when its element is rendered.\n *\n * @typeParam TValue - The value type of the field.\n * @param field - The field the widget renders.\n * @param options - What the widget renders besides the control.\n * @param options.description - The description the widget renders below its label, if any. Only\n * whether it is a non-empty string is read.\n * @defaultValue `options.description`: `undefined`, no description is rendered.\n * @returns The props for the control, the ids for the description and error elements, whether an\n * error is due, and the error texts.\n *\n * @example\n * ```tsx\n * const TextField = ({ field, label, description }: FieldOf<string> & { label: string; description?: string }) => {\n * const { control, descriptionId, errorId, isErrorDue, errors } = fieldParts(field, { description })\n *\n * return (\n * <div>\n * <label htmlFor={control.id}>{label}</label>\n * {description && <p id={descriptionId}>{description}</p>}\n * <input {...control} value={field.state.value} onChange={(event) => field.handleChange(event.target.value)} />\n * {isErrorDue && errors.length > 0 && <p id={errorId}>{errors.join(' ')}</p>}\n * </div>\n * )\n * }\n * ```\n */\nexport function fieldParts<TValue>(\n field: FieldApiOf<TValue>,\n options: { description?: string | undefined } = {},\n): FieldParts {\n const id = fieldId(field.form.formId, field.name)\n const descriptionId = `${id}${DESCRIPTION_SUFFIX}`\n const errorId = `${id}${ERROR_SUFFIX}`\n const isErrorDue = isErrorDueFor(field)\n const errors = errorsOf(field)\n const isErrorNamed = isErrorDue && errors.length > 0\n const hasDescription = options.description !== undefined && options.description !== ''\n\n return {\n control: {\n id,\n name: field.name,\n onBlur: field.handleBlur,\n 'aria-invalid': isErrorDue || undefined,\n 'aria-describedby': idrefsOf([\n hasDescription ? descriptionId : undefined,\n isErrorNamed ? errorId : undefined,\n ]),\n 'aria-errormessage': isErrorNamed ? errorId : undefined,\n },\n descriptionId,\n errorId,\n isErrorDue,\n errors,\n }\n}\n"],"mappings":"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"}
|
package/dist/index.d.cts
CHANGED
|
@@ -605,9 +605,15 @@ type WrongSlotProps = "formdef: this slot does not take the props the renderer p
|
|
|
605
605
|
type DuplicateWidget = "formdef: a widget with this name is already registered";
|
|
606
606
|
type DuplicateFormComponent = "formdef: a form component with this name is already registered";
|
|
607
607
|
type DuplicateSlot = "formdef: this structure slot is already filled";
|
|
608
|
+
type WrongEmptyValue = "formdef: the empty value is no value this widget serves";
|
|
608
609
|
type AcceptsSupplied<TComponent extends ComponentType<AnyProps>, TSupplied> = TSupplied extends Pick<ComponentProps<TComponent>, keyof TSupplied & keyof ComponentProps<TComponent>> ? TComponent : WrongSlotProps;
|
|
609
610
|
/** Checks each slot of `TStructure` against what the renderer passes it. */
|
|
610
611
|
type CheckedStructure<TStructure> = { [Slot in keyof TStructure]: Slot extends keyof SlotSupplied ? TStructure[Slot] extends ComponentType<AnyProps> ? AcceptsSupplied<TStructure[Slot], SlotSupplied[Slot]> : TStructure[Slot] : UnknownSlot; };
|
|
612
|
+
type FitsEmptyValue<TWidget> = TWidget extends {
|
|
613
|
+
emptyValue?: infer TEmpty;
|
|
614
|
+
} ? [TEmpty] extends [ValueOf<TWidget>] ? TWidget : WrongEmptyValue : TWidget;
|
|
615
|
+
/** Checks the `emptyValue` each widget of `TWidgets` declares against the value type it serves. */
|
|
616
|
+
type CheckedWidgets<TWidgets> = { [Name in keyof TWidgets]: "emptyValue" extends keyof TWidgets[Name] ? FitsEmptyValue<TWidgets[Name]> : TWidgets[Name]; };
|
|
611
617
|
type Fresh<TExisting, TMessage extends string> = { [Name in keyof TExisting]?: TMessage; };
|
|
612
618
|
/** Widgets an extension adds: none may take a name the registry already uses. */
|
|
613
619
|
type NewWidgets<TWidgets> = Components & Fresh<TWidgets, DuplicateWidget>;
|
|
@@ -667,6 +673,12 @@ interface Registry<TWidgets extends Components, TFormComponents extends Componen
|
|
|
667
673
|
* The config is typed by its schema; its root `layout` may name only keys `fields` describes. The
|
|
668
674
|
* component takes `FormProps`, with `options.defaultValues` required when the config has none.
|
|
669
675
|
*
|
|
676
|
+
* When the component first renders, it validates the schema once with every leaf holding its
|
|
677
|
+
* widget's `emptyValue`; a widget that declares one receives `required`, true where the schema
|
|
678
|
+
* rejects that value. A `required` in the entry's `props` wins. A config that validates
|
|
679
|
+
* asynchronously (`validation.async`), or a schema that does not validate the empty values
|
|
680
|
+
* synchronously, marks no field and warns once outside production.
|
|
681
|
+
*
|
|
670
682
|
* @typeParam TSchema - The Standard Schema of the form, inferred from `schema`.
|
|
671
683
|
* @typeParam TConfig - The config as written, inferred.
|
|
672
684
|
* @typeParam TSubmitMeta - What `handleSubmit` takes besides the values, inferred from
|
|
@@ -738,14 +750,15 @@ interface Registry<TWidgets extends Components, TFormComponents extends Componen
|
|
|
738
750
|
* @typeParam TNewFormComponents - The form components the extension adds.
|
|
739
751
|
* @typeParam TNewStructure - The structure slots the extension fills.
|
|
740
752
|
* @param extension - What the new registry holds on top of this one.
|
|
741
|
-
* @param extension.widgets - The widgets to add by name.
|
|
753
|
+
* @param extension.widgets - The widgets to add by name. A widget's static `emptyValue` must be
|
|
754
|
+
* of the value type it serves.
|
|
742
755
|
* @param extension.formComponents - The form components to add by name.
|
|
743
756
|
* @param extension.structure - The structure slots to fill.
|
|
744
757
|
* @defaultValue `extension.widgets`, `extension.formComponents` and `extension.structure`: none
|
|
745
758
|
* added.
|
|
746
759
|
*/
|
|
747
760
|
extendRegistry: <const TNewWidgets extends NewWidgets<TWidgets> = Record<never, never>, const TNewFormComponents extends NewFormComponents<TFormComponents> = Record<never, never>, const TNewStructure extends NewStructure<TStructure> = Record<never, never>>(extension: {
|
|
748
|
-
widgets?: TNewWidgets
|
|
761
|
+
widgets?: TNewWidgets & CheckedWidgets<TNewWidgets>;
|
|
749
762
|
formComponents?: TNewFormComponents;
|
|
750
763
|
structure?: TNewStructure & CheckedStructure<TNewStructure>;
|
|
751
764
|
}) => Registry<TWidgets & TNewWidgets, TFormComponents & TNewFormComponents, TStructure & TNewStructure>;
|
|
@@ -779,7 +792,8 @@ type Contexts = {
|
|
|
779
792
|
* @param config - The contexts and what the registry holds.
|
|
780
793
|
* @param config.fieldContext - The field context from `createFormHookContexts()`.
|
|
781
794
|
* @param config.formContext - The form context from `createFormHookContexts()`.
|
|
782
|
-
* @param config.widgets - The widgets by name.
|
|
795
|
+
* @param config.widgets - The widgets by name. A widget's static `emptyValue`, the value it holds
|
|
796
|
+
* while nothing is entered, must be of the value type it serves.
|
|
783
797
|
* @param config.formComponents - The form components by name.
|
|
784
798
|
* @param config.structure - The components that render the structure slots.
|
|
785
799
|
* @defaultValue `config.formComponents` and `config.structure`: none registered.
|
|
@@ -801,7 +815,7 @@ type Contexts = {
|
|
|
801
815
|
* ```
|
|
802
816
|
*/
|
|
803
817
|
export declare function createRegistry<const TWidgets extends Components, const TFormComponents extends Components = Record<never, never>, const TStructure extends Structure = Record<never, never>>(config: Contexts & {
|
|
804
|
-
widgets: TWidgets
|
|
818
|
+
widgets: TWidgets & CheckedWidgets<TWidgets>;
|
|
805
819
|
formComponents?: TFormComponents;
|
|
806
820
|
structure?: TStructure & CheckedStructure<TStructure>;
|
|
807
821
|
}): Registry<TWidgets, TFormComponents, TStructure>;
|
package/dist/index.d.ts
CHANGED
|
@@ -605,9 +605,15 @@ type WrongSlotProps = "formdef: this slot does not take the props the renderer p
|
|
|
605
605
|
type DuplicateWidget = "formdef: a widget with this name is already registered";
|
|
606
606
|
type DuplicateFormComponent = "formdef: a form component with this name is already registered";
|
|
607
607
|
type DuplicateSlot = "formdef: this structure slot is already filled";
|
|
608
|
+
type WrongEmptyValue = "formdef: the empty value is no value this widget serves";
|
|
608
609
|
type AcceptsSupplied<TComponent extends ComponentType<AnyProps>, TSupplied> = TSupplied extends Pick<ComponentProps<TComponent>, keyof TSupplied & keyof ComponentProps<TComponent>> ? TComponent : WrongSlotProps;
|
|
609
610
|
/** Checks each slot of `TStructure` against what the renderer passes it. */
|
|
610
611
|
type CheckedStructure<TStructure> = { [Slot in keyof TStructure]: Slot extends keyof SlotSupplied ? TStructure[Slot] extends ComponentType<AnyProps> ? AcceptsSupplied<TStructure[Slot], SlotSupplied[Slot]> : TStructure[Slot] : UnknownSlot; };
|
|
612
|
+
type FitsEmptyValue<TWidget> = TWidget extends {
|
|
613
|
+
emptyValue?: infer TEmpty;
|
|
614
|
+
} ? [TEmpty] extends [ValueOf<TWidget>] ? TWidget : WrongEmptyValue : TWidget;
|
|
615
|
+
/** Checks the `emptyValue` each widget of `TWidgets` declares against the value type it serves. */
|
|
616
|
+
type CheckedWidgets<TWidgets> = { [Name in keyof TWidgets]: "emptyValue" extends keyof TWidgets[Name] ? FitsEmptyValue<TWidgets[Name]> : TWidgets[Name]; };
|
|
611
617
|
type Fresh<TExisting, TMessage extends string> = { [Name in keyof TExisting]?: TMessage; };
|
|
612
618
|
/** Widgets an extension adds: none may take a name the registry already uses. */
|
|
613
619
|
type NewWidgets<TWidgets> = Components & Fresh<TWidgets, DuplicateWidget>;
|
|
@@ -667,6 +673,12 @@ interface Registry<TWidgets extends Components, TFormComponents extends Componen
|
|
|
667
673
|
* The config is typed by its schema; its root `layout` may name only keys `fields` describes. The
|
|
668
674
|
* component takes `FormProps`, with `options.defaultValues` required when the config has none.
|
|
669
675
|
*
|
|
676
|
+
* When the component first renders, it validates the schema once with every leaf holding its
|
|
677
|
+
* widget's `emptyValue`; a widget that declares one receives `required`, true where the schema
|
|
678
|
+
* rejects that value. A `required` in the entry's `props` wins. A config that validates
|
|
679
|
+
* asynchronously (`validation.async`), or a schema that does not validate the empty values
|
|
680
|
+
* synchronously, marks no field and warns once outside production.
|
|
681
|
+
*
|
|
670
682
|
* @typeParam TSchema - The Standard Schema of the form, inferred from `schema`.
|
|
671
683
|
* @typeParam TConfig - The config as written, inferred.
|
|
672
684
|
* @typeParam TSubmitMeta - What `handleSubmit` takes besides the values, inferred from
|
|
@@ -738,14 +750,15 @@ interface Registry<TWidgets extends Components, TFormComponents extends Componen
|
|
|
738
750
|
* @typeParam TNewFormComponents - The form components the extension adds.
|
|
739
751
|
* @typeParam TNewStructure - The structure slots the extension fills.
|
|
740
752
|
* @param extension - What the new registry holds on top of this one.
|
|
741
|
-
* @param extension.widgets - The widgets to add by name.
|
|
753
|
+
* @param extension.widgets - The widgets to add by name. A widget's static `emptyValue` must be
|
|
754
|
+
* of the value type it serves.
|
|
742
755
|
* @param extension.formComponents - The form components to add by name.
|
|
743
756
|
* @param extension.structure - The structure slots to fill.
|
|
744
757
|
* @defaultValue `extension.widgets`, `extension.formComponents` and `extension.structure`: none
|
|
745
758
|
* added.
|
|
746
759
|
*/
|
|
747
760
|
extendRegistry: <const TNewWidgets extends NewWidgets<TWidgets> = Record<never, never>, const TNewFormComponents extends NewFormComponents<TFormComponents> = Record<never, never>, const TNewStructure extends NewStructure<TStructure> = Record<never, never>>(extension: {
|
|
748
|
-
widgets?: TNewWidgets
|
|
761
|
+
widgets?: TNewWidgets & CheckedWidgets<TNewWidgets>;
|
|
749
762
|
formComponents?: TNewFormComponents;
|
|
750
763
|
structure?: TNewStructure & CheckedStructure<TNewStructure>;
|
|
751
764
|
}) => Registry<TWidgets & TNewWidgets, TFormComponents & TNewFormComponents, TStructure & TNewStructure>;
|
|
@@ -779,7 +792,8 @@ type Contexts = {
|
|
|
779
792
|
* @param config - The contexts and what the registry holds.
|
|
780
793
|
* @param config.fieldContext - The field context from `createFormHookContexts()`.
|
|
781
794
|
* @param config.formContext - The form context from `createFormHookContexts()`.
|
|
782
|
-
* @param config.widgets - The widgets by name.
|
|
795
|
+
* @param config.widgets - The widgets by name. A widget's static `emptyValue`, the value it holds
|
|
796
|
+
* while nothing is entered, must be of the value type it serves.
|
|
783
797
|
* @param config.formComponents - The form components by name.
|
|
784
798
|
* @param config.structure - The components that render the structure slots.
|
|
785
799
|
* @defaultValue `config.formComponents` and `config.structure`: none registered.
|
|
@@ -801,7 +815,7 @@ type Contexts = {
|
|
|
801
815
|
* ```
|
|
802
816
|
*/
|
|
803
817
|
export declare function createRegistry<const TWidgets extends Components, const TFormComponents extends Components = Record<never, never>, const TStructure extends Structure = Record<never, never>>(config: Contexts & {
|
|
804
|
-
widgets: TWidgets
|
|
818
|
+
widgets: TWidgets & CheckedWidgets<TWidgets>;
|
|
805
819
|
formComponents?: TFormComponents;
|
|
806
820
|
structure?: TStructure & CheckedStructure<TStructure>;
|
|
807
821
|
}): Registry<TWidgets, TFormComponents, TStructure>;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { createFormHook, defaultValidationLogic, getBy, revalidateLogic, useSelector } from "@tanstack/react-form";
|
|
2
|
-
import { Fragment, createElement, useRef } from "react";
|
|
2
|
+
import { Fragment, createContext, createElement, use, useRef } from "react";
|
|
3
3
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
// src/widget/declares-empty-value.ts
|
|
5
|
+
/** Whether `widget` declares its empty value, the value it holds while nothing is entered. */
|
|
6
|
+
function declaresEmptyValue(widget) {
|
|
7
|
+
return Object.hasOwn(widget, "emptyValue");
|
|
8
|
+
}
|
|
4
9
|
// src/render/array-entry.tsx
|
|
5
10
|
const removeLabelOf = (removeLabel, item, index) => typeof removeLabel === "function" ? removeLabel(item, index) : String(removeLabel);
|
|
6
11
|
const itemControlsOf = (options, value) => {
|
|
@@ -102,6 +107,17 @@ const Derived = ({ form, entry, children }) => {
|
|
|
102
107
|
}
|
|
103
108
|
});
|
|
104
109
|
};
|
|
110
|
+
// src/render/path-pattern.ts
|
|
111
|
+
const ITEM_INDEX = /\[\d+\]/g;
|
|
112
|
+
const ANY_ITEM = "[]";
|
|
113
|
+
/** The path with every array index left open: `contacts[2].name` becomes `contacts[].name`. */
|
|
114
|
+
function pathPattern(path) {
|
|
115
|
+
return path.replaceAll(ITEM_INDEX, ANY_ITEM);
|
|
116
|
+
}
|
|
117
|
+
// src/render/required-paths.ts
|
|
118
|
+
const NO_REQUIRED_PATHS = /* @__PURE__ */ new Set();
|
|
119
|
+
/** The path patterns of the required fields of the level being rendered. */
|
|
120
|
+
const RequiredPaths = createContext(NO_REQUIRED_PATHS);
|
|
105
121
|
const OWN_ENTRY_KEY_SET = /* @__PURE__ */ new Set([
|
|
106
122
|
"widget",
|
|
107
123
|
"props",
|
|
@@ -160,13 +176,16 @@ const Item = ({ parts, form, entry, path, index, extra }) => {
|
|
|
160
176
|
};
|
|
161
177
|
const FieldContent = ({ parts, form, entry, path, props, extra }) => {
|
|
162
178
|
const { own, fieldOptions } = entry;
|
|
179
|
+
const requiredPaths = use(RequiredPaths);
|
|
163
180
|
if (typeof own.widget === "string") {
|
|
164
181
|
const Widget = byName(parts.widgets, own.widget);
|
|
165
182
|
if (Widget === void 0) throw new Error(`formdef: field "${path}" names the unregistered widget "${own.widget}".`);
|
|
183
|
+
const found = declaresEmptyValue(Widget) ? { required: requiredPaths.has(pathPattern(path)) } : void 0;
|
|
166
184
|
return /* @__PURE__ */ jsx(form.AppField, {
|
|
167
185
|
...fieldOptions,
|
|
168
186
|
name: path,
|
|
169
187
|
children: (field) => /* @__PURE__ */ jsx(Widget, {
|
|
188
|
+
...found,
|
|
170
189
|
...props,
|
|
171
190
|
...extra,
|
|
172
191
|
field
|
|
@@ -332,15 +351,75 @@ const LevelEntries = ({ layout, ...level }) => {
|
|
|
332
351
|
* into sections and rows and places form components and inline entries. A leaf renders as its
|
|
333
352
|
* widget, an object as its own level, a custom entry through its `render`, an array through the
|
|
334
353
|
* registered `structure.arrayFrame` or the bare default. An object entry with TanStack options of
|
|
335
|
-
* its own also mounts a field at its path, so its validators run.
|
|
354
|
+
* its own also mounts a field at its path, so its validators run. A `Fields` nested in a form finds
|
|
355
|
+
* no required fields of its own, since its paths may be relative to a field group.
|
|
336
356
|
*/
|
|
337
|
-
const Fields = ({ parts, form, fields, layout }) => /* @__PURE__ */ jsx(
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
357
|
+
const Fields = ({ parts, form, fields, layout, requiredPaths }) => /* @__PURE__ */ jsx(RequiredPaths, {
|
|
358
|
+
value: requiredPaths ?? NO_REQUIRED_PATHS,
|
|
359
|
+
children: /* @__PURE__ */ jsx(LevelEntries, {
|
|
360
|
+
parts,
|
|
361
|
+
form,
|
|
362
|
+
fields,
|
|
363
|
+
layout,
|
|
364
|
+
path: void 0
|
|
365
|
+
})
|
|
343
366
|
});
|
|
367
|
+
// src/form/find-required-paths.ts
|
|
368
|
+
const NONE = { paths: /* @__PURE__ */ new Set() };
|
|
369
|
+
const ASYNC_SCHEMA = "formdef: the schema validates asynchronously, so no field is found to be required; pass required in props.";
|
|
370
|
+
const UNSETTLED_SCHEMA = "formdef: the schema did not validate empty values synchronously, so no field is found to be required; pass required in props.";
|
|
371
|
+
const THROWING_SCHEMA = "formdef: the schema threw on empty values, so no field is found to be required";
|
|
372
|
+
const FIRST_ITEM = 0;
|
|
373
|
+
const isStandardSchema = (schema) => typeof schema === "object" && schema !== null && "~standard" in schema;
|
|
374
|
+
const emptyLevel = (fields, path, empty) => Object.fromEntries(Object.entries(fields).map(([key, entry]) => [key, emptyEntry(entry, childPath(path, key), empty)]));
|
|
375
|
+
function emptyEntry(entry, path, empty) {
|
|
376
|
+
const { own } = splitEntry(entry);
|
|
377
|
+
if (typeof own.widget === "string") {
|
|
378
|
+
const widget = byName(empty.widgets, own.widget);
|
|
379
|
+
if (widget === void 0 || !declaresEmptyValue(widget)) return;
|
|
380
|
+
empty.declared.add(pathPattern(path));
|
|
381
|
+
return widget.emptyValue;
|
|
382
|
+
}
|
|
383
|
+
if (typeof own.item === "object" && own.item !== null) return [emptyEntry(own.item, childPath(path, FIRST_ITEM), empty)];
|
|
384
|
+
if (typeof own.fields === "object" && own.fields !== null) return emptyLevel(own.fields, path, empty);
|
|
385
|
+
}
|
|
386
|
+
const issuePath = (issue) => pathPattern((issue.path ?? []).reduce((parent, segment) => {
|
|
387
|
+
const key = typeof segment === "object" ? segment.key : segment;
|
|
388
|
+
return childPath(parent, typeof key === "number" ? key : String(key));
|
|
389
|
+
}, void 0) ?? "");
|
|
390
|
+
const parentOf = (path) => path.slice(0, Math.max(path.lastIndexOf("."), path.lastIndexOf("[")));
|
|
391
|
+
const leafOf = (path, leaves) => {
|
|
392
|
+
for (let candidate = path; candidate !== ""; candidate = parentOf(candidate)) if (leaves.has(candidate)) return candidate;
|
|
393
|
+
};
|
|
394
|
+
const ignoreRejection = () => void 0;
|
|
395
|
+
const errorText = (error) => error instanceof Error ? error.message : String(error);
|
|
396
|
+
/**
|
|
397
|
+
* Finds the required fields of a config: validates `schema` once, synchronously, with every leaf
|
|
398
|
+
* holding its widget's empty value and every array one such item. A leaf whose widget declares an
|
|
399
|
+
* empty value and whose path receives an issue is required. A schema the config validates
|
|
400
|
+
* asynchronously is not run, so its asynchronous checks send nothing.
|
|
401
|
+
*/
|
|
402
|
+
function findRequiredPaths({ schema, validatesAsync, fields, widgets }) {
|
|
403
|
+
if (!isStandardSchema(schema)) return NONE;
|
|
404
|
+
if (validatesAsync) return { problem: ASYNC_SCHEMA };
|
|
405
|
+
const empty = {
|
|
406
|
+
widgets,
|
|
407
|
+
declared: /* @__PURE__ */ new Set()
|
|
408
|
+
};
|
|
409
|
+
const values = emptyLevel(fields, void 0, empty);
|
|
410
|
+
let result;
|
|
411
|
+
try {
|
|
412
|
+
result = schema["~standard"].validate(values);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
return { problem: `${THROWING_SCHEMA}: ${errorText(error)}` };
|
|
415
|
+
}
|
|
416
|
+
if (result instanceof Promise) {
|
|
417
|
+
result.then(void 0, ignoreRejection);
|
|
418
|
+
return { problem: UNSETTLED_SCHEMA };
|
|
419
|
+
}
|
|
420
|
+
const leaves = (result.issues ?? []).map((issue) => leafOf(issuePath(issue), empty.declared));
|
|
421
|
+
return { paths: new Set(leaves.filter((leaf) => leaf !== void 0)) };
|
|
422
|
+
}
|
|
344
423
|
// src/form/blur-then-change.ts
|
|
345
424
|
const isSchemaDue = ({ event, form }) => {
|
|
346
425
|
if (event.type === "blur" || event.type === "submit") return true;
|
|
@@ -415,7 +494,7 @@ function formOptionsFor({ config, options }) {
|
|
|
415
494
|
}
|
|
416
495
|
// src/form/create-form.tsx
|
|
417
496
|
const MISSING_DEFAULT_VALUES = "formdef: the form has no default values; set defaultValues in its config or in its options.";
|
|
418
|
-
const ConfiguredForm = ({ binding, config, options, ...formProps }) => {
|
|
497
|
+
const ConfiguredForm = ({ binding, config, requiredPaths, options, ...formProps }) => {
|
|
419
498
|
const formOptions = formOptionsFor({
|
|
420
499
|
config,
|
|
421
500
|
options
|
|
@@ -427,7 +506,8 @@ const ConfiguredForm = ({ binding, config, options, ...formProps }) => {
|
|
|
427
506
|
parts: binding.parts,
|
|
428
507
|
form,
|
|
429
508
|
fields: config.fields,
|
|
430
|
-
layout: config.layout
|
|
509
|
+
layout: config.layout,
|
|
510
|
+
requiredPaths
|
|
431
511
|
});
|
|
432
512
|
const handleSubmit = (event) => {
|
|
433
513
|
event.preventDefault();
|
|
@@ -441,17 +521,36 @@ const ConfiguredForm = ({ binding, config, options, ...formProps }) => {
|
|
|
441
521
|
children: Body === void 0 ? content : /* @__PURE__ */ jsx(Body, { children: content })
|
|
442
522
|
}) });
|
|
443
523
|
};
|
|
524
|
+
const isAsyncValidation = (validation) => typeof validation === "object" && validation !== null && "async" in validation ? validation.async === true : false;
|
|
525
|
+
const requiredPathsOf = (binding, config) => {
|
|
526
|
+
const result = findRequiredPaths({
|
|
527
|
+
schema: config.schema,
|
|
528
|
+
validatesAsync: isAsyncValidation(config.validation),
|
|
529
|
+
fields: config.fields ?? {},
|
|
530
|
+
widgets: binding.parts.widgets
|
|
531
|
+
});
|
|
532
|
+
if ("problem" in result) {
|
|
533
|
+
warnOnce(config, result.problem);
|
|
534
|
+
return /* @__PURE__ */ new Set();
|
|
535
|
+
}
|
|
536
|
+
return result.paths;
|
|
537
|
+
};
|
|
444
538
|
/**
|
|
445
539
|
* Turns a form config into a form component bound to `binding`: TanStack's form with the merged
|
|
446
540
|
* options, a `<form>` element that submits through it, the registered body and the config's fields.
|
|
541
|
+
* When it first renders, it finds the config's required fields once.
|
|
447
542
|
*/
|
|
448
543
|
function createForm(binding, config) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
binding,
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
544
|
+
let requiredPaths;
|
|
545
|
+
return (props) => {
|
|
546
|
+
requiredPaths ??= requiredPathsOf(binding, config);
|
|
547
|
+
return /* @__PURE__ */ jsx(ConfiguredForm, {
|
|
548
|
+
...props,
|
|
549
|
+
binding,
|
|
550
|
+
config,
|
|
551
|
+
requiredPaths
|
|
552
|
+
});
|
|
553
|
+
};
|
|
455
554
|
}
|
|
456
555
|
// src/registry/create-registry.ts
|
|
457
556
|
function buildRegistry(parts) {
|
|
@@ -462,13 +561,14 @@ function buildRegistry(parts) {
|
|
|
462
561
|
formComponents: parts.formComponents
|
|
463
562
|
});
|
|
464
563
|
function extendRegistry(extension) {
|
|
564
|
+
const addedWidgets = extension.widgets ?? {};
|
|
465
565
|
const addedStructure = extension.structure ?? {};
|
|
466
566
|
return buildRegistry({
|
|
467
567
|
fieldContext: parts.fieldContext,
|
|
468
568
|
formContext: parts.formContext,
|
|
469
569
|
widgets: {
|
|
470
570
|
...parts.widgets,
|
|
471
|
-
...
|
|
571
|
+
...addedWidgets
|
|
472
572
|
},
|
|
473
573
|
formComponents: {
|
|
474
574
|
...parts.formComponents,
|
|
@@ -512,7 +612,8 @@ function buildRegistry(parts) {
|
|
|
512
612
|
* @param config - The contexts and what the registry holds.
|
|
513
613
|
* @param config.fieldContext - The field context from `createFormHookContexts()`.
|
|
514
614
|
* @param config.formContext - The form context from `createFormHookContexts()`.
|
|
515
|
-
* @param config.widgets - The widgets by name.
|
|
615
|
+
* @param config.widgets - The widgets by name. A widget's static `emptyValue`, the value it holds
|
|
616
|
+
* while nothing is entered, must be of the value type it serves.
|
|
516
617
|
* @param config.formComponents - The form components by name.
|
|
517
618
|
* @param config.structure - The components that render the structure slots.
|
|
518
619
|
* @defaultValue `config.formComponents` and `config.structure`: none registered.
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/render/array-entry.tsx","../src/render/by-name.ts","../src/render/child-path.ts","../src/render/derived.tsx","../src/render/split-entry.ts","../src/render/warn-once.ts","../src/render/fields.tsx","../src/form/blur-then-change.ts","../src/form/form-options-for.ts","../src/form/create-form.tsx","../src/registry/create-registry.ts","../src/widget/errors-of.ts","../src/widget/field-id.ts","../src/widget/field-parts.ts"],"sourcesContent":["import { type AnyFieldApi, useSelector } from '@tanstack/react-form'\nimport { type ComponentType, Fragment, type ReactNode } from 'react'\n\n// biome-ignore lint/suspicious/noExplicitAny: a registered frame takes props of its own; the config types check them.\ntype AnyProps = any\n\n/** The keys of an array entry the renderer reads besides its item. */\ntype ArrayOptions = {\n newItem?: unknown\n addLabel?: unknown\n removeLabel?: unknown\n itemKey?: unknown\n}\n\ntype ArrayEntryProps = {\n /** The array field, mounted at the entry's path. */\n field: AnyFieldApi\n options: ArrayOptions\n /** The registered `structure.arrayFrame`, if any. */\n frame: ComponentType<AnyProps> | undefined\n /** The entry's props for the frame. */\n props: object | undefined\n renderItem: (index: number, extra?: object) => ReactNode\n}\n\ntype ItemFunction<TResult> = (item: unknown, index?: number) => TResult\n\n/** What the controls of an array entry show: one key and one remove label per item. */\ntype ItemControls = {\n keys: (string | number)[]\n removeLabels: string[]\n}\n\nconst removeLabelOf = (removeLabel: unknown, item: unknown, index: number): string =>\n typeof removeLabel === 'function'\n ? (removeLabel as ItemFunction<string>)(item, index)\n : String(removeLabel)\n\nconst itemControlsOf = (options: ArrayOptions, value: unknown): ItemControls => {\n const items: unknown[] = Array.isArray(value) ? value : []\n\n return {\n keys: items.map((item, index) =>\n typeof options.itemKey === 'function'\n ? (options.itemKey as ItemFunction<string | number>)(item)\n : index,\n ),\n removeLabels: items.map((item, index) => removeLabelOf(options.removeLabel, item, index)),\n }\n}\n\nconst isSameList = <TItem,>(first: readonly TItem[], second: readonly TItem[]): boolean =>\n first.length === second.length && first.every((item, index) => item === second[index])\n\nconst isSameControls = (first: ItemControls, second: ItemControls): boolean =>\n isSameList(first.keys, second.keys) && isSameList(first.removeLabels, second.removeLabels)\n\n/**\n * Renders an array entry: through the registered frame, which receives `ArrayFrameProps`, or as\n * the bare default, each item followed by its remove button and one add button at the end.\n */\nexport const ArrayEntry = ({\n field,\n options,\n frame: Frame,\n props,\n renderItem,\n}: ArrayEntryProps): ReactNode => {\n // The field renders in TanStack's array mode, so only a change to the list reaches it; keys and\n // labels that name an item follow the items through a selection of their own.\n const { keys, removeLabels } = useSelector(\n field.store,\n (state: { value: unknown }) => itemControlsOf(options, state.value),\n { compare: isSameControls },\n )\n const newItem = options.newItem as () => unknown\n const addLabel = String(options.addLabel)\n const add = (): void => field.pushValue(newItem())\n const remove = (index: number): void => field.removeValue(index)\n\n if (Frame === undefined) {\n return (\n <>\n {keys.map((key, index) => (\n <Fragment key={key}>\n {renderItem(index)}\n <button type=\"button\" data-formdef-remove=\"\" onClick={() => remove(index)}>\n {removeLabels[index]}\n </button>\n </Fragment>\n ))}\n <button type=\"button\" data-formdef-add=\"\" onClick={add}>\n {addLabel}\n </button>\n </>\n )\n }\n\n return (\n <Frame\n {...props}\n field={field}\n keys={keys}\n renderItem={renderItem}\n addLabel={addLabel}\n removeLabels={removeLabels}\n add={add}\n remove={remove}\n insert={(index: number, item?: unknown) =>\n field.insertValue(index, item === undefined ? newItem() : item)\n }\n move={(from: number, to: number) => field.moveValue(from, to)}\n swap={(first: number, second: number) => field.swapValues(first, second)}\n replace={(index: number, item: unknown) => field.replaceValue(index, item)}\n clear={() => field.clearValues()}\n />\n )\n}\n","/** What `map` holds under `name` itself; a name every object inherits finds nothing. */\nexport function byName<TValue>(\n map: Readonly<Record<string, TValue>>,\n name: string,\n): TValue | undefined {\n return Object.hasOwn(map, name) ? map[name] : undefined\n}\n","/**\n * The path of `key` below `parent`: a key of the root level by itself, a key of a nested level\n * after a dot, the index of an array item in brackets.\n */\nexport function childPath(parent: string | undefined, key: string | number): string {\n if (typeof key === 'number') {\n return `${parent ?? ''}[${key}]`\n }\n\n return parent === undefined ? key : `${parent}.${key}`\n}\n","import { getBy } from '@tanstack/react-form'\nimport { type ReactNode, useRef } from 'react'\n\nimport type { RenderForm } from './render-form'\n\n/** What an entry reads from the form values: its condition, its props and the paths they read. */\ntype DerivedEntry = {\n visibleWhen?: unknown\n props?: unknown\n dependsOn?: unknown\n}\n\ntype DerivedProps = {\n form: RenderForm\n entry: DerivedEntry\n /** Builds the entry from its props; called only while the entry is visible. */\n children: (props: object | undefined) => ReactNode\n}\n\ntype LastRead = {\n entry: DerivedEntry\n dependencies: unknown[]\n selected: unknown\n}\n\nconst HIDDEN = Symbol('hidden')\nconst VISIBLE = Symbol('visible')\n\nconst propsOf = (props: unknown): object | undefined =>\n typeof props === 'object' && props !== null ? props : undefined\n\nconst isStale = (\n last: LastRead | undefined,\n entry: DerivedEntry,\n dependencies: unknown[],\n): boolean =>\n last === undefined ||\n last.entry !== entry ||\n dependencies.some((value, index) => !Object.is(value, last.dependencies[index]))\n\n/**\n * Renders an entry from what it reads from the form values. An entry with a condition or derived\n * props subscribes to the values; the selected result alone decides whether it re-renders: whether\n * it is shown for a condition, the values for derived props. `dependsOn` narrows when the selector\n * reads again, never what it returns. A hidden entry is never built.\n */\nexport const Derived = ({ form, entry, children }: DerivedProps): ReactNode => {\n const lastRead = useRef<LastRead | undefined>(undefined)\n const { visibleWhen, props, dependsOn } = entry\n const isConditional = typeof visibleWhen === 'function'\n const isDerived = typeof props === 'function'\n\n if (!(isConditional || isDerived)) {\n return children(propsOf(props))\n }\n\n const selectFrom = (values: unknown): unknown => {\n if (isConditional && !visibleWhen(values)) {\n return HIDDEN\n }\n\n return isDerived ? values : VISIBLE\n }\n\n const select = ({ values }: { values: unknown }): unknown => {\n if (!Array.isArray(dependsOn)) {\n return selectFrom(values)\n }\n\n const dependencies = dependsOn.map((path: string) => getBy(values, path))\n if (isStale(lastRead.current, entry, dependencies)) {\n lastRead.current = { entry, dependencies, selected: selectFrom(values) }\n }\n\n return lastRead.current?.selected\n }\n\n return (\n <form.Subscribe selector={select}>\n {(selected) => {\n if (selected === HIDDEN) {\n return null\n }\n\n return children(propsOf(isDerived ? props(selected) : props))\n }}\n </form.Subscribe>\n )\n}\n","/**\n * The keys formdef reads from a field entry. Every other key is one of TanStack's field options and\n * goes to the field unchanged, so an option TanStack adds works without a formdef release.\n */\nexport const OWN_ENTRY_KEYS = [\n 'widget',\n 'props',\n 'fields',\n 'layout',\n 'item',\n 'newItem',\n 'addLabel',\n 'removeLabel',\n 'itemKey',\n 'render',\n 'visibleWhen',\n 'dependsOn',\n] as const\n\n/** A key formdef reads from a field entry. */\nexport type OwnEntryKey = (typeof OWN_ENTRY_KEYS)[number]\n\nconst OWN_ENTRY_KEY_SET: ReadonlySet<string> = new Set(OWN_ENTRY_KEYS)\n\n/** Splits a field entry into the keys formdef reads and TanStack's field options. */\nexport function splitEntry(entry: object): {\n own: Partial<Record<OwnEntryKey, unknown>>\n fieldOptions: Record<string, unknown>\n} {\n const entries = Object.entries(entry)\n\n // `fromEntries` defines each key as data, so a key named `__proto__` cannot set a prototype.\n return {\n own: Object.fromEntries(entries.filter(([key]) => OWN_ENTRY_KEY_SET.has(key))),\n fieldOptions: Object.fromEntries(entries.filter(([key]) => !OWN_ENTRY_KEY_SET.has(key))),\n }\n}\n","// Bundlers replace `process.env.NODE_ENV` in exactly this spelling; without one, `process` may not\n// exist at all.\n// biome-ignore lint/style/useNamingConvention: the name bundlers replace.\ndeclare const process: { env: { NODE_ENV?: string } } | undefined\n\nconst warnedOwners = new WeakSet<object>()\n\nconst isProduction = (): boolean =>\n typeof process !== 'undefined' && process.env.NODE_ENV === 'production'\n\n/** Warns once per `owner`, and never in production. */\nexport function warnOnce(owner: object, message: string): void {\n if (isProduction() || warnedOwners.has(owner)) {\n return\n }\n\n warnedOwners.add(owner)\n console.warn(message)\n}\n","import type { ComponentType, ReactNode } from 'react'\n\nimport { ArrayEntry } from './array-entry'\nimport { byName } from './by-name'\nimport { childPath } from './child-path'\nimport { Derived } from './derived'\nimport type { AppFieldProps, RenderForm } from './render-form'\nimport { splitEntry } from './split-entry'\nimport { warnOnce } from './warn-once'\n\n// biome-ignore lint/suspicious/noExplicitAny: registered components take props of their own; the config types check them.\ntype AnyProps = any\n\ntype ComponentMap = Readonly<Record<string, ComponentType<AnyProps>>>\n\n/** The maps of a registry the renderer looks names up in. */\ntype RenderParts = {\n widgets: ComponentMap\n formComponents: ComponentMap\n structure: {\n section?: ComponentType<AnyProps>\n row?: ComponentType<AnyProps>\n arrayFrame?: ComponentType<AnyProps>\n }\n}\n\ntype LevelFields = Readonly<Record<string, object>>\n\ntype LayoutNodeEntry = string | Readonly<Record<string, unknown>>\n\n/** The props of {@link Fields}. */\nexport type FieldsProps = {\n parts: RenderParts\n form: RenderForm\n fields: LevelFields\n layout?: readonly unknown[] | undefined\n}\n\ntype Level = Omit<FieldsProps, 'layout'> & {\n path: string | undefined\n /** Props an array frame passes the one widget of an item, through this level. */\n extra?: object | undefined\n}\n\ntype LevelProps = Level & {\n layout: readonly unknown[] | undefined\n}\n\ntype EntryProps = Omit<Level, 'fields'> & {\n entry: object\n path: string\n}\n\ntype FieldContentProps = Omit<EntryProps, 'entry'> & {\n entry: ReturnType<typeof splitEntry> & { source: object }\n props: object | undefined\n}\n\ntype ItemProps = Omit<EntryProps, 'extra'> & {\n index: number\n extra: object | undefined\n}\n\ntype LayoutNodeProps = {\n level: Level\n entry: LayoutNodeEntry\n}\n\ntype LayoutKindProps = {\n level: Level\n entry: Readonly<Record<string, unknown>>\n props: object | undefined\n}\n\nconst ARRAY_MODE = 'array'\n\nconst DROPPED_EXTRA = 'passes extra props to an item that has no single widget to receive them'\n\nconst renderNothing = (): ReactNode => null\n\nconst layoutOwner = (path: string | undefined): string =>\n path === undefined ? 'the form' : `\"${path}\"`\n\nconst isLeaf = (entry: unknown): boolean =>\n typeof entry === 'object' && entry !== null && 'widget' in entry\n\n// A frame's extra props have a single target only in a leaf, or an object level whose one field\n// is a leaf and which has no layout.\nconst takesExtra = (item: object): boolean => {\n if (isLeaf(item)) {\n return true\n }\n const { own } = splitEntry(item)\n const fields =\n typeof own.fields === 'object' && own.fields !== null ? Object.values(own.fields) : []\n\n return own.layout === undefined && fields.length === 1 && isLeaf(fields[0])\n}\n\nconst Item = ({ parts, form, entry, path, index, extra }: ItemProps): ReactNode => {\n const { own } = splitEntry(entry)\n const item = own.item as object\n const reachesWidget = extra !== undefined && takesExtra(item)\n if (extra !== undefined && !reachesWidget) {\n warnOnce(entry, `formdef: the array frame of \"${path}\" ${DROPPED_EXTRA}; they are dropped.`)\n }\n\n return (\n <Entry\n parts={parts}\n form={form}\n entry={item}\n path={childPath(path, index)}\n extra={reachesWidget ? extra : undefined}\n />\n )\n}\n\nconst FieldContent = ({ parts, form, entry, path, props, extra }: FieldContentProps): ReactNode => {\n const { own, fieldOptions } = entry\n\n if (typeof own.widget === 'string') {\n const Widget = byName(parts.widgets, own.widget)\n if (Widget === undefined) {\n throw new Error(`formdef: field \"${path}\" names the unregistered widget \"${own.widget}\".`)\n }\n\n return (\n <form.AppField {...fieldOptions} name={path}>\n {(field) => <Widget {...props} {...extra} field={field} />}\n </form.AppField>\n )\n }\n\n if (typeof own.render === 'function') {\n return (\n <form.AppField {...fieldOptions} name={path}>\n {own.render as AppFieldProps['children']}\n </form.AppField>\n )\n }\n\n if (typeof own.item === 'object' && own.item !== null) {\n return (\n <form.AppField {...fieldOptions} name={path} mode={ARRAY_MODE}>\n {(field) => (\n <ArrayEntry\n field={field}\n options={own}\n frame={parts.structure.arrayFrame}\n props={props}\n renderItem={(index, itemExtra) => (\n <Item\n parts={parts}\n form={form}\n entry={entry.source}\n path={path}\n index={index}\n extra={itemExtra}\n />\n )}\n />\n )}\n </form.AppField>\n )\n }\n\n if (typeof own.fields !== 'object' || own.fields === null) {\n return null\n }\n\n return (\n <>\n {Object.keys(fieldOptions).length > 0 && (\n <form.AppField {...fieldOptions} name={path}>\n {renderNothing}\n </form.AppField>\n )}\n <LevelEntries\n parts={parts}\n form={form}\n fields={own.fields as LevelFields}\n layout={Array.isArray(own.layout) ? own.layout : undefined}\n path={path}\n extra={extra}\n />\n </>\n )\n}\n\nconst Entry = ({ parts, form, entry, path, extra }: EntryProps): ReactNode => {\n const split = { ...splitEntry(entry), source: entry }\n\n return (\n <Derived form={form} entry={split.own}>\n {(props) => (\n <FieldContent\n parts={parts}\n form={form}\n entry={split}\n path={path}\n props={props}\n extra={extra}\n />\n )}\n </Derived>\n )\n}\n\nconst FieldNode = ({ level, name }: { level: Level; name: string }): ReactNode => {\n const entry = byName(level.fields, name)\n if (entry === undefined) {\n throw new Error(\n `formdef: the layout of ${layoutOwner(level.path)} names \"${name}\", which the level does not describe.`,\n )\n }\n\n return (\n <Entry\n parts={level.parts}\n form={level.form}\n entry={entry}\n path={childPath(level.path, name)}\n extra={level.extra}\n />\n )\n}\n\nconst LayoutNodes = ({ level, entries }: { level: Level; entries: unknown }): ReactNode =>\n (Array.isArray(entries) ? (entries as LayoutNodeEntry[]) : []).map((entry, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: a layout comes from the config and never reorders; a name may appear twice.\n <LayoutNode key={index} level={level} entry={entry} />\n ))\n\nconst SectionNode = ({ level, entry, props }: LayoutKindProps): ReactNode => {\n const children = <LayoutNodes level={level} entries={entry.layout} />\n const Section = level.parts.structure.section\n\n if (Section !== undefined) {\n return (\n <Section {...props} label={entry.section}>\n {children}\n </Section>\n )\n }\n\n return (\n <fieldset {...props}>\n <legend>{entry.section as string}</legend>\n {children}\n </fieldset>\n )\n}\n\nconst RowNode = ({ level, entry, props }: LayoutKindProps): ReactNode => {\n const children = <LayoutNodes level={level} entries={entry.row} />\n const Row = level.parts.structure.row\n\n if (Row !== undefined) {\n return <Row {...props}>{children}</Row>\n }\n\n return (\n <div {...props} data-formdef-row=\"\">\n {children}\n </div>\n )\n}\n\nconst ComponentNode = ({ level, entry, props }: LayoutKindProps): ReactNode => {\n const name = entry.component as string\n const Component = byName(level.parts.formComponents, name)\n if (Component === undefined) {\n throw new Error(\n `formdef: the layout of ${layoutOwner(level.path)} places the unregistered form component \"${name}\".`,\n )\n }\n\n return <Component {...props} />\n}\n\nconst LayoutKind = ({ level, entry, props }: LayoutKindProps): ReactNode => {\n if ('section' in entry) {\n return <SectionNode level={level} entry={entry} props={props} />\n }\n if ('row' in entry) {\n return <RowNode level={level} entry={entry} props={props} />\n }\n if (typeof entry.component === 'string') {\n return <ComponentNode level={level} entry={entry} props={props} />\n }\n if (typeof entry.render === 'function') {\n return (entry.render as (form: RenderForm) => ReactNode)(level.form)\n }\n\n return null\n}\n\nconst LayoutNode = ({ level, entry }: LayoutNodeProps): ReactNode => {\n if (typeof entry === 'string') {\n return <FieldNode level={level} name={entry} />\n }\n\n return (\n <Derived form={level.form} entry={entry}>\n {(props) => <LayoutKind level={level} entry={entry} props={props} />}\n </Derived>\n )\n}\n\nconst LevelEntries = ({ layout, ...level }: LevelProps): ReactNode => {\n if (layout !== undefined) {\n return <LayoutNodes level={level} entries={layout} />\n }\n\n return Object.keys(level.fields).map((name) => <FieldNode key={name} level={level} name={name} />)\n}\n\n/**\n * Renders the field entries of a level with the widgets of `parts`. Without a `layout` the entries\n * appear in the order `fields` declares them; with one, the layout decides the order, groups them\n * into sections and rows and places form components and inline entries. A leaf renders as its\n * widget, an object as its own level, a custom entry through its `render`, an array through the\n * registered `structure.arrayFrame` or the bare default. An object entry with TanStack options of\n * its own also mounts a field at its path, so its validators run.\n */\nexport const Fields = ({ parts, form, fields, layout }: FieldsProps): ReactNode => (\n <LevelEntries parts={parts} form={form} fields={fields} layout={layout} path={undefined} />\n)\n","import {\n defaultValidationLogic,\n type ValidationLogicFn,\n type ValidationLogicProps,\n} from '@tanstack/react-form'\n\ntype ScheduledValidators = Parameters<ValidationLogicProps['runValidation']>[0]['validators']\n\n// Gated on the form, not the changed field: the schema validates the whole form, and TanStack names\n// the changed field only on the field-level call, where the schema does not sit.\nconst isSchemaDue = ({ event, form }: ValidationLogicProps): boolean => {\n if (event.type === 'blur' || event.type === 'submit') {\n return true\n }\n\n return event.type === 'change' && !form.state.isValid\n}\n\n/**\n * The validation logic of the `blurThenChange` mode: TanStack's default logic, plus the schema in\n * `onDynamic` when a field is left, on submit, and on every change while the form is invalid.\n */\nexport const blurThenChange: ValidationLogicFn = (props) => {\n let defaults: ScheduledValidators = []\n defaultValidationLogic({\n ...props,\n runValidation: ({ validators }) => {\n defaults = validators\n },\n })\n\n const schema = props.event.async ? props.validators?.onDynamicAsync : props.validators?.onDynamic\n if (schema === undefined || !isSchemaDue(props)) {\n return props.runValidation({ validators: defaults, form: props.form })\n }\n\n return props.runValidation({\n validators: [...defaults, { fn: schema, cause: 'dynamic' }],\n form: props.form,\n })\n}\n","import { revalidateLogic, type ValidationLogicFn } from '@tanstack/react-form'\n\nimport type { Validation } from '../config/validation'\nimport { blurThenChange } from './blur-then-change'\n\ntype Mode = NonNullable<Validation['mode']>\ntype DynamicMode = 'revalidate' | 'blurThenChange'\ntype Slot = 'onDynamic' | 'onChange' | 'onBlur' | 'onSubmit'\n\ntype Options = Readonly<Record<string, unknown>>\n\ntype FormOptionsInput = {\n /** The form config: the schema, formdef's own keys and TanStack's form options. */\n config: Options\n /** The instance's TanStack form options, from the form component's `options` prop. */\n options: Options | undefined\n}\n\nconst DEFAULT_MODE: Mode = 'revalidate'\n\nconst isDynamic = (mode: Mode): mode is DynamicMode =>\n mode === 'revalidate' || mode === 'blurThenChange'\n\nconst logicFor = (mode: DynamicMode): ValidationLogicFn =>\n mode === 'revalidate' ? revalidateLogic() : blurThenChange\n\nconst slotFor = (mode: Mode, isAsync: boolean): Slot | `${Slot}Async` => {\n const slot: Slot = isDynamic(mode) ? 'onDynamic' : mode\n\n return isAsync ? `${slot}Async` : slot\n}\n\nconst recordOf = (value: unknown): Options =>\n typeof value === 'object' && value !== null ? (value as Options) : {}\n\n// An instance passing `undefined`, such as data still loading, leaves the config's value in place.\nconst setEntriesOf = (value: unknown): Options =>\n Object.fromEntries(Object.entries(recordOf(value)).filter(([, entry]) => entry !== undefined))\n\n/**\n * The TanStack form options of a form: the config's options and the instance's merged key by key,\n * the instance winning, except `validators` and `listeners`, which merge slot by slot; a key or\n * slot the instance sets to `undefined` counts as unset. The schema\n * goes into the slot `validation` picks unless a validator already fills it; a dynamic mode brings\n * its validation logic, an asynchronous schema its debounce unless the options set one.\n */\nexport function formOptionsFor({ config, options }: FormOptionsInput): Record<string, unknown> {\n const {\n schema,\n validation,\n fields: _renderedByFields,\n layout: _arrangedByFields,\n ...configOptions\n } = config\n const { mode = DEFAULT_MODE, ...timing } = recordOf(validation) as Validation\n const isAsync = timing.async === true\n const slot = slotFor(mode, isAsync)\n const instanceOptions = setEntriesOf(options)\n const validators = {\n ...recordOf(configOptions.validators),\n ...setEntriesOf(instanceOptions.validators),\n }\n\n return {\n ...(isDynamic(mode) && { validationLogic: logicFor(mode) }),\n ...(isAsync &&\n timing.asyncDebounceMs !== undefined && { asyncDebounceMs: timing.asyncDebounceMs }),\n ...configOptions,\n ...instanceOptions,\n validators: validators[slot] === undefined ? { ...validators, [slot]: schema } : validators,\n listeners: { ...recordOf(configOptions.listeners), ...setEntriesOf(instanceOptions.listeners) },\n }\n}\n","import type { ComponentType, FormEvent, ReactNode } from 'react'\n\nimport type { FormElementProps } from '../config/form-props'\nimport type { BodyProps } from '../config/structure'\nimport { Fields, type FieldsProps } from '../render/fields'\nimport type { RenderForm } from '../render/render-form'\nimport { formOptionsFor } from './form-options-for'\n\ntype Options = Readonly<Record<string, unknown>>\n\n/** The form `useAppForm` returns, as far as the shell uses it. */\ntype ShellForm = RenderForm & {\n AppForm: ComponentType<{ children?: ReactNode }>\n handleSubmit: () => Promise<void>\n}\n\n/** What a registry binds a form to: its form hook and the maps the renderer looks names up in. */\nexport type FormBinding = {\n useAppForm: (options: Options) => ShellForm\n parts: FieldsProps['parts'] & {\n structure: { body?: ComponentType<BodyProps> }\n }\n}\n\n/** The props of a form component: the instance's options and what a `<form>` element takes. */\ntype FormComponentProps = FormElementProps & {\n options?: Options\n}\n\n/** A form config: TanStack's form options plus formdef's own keys. */\ntype ConfigInput = Options & {\n fields?: unknown\n layout?: unknown\n}\n\ntype ConfiguredFormProps = FormComponentProps & {\n binding: FormBinding\n config: ConfigInput\n}\n\nconst MISSING_DEFAULT_VALUES =\n 'formdef: the form has no default values; set defaultValues in its config or in its options.'\n\nconst ConfiguredForm = ({\n binding,\n config,\n options,\n ...formProps\n}: ConfiguredFormProps): ReactNode => {\n const formOptions = formOptionsFor({ config, options })\n if (formOptions.defaultValues === undefined) {\n throw new Error(MISSING_DEFAULT_VALUES)\n }\n\n const form = binding.useAppForm(formOptions)\n const Body = binding.parts.structure.body\n const content = (\n <Fields\n parts={binding.parts}\n form={form}\n fields={config.fields as FieldsProps['fields']}\n layout={config.layout as FieldsProps['layout']}\n />\n )\n const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {\n event.preventDefault()\n event.stopPropagation()\n void form.handleSubmit()\n }\n\n return (\n <form.AppForm>\n <form noValidate {...formProps} onSubmit={handleSubmit}>\n {Body === undefined ? content : <Body>{content}</Body>}\n </form>\n </form.AppForm>\n )\n}\n\n/**\n * Turns a form config into a form component bound to `binding`: TanStack's form with the merged\n * options, a `<form>` element that submits through it, the registered body and the config's fields.\n */\nexport function createForm(\n binding: FormBinding,\n config: ConfigInput,\n): (props: FormComponentProps) => ReactNode {\n // biome-ignore lint/correctness/noComponentHookFactories: runs once per form at module level, like TanStack's createFormHook.\n const Form = (props: FormComponentProps): ReactNode => (\n <ConfiguredForm {...props} binding={binding} config={config} />\n )\n\n return Form\n}\n","import { type AnyFieldApi, type AnyFormApi, createFormHook } from '@tanstack/react-form'\nimport { type Context, createElement, type ReactNode } from 'react'\n\nimport { createForm, type FormBinding } from '../form/create-form'\nimport { Fields, type FieldsProps } from '../render/fields'\n\nimport type {\n CheckedStructure,\n Components,\n NewFormComponents,\n NewStructure,\n NewWidgets,\n Registry,\n Structure,\n} from './registry'\n\ntype Contexts = {\n fieldContext: Context<AnyFieldApi>\n formContext: Context<AnyFormApi>\n}\n\ntype RegistryParts<TWidgets, TFormComponents, TStructure> = Contexts & {\n widgets: TWidgets\n formComponents: TFormComponents\n structure: TStructure\n}\n\nfunction buildRegistry<\n TWidgets extends Components,\n TFormComponents extends Components,\n TStructure extends Structure,\n>(\n parts: RegistryParts<TWidgets, TFormComponents, TStructure>,\n): Registry<TWidgets, TFormComponents, TStructure> {\n const { extendForm: _replacedByExtendRegistry, ...formHook } = createFormHook({\n fieldContext: parts.fieldContext,\n formContext: parts.formContext,\n fieldComponents: parts.widgets,\n formComponents: parts.formComponents,\n })\n\n function extendRegistry<\n const TNewWidgets extends NewWidgets<TWidgets> = Record<never, never>,\n const TNewFormComponents extends NewFormComponents<TFormComponents> = Record<never, never>,\n const TNewStructure extends NewStructure<TStructure> = Record<never, never>,\n >(extension: {\n widgets?: TNewWidgets\n formComponents?: TNewFormComponents\n structure?: TNewStructure & CheckedStructure<TNewStructure>\n }): Registry<\n TWidgets & TNewWidgets,\n TFormComponents & TNewFormComponents,\n TStructure & TNewStructure\n > {\n const addedStructure: Structure = extension.structure ?? {}\n\n // The extension's maps are optional, and spreading an optional generic map drops its type.\n return buildRegistry({\n fieldContext: parts.fieldContext,\n formContext: parts.formContext,\n widgets: { ...parts.widgets, ...extension.widgets } as TWidgets & TNewWidgets,\n formComponents: { ...parts.formComponents, ...extension.formComponents } as TFormComponents &\n TNewFormComponents,\n structure: { ...parts.structure, ...addedStructure } as TStructure & TNewStructure,\n })\n }\n\n // TanStack's hook is generic over the form's types; the config's types are checked at the\n // registry's `createForm`, so the binding passes the merged options on untyped.\n const binding: FormBinding = {\n useAppForm: formHook.useAppForm as unknown as FormBinding['useAppForm'],\n parts: {\n widgets: parts.widgets,\n formComponents: parts.formComponents,\n structure: parts.structure,\n },\n }\n\n // biome-ignore lint/correctness/noComponentHookFactories: created once per registry, like TanStack's createFormHook.\n const BoundFields = (props: Omit<FieldsProps, 'parts'>): ReactNode =>\n createElement(Fields, { ...props, parts: binding.parts })\n\n return {\n formHook,\n createForm: (config) => createForm(binding, config),\n // The member's signature types the level by the form it is given; the renderer reads it untyped.\n Fields: BoundFields as unknown as Registry<TWidgets, TFormComponents, TStructure>['Fields'],\n extendRegistry,\n }\n}\n\n/**\n * Binds widgets, form components and structure slots to the app's TanStack contexts and returns a\n * registry, the value every form of the app is built with.\n *\n * Create the contexts with TanStack's `createFormHookContexts()` in a module of their own, so a\n * form component can read the form through `useFormContext()` without importing the registry.\n *\n * @typeParam TWidgets - The widgets by name; each takes its field as a prop typed `FieldOf<V>`.\n * @typeParam TFormComponents - The form components by name, placed in a layout by `{ component }`.\n * @typeParam TStructure - The structure slots: `body`, `section`, `row` and `arrayFrame`.\n * @param config - The contexts and what the registry holds.\n * @param config.fieldContext - The field context from `createFormHookContexts()`.\n * @param config.formContext - The form context from `createFormHookContexts()`.\n * @param config.widgets - The widgets by name.\n * @param config.formComponents - The form components by name.\n * @param config.structure - The components that render the structure slots.\n * @defaultValue `config.formComponents` and `config.structure`: none registered.\n * @returns The registry.\n *\n * @example\n * ```tsx\n * 'use client'\n * import { createRegistry } from '@formdef/core'\n * import { fieldContext, formContext } from './context'\n *\n * export const registry = createRegistry({\n * fieldContext,\n * formContext,\n * widgets: { text: TextField, checkbox: CheckboxField },\n * formComponents: { SubmitButton },\n * structure: { section: Section },\n * })\n * ```\n */\nexport function createRegistry<\n const TWidgets extends Components,\n const TFormComponents extends Components = Record<never, never>,\n const TStructure extends Structure = Record<never, never>,\n>(\n config: Contexts & {\n widgets: TWidgets\n formComponents?: TFormComponents\n structure?: TStructure & CheckedStructure<TStructure>\n },\n): Registry<TWidgets, TFormComponents, TStructure> {\n // Both maps fall back to their empty defaults exactly when they are left out.\n return buildRegistry({\n fieldContext: config.fieldContext,\n formContext: config.formContext,\n widgets: config.widgets,\n formComponents: config.formComponents ?? ({} as TFormComponents),\n structure: config.structure ?? ({} as TStructure),\n })\n}\n","import type { FieldApiOf } from './field-of'\n\nfunction textOf(error: unknown): string | undefined {\n if (typeof error === 'string') {\n return error\n }\n if (typeof error !== 'object' || error === null || !('message' in error)) {\n return undefined\n }\n\n return typeof error.message === 'string' ? error.message : undefined\n}\n\nfunction hasText(text: string | undefined): text is string {\n return text !== undefined && text !== ''\n}\n\n/**\n * Turns the errors of a field into the texts a widget renders.\n *\n * An error is either a string or an object with a `message`, such as an issue of a Standard\n * Schema. Errors without text are left out, and a text that several validators report appears\n * once, where it first occurs. A field's errors stay readable with TanStack's `disableErrorFlat`\n * option.\n *\n * @typeParam TValue - The value type of the field.\n * @param field - The field the widget renders.\n * @returns The distinct texts of the field's current errors, in order; empty when there are none.\n *\n * @example\n * ```tsx\n * const TextField = ({ field }: FieldOf<string>) => (\n * <>\n * <input value={field.state.value} onChange={(event) => field.handleChange(event.target.value)} />\n * <p>{errorsOf(field).join(' ')}</p>\n * </>\n * )\n * ```\n */\nexport function errorsOf<TValue>(field: FieldApiOf<TValue>): string[] {\n const texts = field.state.meta.errors.flat().map(textOf).filter(hasText)\n\n return [...new Set(texts)]\n}\n","const PATH_SEPARATORS = /[.[\\]]+/g\nconst SELECTOR_SAFE_SEPARATOR = '__'\n\n/**\n * Builds the DOM id that `fieldParts` gives the control of a field, for code that has no\n * field at hand — an error summary above the form that links to each invalid field, for example.\n *\n * The id is the form's id, a hyphen, and the field's path with every run of dots and brackets\n * replaced by `__`. Fields of two different forms therefore never share an id, and the id can be\n * used in a CSS selector without escaping.\n *\n * @param formId - The id of the form, as TanStack Form reports it in `form.formId`.\n * @param path - The path of the field, such as `email`, `address.zip` or `contacts[0].name`.\n * @returns The id of the field's control, such as `signup-address__zip`.\n *\n * @remarks\n * The replacement cannot be reversed: a field literally named `a__b` gets the same id as the\n * path `a.b`.\n *\n * @example\n * ```tsx\n * <a href={`#${fieldId(form.formId, 'address.zip')}`}>Postal code</a>\n * ```\n */\nexport function fieldId(formId: string, path: string): string {\n return `${formId}-${path.replace(PATH_SEPARATORS, SELECTOR_SAFE_SEPARATOR)}`\n}\n","import { errorsOf } from './errors-of'\nimport { fieldId } from './field-id'\nimport type { FieldApiOf } from './field-of'\n\nconst DESCRIPTION_SUFFIX = '-description'\nconst ERROR_SUFFIX = '-error'\nconst IDREF_SEPARATOR = ' '\n\n/**\n * What {@link fieldParts} returns: the wiring of a widget's control and what it needs to render\n * its description and its errors.\n */\nexport interface FieldParts {\n /** Props to spread onto the control; its value and change handler stay with the widget. */\n control: {\n /** The control's id, as {@link fieldId} builds it. */\n id: string\n /** The field's path, so the browser can autofill the control. */\n name: string\n /** Reports to the field that the control was left. */\n onBlur: () => void\n /** `true` while an error is due; absent otherwise, never `false`. */\n 'aria-invalid': true | undefined\n /** The ids of the description and of a due error with text, when there are any. */\n 'aria-describedby': string | undefined\n /** The id of the error element, while an error with text is due. */\n 'aria-errormessage': string | undefined\n }\n /** The id to give the element that renders the description. */\n descriptionId: string\n /** The id to give the element that renders the error texts. */\n errorId: string\n /**\n * Whether the widget shows the field's errors now: the field is invalid and was edited and\n * left, or the form was submitted.\n */\n isErrorDue: boolean\n /** The texts of the field's errors, as {@link errorsOf} returns them. */\n errors: string[]\n}\n\nfunction isErrorDueFor<TValue>(field: FieldApiOf<TValue>): boolean {\n const { isBlurred, isDirty, isValid } = field.state.meta\n const wasEditedAndLeft = isBlurred && isDirty\n const wasSubmitted = field.form.state.submissionAttempts > 0\n\n return !isValid && (wasEditedAndLeft || wasSubmitted)\n}\n\nfunction idrefsOf(ids: (string | undefined)[]): string | undefined {\n const present = ids.filter((id) => id !== undefined)\n\n return present.length > 0 ? present.join(IDREF_SEPARATOR) : undefined\n}\n\n/**\n * Wires a widget's control to its field: its id and name, the blur report, and the aria\n * attributes that tie it to its description and its errors.\n *\n * An error is due once the field is invalid and was edited and left, or once the form was\n * submitted. While one is due, `aria-invalid` is `true`; while one with text is due, the control\n * points at the error element through `aria-describedby` and `aria-errormessage`. An id appears in\n * them only when its element is rendered.\n *\n * @typeParam TValue - The value type of the field.\n * @param field - The field the widget renders.\n * @param options - What the widget renders besides the control.\n * @param options.description - The description the widget renders below its label, if any. Only\n * whether it is a non-empty string is read.\n * @defaultValue `options.description`: `undefined`, no description is rendered.\n * @returns The props for the control, the ids for the description and error elements, whether an\n * error is due, and the error texts.\n *\n * @example\n * ```tsx\n * const TextField = ({ field, label, description }: FieldOf<string> & { label: string; description?: string }) => {\n * const { control, descriptionId, errorId, isErrorDue, errors } = fieldParts(field, { description })\n *\n * return (\n * <div>\n * <label htmlFor={control.id}>{label}</label>\n * {description && <p id={descriptionId}>{description}</p>}\n * <input {...control} value={field.state.value} onChange={(event) => field.handleChange(event.target.value)} />\n * {isErrorDue && errors.length > 0 && <p id={errorId}>{errors.join(' ')}</p>}\n * </div>\n * )\n * }\n * ```\n */\nexport function fieldParts<TValue>(\n field: FieldApiOf<TValue>,\n options: { description?: string | undefined } = {},\n): FieldParts {\n const id = fieldId(field.form.formId, field.name)\n const descriptionId = `${id}${DESCRIPTION_SUFFIX}`\n const errorId = `${id}${ERROR_SUFFIX}`\n const isErrorDue = isErrorDueFor(field)\n const errors = errorsOf(field)\n const isErrorNamed = isErrorDue && errors.length > 0\n const hasDescription = options.description !== undefined && options.description !== ''\n\n return {\n control: {\n id,\n name: field.name,\n onBlur: field.handleBlur,\n 'aria-invalid': isErrorDue || undefined,\n 'aria-describedby': idrefsOf([\n hasDescription ? descriptionId : undefined,\n isErrorNamed ? errorId : undefined,\n ]),\n 'aria-errormessage': isErrorNamed ? errorId : undefined,\n },\n descriptionId,\n errorId,\n isErrorDue,\n errors,\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/widget/declares-empty-value.ts","../src/render/array-entry.tsx","../src/render/by-name.ts","../src/render/child-path.ts","../src/render/derived.tsx","../src/render/path-pattern.ts","../src/render/required-paths.ts","../src/render/split-entry.ts","../src/render/warn-once.ts","../src/render/fields.tsx","../src/form/find-required-paths.ts","../src/form/blur-then-change.ts","../src/form/form-options-for.ts","../src/form/create-form.tsx","../src/registry/create-registry.ts","../src/widget/errors-of.ts","../src/widget/field-id.ts","../src/widget/field-parts.ts"],"sourcesContent":["/** Whether `widget` declares its empty value, the value it holds while nothing is entered. */\nexport function declaresEmptyValue(widget: object): widget is { emptyValue: unknown } {\n return Object.hasOwn(widget, 'emptyValue')\n}\n","import { type AnyFieldApi, useSelector } from '@tanstack/react-form'\nimport { type ComponentType, Fragment, type ReactNode } from 'react'\n\n// biome-ignore lint/suspicious/noExplicitAny: a registered frame takes props of its own; the config types check them.\ntype AnyProps = any\n\n/** The keys of an array entry the renderer reads besides its item. */\ntype ArrayOptions = {\n newItem?: unknown\n addLabel?: unknown\n removeLabel?: unknown\n itemKey?: unknown\n}\n\ntype ArrayEntryProps = {\n /** The array field, mounted at the entry's path. */\n field: AnyFieldApi\n options: ArrayOptions\n /** The registered `structure.arrayFrame`, if any. */\n frame: ComponentType<AnyProps> | undefined\n /** The entry's props for the frame. */\n props: object | undefined\n renderItem: (index: number, extra?: object) => ReactNode\n}\n\ntype ItemFunction<TResult> = (item: unknown, index?: number) => TResult\n\n/** What the controls of an array entry show: one key and one remove label per item. */\ntype ItemControls = {\n keys: (string | number)[]\n removeLabels: string[]\n}\n\nconst removeLabelOf = (removeLabel: unknown, item: unknown, index: number): string =>\n typeof removeLabel === 'function'\n ? (removeLabel as ItemFunction<string>)(item, index)\n : String(removeLabel)\n\nconst itemControlsOf = (options: ArrayOptions, value: unknown): ItemControls => {\n const items: unknown[] = Array.isArray(value) ? value : []\n\n return {\n keys: items.map((item, index) =>\n typeof options.itemKey === 'function'\n ? (options.itemKey as ItemFunction<string | number>)(item)\n : index,\n ),\n removeLabels: items.map((item, index) => removeLabelOf(options.removeLabel, item, index)),\n }\n}\n\nconst isSameList = <TItem,>(first: readonly TItem[], second: readonly TItem[]): boolean =>\n first.length === second.length && first.every((item, index) => item === second[index])\n\nconst isSameControls = (first: ItemControls, second: ItemControls): boolean =>\n isSameList(first.keys, second.keys) && isSameList(first.removeLabels, second.removeLabels)\n\n/**\n * Renders an array entry: through the registered frame, which receives `ArrayFrameProps`, or as\n * the bare default, each item followed by its remove button and one add button at the end.\n */\nexport const ArrayEntry = ({\n field,\n options,\n frame: Frame,\n props,\n renderItem,\n}: ArrayEntryProps): ReactNode => {\n // The field renders in TanStack's array mode, so only a change to the list reaches it; keys and\n // labels that name an item follow the items through a selection of their own.\n const { keys, removeLabels } = useSelector(\n field.store,\n (state: { value: unknown }) => itemControlsOf(options, state.value),\n { compare: isSameControls },\n )\n const newItem = options.newItem as () => unknown\n const addLabel = String(options.addLabel)\n const add = (): void => field.pushValue(newItem())\n const remove = (index: number): void => field.removeValue(index)\n\n if (Frame === undefined) {\n return (\n <>\n {keys.map((key, index) => (\n <Fragment key={key}>\n {renderItem(index)}\n <button type=\"button\" data-formdef-remove=\"\" onClick={() => remove(index)}>\n {removeLabels[index]}\n </button>\n </Fragment>\n ))}\n <button type=\"button\" data-formdef-add=\"\" onClick={add}>\n {addLabel}\n </button>\n </>\n )\n }\n\n return (\n <Frame\n {...props}\n field={field}\n keys={keys}\n renderItem={renderItem}\n addLabel={addLabel}\n removeLabels={removeLabels}\n add={add}\n remove={remove}\n insert={(index: number, item?: unknown) =>\n field.insertValue(index, item === undefined ? newItem() : item)\n }\n move={(from: number, to: number) => field.moveValue(from, to)}\n swap={(first: number, second: number) => field.swapValues(first, second)}\n replace={(index: number, item: unknown) => field.replaceValue(index, item)}\n clear={() => field.clearValues()}\n />\n )\n}\n","/** What `map` holds under `name` itself; a name every object inherits finds nothing. */\nexport function byName<TValue>(\n map: Readonly<Record<string, TValue>>,\n name: string,\n): TValue | undefined {\n return Object.hasOwn(map, name) ? map[name] : undefined\n}\n","/**\n * The path of `key` below `parent`: a key of the root level by itself, a key of a nested level\n * after a dot, the index of an array item in brackets.\n */\nexport function childPath(parent: string | undefined, key: string | number): string {\n if (typeof key === 'number') {\n return `${parent ?? ''}[${key}]`\n }\n\n return parent === undefined ? key : `${parent}.${key}`\n}\n","import { getBy } from '@tanstack/react-form'\nimport { type ReactNode, useRef } from 'react'\n\nimport type { RenderForm } from './render-form'\n\n/** What an entry reads from the form values: its condition, its props and the paths they read. */\ntype DerivedEntry = {\n visibleWhen?: unknown\n props?: unknown\n dependsOn?: unknown\n}\n\ntype DerivedProps = {\n form: RenderForm\n entry: DerivedEntry\n /** Builds the entry from its props; called only while the entry is visible. */\n children: (props: object | undefined) => ReactNode\n}\n\ntype LastRead = {\n entry: DerivedEntry\n dependencies: unknown[]\n selected: unknown\n}\n\nconst HIDDEN = Symbol('hidden')\nconst VISIBLE = Symbol('visible')\n\nconst propsOf = (props: unknown): object | undefined =>\n typeof props === 'object' && props !== null ? props : undefined\n\nconst isStale = (\n last: LastRead | undefined,\n entry: DerivedEntry,\n dependencies: unknown[],\n): boolean =>\n last === undefined ||\n last.entry !== entry ||\n dependencies.some((value, index) => !Object.is(value, last.dependencies[index]))\n\n/**\n * Renders an entry from what it reads from the form values. An entry with a condition or derived\n * props subscribes to the values; the selected result alone decides whether it re-renders: whether\n * it is shown for a condition, the values for derived props. `dependsOn` narrows when the selector\n * reads again, never what it returns. A hidden entry is never built.\n */\nexport const Derived = ({ form, entry, children }: DerivedProps): ReactNode => {\n const lastRead = useRef<LastRead | undefined>(undefined)\n const { visibleWhen, props, dependsOn } = entry\n const isConditional = typeof visibleWhen === 'function'\n const isDerived = typeof props === 'function'\n\n if (!(isConditional || isDerived)) {\n return children(propsOf(props))\n }\n\n const selectFrom = (values: unknown): unknown => {\n if (isConditional && !visibleWhen(values)) {\n return HIDDEN\n }\n\n return isDerived ? values : VISIBLE\n }\n\n const select = ({ values }: { values: unknown }): unknown => {\n if (!Array.isArray(dependsOn)) {\n return selectFrom(values)\n }\n\n const dependencies = dependsOn.map((path: string) => getBy(values, path))\n if (isStale(lastRead.current, entry, dependencies)) {\n lastRead.current = { entry, dependencies, selected: selectFrom(values) }\n }\n\n return lastRead.current?.selected\n }\n\n return (\n <form.Subscribe selector={select}>\n {(selected) => {\n if (selected === HIDDEN) {\n return null\n }\n\n return children(propsOf(isDerived ? props(selected) : props))\n }}\n </form.Subscribe>\n )\n}\n","const ITEM_INDEX = /\\[\\d+\\]/g\nconst ANY_ITEM = '[]'\n\n/** The path with every array index left open: `contacts[2].name` becomes `contacts[].name`. */\nexport function pathPattern(path: string): string {\n return path.replaceAll(ITEM_INDEX, ANY_ITEM)\n}\n","import { type Context, createContext } from 'react'\n\nexport const NO_REQUIRED_PATHS: ReadonlySet<string> = new Set()\n\n/** The path patterns of the required fields of the level being rendered. */\nexport const RequiredPaths: Context<ReadonlySet<string>> = createContext(NO_REQUIRED_PATHS)\n","/**\n * The keys formdef reads from a field entry. Every other key is one of TanStack's field options and\n * goes to the field unchanged, so an option TanStack adds works without a formdef release.\n */\nexport const OWN_ENTRY_KEYS = [\n 'widget',\n 'props',\n 'fields',\n 'layout',\n 'item',\n 'newItem',\n 'addLabel',\n 'removeLabel',\n 'itemKey',\n 'render',\n 'visibleWhen',\n 'dependsOn',\n] as const\n\n/** A key formdef reads from a field entry. */\nexport type OwnEntryKey = (typeof OWN_ENTRY_KEYS)[number]\n\nconst OWN_ENTRY_KEY_SET: ReadonlySet<string> = new Set(OWN_ENTRY_KEYS)\n\n/** Splits a field entry into the keys formdef reads and TanStack's field options. */\nexport function splitEntry(entry: object): {\n own: Partial<Record<OwnEntryKey, unknown>>\n fieldOptions: Record<string, unknown>\n} {\n const entries = Object.entries(entry)\n\n // `fromEntries` defines each key as data, so a key named `__proto__` cannot set a prototype.\n return {\n own: Object.fromEntries(entries.filter(([key]) => OWN_ENTRY_KEY_SET.has(key))),\n fieldOptions: Object.fromEntries(entries.filter(([key]) => !OWN_ENTRY_KEY_SET.has(key))),\n }\n}\n","// Bundlers replace `process.env.NODE_ENV` in exactly this spelling; without one, `process` may not\n// exist at all.\n// biome-ignore lint/style/useNamingConvention: the name bundlers replace.\ndeclare const process: { env: { NODE_ENV?: string } } | undefined\n\nconst warnedOwners = new WeakSet<object>()\n\nconst isProduction = (): boolean =>\n typeof process !== 'undefined' && process.env.NODE_ENV === 'production'\n\n/** Warns once per `owner`, and never in production. */\nexport function warnOnce(owner: object, message: string): void {\n if (isProduction() || warnedOwners.has(owner)) {\n return\n }\n\n warnedOwners.add(owner)\n console.warn(message)\n}\n","import { type ComponentType, type ReactNode, use } from 'react'\n\nimport { declaresEmptyValue } from '../widget/declares-empty-value'\n\nimport { ArrayEntry } from './array-entry'\nimport { byName } from './by-name'\nimport { childPath } from './child-path'\nimport { Derived } from './derived'\nimport { pathPattern } from './path-pattern'\nimport type { AppFieldProps, RenderForm } from './render-form'\nimport { NO_REQUIRED_PATHS, RequiredPaths } from './required-paths'\nimport { splitEntry } from './split-entry'\nimport { warnOnce } from './warn-once'\n\n// biome-ignore lint/suspicious/noExplicitAny: registered components take props of their own; the config types check them.\ntype AnyProps = any\n\ntype ComponentMap = Readonly<Record<string, ComponentType<AnyProps>>>\n\n/** The maps of a registry the renderer looks names up in. */\ntype RenderParts = {\n widgets: ComponentMap\n formComponents: ComponentMap\n structure: {\n section?: ComponentType<AnyProps>\n row?: ComponentType<AnyProps>\n arrayFrame?: ComponentType<AnyProps>\n }\n}\n\ntype LevelFields = Readonly<Record<string, object>>\n\ntype LayoutNodeEntry = string | Readonly<Record<string, unknown>>\n\n/** The props of {@link Fields}. */\nexport type FieldsProps = {\n parts: RenderParts\n form: RenderForm\n fields: LevelFields\n layout?: readonly unknown[] | undefined\n /** The path patterns of the required fields; none unless `createForm` renders the level. */\n requiredPaths?: ReadonlySet<string> | undefined\n}\n\ntype Level = Omit<FieldsProps, 'layout'> & {\n path: string | undefined\n /** Props an array frame passes the one widget of an item, through this level. */\n extra?: object | undefined\n}\n\ntype LevelProps = Level & {\n layout: readonly unknown[] | undefined\n}\n\ntype EntryProps = Omit<Level, 'fields'> & {\n entry: object\n path: string\n}\n\ntype FieldContentProps = Omit<EntryProps, 'entry'> & {\n entry: ReturnType<typeof splitEntry> & { source: object }\n props: object | undefined\n}\n\ntype ItemProps = Omit<EntryProps, 'extra'> & {\n index: number\n extra: object | undefined\n}\n\ntype LayoutNodeProps = {\n level: Level\n entry: LayoutNodeEntry\n}\n\ntype LayoutKindProps = {\n level: Level\n entry: Readonly<Record<string, unknown>>\n props: object | undefined\n}\n\nconst ARRAY_MODE = 'array'\n\nconst DROPPED_EXTRA = 'passes extra props to an item that has no single widget to receive them'\n\nconst renderNothing = (): ReactNode => null\n\nconst layoutOwner = (path: string | undefined): string =>\n path === undefined ? 'the form' : `\"${path}\"`\n\nconst isLeaf = (entry: unknown): boolean =>\n typeof entry === 'object' && entry !== null && 'widget' in entry\n\n// A frame's extra props have a single target only in a leaf, or an object level whose one field\n// is a leaf and which has no layout.\nconst takesExtra = (item: object): boolean => {\n if (isLeaf(item)) {\n return true\n }\n const { own } = splitEntry(item)\n const fields =\n typeof own.fields === 'object' && own.fields !== null ? Object.values(own.fields) : []\n\n return own.layout === undefined && fields.length === 1 && isLeaf(fields[0])\n}\n\nconst Item = ({ parts, form, entry, path, index, extra }: ItemProps): ReactNode => {\n const { own } = splitEntry(entry)\n const item = own.item as object\n const reachesWidget = extra !== undefined && takesExtra(item)\n if (extra !== undefined && !reachesWidget) {\n warnOnce(entry, `formdef: the array frame of \"${path}\" ${DROPPED_EXTRA}; they are dropped.`)\n }\n\n return (\n <Entry\n parts={parts}\n form={form}\n entry={item}\n path={childPath(path, index)}\n extra={reachesWidget ? extra : undefined}\n />\n )\n}\n\nconst FieldContent = ({ parts, form, entry, path, props, extra }: FieldContentProps): ReactNode => {\n const { own, fieldOptions } = entry\n const requiredPaths = use(RequiredPaths)\n\n if (typeof own.widget === 'string') {\n const Widget = byName(parts.widgets, own.widget)\n if (Widget === undefined) {\n throw new Error(`formdef: field \"${path}\" names the unregistered widget \"${own.widget}\".`)\n }\n\n // The config's `required` comes after the found one, so it wins.\n const found = declaresEmptyValue(Widget)\n ? { required: requiredPaths.has(pathPattern(path)) }\n : undefined\n\n return (\n <form.AppField {...fieldOptions} name={path}>\n {(field) => <Widget {...found} {...props} {...extra} field={field} />}\n </form.AppField>\n )\n }\n\n if (typeof own.render === 'function') {\n return (\n <form.AppField {...fieldOptions} name={path}>\n {own.render as AppFieldProps['children']}\n </form.AppField>\n )\n }\n\n if (typeof own.item === 'object' && own.item !== null) {\n return (\n <form.AppField {...fieldOptions} name={path} mode={ARRAY_MODE}>\n {(field) => (\n <ArrayEntry\n field={field}\n options={own}\n frame={parts.structure.arrayFrame}\n props={props}\n renderItem={(index, itemExtra) => (\n <Item\n parts={parts}\n form={form}\n entry={entry.source}\n path={path}\n index={index}\n extra={itemExtra}\n />\n )}\n />\n )}\n </form.AppField>\n )\n }\n\n if (typeof own.fields !== 'object' || own.fields === null) {\n return null\n }\n\n return (\n <>\n {Object.keys(fieldOptions).length > 0 && (\n <form.AppField {...fieldOptions} name={path}>\n {renderNothing}\n </form.AppField>\n )}\n <LevelEntries\n parts={parts}\n form={form}\n fields={own.fields as LevelFields}\n layout={Array.isArray(own.layout) ? own.layout : undefined}\n path={path}\n extra={extra}\n />\n </>\n )\n}\n\nconst Entry = ({ parts, form, entry, path, extra }: EntryProps): ReactNode => {\n const split = { ...splitEntry(entry), source: entry }\n\n return (\n <Derived form={form} entry={split.own}>\n {(props) => (\n <FieldContent\n parts={parts}\n form={form}\n entry={split}\n path={path}\n props={props}\n extra={extra}\n />\n )}\n </Derived>\n )\n}\n\nconst FieldNode = ({ level, name }: { level: Level; name: string }): ReactNode => {\n const entry = byName(level.fields, name)\n if (entry === undefined) {\n throw new Error(\n `formdef: the layout of ${layoutOwner(level.path)} names \"${name}\", which the level does not describe.`,\n )\n }\n\n return (\n <Entry\n parts={level.parts}\n form={level.form}\n entry={entry}\n path={childPath(level.path, name)}\n extra={level.extra}\n />\n )\n}\n\nconst LayoutNodes = ({ level, entries }: { level: Level; entries: unknown }): ReactNode =>\n (Array.isArray(entries) ? (entries as LayoutNodeEntry[]) : []).map((entry, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: a layout comes from the config and never reorders; a name may appear twice.\n <LayoutNode key={index} level={level} entry={entry} />\n ))\n\nconst SectionNode = ({ level, entry, props }: LayoutKindProps): ReactNode => {\n const children = <LayoutNodes level={level} entries={entry.layout} />\n const Section = level.parts.structure.section\n\n if (Section !== undefined) {\n return (\n <Section {...props} label={entry.section}>\n {children}\n </Section>\n )\n }\n\n return (\n <fieldset {...props}>\n <legend>{entry.section as string}</legend>\n {children}\n </fieldset>\n )\n}\n\nconst RowNode = ({ level, entry, props }: LayoutKindProps): ReactNode => {\n const children = <LayoutNodes level={level} entries={entry.row} />\n const Row = level.parts.structure.row\n\n if (Row !== undefined) {\n return <Row {...props}>{children}</Row>\n }\n\n return (\n <div {...props} data-formdef-row=\"\">\n {children}\n </div>\n )\n}\n\nconst ComponentNode = ({ level, entry, props }: LayoutKindProps): ReactNode => {\n const name = entry.component as string\n const Component = byName(level.parts.formComponents, name)\n if (Component === undefined) {\n throw new Error(\n `formdef: the layout of ${layoutOwner(level.path)} places the unregistered form component \"${name}\".`,\n )\n }\n\n return <Component {...props} />\n}\n\nconst LayoutKind = ({ level, entry, props }: LayoutKindProps): ReactNode => {\n if ('section' in entry) {\n return <SectionNode level={level} entry={entry} props={props} />\n }\n if ('row' in entry) {\n return <RowNode level={level} entry={entry} props={props} />\n }\n if (typeof entry.component === 'string') {\n return <ComponentNode level={level} entry={entry} props={props} />\n }\n if (typeof entry.render === 'function') {\n return (entry.render as (form: RenderForm) => ReactNode)(level.form)\n }\n\n return null\n}\n\nconst LayoutNode = ({ level, entry }: LayoutNodeProps): ReactNode => {\n if (typeof entry === 'string') {\n return <FieldNode level={level} name={entry} />\n }\n\n return (\n <Derived form={level.form} entry={entry}>\n {(props) => <LayoutKind level={level} entry={entry} props={props} />}\n </Derived>\n )\n}\n\nconst LevelEntries = ({ layout, ...level }: LevelProps): ReactNode => {\n if (layout !== undefined) {\n return <LayoutNodes level={level} entries={layout} />\n }\n\n return Object.keys(level.fields).map((name) => <FieldNode key={name} level={level} name={name} />)\n}\n\n/**\n * Renders the field entries of a level with the widgets of `parts`. Without a `layout` the entries\n * appear in the order `fields` declares them; with one, the layout decides the order, groups them\n * into sections and rows and places form components and inline entries. A leaf renders as its\n * widget, an object as its own level, a custom entry through its `render`, an array through the\n * registered `structure.arrayFrame` or the bare default. An object entry with TanStack options of\n * its own also mounts a field at its path, so its validators run. A `Fields` nested in a form finds\n * no required fields of its own, since its paths may be relative to a field group.\n */\nexport const Fields = ({ parts, form, fields, layout, requiredPaths }: FieldsProps): ReactNode => (\n <RequiredPaths value={requiredPaths ?? NO_REQUIRED_PATHS}>\n <LevelEntries parts={parts} form={form} fields={fields} layout={layout} path={undefined} />\n </RequiredPaths>\n)\n","import type { StandardSchemaV1 } from '@tanstack/react-form'\n\nimport { byName } from '../render/by-name'\nimport { childPath } from '../render/child-path'\nimport { pathPattern } from '../render/path-pattern'\nimport { splitEntry } from '../render/split-entry'\nimport { declaresEmptyValue } from '../widget/declares-empty-value'\n\ntype LevelFields = Readonly<Record<string, object>>\n\ntype Widgets = Readonly<Record<string, object>>\n\ntype RequiredPathsInput = {\n schema: unknown\n /** Whether the config validates the schema asynchronously (`validation.async`). */\n validatesAsync: boolean\n fields: LevelFields\n widgets: Widgets\n}\n\n/** The path patterns of the required fields, or why none could be found. */\ntype RequiredPathsResult = { paths: ReadonlySet<string> } | { problem: string }\n\ntype EmptyValues = {\n widgets: Widgets\n /** The path patterns of the leaves whose widget declares an empty value. */\n declared: Set<string>\n}\n\ntype Validation = ReturnType<StandardSchemaV1['~standard']['validate']>\ntype Issue = NonNullable<Exclude<Validation, Promise<unknown>>['issues']>[number]\n\nconst NONE: RequiredPathsResult = { paths: new Set() }\n\nconst ASYNC_SCHEMA =\n 'formdef: the schema validates asynchronously, so no field is found to be required; pass required in props.'\nconst UNSETTLED_SCHEMA =\n 'formdef: the schema did not validate empty values synchronously, so no field is found to be required; pass required in props.'\nconst THROWING_SCHEMA =\n 'formdef: the schema threw on empty values, so no field is found to be required'\n\nconst FIRST_ITEM = 0\n\nconst isStandardSchema = (schema: unknown): schema is StandardSchemaV1 =>\n typeof schema === 'object' && schema !== null && '~standard' in schema\n\nconst emptyLevel = (fields: LevelFields, path: string | undefined, empty: EmptyValues): object =>\n Object.fromEntries(\n Object.entries(fields).map(([key, entry]) => [\n key,\n emptyEntry(entry, childPath(path, key), empty),\n ]),\n )\n\nfunction emptyEntry(entry: object, path: string, empty: EmptyValues): unknown {\n const { own } = splitEntry(entry)\n\n if (typeof own.widget === 'string') {\n const widget = byName(empty.widgets, own.widget)\n if (widget === undefined || !declaresEmptyValue(widget)) {\n return undefined\n }\n empty.declared.add(pathPattern(path))\n\n return widget.emptyValue\n }\n if (typeof own.item === 'object' && own.item !== null) {\n return [emptyEntry(own.item, childPath(path, FIRST_ITEM), empty)]\n }\n if (typeof own.fields === 'object' && own.fields !== null) {\n return emptyLevel(own.fields as LevelFields, path, empty)\n }\n\n return undefined\n}\n\nconst issuePath = (issue: Issue): string =>\n pathPattern(\n (issue.path ?? []).reduce<string | undefined>((parent, segment) => {\n const key = typeof segment === 'object' ? segment.key : segment\n\n return childPath(parent, typeof key === 'number' ? key : String(key))\n }, undefined) ?? '',\n )\n\nconst parentOf = (path: string): string =>\n path.slice(0, Math.max(path.lastIndexOf('.'), path.lastIndexOf('[')))\n\n// A leaf's value may be an object or a list, so an issue below a leaf's path is the leaf's.\nconst leafOf = (path: string, leaves: ReadonlySet<string>): string | undefined => {\n for (let candidate = path; candidate !== ''; candidate = parentOf(candidate)) {\n if (leaves.has(candidate)) {\n return candidate\n }\n }\n\n return undefined\n}\n\n// The promise is dropped unread; a rejection is the schema failing on empty values, not the form.\nconst ignoreRejection = (): undefined => undefined\n\nconst errorText = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\n/**\n * Finds the required fields of a config: validates `schema` once, synchronously, with every leaf\n * holding its widget's empty value and every array one such item. A leaf whose widget declares an\n * empty value and whose path receives an issue is required. A schema the config validates\n * asynchronously is not run, so its asynchronous checks send nothing.\n */\nexport function findRequiredPaths({\n schema,\n validatesAsync,\n fields,\n widgets,\n}: RequiredPathsInput): RequiredPathsResult {\n if (!isStandardSchema(schema)) {\n return NONE\n }\n if (validatesAsync) {\n return { problem: ASYNC_SCHEMA }\n }\n\n const empty: EmptyValues = { widgets, declared: new Set() }\n const values = emptyLevel(fields, undefined, empty)\n let result: Validation\n try {\n result = schema['~standard'].validate(values)\n } catch (error) {\n return { problem: `${THROWING_SCHEMA}: ${errorText(error)}` }\n }\n if (result instanceof Promise) {\n result.then(undefined, ignoreRejection)\n\n return { problem: UNSETTLED_SCHEMA }\n }\n\n const leaves = (result.issues ?? []).map((issue) => leafOf(issuePath(issue), empty.declared))\n\n return { paths: new Set(leaves.filter((leaf) => leaf !== undefined)) }\n}\n","import {\n defaultValidationLogic,\n type ValidationLogicFn,\n type ValidationLogicProps,\n} from '@tanstack/react-form'\n\ntype ScheduledValidators = Parameters<ValidationLogicProps['runValidation']>[0]['validators']\n\n// Gated on the form, not the changed field: the schema validates the whole form, and TanStack names\n// the changed field only on the field-level call, where the schema does not sit.\nconst isSchemaDue = ({ event, form }: ValidationLogicProps): boolean => {\n if (event.type === 'blur' || event.type === 'submit') {\n return true\n }\n\n return event.type === 'change' && !form.state.isValid\n}\n\n/**\n * The validation logic of the `blurThenChange` mode: TanStack's default logic, plus the schema in\n * `onDynamic` when a field is left, on submit, and on every change while the form is invalid.\n */\nexport const blurThenChange: ValidationLogicFn = (props) => {\n let defaults: ScheduledValidators = []\n defaultValidationLogic({\n ...props,\n runValidation: ({ validators }) => {\n defaults = validators\n },\n })\n\n const schema = props.event.async ? props.validators?.onDynamicAsync : props.validators?.onDynamic\n if (schema === undefined || !isSchemaDue(props)) {\n return props.runValidation({ validators: defaults, form: props.form })\n }\n\n return props.runValidation({\n validators: [...defaults, { fn: schema, cause: 'dynamic' }],\n form: props.form,\n })\n}\n","import { revalidateLogic, type ValidationLogicFn } from '@tanstack/react-form'\n\nimport type { Validation } from '../config/validation'\nimport { blurThenChange } from './blur-then-change'\n\ntype Mode = NonNullable<Validation['mode']>\ntype DynamicMode = 'revalidate' | 'blurThenChange'\ntype Slot = 'onDynamic' | 'onChange' | 'onBlur' | 'onSubmit'\n\ntype Options = Readonly<Record<string, unknown>>\n\ntype FormOptionsInput = {\n /** The form config: the schema, formdef's own keys and TanStack's form options. */\n config: Options\n /** The instance's TanStack form options, from the form component's `options` prop. */\n options: Options | undefined\n}\n\nconst DEFAULT_MODE: Mode = 'revalidate'\n\nconst isDynamic = (mode: Mode): mode is DynamicMode =>\n mode === 'revalidate' || mode === 'blurThenChange'\n\nconst logicFor = (mode: DynamicMode): ValidationLogicFn =>\n mode === 'revalidate' ? revalidateLogic() : blurThenChange\n\nconst slotFor = (mode: Mode, isAsync: boolean): Slot | `${Slot}Async` => {\n const slot: Slot = isDynamic(mode) ? 'onDynamic' : mode\n\n return isAsync ? `${slot}Async` : slot\n}\n\nconst recordOf = (value: unknown): Options =>\n typeof value === 'object' && value !== null ? (value as Options) : {}\n\n// An instance passing `undefined`, such as data still loading, leaves the config's value in place.\nconst setEntriesOf = (value: unknown): Options =>\n Object.fromEntries(Object.entries(recordOf(value)).filter(([, entry]) => entry !== undefined))\n\n/**\n * The TanStack form options of a form: the config's options and the instance's merged key by key,\n * the instance winning, except `validators` and `listeners`, which merge slot by slot; a key or\n * slot the instance sets to `undefined` counts as unset. The schema\n * goes into the slot `validation` picks unless a validator already fills it; a dynamic mode brings\n * its validation logic, an asynchronous schema its debounce unless the options set one.\n */\nexport function formOptionsFor({ config, options }: FormOptionsInput): Record<string, unknown> {\n const {\n schema,\n validation,\n fields: _renderedByFields,\n layout: _arrangedByFields,\n ...configOptions\n } = config\n const { mode = DEFAULT_MODE, ...timing } = recordOf(validation) as Validation\n const isAsync = timing.async === true\n const slot = slotFor(mode, isAsync)\n const instanceOptions = setEntriesOf(options)\n const validators = {\n ...recordOf(configOptions.validators),\n ...setEntriesOf(instanceOptions.validators),\n }\n\n return {\n ...(isDynamic(mode) && { validationLogic: logicFor(mode) }),\n ...(isAsync &&\n timing.asyncDebounceMs !== undefined && { asyncDebounceMs: timing.asyncDebounceMs }),\n ...configOptions,\n ...instanceOptions,\n validators: validators[slot] === undefined ? { ...validators, [slot]: schema } : validators,\n listeners: { ...recordOf(configOptions.listeners), ...setEntriesOf(instanceOptions.listeners) },\n }\n}\n","import type { ComponentType, FormEvent, ReactNode } from 'react'\n\nimport type { FormElementProps } from '../config/form-props'\nimport type { BodyProps } from '../config/structure'\nimport { Fields, type FieldsProps } from '../render/fields'\nimport type { RenderForm } from '../render/render-form'\nimport { warnOnce } from '../render/warn-once'\nimport { findRequiredPaths } from './find-required-paths'\nimport { formOptionsFor } from './form-options-for'\n\ntype Options = Readonly<Record<string, unknown>>\n\n/** The form `useAppForm` returns, as far as the shell uses it. */\ntype ShellForm = RenderForm & {\n AppForm: ComponentType<{ children?: ReactNode }>\n handleSubmit: () => Promise<void>\n}\n\n/** What a registry binds a form to: its form hook and the maps the renderer looks names up in. */\nexport type FormBinding = {\n useAppForm: (options: Options) => ShellForm\n parts: FieldsProps['parts'] & {\n structure: { body?: ComponentType<BodyProps> }\n }\n}\n\n/** The props of a form component: the instance's options and what a `<form>` element takes. */\ntype FormComponentProps = FormElementProps & {\n options?: Options\n}\n\n/** A form config: TanStack's form options plus formdef's own keys. */\ntype ConfigInput = Options & {\n fields?: unknown\n layout?: unknown\n}\n\ntype ConfiguredFormProps = FormComponentProps & {\n binding: FormBinding\n config: ConfigInput\n requiredPaths: ReadonlySet<string>\n}\n\nconst MISSING_DEFAULT_VALUES =\n 'formdef: the form has no default values; set defaultValues in its config or in its options.'\n\nconst ConfiguredForm = ({\n binding,\n config,\n requiredPaths,\n options,\n ...formProps\n}: ConfiguredFormProps): ReactNode => {\n const formOptions = formOptionsFor({ config, options })\n if (formOptions.defaultValues === undefined) {\n throw new Error(MISSING_DEFAULT_VALUES)\n }\n\n const form = binding.useAppForm(formOptions)\n const Body = binding.parts.structure.body\n const content = (\n <Fields\n parts={binding.parts}\n form={form}\n fields={config.fields as FieldsProps['fields']}\n layout={config.layout as FieldsProps['layout']}\n requiredPaths={requiredPaths}\n />\n )\n const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {\n event.preventDefault()\n event.stopPropagation()\n void form.handleSubmit()\n }\n\n return (\n <form.AppForm>\n <form noValidate {...formProps} onSubmit={handleSubmit}>\n {Body === undefined ? content : <Body>{content}</Body>}\n </form>\n </form.AppForm>\n )\n}\n\nconst isAsyncValidation = (validation: unknown): boolean =>\n typeof validation === 'object' && validation !== null && 'async' in validation\n ? validation.async === true\n : false\n\nconst requiredPathsOf = (binding: FormBinding, config: ConfigInput): ReadonlySet<string> => {\n const result = findRequiredPaths({\n schema: config.schema,\n validatesAsync: isAsyncValidation(config.validation),\n fields: (config.fields ?? {}) as Readonly<Record<string, object>>,\n widgets: binding.parts.widgets,\n })\n if ('problem' in result) {\n warnOnce(config, result.problem)\n\n return new Set()\n }\n\n return result.paths\n}\n\n/**\n * Turns a form config into a form component bound to `binding`: TanStack's form with the merged\n * options, a `<form>` element that submits through it, the registered body and the config's fields.\n * When it first renders, it finds the config's required fields once.\n */\nexport function createForm(\n binding: FormBinding,\n config: ConfigInput,\n): (props: FormComponentProps) => ReactNode {\n let requiredPaths: ReadonlySet<string> | undefined\n\n return (props: FormComponentProps): ReactNode => {\n requiredPaths ??= requiredPathsOf(binding, config)\n\n return (\n <ConfiguredForm {...props} binding={binding} config={config} requiredPaths={requiredPaths} />\n )\n }\n}\n","import { type AnyFieldApi, type AnyFormApi, createFormHook } from '@tanstack/react-form'\nimport { type Context, createElement, type ReactNode } from 'react'\n\nimport { createForm, type FormBinding } from '../form/create-form'\nimport { Fields, type FieldsProps } from '../render/fields'\n\nimport type {\n CheckedStructure,\n CheckedWidgets,\n Components,\n NewFormComponents,\n NewStructure,\n NewWidgets,\n Registry,\n Structure,\n} from './registry'\n\ntype Contexts = {\n fieldContext: Context<AnyFieldApi>\n formContext: Context<AnyFormApi>\n}\n\ntype RegistryParts<TWidgets, TFormComponents, TStructure> = Contexts & {\n widgets: TWidgets\n formComponents: TFormComponents\n structure: TStructure\n}\n\nfunction buildRegistry<\n TWidgets extends Components,\n TFormComponents extends Components,\n TStructure extends Structure,\n>(\n parts: RegistryParts<TWidgets, TFormComponents, TStructure>,\n): Registry<TWidgets, TFormComponents, TStructure> {\n const { extendForm: _replacedByExtendRegistry, ...formHook } = createFormHook({\n fieldContext: parts.fieldContext,\n formContext: parts.formContext,\n fieldComponents: parts.widgets,\n formComponents: parts.formComponents,\n })\n\n function extendRegistry<\n const TNewWidgets extends NewWidgets<TWidgets> = Record<never, never>,\n const TNewFormComponents extends NewFormComponents<TFormComponents> = Record<never, never>,\n const TNewStructure extends NewStructure<TStructure> = Record<never, never>,\n >(extension: {\n widgets?: TNewWidgets\n formComponents?: TNewFormComponents\n structure?: TNewStructure & CheckedStructure<TNewStructure>\n }): Registry<\n TWidgets & TNewWidgets,\n TFormComponents & TNewFormComponents,\n TStructure & TNewStructure\n > {\n const addedWidgets: Components = extension.widgets ?? {}\n const addedStructure: Structure = extension.structure ?? {}\n\n // The extension's maps are optional, and spreading an optional generic map drops its type.\n return buildRegistry({\n fieldContext: parts.fieldContext,\n formContext: parts.formContext,\n widgets: { ...parts.widgets, ...addedWidgets } as TWidgets & TNewWidgets,\n formComponents: { ...parts.formComponents, ...extension.formComponents } as TFormComponents &\n TNewFormComponents,\n structure: { ...parts.structure, ...addedStructure } as TStructure & TNewStructure,\n })\n }\n\n // TanStack's hook is generic over the form's types; the config's types are checked at the\n // registry's `createForm`, so the binding passes the merged options on untyped.\n const binding: FormBinding = {\n useAppForm: formHook.useAppForm as unknown as FormBinding['useAppForm'],\n parts: {\n widgets: parts.widgets,\n formComponents: parts.formComponents,\n structure: parts.structure,\n },\n }\n\n // biome-ignore lint/correctness/noComponentHookFactories: created once per registry, like TanStack's createFormHook.\n const BoundFields = (props: Omit<FieldsProps, 'parts'>): ReactNode =>\n createElement(Fields, { ...props, parts: binding.parts })\n\n return {\n formHook,\n createForm: (config) => createForm(binding, config),\n // The member's signature types the level by the form it is given; the renderer reads it untyped.\n Fields: BoundFields as unknown as Registry<TWidgets, TFormComponents, TStructure>['Fields'],\n // The member's signature checks the widgets' empty values; the implementation merges any map.\n extendRegistry: extendRegistry as Registry<\n TWidgets,\n TFormComponents,\n TStructure\n >['extendRegistry'],\n }\n}\n\n/**\n * Binds widgets, form components and structure slots to the app's TanStack contexts and returns a\n * registry, the value every form of the app is built with.\n *\n * Create the contexts with TanStack's `createFormHookContexts()` in a module of their own, so a\n * form component can read the form through `useFormContext()` without importing the registry.\n *\n * @typeParam TWidgets - The widgets by name; each takes its field as a prop typed `FieldOf<V>`.\n * @typeParam TFormComponents - The form components by name, placed in a layout by `{ component }`.\n * @typeParam TStructure - The structure slots: `body`, `section`, `row` and `arrayFrame`.\n * @param config - The contexts and what the registry holds.\n * @param config.fieldContext - The field context from `createFormHookContexts()`.\n * @param config.formContext - The form context from `createFormHookContexts()`.\n * @param config.widgets - The widgets by name. A widget's static `emptyValue`, the value it holds\n * while nothing is entered, must be of the value type it serves.\n * @param config.formComponents - The form components by name.\n * @param config.structure - The components that render the structure slots.\n * @defaultValue `config.formComponents` and `config.structure`: none registered.\n * @returns The registry.\n *\n * @example\n * ```tsx\n * 'use client'\n * import { createRegistry } from '@formdef/core'\n * import { fieldContext, formContext } from './context'\n *\n * export const registry = createRegistry({\n * fieldContext,\n * formContext,\n * widgets: { text: TextField, checkbox: CheckboxField },\n * formComponents: { SubmitButton },\n * structure: { section: Section },\n * })\n * ```\n */\nexport function createRegistry<\n const TWidgets extends Components,\n const TFormComponents extends Components = Record<never, never>,\n const TStructure extends Structure = Record<never, never>,\n>(\n config: Contexts & {\n widgets: TWidgets & CheckedWidgets<TWidgets>\n formComponents?: TFormComponents\n structure?: TStructure & CheckedStructure<TStructure>\n },\n): Registry<TWidgets, TFormComponents, TStructure> {\n // Both maps fall back to their empty defaults exactly when they are left out.\n return buildRegistry<TWidgets, TFormComponents, TStructure>({\n fieldContext: config.fieldContext,\n formContext: config.formContext,\n widgets: config.widgets,\n formComponents: config.formComponents ?? ({} as TFormComponents),\n structure: config.structure ?? ({} as TStructure),\n })\n}\n","import type { FieldApiOf } from './field-of'\n\nfunction textOf(error: unknown): string | undefined {\n if (typeof error === 'string') {\n return error\n }\n if (typeof error !== 'object' || error === null || !('message' in error)) {\n return undefined\n }\n\n return typeof error.message === 'string' ? error.message : undefined\n}\n\nfunction hasText(text: string | undefined): text is string {\n return text !== undefined && text !== ''\n}\n\n/**\n * Turns the errors of a field into the texts a widget renders.\n *\n * An error is either a string or an object with a `message`, such as an issue of a Standard\n * Schema. Errors without text are left out, and a text that several validators report appears\n * once, where it first occurs. A field's errors stay readable with TanStack's `disableErrorFlat`\n * option.\n *\n * @typeParam TValue - The value type of the field.\n * @param field - The field the widget renders.\n * @returns The distinct texts of the field's current errors, in order; empty when there are none.\n *\n * @example\n * ```tsx\n * const TextField = ({ field }: FieldOf<string>) => (\n * <>\n * <input value={field.state.value} onChange={(event) => field.handleChange(event.target.value)} />\n * <p>{errorsOf(field).join(' ')}</p>\n * </>\n * )\n * ```\n */\nexport function errorsOf<TValue>(field: FieldApiOf<TValue>): string[] {\n const texts = field.state.meta.errors.flat().map(textOf).filter(hasText)\n\n return [...new Set(texts)]\n}\n","const PATH_SEPARATORS = /[.[\\]]+/g\nconst SELECTOR_SAFE_SEPARATOR = '__'\n\n/**\n * Builds the DOM id that `fieldParts` gives the control of a field, for code that has no\n * field at hand — an error summary above the form that links to each invalid field, for example.\n *\n * The id is the form's id, a hyphen, and the field's path with every run of dots and brackets\n * replaced by `__`. Fields of two different forms therefore never share an id, and the id can be\n * used in a CSS selector without escaping.\n *\n * @param formId - The id of the form, as TanStack Form reports it in `form.formId`.\n * @param path - The path of the field, such as `email`, `address.zip` or `contacts[0].name`.\n * @returns The id of the field's control, such as `signup-address__zip`.\n *\n * @remarks\n * The replacement cannot be reversed: a field literally named `a__b` gets the same id as the\n * path `a.b`.\n *\n * @example\n * ```tsx\n * <a href={`#${fieldId(form.formId, 'address.zip')}`}>Postal code</a>\n * ```\n */\nexport function fieldId(formId: string, path: string): string {\n return `${formId}-${path.replace(PATH_SEPARATORS, SELECTOR_SAFE_SEPARATOR)}`\n}\n","import { errorsOf } from './errors-of'\nimport { fieldId } from './field-id'\nimport type { FieldApiOf } from './field-of'\n\nconst DESCRIPTION_SUFFIX = '-description'\nconst ERROR_SUFFIX = '-error'\nconst IDREF_SEPARATOR = ' '\n\n/**\n * What {@link fieldParts} returns: the wiring of a widget's control and what it needs to render\n * its description and its errors.\n */\nexport interface FieldParts {\n /** Props to spread onto the control; its value and change handler stay with the widget. */\n control: {\n /** The control's id, as {@link fieldId} builds it. */\n id: string\n /** The field's path, so the browser can autofill the control. */\n name: string\n /** Reports to the field that the control was left. */\n onBlur: () => void\n /** `true` while an error is due; absent otherwise, never `false`. */\n 'aria-invalid': true | undefined\n /** The ids of the description and of a due error with text, when there are any. */\n 'aria-describedby': string | undefined\n /** The id of the error element, while an error with text is due. */\n 'aria-errormessage': string | undefined\n }\n /** The id to give the element that renders the description. */\n descriptionId: string\n /** The id to give the element that renders the error texts. */\n errorId: string\n /**\n * Whether the widget shows the field's errors now: the field is invalid and was edited and\n * left, or the form was submitted.\n */\n isErrorDue: boolean\n /** The texts of the field's errors, as {@link errorsOf} returns them. */\n errors: string[]\n}\n\nfunction isErrorDueFor<TValue>(field: FieldApiOf<TValue>): boolean {\n const { isBlurred, isDirty, isValid } = field.state.meta\n const wasEditedAndLeft = isBlurred && isDirty\n const wasSubmitted = field.form.state.submissionAttempts > 0\n\n return !isValid && (wasEditedAndLeft || wasSubmitted)\n}\n\nfunction idrefsOf(ids: (string | undefined)[]): string | undefined {\n const present = ids.filter((id) => id !== undefined)\n\n return present.length > 0 ? present.join(IDREF_SEPARATOR) : undefined\n}\n\n/**\n * Wires a widget's control to its field: its id and name, the blur report, and the aria\n * attributes that tie it to its description and its errors.\n *\n * An error is due once the field is invalid and was edited and left, or once the form was\n * submitted. While one is due, `aria-invalid` is `true`; while one with text is due, the control\n * points at the error element through `aria-describedby` and `aria-errormessage`. An id appears in\n * them only when its element is rendered.\n *\n * @typeParam TValue - The value type of the field.\n * @param field - The field the widget renders.\n * @param options - What the widget renders besides the control.\n * @param options.description - The description the widget renders below its label, if any. Only\n * whether it is a non-empty string is read.\n * @defaultValue `options.description`: `undefined`, no description is rendered.\n * @returns The props for the control, the ids for the description and error elements, whether an\n * error is due, and the error texts.\n *\n * @example\n * ```tsx\n * const TextField = ({ field, label, description }: FieldOf<string> & { label: string; description?: string }) => {\n * const { control, descriptionId, errorId, isErrorDue, errors } = fieldParts(field, { description })\n *\n * return (\n * <div>\n * <label htmlFor={control.id}>{label}</label>\n * {description && <p id={descriptionId}>{description}</p>}\n * <input {...control} value={field.state.value} onChange={(event) => field.handleChange(event.target.value)} />\n * {isErrorDue && errors.length > 0 && <p id={errorId}>{errors.join(' ')}</p>}\n * </div>\n * )\n * }\n * ```\n */\nexport function fieldParts<TValue>(\n field: FieldApiOf<TValue>,\n options: { description?: string | undefined } = {},\n): FieldParts {\n const id = fieldId(field.form.formId, field.name)\n const descriptionId = `${id}${DESCRIPTION_SUFFIX}`\n const errorId = `${id}${ERROR_SUFFIX}`\n const isErrorDue = isErrorDueFor(field)\n const errors = errorsOf(field)\n const isErrorNamed = isErrorDue && errors.length > 0\n const hasDescription = options.description !== undefined && options.description !== ''\n\n return {\n control: {\n id,\n name: field.name,\n onBlur: field.handleBlur,\n 'aria-invalid': isErrorDue || undefined,\n 'aria-describedby': idrefsOf([\n hasDescription ? descriptionId : undefined,\n isErrorNamed ? errorId : undefined,\n ]),\n 'aria-errormessage': isErrorNamed ? errorId : undefined,\n },\n descriptionId,\n errorId,\n isErrorDue,\n errors,\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formdef/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Describe a form once as a typed object, render it as a React component, with the compiler checking that every widget fits its value. Built on TanStack Form.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|