rhf-dynamic-forms 1.1.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.mjs ADDED
@@ -0,0 +1,1557 @@
1
+ import { createContext, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
2
+ import { FormProvider, useController, useForm, useFormState } from "react-hook-form";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ import { ZodObject, z } from "zod";
5
+ import { zodResolver } from "@hookform/resolvers/zod";
6
+ import jsonLogic from "json-logic-js";
7
+
8
+ //#region src/context/DynamicFormContext.tsx
9
+ /**
10
+ * Context for sharing form state and configuration with child components.
11
+ *
12
+ * This context is set up by the DynamicForm component and consumed by
13
+ * field renderers and other internal components.
14
+ */
15
+ const DynamicFormContext = createContext(null);
16
+ DynamicFormContext.displayName = "DynamicFormContext";
17
+
18
+ //#endregion
19
+ //#region src/customComponents/ConfigurationError.ts
20
+ var ConfigurationError$1 = class ConfigurationError$1 extends Error {
21
+ path;
22
+ component;
23
+ constructor(message, path, component) {
24
+ super(message);
25
+ this.name = "ConfigurationError";
26
+ this.path = path;
27
+ this.component = component;
28
+ const ErrorWithCapture = Error;
29
+ if (ErrorWithCapture.captureStackTrace) ErrorWithCapture.captureStackTrace(this, ConfigurationError$1);
30
+ }
31
+ static formatMessage(baseMessage, path, component) {
32
+ const parts = [];
33
+ if (component) parts.push(`Component "${component}"`);
34
+ if (path) parts.push(`at ${path}`);
35
+ if (parts.length > 0) return `${parts.join(" ")}: ${baseMessage}`;
36
+ return baseMessage;
37
+ }
38
+ };
39
+
40
+ //#endregion
41
+ //#region src/customComponents/defineCustomComponent.ts
42
+ /**
43
+ * Type-safe helper for defining custom components.
44
+ * Provides TypeScript inference for component props based on propsSchema.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * const RatingField = defineCustomComponent({
49
+ * component: RatingFieldComponent,
50
+ * propsSchema: z.object({ maxStars: z.number() }),
51
+ * defaultProps: { maxStars: 5 },
52
+ * });
53
+ * ```
54
+ */
55
+ function defineCustomComponent(definition) {
56
+ return definition;
57
+ }
58
+
59
+ //#endregion
60
+ //#region src/customComponents/types.ts
61
+ const isCustomComponentDefinition = (entry) => {
62
+ return typeof entry === "object" && entry !== null && "component" in entry && typeof entry.component === "function";
63
+ };
64
+ const normalizeComponentDefinition = (entry, name) => {
65
+ if (isCustomComponentDefinition(entry)) return {
66
+ ...entry,
67
+ displayName: entry.displayName ?? name
68
+ };
69
+ return {
70
+ component: entry,
71
+ displayName: name
72
+ };
73
+ };
74
+
75
+ //#endregion
76
+ //#region src/customComponents/validateCustomElement.ts
77
+ /**
78
+ * Validate custom element against its component definition.
79
+ */
80
+ function validateCustomElement(element, registry, path) {
81
+ const entry = registry[element.component];
82
+ if (!entry) {
83
+ const available = Object.keys(registry);
84
+ const availableMessage = available.length > 0 ? `Available components: ${available.join(", ")}` : "No custom components registered.";
85
+ throw new ConfigurationError$1(`Unknown custom component "${element.component}" at ${path}. ${availableMessage}`, path, element.component);
86
+ }
87
+ const definition = normalizeComponentDefinition(entry, element.component);
88
+ const mergedProps = {
89
+ ...definition.defaultProps,
90
+ ...element.componentProps
91
+ };
92
+ if (definition.propsSchema) {
93
+ const result = definition.propsSchema.safeParse(mergedProps);
94
+ if (!result.success) throw new ConfigurationError$1(`Invalid props for "${definition.displayName || element.component}" at ${path}:\n${result.error.issues.map((issue) => ` - ${issue.path.join(".") || "root"}: ${issue.message}`).join("\n")}`, path, element.component);
95
+ return {
96
+ ...element,
97
+ componentProps: result.data,
98
+ __definition: definition
99
+ };
100
+ }
101
+ return {
102
+ ...element,
103
+ componentProps: mergedProps,
104
+ __definition: definition
105
+ };
106
+ }
107
+ function isCustomElement(element) {
108
+ return typeof element === "object" && element !== null && "type" in element && element.type === "custom" && "component" in element && typeof element.component === "string";
109
+ }
110
+
111
+ //#endregion
112
+ //#region src/customComponents/validateConfiguration.ts
113
+ /**
114
+ * Recursively validate all custom elements in a form configuration.
115
+ */
116
+ function validateCustomComponents(config, registry = {}) {
117
+ const validatedElements = validateElements(config.elements, registry, "elements");
118
+ return {
119
+ ...config,
120
+ elements: validatedElements
121
+ };
122
+ }
123
+ function validateElements(elements, registry, basePath) {
124
+ return elements.map((element, index) => {
125
+ return validateElement(element, registry, `${basePath}[${index}]`);
126
+ });
127
+ }
128
+ function validateElement(element, registry, path) {
129
+ if (isCustomElement(element)) return validateCustomElement(element, registry, path);
130
+ if (isContainerElement$1(element)) return validateContainer(element, registry, path);
131
+ if (isColumnElement$1(element)) return validateColumn(element, registry, path);
132
+ return element;
133
+ }
134
+ function validateContainer(container, registry, path) {
135
+ const validatedColumns = container.columns.map((column, index) => validateColumn(column, registry, `${path}.columns[${index}]`));
136
+ return {
137
+ ...container,
138
+ columns: validatedColumns
139
+ };
140
+ }
141
+ function validateColumn(column, registry, path) {
142
+ const validatedElements = validateElements(column.elements, registry, `${path}.elements`);
143
+ return {
144
+ ...column,
145
+ elements: validatedElements
146
+ };
147
+ }
148
+ function isContainerElement$1(element) {
149
+ return element.type === "container";
150
+ }
151
+ function isColumnElement$1(element) {
152
+ return element.type === "column";
153
+ }
154
+
155
+ //#endregion
156
+ //#region src/types/elements.ts
157
+ /**
158
+ * Type guard to check if an element is a field element.
159
+ */
160
+ const isFieldElement = (element) => element.type === "text" || element.type === "email" || element.type === "boolean" || element.type === "phone" || element.type === "date" || element.type === "select" || element.type === "array" || element.type === "custom";
161
+ /**
162
+ * Type guard to check if an element is an array field element.
163
+ */
164
+ const isArrayFieldElement = (element) => element.type === "array";
165
+ /**
166
+ * Type guard to check if an element is a container element.
167
+ */
168
+ const isContainerElement = (element) => element.type === "container";
169
+ /**
170
+ * Type guard to check if an element is a column element.
171
+ */
172
+ const isColumnElement = (element) => element.type === "column";
173
+ /**
174
+ * Type guard to check if an element is a custom field element.
175
+ */
176
+ const isCustomFieldElement = (element) => element.type === "custom";
177
+
178
+ //#endregion
179
+ //#region src/hooks/useDynamicFormContext.ts
180
+ /**
181
+ * Hook to access the DynamicForm context.
182
+ *
183
+ * Must be used within a DynamicForm component.
184
+ * Throws an error if used outside of the form context.
185
+ *
186
+ * @returns The DynamicFormContext value
187
+ * @throws Error if used outside of DynamicForm
188
+ *
189
+ * @example
190
+ * ```tsx
191
+ * function MyCustomField({ config }) {
192
+ * const { form, fieldComponents } = useDynamicFormContext();
193
+ *
194
+ * const value = form.watch(config.name);
195
+ * // ... render field
196
+ * }
197
+ * ```
198
+ */
199
+ const useDynamicFormContext = () => {
200
+ const context = useContext(DynamicFormContext);
201
+ if (!context) throw new Error("useDynamicFormContext must be used within a DynamicForm component. Make sure your component is a child of <DynamicForm>.");
202
+ return context;
203
+ };
204
+ /**
205
+ * Hook to safely access the DynamicForm context.
206
+ * Returns null if used outside of the form context instead of throwing.
207
+ *
208
+ * @returns The DynamicFormContext value or null
209
+ *
210
+ * @example
211
+ * ```tsx
212
+ * function MaybeInForm() {
213
+ * const context = useDynamicFormContextSafe();
214
+ *
215
+ * if (!context) {
216
+ * return <span>Not in a form</span>;
217
+ * }
218
+ *
219
+ * return <span>In a form!</span>;
220
+ * }
221
+ * ```
222
+ */
223
+ const useDynamicFormContextSafe = () => {
224
+ return useContext(DynamicFormContext);
225
+ };
226
+
227
+ //#endregion
228
+ //#region src/components/ContainerRenderer.tsx
229
+ /**
230
+ * Default container styles using flexbox.
231
+ */
232
+ const defaultContainerStyle = {
233
+ display: "flex",
234
+ gap: "16px",
235
+ flexWrap: "wrap"
236
+ };
237
+ /**
238
+ * Default container component used when no custom container is provided.
239
+ * Receives config and children props as per ContainerProps interface.
240
+ */
241
+ const DefaultContainer = ({ children }) => {
242
+ return /* @__PURE__ */ jsx("div", {
243
+ style: defaultContainerStyle,
244
+ children
245
+ });
246
+ };
247
+ /**
248
+ * Renders a container element with its columns.
249
+ *
250
+ * The ContainerRenderer:
251
+ * 1. Checks visibility (Phase 4 - currently all containers are visible)
252
+ * 2. Looks up custom container component if specified
253
+ * 3. Renders columns as children using ColumnRenderer
254
+ *
255
+ * @example
256
+ * ```tsx
257
+ * <ContainerRenderer
258
+ * config={{
259
+ * type: 'container',
260
+ * columns: [
261
+ * { type: 'column', width: '50%', elements: [...] },
262
+ * { type: 'column', width: '50%', elements: [...] }
263
+ * ]
264
+ * }}
265
+ * />
266
+ * ```
267
+ */
268
+ const ContainerRenderer = ({ config }) => {
269
+ const { customContainers } = useDynamicFormContext();
270
+ return /* @__PURE__ */ jsx(customContainers?.default ?? DefaultContainer, {
271
+ config,
272
+ children: config.columns.map((column, index) => /* @__PURE__ */ jsx(ColumnRenderer, { config: column }, `column-${index}`))
273
+ });
274
+ };
275
+ ContainerRenderer.displayName = "ContainerRenderer";
276
+
277
+ //#endregion
278
+ //#region src/components/FieldRenderer.tsx
279
+ const CustomFieldRenderer = ({ config, field, fieldState, formValues, setValue }) => {
280
+ const { customComponents } = useDynamicFormContext();
281
+ if (config.type !== "custom") return null;
282
+ const customConfig = config;
283
+ const entry = customComponents[customConfig.component];
284
+ if (!entry) {
285
+ console.warn(`No custom component registered for: "${customConfig.component}". Make sure to pass it in the customComponents prop.`);
286
+ return null;
287
+ }
288
+ const FieldComponent = normalizeComponentDefinition(entry, customConfig.component).component;
289
+ return /* @__PURE__ */ jsx(FieldComponent, {
290
+ componentProps: customConfig.componentProps ?? {},
291
+ config: customConfig,
292
+ field,
293
+ fieldState,
294
+ formValues,
295
+ setValue
296
+ });
297
+ };
298
+ const StandardFieldRenderer = ({ config, field, fieldState, formValues, setValue }) => {
299
+ const { fieldComponents } = useDynamicFormContext();
300
+ const FieldComponent = fieldComponents[config.type];
301
+ if (!FieldComponent) {
302
+ console.warn(`No field component registered for type: "${config.type}". Make sure to provide all field types in the fieldComponents prop.`);
303
+ return null;
304
+ }
305
+ return /* @__PURE__ */ jsx(FieldComponent, {
306
+ config,
307
+ field,
308
+ fieldState,
309
+ formValues,
310
+ setValue
311
+ });
312
+ };
313
+ const FieldRenderer = ({ config }) => {
314
+ const { form, visibility, fieldWrapper } = useDynamicFormContext();
315
+ const { field, fieldState } = useController({
316
+ name: config.name,
317
+ control: form.control
318
+ });
319
+ if (!(visibility[config.name] !== false)) return null;
320
+ const formValues = form.getValues();
321
+ const setValue = (name, value) => form.setValue(name, value);
322
+ let fieldElement;
323
+ if (isCustomFieldElement(config)) fieldElement = /* @__PURE__ */ jsx(CustomFieldRenderer, {
324
+ config,
325
+ field,
326
+ fieldState,
327
+ formValues,
328
+ setValue
329
+ });
330
+ else fieldElement = /* @__PURE__ */ jsx(StandardFieldRenderer, {
331
+ config,
332
+ field,
333
+ fieldState,
334
+ formValues,
335
+ setValue
336
+ });
337
+ if (fieldWrapper) return fieldWrapper({
338
+ name: config.name,
339
+ config,
340
+ fieldState,
341
+ value: field.value,
342
+ formValues,
343
+ setValue
344
+ }, fieldElement);
345
+ return fieldElement;
346
+ };
347
+ FieldRenderer.displayName = "FieldRenderer";
348
+
349
+ //#endregion
350
+ //#region src/components/ElementRenderer.tsx
351
+ /**
352
+ * Dispatches rendering to the appropriate component based on element type.
353
+ *
354
+ * Supports field elements (Phase 1) and container/column layouts (Phase 2).
355
+ *
356
+ * @example
357
+ * ```tsx
358
+ * // Field element
359
+ * <ElementRenderer element={{ type: 'text', name: 'name', label: 'Name' }} />
360
+ *
361
+ * // Container element with columns
362
+ * <ElementRenderer element={{
363
+ * type: 'container',
364
+ * columns: [
365
+ * { type: 'column', width: '50%', elements: [...] }
366
+ * ]
367
+ * }} />
368
+ * ```
369
+ */
370
+ const ElementRenderer = ({ element }) => {
371
+ if (isFieldElement(element)) return /* @__PURE__ */ jsx(FieldRenderer, { config: element });
372
+ if (isContainerElement(element)) return /* @__PURE__ */ jsx(ContainerRenderer, { config: element });
373
+ if (isColumnElement(element)) {
374
+ console.warn("Column elements should not be rendered directly. They should be children of a container element.");
375
+ return null;
376
+ }
377
+ console.warn(`Unknown element type: ${element.type}`);
378
+ return null;
379
+ };
380
+ ElementRenderer.displayName = "ElementRenderer";
381
+
382
+ //#endregion
383
+ //#region src/components/ColumnRenderer.tsx
384
+ /**
385
+ * Renders a column element with its nested form elements.
386
+ *
387
+ * The ColumnRenderer:
388
+ * 1. Applies the configured width to the column wrapper
389
+ * 2. Recursively renders nested elements via ElementRenderer
390
+ *
391
+ * Columns support nested containers, enabling complex layout hierarchies.
392
+ *
393
+ * @example
394
+ * ```tsx
395
+ * <ColumnRenderer
396
+ * config={{
397
+ * type: 'column',
398
+ * width: '50%',
399
+ * elements: [
400
+ * { type: 'text', name: 'firstName', label: 'First Name' },
401
+ * { type: 'text', name: 'lastName', label: 'Last Name' }
402
+ * ]
403
+ * }}
404
+ * />
405
+ * ```
406
+ */
407
+ const ColumnRenderer = ({ config }) => {
408
+ return /* @__PURE__ */ jsx("div", {
409
+ style: {
410
+ flex: `0 1 ${config.width}`,
411
+ maxWidth: config.width,
412
+ minWidth: 0,
413
+ boxSizing: "border-box"
414
+ },
415
+ children: config.elements.map((element, index) => /* @__PURE__ */ jsx(ElementRenderer, { element }, "name" in element ? element.name : `element-${index}`))
416
+ });
417
+ };
418
+ ColumnRenderer.displayName = "ColumnRenderer";
419
+
420
+ //#endregion
421
+ //#region src/components/FormRenderer.tsx
422
+ /**
423
+ * Renders all form elements from the configuration.
424
+ *
425
+ * Maps over the elements array and renders each element using ElementRenderer.
426
+ * Elements are rendered vertically (one under another) in Phase 1.
427
+ *
428
+ * @example
429
+ * ```tsx
430
+ * const elements = [
431
+ * { type: 'text', name: 'name', label: 'Name' },
432
+ * { type: 'email', name: 'email', label: 'Email' },
433
+ * ];
434
+ *
435
+ * <FormRenderer elements={elements} />
436
+ * ```
437
+ */
438
+ const FormRenderer = ({ elements }) => {
439
+ return /* @__PURE__ */ jsx(Fragment, { children: elements.map((element, index) => {
440
+ const key = "name" in element && element.name ? element.name : `element-${index}`;
441
+ return /* @__PURE__ */ jsx(ElementRenderer, { element }, key);
442
+ }) });
443
+ };
444
+ FormRenderer.displayName = "FormRenderer";
445
+
446
+ //#endregion
447
+ //#region src/parser/configValidator.ts
448
+ /**
449
+ * JSON Logic rule schema - accepts any object structure.
450
+ * Actual JSON Logic validation happens at runtime.
451
+ */
452
+ const jsonLogicRuleSchema = z.record(z.string(), z.unknown());
453
+ /**
454
+ * Validation configuration schema.
455
+ */
456
+ const validationConfigSchema = z.object({
457
+ required: z.boolean().optional(),
458
+ type: z.enum([
459
+ "number",
460
+ "email",
461
+ "date"
462
+ ]).optional(),
463
+ minLength: z.number().int().min(0).optional(),
464
+ maxLength: z.number().int().min(0).optional(),
465
+ pattern: z.string().optional(),
466
+ message: z.string().optional(),
467
+ condition: jsonLogicRuleSchema.optional()
468
+ }).strict().optional();
469
+ /**
470
+ * Base field element schema (common properties).
471
+ */
472
+ const baseFieldSchema = z.object({
473
+ name: z.string().min(1, "Field name is required"),
474
+ label: z.string().optional(),
475
+ placeholder: z.string().optional(),
476
+ defaultValue: z.union([
477
+ z.string(),
478
+ z.number(),
479
+ z.boolean(),
480
+ z.null(),
481
+ z.array(z.unknown()),
482
+ z.record(z.string(), z.unknown())
483
+ ]).optional(),
484
+ validation: validationConfigSchema,
485
+ visible: jsonLogicRuleSchema.optional(),
486
+ dependsOn: z.string().optional(),
487
+ resetOnParentChange: z.boolean().optional()
488
+ });
489
+ /**
490
+ * Text field element schema.
491
+ */
492
+ const textFieldSchema = baseFieldSchema.extend({ type: z.literal("text") });
493
+ /**
494
+ * Email field element schema.
495
+ */
496
+ const emailFieldSchema = baseFieldSchema.extend({ type: z.literal("email") });
497
+ /**
498
+ * Boolean field element schema.
499
+ */
500
+ const booleanFieldSchema = baseFieldSchema.extend({ type: z.literal("boolean") });
501
+ /**
502
+ * Phone field element schema.
503
+ */
504
+ const phoneFieldSchema = baseFieldSchema.extend({ type: z.literal("phone") });
505
+ /**
506
+ * Date field element schema.
507
+ */
508
+ const dateFieldSchema = baseFieldSchema.extend({ type: z.literal("date") });
509
+ /**
510
+ * Select option schema.
511
+ */
512
+ const selectOptionSchema = z.object({
513
+ value: z.union([z.string(), z.number()]),
514
+ label: z.string(),
515
+ disabled: z.boolean().optional()
516
+ });
517
+ /**
518
+ * Options source schema - describes how to resolve options.
519
+ */
520
+ const optionsSourceSchema = z.discriminatedUnion("type", [
521
+ z.object({ type: z.literal("static") }),
522
+ z.object({
523
+ type: z.literal("map"),
524
+ key: z.string()
525
+ }),
526
+ z.object({
527
+ type: z.literal("api"),
528
+ endpoint: z.string()
529
+ }),
530
+ z.object({
531
+ type: z.literal("search"),
532
+ endpoint: z.string(),
533
+ minChars: z.number().optional()
534
+ }),
535
+ z.object({
536
+ type: z.literal("resolver"),
537
+ name: z.string()
538
+ })
539
+ ]);
540
+ /**
541
+ * Select field element schema.
542
+ * Options are required when optionsSource is not provided.
543
+ */
544
+ const selectFieldSchema = baseFieldSchema.extend({
545
+ type: z.literal("select"),
546
+ options: z.array(selectOptionSchema).optional(),
547
+ optionsSource: optionsSourceSchema.optional(),
548
+ multiple: z.boolean().optional(),
549
+ clearable: z.boolean().optional(),
550
+ searchable: z.boolean().optional(),
551
+ creatable: z.boolean().optional()
552
+ }).refine((data) => {
553
+ if (!data.optionsSource || data.optionsSource.type === "static") return data.options !== void 0 && data.options.length >= 0;
554
+ return true;
555
+ }, { message: "Options are required when optionsSource is not provided" });
556
+ /**
557
+ * Custom field element schema.
558
+ */
559
+ const customFieldSchema = baseFieldSchema.extend({
560
+ type: z.literal("custom"),
561
+ component: z.string().min(1, "Custom component name is required"),
562
+ componentProps: z.record(z.string(), z.unknown()).optional()
563
+ });
564
+ /**
565
+ * Array field element schema.
566
+ * Contains repeatable group of fields.
567
+ */
568
+ const arrayFieldSchema = baseFieldSchema.extend({
569
+ type: z.literal("array"),
570
+ itemFields: z.lazy(() => z.array(fieldElementSchema)),
571
+ minItems: z.number().int().min(0).optional(),
572
+ maxItems: z.number().int().min(0).optional(),
573
+ addButtonLabel: z.string().optional(),
574
+ sortable: z.boolean().optional()
575
+ }).refine((data) => {
576
+ if (data.minItems !== void 0 && data.maxItems !== void 0) return data.minItems <= data.maxItems;
577
+ return true;
578
+ }, { message: "minItems must be less than or equal to maxItems" });
579
+ /**
580
+ * Field element schema - union of all field types.
581
+ */
582
+ const fieldElementSchema = z.discriminatedUnion("type", [
583
+ textFieldSchema,
584
+ emailFieldSchema,
585
+ booleanFieldSchema,
586
+ phoneFieldSchema,
587
+ dateFieldSchema,
588
+ selectFieldSchema,
589
+ customFieldSchema,
590
+ arrayFieldSchema
591
+ ]);
592
+ /**
593
+ * Form element schema - for Phase 1, only field elements are supported.
594
+ * Phase 2 will add container and column schemas.
595
+ *
596
+ * We use a lazy schema to allow for future recursive definitions
597
+ * (containers containing columns containing elements).
598
+ */
599
+ const formElementSchema = z.lazy(() => z.union([
600
+ fieldElementSchema,
601
+ containerElementSchema,
602
+ columnElementSchema
603
+ ]));
604
+ /**
605
+ * Column element schema (for Phase 2, but defined here for type completeness).
606
+ */
607
+ const columnElementSchema = z.object({
608
+ type: z.literal("column"),
609
+ width: z.string().min(1, "Column width is required"),
610
+ elements: z.array(z.lazy(() => formElementSchema)),
611
+ visible: jsonLogicRuleSchema.optional()
612
+ });
613
+ /**
614
+ * Container element schema (for Phase 2).
615
+ */
616
+ const containerElementSchema = z.object({
617
+ type: z.literal("container"),
618
+ columns: z.array(columnElementSchema),
619
+ visible: jsonLogicRuleSchema.optional()
620
+ });
621
+ /**
622
+ * Custom component definition schema.
623
+ */
624
+ const customComponentDefinitionSchema = z.object({ defaultProps: z.record(z.string(), z.unknown()).optional() });
625
+ /**
626
+ * Root form configuration schema.
627
+ */
628
+ const formConfigurationSchema = z.object({
629
+ name: z.string().optional(),
630
+ elements: z.array(formElementSchema).min(1, "At least one element is required"),
631
+ customComponents: z.record(z.string(), customComponentDefinitionSchema).optional()
632
+ });
633
+ /**
634
+ * Validates a form configuration object.
635
+ *
636
+ * @param config - Configuration object to validate
637
+ * @returns Validated and typed configuration
638
+ * @throws ZodError if validation fails
639
+ */
640
+ const validateConfiguration = (config) => {
641
+ return formConfigurationSchema.parse(config);
642
+ };
643
+ /**
644
+ * Safely validates a form configuration object without throwing.
645
+ *
646
+ * @param config - Configuration object to validate
647
+ * @returns Result object with success status and data or error
648
+ */
649
+ const safeValidateConfiguration = (config) => {
650
+ return formConfigurationSchema.safeParse(config);
651
+ };
652
+
653
+ //#endregion
654
+ //#region src/parser/configParser.ts
655
+ /**
656
+ * Error thrown when configuration parsing fails.
657
+ */
658
+ var ConfigurationError = class extends Error {
659
+ /** The original validation errors */
660
+ errors;
661
+ constructor(message, errors) {
662
+ super(message);
663
+ this.name = "ConfigurationError";
664
+ this.errors = errors;
665
+ }
666
+ };
667
+ /**
668
+ * Parses and validates a form configuration.
669
+ *
670
+ * @param config - Raw configuration object (typically from JSON)
671
+ * @returns Validated FormConfiguration
672
+ * @throws ConfigurationError if validation fails
673
+ *
674
+ * @example
675
+ * ```typescript
676
+ * try {
677
+ * const config = parseConfiguration({
678
+ * elements: [
679
+ * { type: 'text', name: 'name', label: 'Name' }
680
+ * ]
681
+ * });
682
+ * // config is now typed as FormConfiguration
683
+ * } catch (error) {
684
+ * if (error instanceof ConfigurationError) {
685
+ * console.error('Invalid configuration:', error.errors);
686
+ * }
687
+ * }
688
+ * ```
689
+ */
690
+ const parseConfiguration = (config) => {
691
+ try {
692
+ return validateConfiguration(config);
693
+ } catch (error) {
694
+ if (error && typeof error === "object" && "issues" in error) throw new ConfigurationError("Invalid form configuration", error.issues);
695
+ throw error;
696
+ }
697
+ };
698
+ /**
699
+ * Safely parses and validates a form configuration without throwing.
700
+ *
701
+ * @param config - Raw configuration object
702
+ * @returns ParseResult with success status and config or errors
703
+ *
704
+ * @example
705
+ * ```typescript
706
+ * const result = safeParseConfiguration(rawConfig);
707
+ * if (result.success) {
708
+ * // result.config is available
709
+ * renderForm(result.config);
710
+ * } else {
711
+ * // result.errors contains validation messages
712
+ * showErrors(result.errors);
713
+ * }
714
+ * ```
715
+ */
716
+ const safeParseConfiguration = (config) => {
717
+ const result = safeValidateConfiguration(config);
718
+ if (result.success) return {
719
+ success: true,
720
+ config: result.data
721
+ };
722
+ return {
723
+ success: false,
724
+ errors: result.error.issues.map((issue) => {
725
+ const path = issue.path.join(".");
726
+ return path ? `${path}: ${issue.message}` : issue.message;
727
+ })
728
+ };
729
+ };
730
+
731
+ //#endregion
732
+ //#region src/resolver/visibilityAwareResolver.ts
733
+ /**
734
+ * Check if a value is a leaf error node.
735
+ * React-hook-form leaf errors have both 'type' and 'message' properties.
736
+ */
737
+ const isLeafError = (value) => value !== null && typeof value === "object" && "type" in value && "message" in value;
738
+ /**
739
+ * Create a warning error from an existing error object.
740
+ */
741
+ const createWarningError = (error) => ({
742
+ ...error,
743
+ type: "warning"
744
+ });
745
+ /**
746
+ * Process a single error entry based on visibility rules.
747
+ * Returns the error to include or undefined to skip.
748
+ */
749
+ const processLeafError = (value, path, visibility, warnMode) => {
750
+ if (visibility[path] !== false) return value;
751
+ if (warnMode) return createWarningError(value);
752
+ };
753
+ /**
754
+ * Recursively filter errors based on field visibility.
755
+ * Handles nested error structures for dot-notation paths.
756
+ */
757
+ const filterErrorsByVisibility = (errors, visibility, warnMode, parentPath = "") => {
758
+ const acc = {};
759
+ for (const [key, value] of Object.entries(errors)) {
760
+ const path = parentPath ? `${parentPath}.${key}` : key;
761
+ if (!isLeafError(value) && value && typeof value === "object") {
762
+ const nested = filterErrorsByVisibility(value, visibility, warnMode, path);
763
+ if (Object.keys(nested).length > 0) acc[key] = nested;
764
+ continue;
765
+ }
766
+ const processed = processLeafError(value, path, visibility, warnMode);
767
+ if (processed !== void 0) acc[key] = processed;
768
+ }
769
+ return acc;
770
+ };
771
+ /**
772
+ * Creates a resolver that respects field visibility.
773
+ *
774
+ * This resolver wraps the standard zodResolver and filters validation
775
+ * errors based on field visibility. This is useful when fields are
776
+ * conditionally shown/hidden and you want to skip validation for
777
+ * hidden fields.
778
+ *
779
+ * @param options - Configuration for the resolver
780
+ * @returns A react-hook-form resolver
781
+ *
782
+ * @example
783
+ * ```typescript
784
+ * const resolver = createVisibilityAwareResolver({
785
+ * schema: myZodSchema,
786
+ * getVisibility: () => ({ name: true, phone: false }),
787
+ * invisibleFieldValidation: "skip",
788
+ * });
789
+ *
790
+ * const form = useForm({ resolver });
791
+ * ```
792
+ */
793
+ const createVisibilityAwareResolver = (options) => {
794
+ const baseResolver = zodResolver(options.schema);
795
+ return async (values, context, resolverOptions) => {
796
+ const result = await baseResolver(values, context, resolverOptions);
797
+ if (!result.errors || options.invisibleFieldValidation === "validate") return result;
798
+ const visibility = options.getVisibility();
799
+ const warnMode = options.invisibleFieldValidation === "warn";
800
+ const filteredErrors = filterErrorsByVisibility(result.errors, visibility, warnMode);
801
+ return {
802
+ values: result.values,
803
+ errors: filteredErrors
804
+ };
805
+ };
806
+ };
807
+
808
+ //#endregion
809
+ //#region src/schema/fieldSchemas.ts
810
+ /**
811
+ * Build the base Zod schema for a select field.
812
+ *
813
+ * @param field - Select field configuration
814
+ * @returns Base Zod schema for the select field
815
+ */
816
+ const buildSelectSchema = (field) => {
817
+ if (field.multiple) return z.array(z.union([z.string(), z.number()]));
818
+ return z.union([
819
+ z.string(),
820
+ z.number(),
821
+ z.null()
822
+ ]);
823
+ };
824
+ /**
825
+ * Build the base Zod schema for an array field.
826
+ * Recursively generates schema from itemFields.
827
+ *
828
+ * @param field - Array field configuration
829
+ * @returns Base Zod schema for the array field
830
+ */
831
+ const buildArraySchema = (field) => {
832
+ const itemShape = {};
833
+ for (const itemField of field.itemFields) itemShape[itemField.name] = buildFieldSchema(itemField);
834
+ let arraySchema = z.array(z.object(itemShape));
835
+ if (field.minItems !== void 0) arraySchema = arraySchema.min(field.minItems, `At least ${field.minItems} item(s) required`);
836
+ if (field.maxItems !== void 0) arraySchema = arraySchema.max(field.maxItems, `Maximum ${field.maxItems} item(s) allowed`);
837
+ return arraySchema;
838
+ };
839
+ /**
840
+ * Build the base Zod schema for a field based on its type.
841
+ *
842
+ * @param field - The field element configuration
843
+ * @returns Base Zod schema for the field type
844
+ */
845
+ const buildBaseSchema = (field) => {
846
+ switch (field.type) {
847
+ case "text":
848
+ case "phone": return z.string();
849
+ case "email": return z.string().email("Invalid email address");
850
+ case "boolean": return z.boolean();
851
+ case "date": return z.string();
852
+ case "select": return buildSelectSchema(field);
853
+ case "array": return buildArraySchema(field);
854
+ case "custom": return z.unknown();
855
+ default: return z.unknown();
856
+ }
857
+ };
858
+ /**
859
+ * Apply validation rules to a string schema.
860
+ *
861
+ * @param schema - Base string schema
862
+ * @param validation - Validation configuration
863
+ * @returns Schema with validation rules applied
864
+ */
865
+ const applyStringValidation = (schema, validation) => {
866
+ let result = schema;
867
+ if (validation.required) result = result.min(1, "This field is required");
868
+ if (validation.minLength !== void 0) result = result.min(validation.minLength, `Must be at least ${validation.minLength} characters`);
869
+ if (validation.maxLength !== void 0) result = result.max(validation.maxLength, `Must be no more than ${validation.maxLength} characters`);
870
+ if (validation.pattern) try {
871
+ const regex = new RegExp(validation.pattern);
872
+ result = result.regex(regex, validation.message || "Invalid format");
873
+ } catch {
874
+ console.warn(`Invalid regex pattern: ${validation.pattern}`);
875
+ }
876
+ return result;
877
+ };
878
+ /**
879
+ * Apply validation rules to a boolean schema.
880
+ *
881
+ * @param schema - Base boolean schema
882
+ * @param validation - Validation configuration
883
+ * @returns Schema with validation rules applied
884
+ */
885
+ const applyBooleanValidation = (schema, validation) => {
886
+ if (validation.required) return schema.refine((val) => val === true, { message: "This field is required" });
887
+ return schema;
888
+ };
889
+ /**
890
+ * Apply validation rules to a select schema.
891
+ *
892
+ * @param schema - Base select schema
893
+ * @param validation - Validation configuration
894
+ * @param isMultiple - Whether this is a multi-select
895
+ * @returns Schema with validation rules applied
896
+ */
897
+ const applySelectValidation = (schema, validation, isMultiple) => {
898
+ if (validation.required && isMultiple) return schema.min(1, "At least one selection is required");
899
+ if (validation.required && !isMultiple) return schema.refine((val) => val !== null && val !== void 0, { message: "This field is required" });
900
+ return schema;
901
+ };
902
+ /**
903
+ * Apply validation configuration to a Zod schema based on field type.
904
+ *
905
+ * @param schema - Base Zod schema
906
+ * @param validation - Validation configuration
907
+ * @param field - Field element configuration
908
+ * @returns Schema with validation rules applied
909
+ */
910
+ const applyValidationRules = (schema, validation, field) => {
911
+ const fieldType = field.type;
912
+ if (fieldType === "text" || fieldType === "phone" || fieldType === "email" || fieldType === "date") return applyStringValidation(schema, validation);
913
+ if (fieldType === "boolean") return applyBooleanValidation(schema, validation);
914
+ if (fieldType === "select") return applySelectValidation(schema, validation, field.multiple ?? false);
915
+ return schema;
916
+ };
917
+ /**
918
+ * Build a complete Zod schema for a single field.
919
+ *
920
+ * @param field - Field element configuration
921
+ * @returns Zod schema for the field
922
+ *
923
+ * @example
924
+ * ```typescript
925
+ * const textField = {
926
+ * type: 'text',
927
+ * name: 'name',
928
+ * validation: { required: true, minLength: 3 }
929
+ * };
930
+ *
931
+ * const schema = buildFieldSchema(textField);
932
+ * // schema is z.string().min(1, 'required').min(3, '...')
933
+ * ```
934
+ */
935
+ const buildFieldSchema = (field) => {
936
+ let schema = buildBaseSchema(field);
937
+ if (field.validation) schema = applyValidationRules(schema, field.validation, field);
938
+ return schema;
939
+ };
940
+
941
+ //#endregion
942
+ //#region src/validation/jsonLogic.ts
943
+ /**
944
+ * Register custom JSON Logic operations.
945
+ * Called once at module initialization.
946
+ */
947
+ const registerCustomOperations = () => {
948
+ /**
949
+ * regex_match: Tests if a value matches a regex pattern.
950
+ *
951
+ * @example
952
+ * ```json
953
+ * { "regex_match": ["^[0-9]{10}$", { "var": "phone" }] }
954
+ * ```
955
+ */
956
+ jsonLogic.add_operation("regex_match", (pattern, value) => {
957
+ if (typeof value !== "string" || typeof pattern !== "string") return false;
958
+ try {
959
+ return new RegExp(pattern).test(value);
960
+ } catch {
961
+ return false;
962
+ }
963
+ });
964
+ };
965
+ registerCustomOperations();
966
+ /**
967
+ * Evaluate a JSON Logic rule against form data.
968
+ *
969
+ * @param rule - JSON Logic rule to evaluate
970
+ * @param data - Form data to evaluate against
971
+ * @returns Result of the evaluation
972
+ *
973
+ * @example
974
+ * ```typescript
975
+ * const rule = { "==": [{ var: "status" }, "active"] };
976
+ * const data = { status: "active" };
977
+ * applyJsonLogic(rule, data); // true
978
+ * ```
979
+ */
980
+ const applyJsonLogic = (rule, data) => {
981
+ return jsonLogic.apply(rule, data);
982
+ };
983
+ /**
984
+ * Evaluate a JSON Logic rule and return boolean result.
985
+ * Returns true if rule evaluates to a truthy value.
986
+ *
987
+ * @param rule - JSON Logic condition
988
+ * @param data - Form data
989
+ * @returns true if condition passes, false otherwise
990
+ *
991
+ * @example
992
+ * ```typescript
993
+ * const rule = { "and": [{ var: "active" }, { var: "confirmed" }] };
994
+ * evaluateCondition(rule, { active: true, confirmed: true }); // true
995
+ * evaluateCondition(rule, { active: true, confirmed: false }); // false
996
+ * ```
997
+ */
998
+ const evaluateCondition = (rule, data) => {
999
+ const result = applyJsonLogic(rule, data);
1000
+ return Boolean(result);
1001
+ };
1002
+
1003
+ //#endregion
1004
+ //#region src/utils/calculateVisibility.ts
1005
+ /**
1006
+ * Compare two visibility states and return the new state only if changed.
1007
+ * Returns prev if no change (preserves reference equality for React).
1008
+ */
1009
+ const getUpdatedVisibility = (prev, next) => {
1010
+ const keys = new Set([...Object.keys(prev), ...Object.keys(next)]);
1011
+ for (const key of keys) if (prev[key] !== next[key]) return next;
1012
+ return prev;
1013
+ };
1014
+ /**
1015
+ * Calculate visibility state for all fields based on their visibility rules.
1016
+ * Evaluates JSON Logic rules against current form data.
1017
+ *
1018
+ * @param elements - Array of form elements
1019
+ * @param formData - Current form values
1020
+ * @returns Visibility state for all fields
1021
+ *
1022
+ * @example
1023
+ * ```typescript
1024
+ * const elements = [
1025
+ * { type: 'text', name: 'reason', visible: { "==": [{ "var": "type" }, "other"] } },
1026
+ * { type: 'text', name: 'name' }
1027
+ * ];
1028
+ *
1029
+ * const visibility = calculateVisibility(elements, { type: 'other' });
1030
+ * // Returns: { reason: true, name: true }
1031
+ *
1032
+ * const visibility2 = calculateVisibility(elements, { type: 'standard' });
1033
+ * // Returns: { reason: false, name: true }
1034
+ * ```
1035
+ */
1036
+ const calculateVisibility = (elements, formData) => {
1037
+ const visibility = {};
1038
+ const processElement = (element, parentVisible) => {
1039
+ if (isFieldElement(element)) processField(element, parentVisible);
1040
+ else if (isContainerElement(element)) processContainer(element, parentVisible);
1041
+ else if (isColumnElement(element)) processColumn(element, parentVisible);
1042
+ };
1043
+ const processField = (field, parentVisible) => {
1044
+ if (!parentVisible) {
1045
+ visibility[field.name] = false;
1046
+ return;
1047
+ }
1048
+ if (!field.visible) {
1049
+ visibility[field.name] = true;
1050
+ return;
1051
+ }
1052
+ visibility[field.name] = evaluateCondition(field.visible, formData);
1053
+ };
1054
+ const processContainer = (container, parentVisible) => {
1055
+ let containerVisible = parentVisible;
1056
+ if (container.visible && parentVisible) containerVisible = evaluateCondition(container.visible, formData);
1057
+ for (const column of container.columns) processColumn(column, containerVisible);
1058
+ };
1059
+ const processColumn = (column, parentVisible) => {
1060
+ let columnVisible = parentVisible;
1061
+ if (column.visible && parentVisible) columnVisible = evaluateCondition(column.visible, formData);
1062
+ for (const element of column.elements) processElement(element, columnVisible);
1063
+ };
1064
+ for (const element of elements) processElement(element, true);
1065
+ return visibility;
1066
+ };
1067
+
1068
+ //#endregion
1069
+ //#region src/utils/flattenFields.ts
1070
+ /**
1071
+ * Recursively extracts all field elements from a form configuration.
1072
+ * Traverses containers and columns to find nested fields.
1073
+ *
1074
+ * @param elements - Array of form elements (may include containers/columns)
1075
+ * @returns Flat array of all field elements
1076
+ *
1077
+ * @example
1078
+ * ```typescript
1079
+ * const config = {
1080
+ * elements: [
1081
+ * { type: 'text', name: 'name' },
1082
+ * {
1083
+ * type: 'container',
1084
+ * columns: [{
1085
+ * type: 'column',
1086
+ * width: '50%',
1087
+ * elements: [{ type: 'email', name: 'email' }]
1088
+ * }]
1089
+ * }
1090
+ * ]
1091
+ * };
1092
+ *
1093
+ * const fields = flattenFields(config.elements);
1094
+ * // Returns: [{ type: 'text', name: 'name' }, { type: 'email', name: 'email' }]
1095
+ * ```
1096
+ */
1097
+ const flattenFields = (elements) => {
1098
+ const fields = [];
1099
+ const processElement = (element) => {
1100
+ if (isFieldElement(element)) fields.push(element);
1101
+ else if (isContainerElement(element)) processContainer(element);
1102
+ else if (isColumnElement(element)) processColumn(element);
1103
+ };
1104
+ const processContainer = (container) => {
1105
+ for (const column of container.columns) processColumn(column);
1106
+ };
1107
+ const processColumn = (column) => {
1108
+ for (const element of column.elements) processElement(element);
1109
+ };
1110
+ for (const element of elements) processElement(element);
1111
+ return fields;
1112
+ };
1113
+ /**
1114
+ * Gets all field names from a form configuration.
1115
+ * Useful for initializing form state or visibility tracking.
1116
+ *
1117
+ * @param elements - Array of form elements
1118
+ * @returns Array of field names (including nested paths like 'source.name')
1119
+ */
1120
+ const getFieldNames = (elements) => {
1121
+ return flattenFields(elements).map((field) => field.name);
1122
+ };
1123
+
1124
+ //#endregion
1125
+ //#region src/utils/dependencies.ts
1126
+ /**
1127
+ * Builds a dependency map from form elements.
1128
+ * Maps parent field names to their dependent children.
1129
+ *
1130
+ * @param elements - Array of form elements
1131
+ * @returns Map of parent field names to dependent field names
1132
+ *
1133
+ * @example
1134
+ * ```typescript
1135
+ * const elements = [
1136
+ * { type: 'select', name: 'country', options: [...] },
1137
+ * { type: 'select', name: 'city', dependsOn: 'country', options: [...] }
1138
+ * ];
1139
+ *
1140
+ * const map = buildDependencyMap(elements);
1141
+ * // Returns: { country: ['city'] }
1142
+ * ```
1143
+ */
1144
+ const buildDependencyMap = (elements) => {
1145
+ const map = {};
1146
+ const fields = flattenFields(elements);
1147
+ for (const field of fields) if (field.dependsOn) {
1148
+ const parent = field.dependsOn;
1149
+ if (!map[parent]) map[parent] = [];
1150
+ map[parent].push(field.name);
1151
+ }
1152
+ return map;
1153
+ };
1154
+ /**
1155
+ * Finds a field by name in the form elements.
1156
+ *
1157
+ * @param elements - Array of form elements
1158
+ * @param name - Field name to find
1159
+ * @returns Field element if found, undefined otherwise
1160
+ */
1161
+ const findFieldByName = (elements, name) => {
1162
+ return flattenFields(elements).find((field) => field.name === name);
1163
+ };
1164
+ /**
1165
+ * Gets the default value for a field based on its type.
1166
+ * Used when resetting dependent fields.
1167
+ *
1168
+ * @param field - Field element
1169
+ * @returns Default value appropriate for the field type
1170
+ */
1171
+ const getFieldTypeDefault = (field) => {
1172
+ switch (field.type) {
1173
+ case "boolean": return false;
1174
+ case "select": return field.multiple ? [] : null;
1175
+ case "array": return [];
1176
+ default: return "";
1177
+ }
1178
+ };
1179
+ /**
1180
+ * Gets the effective default value for a field.
1181
+ * Priority: config.defaultValue > type default
1182
+ *
1183
+ * @param field - Field element
1184
+ * @returns Default value to use when resetting
1185
+ */
1186
+ const getFieldDefault = (field) => {
1187
+ if (field.defaultValue !== void 0) return field.defaultValue;
1188
+ return getFieldTypeDefault(field);
1189
+ };
1190
+
1191
+ //#endregion
1192
+ //#region src/utils/mergeDefaults.ts
1193
+ /**
1194
+ * Sets a value in a nested object using dot notation path.
1195
+ *
1196
+ * @param obj - Object to modify
1197
+ * @param path - Dot-notation path (e.g., 'source.name')
1198
+ * @param value - Value to set
1199
+ *
1200
+ * @example
1201
+ * ```typescript
1202
+ * const obj = {};
1203
+ * setNestedValue(obj, 'source.name', 'John');
1204
+ * // obj is now { source: { name: 'John' } }
1205
+ * ```
1206
+ */
1207
+ const setNestedValue = (obj, path, value) => {
1208
+ const parts = path.split(".");
1209
+ let current = obj;
1210
+ for (let i = 0; i < parts.length - 1; i++) {
1211
+ const part = parts[i];
1212
+ if (!(part in current) || typeof current[part] !== "object" || current[part] === null) current[part] = {};
1213
+ current = current[part];
1214
+ }
1215
+ const lastPart = parts.at(-1);
1216
+ if (lastPart !== void 0) current[lastPart] = value;
1217
+ };
1218
+ /**
1219
+ * Gets a value from a nested object using dot notation path.
1220
+ *
1221
+ * @param obj - Object to read from
1222
+ * @param path - Dot-notation path (e.g., 'source.name')
1223
+ * @returns The value at the path, or undefined if not found
1224
+ *
1225
+ * @example
1226
+ * ```typescript
1227
+ * const obj = { source: { name: 'John' } };
1228
+ * getNestedValue(obj, 'source.name'); // 'John'
1229
+ * getNestedValue(obj, 'source.email'); // undefined
1230
+ * ```
1231
+ */
1232
+ const getNestedValue = (obj, path) => {
1233
+ const parts = path.split(".");
1234
+ let current = obj;
1235
+ for (const part of parts) {
1236
+ if (current === null || current === void 0 || typeof current !== "object") return;
1237
+ current = current[part];
1238
+ }
1239
+ return current;
1240
+ };
1241
+ /**
1242
+ * Deep merge two objects. Source values override target values.
1243
+ *
1244
+ * @param target - Base object
1245
+ * @param source - Object to merge in (takes precedence)
1246
+ * @returns New merged object
1247
+ */
1248
+ const deepMerge = (target, source) => {
1249
+ const result = { ...target };
1250
+ for (const key in source) if (Object.hasOwn(source, key)) {
1251
+ const sourceValue = source[key];
1252
+ const targetValue = result[key];
1253
+ if (sourceValue !== null && typeof sourceValue === "object" && !Array.isArray(sourceValue) && targetValue !== null && typeof targetValue === "object" && !Array.isArray(targetValue)) result[key] = deepMerge(targetValue, sourceValue);
1254
+ else result[key] = sourceValue;
1255
+ }
1256
+ return result;
1257
+ };
1258
+ /**
1259
+ * Gets the default value for a field based on its type.
1260
+ *
1261
+ * @param field - Field element
1262
+ * @returns Appropriate default value for the field type
1263
+ */
1264
+ const getTypeDefault = (field) => {
1265
+ switch (field.type) {
1266
+ case "boolean": return false;
1267
+ case "select": return field.multiple ? [] : null;
1268
+ case "array": return [];
1269
+ case "text":
1270
+ case "email":
1271
+ case "phone":
1272
+ case "date": return "";
1273
+ default: return "";
1274
+ }
1275
+ };
1276
+ /**
1277
+ * Merges configuration default values with initial data.
1278
+ * Priority: initialData > config.defaultValue > type default
1279
+ *
1280
+ * @param config - Form configuration containing field definitions
1281
+ * @param initialData - Initial data provided by the user
1282
+ * @returns Merged default values for react-hook-form
1283
+ *
1284
+ * @example
1285
+ * ```typescript
1286
+ * const config = {
1287
+ * elements: [
1288
+ * { type: 'text', name: 'source.name', defaultValue: 'Default Name' },
1289
+ * { type: 'boolean', name: 'source.active' }
1290
+ * ]
1291
+ * };
1292
+ *
1293
+ * const initialData = { source: { name: 'Provided Name' } };
1294
+ * const defaults = mergeDefaults(config, initialData);
1295
+ * // Result: { source: { name: 'Provided Name', active: false } }
1296
+ * ```
1297
+ */
1298
+ const mergeDefaults = (config, initialData) => {
1299
+ const defaults = {};
1300
+ const fields = flattenFields(config.elements);
1301
+ for (const field of fields) {
1302
+ const typeDefault = getTypeDefault(field);
1303
+ const configDefault = field.defaultValue ?? typeDefault;
1304
+ setNestedValue(defaults, field.name, configDefault);
1305
+ }
1306
+ if (initialData) return deepMerge(defaults, initialData);
1307
+ return defaults;
1308
+ };
1309
+
1310
+ //#endregion
1311
+ //#region src/schema/nestedPaths.ts
1312
+ /**
1313
+ * Sets a nested schema value using dot notation path.
1314
+ * Builds nested z.object structures as needed.
1315
+ *
1316
+ * @param shape - The shape object to modify
1317
+ * @param path - Dot-notation path (e.g., 'source.name')
1318
+ * @param schema - Zod schema to set at the path
1319
+ *
1320
+ * @example
1321
+ * ```typescript
1322
+ * const shape = {};
1323
+ * setNestedSchema(shape, 'source.name', z.string());
1324
+ * // shape is now: { source: ZodObject({ name: ZodString }) }
1325
+ *
1326
+ * setNestedSchema(shape, 'source.email', z.string().email());
1327
+ * // shape is now: { source: ZodObject({ name: ZodString, email: ZodString }) }
1328
+ * ```
1329
+ */
1330
+ const setNestedSchema = (shape, path, schema) => {
1331
+ const parts = path.split(".");
1332
+ if (parts.length === 1) {
1333
+ shape[path] = schema;
1334
+ return;
1335
+ }
1336
+ const [first, ...rest] = parts;
1337
+ const remainingPath = rest.join(".");
1338
+ if (!shape[first]) shape[first] = z.object({});
1339
+ const existingSchema = shape[first];
1340
+ if (existingSchema instanceof ZodObject) {
1341
+ const innerShape = { ...existingSchema.shape };
1342
+ setNestedSchema(innerShape, remainingPath, schema);
1343
+ shape[first] = z.object(innerShape);
1344
+ } else {
1345
+ const innerShape = {};
1346
+ setNestedSchema(innerShape, remainingPath, schema);
1347
+ shape[first] = z.object(innerShape);
1348
+ }
1349
+ };
1350
+
1351
+ //#endregion
1352
+ //#region src/schema/generateSchema.ts
1353
+ /**
1354
+ * Extract all JSON Logic validation conditions from fields.
1355
+ *
1356
+ * @param fields - Array of field elements
1357
+ * @returns Array of field conditions to evaluate
1358
+ */
1359
+ const collectConditions = (fields) => {
1360
+ const conditions = [];
1361
+ for (const field of fields) if (field.validation?.condition) conditions.push({
1362
+ fieldPath: field.name,
1363
+ condition: field.validation.condition,
1364
+ message: field.validation.message || "Validation failed"
1365
+ });
1366
+ return conditions;
1367
+ };
1368
+ /**
1369
+ * Generate a Zod schema from form configuration.
1370
+ * Supports nested field paths via dot notation.
1371
+ *
1372
+ * This function is called once when the form initializes and the schema
1373
+ * is memoized. Visibility changes are handled at validation time, not
1374
+ * by regenerating the schema.
1375
+ *
1376
+ * @param config - Form configuration object
1377
+ * @returns Zod object schema for validating form data
1378
+ *
1379
+ * @example
1380
+ * ```typescript
1381
+ * const config = {
1382
+ * elements: [
1383
+ * { type: 'text', name: 'source.name', validation: { required: true } },
1384
+ * { type: 'email', name: 'source.email' },
1385
+ * { type: 'boolean', name: 'active' }
1386
+ * ]
1387
+ * };
1388
+ *
1389
+ * const schema = generateZodSchema(config);
1390
+ *
1391
+ * // The generated schema is equivalent to:
1392
+ * // z.object({
1393
+ * // source: z.object({
1394
+ * // name: z.string().min(1, 'required'),
1395
+ * // email: z.string().email()
1396
+ * // }),
1397
+ * // active: z.boolean()
1398
+ * // })
1399
+ *
1400
+ * schema.parse({
1401
+ * source: { name: 'John', email: 'john@example.com' },
1402
+ * active: true
1403
+ * }); // Valid
1404
+ * ```
1405
+ */
1406
+ const generateZodSchema = (config) => {
1407
+ const fields = flattenFields(config.elements);
1408
+ const schemaShape = {};
1409
+ for (const field of fields) {
1410
+ const fieldSchema = buildFieldSchema(field);
1411
+ setNestedSchema(schemaShape, field.name, fieldSchema);
1412
+ }
1413
+ let schema = z.object(schemaShape);
1414
+ const conditions = collectConditions(fields);
1415
+ if (conditions.length > 0) schema = schema.superRefine((data, ctx) => {
1416
+ for (const { fieldPath, condition, message } of conditions) if (!evaluateCondition(condition, data)) ctx.addIssue({
1417
+ code: z.ZodIssueCode.custom,
1418
+ message,
1419
+ path: fieldPath.split(".")
1420
+ });
1421
+ });
1422
+ return schema;
1423
+ };
1424
+ /**
1425
+ * Extract field paths from a generated schema.
1426
+ * Returns all top-level and nested paths.
1427
+ *
1428
+ * @param schema - Generated Zod schema
1429
+ * @param prefix - Current path prefix (used in recursion)
1430
+ * @returns Array of all field paths
1431
+ */
1432
+ const getSchemaFieldPaths = (schema, prefix = "") => {
1433
+ const paths = [];
1434
+ const shape = schema.shape;
1435
+ for (const key in shape) if (Object.hasOwn(shape, key)) {
1436
+ const fullPath = prefix ? `${prefix}.${key}` : key;
1437
+ const fieldSchema = shape[key];
1438
+ if (fieldSchema instanceof ZodObject) paths.push(...getSchemaFieldPaths(fieldSchema, fullPath));
1439
+ else paths.push(fullPath);
1440
+ }
1441
+ return paths;
1442
+ };
1443
+
1444
+ //#endregion
1445
+ //#region src/DynamicForm.tsx
1446
+ const DynamicForm = ({ config, initialData, fieldComponents, customComponents = {}, customContainers = {}, onSubmit, onChange, onValidationChange, onReset, onError, mode = "onChange", invisibleFieldValidation = "skip", className, style, id, children, fieldWrapper, ref }) => {
1447
+ const parsedConfig = useMemo(() => {
1448
+ return validateCustomComponents(parseConfiguration(config), customComponents);
1449
+ }, [config, customComponents]);
1450
+ const zodSchema = useMemo(() => generateZodSchema(parsedConfig), [parsedConfig]);
1451
+ const defaultValues = useMemo(() => mergeDefaults(parsedConfig, initialData), [parsedConfig, initialData]);
1452
+ const [visibility, setVisibility] = useState(() => calculateVisibility(parsedConfig.elements, defaultValues));
1453
+ const visibilityRef = useRef(visibility);
1454
+ visibilityRef.current = visibility;
1455
+ const onChangeRef = useRef(onChange);
1456
+ onChangeRef.current = onChange;
1457
+ const form = useForm({
1458
+ defaultValues,
1459
+ resolver: useMemo(() => createVisibilityAwareResolver({
1460
+ schema: zodSchema,
1461
+ getVisibility: () => visibilityRef.current,
1462
+ invisibleFieldValidation
1463
+ }), [zodSchema, invisibleFieldValidation]),
1464
+ mode
1465
+ });
1466
+ useImperativeHandle(ref, () => ({
1467
+ getValues: () => form.getValues(),
1468
+ setValue: (name, value) => form.setValue(name, value),
1469
+ watchAll: () => form.watch(),
1470
+ watchField: (name) => form.watch(name),
1471
+ reset: (values) => form.reset(values ?? defaultValues),
1472
+ trigger: (name) => form.trigger(name)
1473
+ }), [form, defaultValues]);
1474
+ const dependencyMap = useMemo(() => buildDependencyMap(parsedConfig.elements), [parsedConfig]);
1475
+ const previousValuesRef = useRef({});
1476
+ useEffect(() => {
1477
+ const handleDependencyReset = (fieldName, formValues) => {
1478
+ const dependents = dependencyMap[fieldName];
1479
+ if (!dependents) return;
1480
+ const currentValue = getNestedValue(formValues, fieldName);
1481
+ if (currentValue === getNestedValue(previousValuesRef.current, fieldName)) return;
1482
+ setNestedValue(previousValuesRef.current, fieldName, currentValue);
1483
+ for (const dep of dependents) {
1484
+ const field = findFieldByName(parsedConfig.elements, dep);
1485
+ if (field && field.resetOnParentChange !== false) form.setValue(dep, getFieldDefault(field));
1486
+ }
1487
+ };
1488
+ const subscription = form.watch((values, { name }) => {
1489
+ const formValues = values;
1490
+ const newVisibility = calculateVisibility(parsedConfig.elements, formValues);
1491
+ setVisibility((prev) => getUpdatedVisibility(prev, newVisibility));
1492
+ if (!name) return;
1493
+ handleDependencyReset(name, formValues);
1494
+ onChangeRef.current?.(values, name);
1495
+ });
1496
+ return () => subscription.unsubscribe();
1497
+ }, [
1498
+ form,
1499
+ parsedConfig,
1500
+ dependencyMap
1501
+ ]);
1502
+ const { errors: formErrors, isValid: formIsValid } = useFormState({ control: form.control });
1503
+ useEffect(() => {
1504
+ if (!onValidationChange) return;
1505
+ onValidationChange(formErrors, formIsValid);
1506
+ }, [
1507
+ formErrors,
1508
+ formIsValid,
1509
+ onValidationChange
1510
+ ]);
1511
+ const contextValue = useMemo(() => ({
1512
+ form,
1513
+ config: parsedConfig,
1514
+ fieldComponents,
1515
+ customComponents,
1516
+ customContainers,
1517
+ visibility,
1518
+ fieldWrapper
1519
+ }), [
1520
+ form,
1521
+ parsedConfig,
1522
+ fieldComponents,
1523
+ customComponents,
1524
+ customContainers,
1525
+ visibility,
1526
+ fieldWrapper
1527
+ ]);
1528
+ const handleSubmit = form.handleSubmit(onSubmit, (errors) => onError?.(errors));
1529
+ const handleReset = useCallback(() => {
1530
+ form.reset(defaultValues);
1531
+ onReset?.();
1532
+ }, [
1533
+ defaultValues,
1534
+ onReset,
1535
+ form
1536
+ ]);
1537
+ return /* @__PURE__ */ jsx(FormProvider, {
1538
+ ...form,
1539
+ children: /* @__PURE__ */ jsx(DynamicFormContext.Provider, {
1540
+ value: contextValue,
1541
+ children: /* @__PURE__ */ jsxs("form", {
1542
+ className,
1543
+ id,
1544
+ noValidate: true,
1545
+ onReset: handleReset,
1546
+ onSubmit: handleSubmit,
1547
+ style,
1548
+ children: [/* @__PURE__ */ jsx(FormRenderer, { elements: parsedConfig.elements }), children]
1549
+ })
1550
+ })
1551
+ });
1552
+ };
1553
+ DynamicForm.displayName = "DynamicForm";
1554
+ var DynamicForm_default = DynamicForm;
1555
+
1556
+ //#endregion
1557
+ export { ConfigurationError, DynamicForm, DynamicFormContext, applyJsonLogic, buildFieldSchema, calculateVisibility, createVisibilityAwareResolver, DynamicForm_default as default, defineCustomComponent, evaluateCondition, flattenFields, generateZodSchema, getFieldNames, getNestedValue, getSchemaFieldPaths, isArrayFieldElement, isColumnElement, isContainerElement, isCustomFieldElement, isFieldElement, mergeDefaults, parseConfiguration, safeParseConfiguration, setNestedValue, useDynamicFormContext, useDynamicFormContextSafe };