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/README.md ADDED
@@ -0,0 +1,692 @@
1
+ # Dynamic Forms
2
+
3
+ Configuration-driven form generation library for React with react-hook-form and Zod integration.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ - [Overview](#overview)
10
+ - [Installation](#installation)
11
+ - [Quick Start](#quick-start)
12
+ - [Configuration Reference](#configuration-reference)
13
+ - [FormConfiguration](#formconfiguration)
14
+ - [Field Types](#field-types)
15
+ - [Validation Configuration](#validation-configuration)
16
+ - [Container Layout](#container-layout)
17
+ - [Usage Examples](#usage-examples)
18
+ - [Nested Field Paths](#nested-field-paths)
19
+ - [Two-Column Layout](#two-column-layout)
20
+ - [Custom Field Component](#custom-field-component)
21
+ - [JSON Logic Conditional Validation](#json-logic-conditional-validation)
22
+ - [API Reference](#api-reference)
23
+ - [DynamicForm Props](#dynamicform-props)
24
+ - [Validation Options](#validation-options)
25
+ - [Hooks](#hooks)
26
+ - [Exports](#exports)
27
+ - [Creating Field Components](#creating-field-components)
28
+ - [Development](#development)
29
+ - [Tech Stack](#tech-stack)
30
+ - [Contributing](#contributing)
31
+ - [License](#license)
32
+
33
+ ---
34
+
35
+ ## Overview
36
+
37
+ Dynamic Forms enables rapid deployment of data collection forms by defining form structures, validations, and display logic through declarative JSON configurations. Instead of writing custom form components for each use case, describe your form as data and let the library handle rendering and validation.
38
+
39
+ **Key Benefits:**
40
+ - Define forms as JSON configuration
41
+ - Flexible validation: external resolver, Zod schema, or config-driven
42
+ - Full react-hook-form integration
43
+ - Nested field paths with dot notation
44
+ - Conditional visibility and validation with JSON Logic
45
+ - Field dependencies with cascading resets
46
+ - Select fields with static/dynamic options
47
+ - Array fields for repeatable groups
48
+ - Extensible component architecture
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ npm install dynamic-forms
54
+ # or
55
+ pnpm add dynamic-forms
56
+ # or
57
+ yarn add dynamic-forms
58
+ ```
59
+
60
+ **Peer Dependencies:**
61
+
62
+ ```bash
63
+ npm install react react-dom
64
+ ```
65
+
66
+ ## Quick Start
67
+
68
+ ```tsx
69
+ import { DynamicForm, type FormConfiguration, type FieldComponentRegistry } from 'dynamic-forms';
70
+
71
+ // 1. Define your form configuration
72
+ const config: FormConfiguration = {
73
+ name: "Contact Form",
74
+ elements: [
75
+ {
76
+ type: "text",
77
+ name: "fullName",
78
+ label: "Full Name",
79
+ validation: { required: true, minLength: 2 },
80
+ },
81
+ {
82
+ type: "email",
83
+ name: "email",
84
+ label: "Email Address",
85
+ validation: { required: true },
86
+ },
87
+ ],
88
+ };
89
+
90
+ // 2. Create field components (or use a UI library)
91
+ const fieldComponents: FieldComponentRegistry = {
92
+ text: ({ field, fieldState, config }) => (
93
+ <div>
94
+ <label>{config.label}</label>
95
+ <input {...field} placeholder={config.placeholder} />
96
+ {fieldState.error && <span>{fieldState.error.message}</span>}
97
+ </div>
98
+ ),
99
+ email: ({ field, fieldState, config }) => (
100
+ <div>
101
+ <label>{config.label}</label>
102
+ <input {...field} type="email" placeholder={config.placeholder} />
103
+ {fieldState.error && <span>{fieldState.error.message}</span>}
104
+ </div>
105
+ ),
106
+ boolean: ({ field, config }) => (
107
+ <label>
108
+ <input {...field} type="checkbox" checked={field.value} />
109
+ {config.label}
110
+ </label>
111
+ ),
112
+ phone: ({ field, config }) => (
113
+ <div>
114
+ <label>{config.label}</label>
115
+ <input {...field} type="tel" />
116
+ </div>
117
+ ),
118
+ date: ({ field, config }) => (
119
+ <div>
120
+ <label>{config.label}</label>
121
+ <input {...field} type="date" />
122
+ </div>
123
+ ),
124
+ };
125
+
126
+ // 3. Render the form
127
+ function App() {
128
+ return (
129
+ <DynamicForm
130
+ config={config}
131
+ fieldComponents={fieldComponents}
132
+ onSubmit={(data) => console.log('Submitted:', data)}
133
+ >
134
+ <button type="submit">Submit</button>
135
+ </DynamicForm>
136
+ );
137
+ }
138
+ ```
139
+
140
+ ## Configuration Reference
141
+
142
+ ### FormConfiguration
143
+
144
+ The root configuration object that defines your form structure.
145
+
146
+ ```typescript
147
+ interface FormConfiguration {
148
+ name?: string; // Optional form identifier
149
+ elements: FormElement[]; // Array of fields and layouts
150
+ }
151
+ ```
152
+
153
+ ### Field Types
154
+
155
+ The library supports the following built-in field types:
156
+
157
+ | Type | Description | Default Value Type |
158
+ |------|-------------|-------------------|
159
+ | `text` | Single-line text input | `string` |
160
+ | `email` | Email input with validation | `string` |
161
+ | `boolean` | Checkbox or toggle | `boolean` |
162
+ | `phone` | Telephone number input | `string` |
163
+ | `date` | Date picker | `string` |
164
+ | `select` | Dropdown/multi-select with options | `string \| string[]` |
165
+ | `array` | Repeatable field groups | `array` |
166
+ | `custom` | User-defined component | `unknown` |
167
+
168
+ ### Field Element Structure
169
+
170
+ ```typescript
171
+ interface FieldElement {
172
+ type: "text" | "email" | "boolean" | "phone" | "date" | "custom";
173
+ name: string; // Field path (supports dot notation)
174
+ label?: string; // Display label
175
+ placeholder?: string; // Placeholder text
176
+ defaultValue?: string | number | boolean | null;
177
+ validation?: ValidationConfig; // Validation rules
178
+ }
179
+ ```
180
+
181
+ ### Validation Configuration
182
+
183
+ ```typescript
184
+ interface ValidationConfig {
185
+ required?: boolean; // Field must have a value
186
+ minLength?: number; // Minimum text length
187
+ maxLength?: number; // Maximum text length
188
+ pattern?: string; // Regex pattern
189
+ message?: string; // Custom error message
190
+ condition?: JsonLogicRule; // JSON Logic condition
191
+ }
192
+ ```
193
+
194
+ ### Container Layout
195
+
196
+ Create multi-column layouts with containers:
197
+
198
+ ```typescript
199
+ {
200
+ type: "container",
201
+ columns: [
202
+ {
203
+ type: "column",
204
+ width: "50%",
205
+ elements: [
206
+ { type: "text", name: "firstName", label: "First Name" },
207
+ ],
208
+ },
209
+ {
210
+ type: "column",
211
+ width: "50%",
212
+ elements: [
213
+ { type: "text", name: "lastName", label: "Last Name" },
214
+ ],
215
+ },
216
+ ],
217
+ }
218
+ ```
219
+
220
+ ## Usage Examples
221
+
222
+ ### Nested Field Paths
223
+
224
+ Use dot notation to create nested data structures:
225
+
226
+ ```typescript
227
+ const config: FormConfiguration = {
228
+ elements: [
229
+ { type: "text", name: "contact.firstName", label: "First Name" },
230
+ { type: "text", name: "contact.lastName", label: "Last Name" },
231
+ { type: "email", name: "contact.email", label: "Email" },
232
+ { type: "text", name: "address.street", label: "Street" },
233
+ { type: "text", name: "address.city", label: "City" },
234
+ ],
235
+ };
236
+
237
+ // Submitted data structure:
238
+ // {
239
+ // contact: { firstName: "John", lastName: "Doe", email: "john@example.com" },
240
+ // address: { street: "123 Main St", city: "New York" }
241
+ // }
242
+ ```
243
+
244
+ ### Two-Column Layout
245
+
246
+ ```typescript
247
+ const config: FormConfiguration = {
248
+ elements: [
249
+ {
250
+ type: "container",
251
+ columns: [
252
+ {
253
+ type: "column",
254
+ width: "calc(50% - 0.5rem)", // Account for gap
255
+ elements: [
256
+ { type: "email", name: "email", label: "Email", validation: { required: true } },
257
+ { type: "date", name: "birthDate", label: "Birth Date" },
258
+ ],
259
+ },
260
+ {
261
+ type: "column",
262
+ width: "calc(50% - 0.5rem)",
263
+ elements: [
264
+ { type: "phone", name: "phone", label: "Phone" },
265
+ { type: "text", name: "company", label: "Company" },
266
+ ],
267
+ },
268
+ ],
269
+ },
270
+ ],
271
+ };
272
+ ```
273
+
274
+ ### Custom Field Component
275
+
276
+ Register custom components for specialized inputs. You can use simple components or fully typed definitions with Zod schema validation:
277
+
278
+ ```tsx
279
+ import {
280
+ defineCustomComponent,
281
+ type CustomComponentRegistry,
282
+ type CustomComponentRenderProps,
283
+ } from 'dynamic-forms';
284
+ import { z } from 'zod/v4';
285
+
286
+ // Option 1: Simple component
287
+ const SimpleRating = ({ field, config, componentProps }: CustomComponentRenderProps) => {
288
+ const maxStars = (componentProps?.maxStars as number) ?? 5;
289
+ return (
290
+ <div>
291
+ <label>{config.label}</label>
292
+ <div>
293
+ {Array.from({ length: maxStars }, (_, i) => (
294
+ <button key={i} type="button" onClick={() => field.onChange(i + 1)}>
295
+ {i < (field.value ?? 0) ? '★' : '☆'}
296
+ </button>
297
+ ))}
298
+ </div>
299
+ </div>
300
+ );
301
+ };
302
+
303
+ // Option 2: Type-safe definition with Zod schema validation
304
+ const RatingField = defineCustomComponent({
305
+ component: ({ field, componentProps }) => (
306
+ <div className="rating">
307
+ {Array.from({ length: componentProps.maxStars }, (_, i) => (
308
+ <button key={i} type="button" onClick={() => field.onChange(i + 1)}>
309
+ {i < (field.value as number ?? 0) ? '★' : '☆'}
310
+ </button>
311
+ ))}
312
+ </div>
313
+ ),
314
+ propsSchema: z.object({
315
+ maxStars: z.number().int().min(1).max(10).default(5),
316
+ }),
317
+ defaultProps: { maxStars: 5 },
318
+ displayName: 'RatingField',
319
+ });
320
+
321
+ // Register custom components
322
+ const customComponents: CustomComponentRegistry = {
323
+ SimpleRating,
324
+ RatingField,
325
+ };
326
+
327
+ // Use in configuration
328
+ const config: FormConfiguration = {
329
+ elements: [
330
+ {
331
+ type: "custom",
332
+ name: "rating",
333
+ label: "Rate our service",
334
+ component: "RatingField",
335
+ componentProps: { maxStars: 10 }, // Validated against propsSchema
336
+ },
337
+ ],
338
+ };
339
+
340
+ // Pass to DynamicForm
341
+ <DynamicForm
342
+ config={config}
343
+ fieldComponents={fieldComponents}
344
+ customComponents={customComponents}
345
+ onSubmit={handleSubmit}
346
+ />
347
+ ```
348
+
349
+ ### JSON Logic Conditional Validation
350
+
351
+ Use JSON Logic for complex validation rules that depend on other field values:
352
+
353
+ ```typescript
354
+ const config: FormConfiguration = {
355
+ elements: [
356
+ {
357
+ type: "boolean",
358
+ name: "hasPhone",
359
+ label: "I have a phone number",
360
+ },
361
+ {
362
+ type: "phone",
363
+ name: "phone",
364
+ label: "Phone Number",
365
+ validation: {
366
+ // Valid if: hasPhone is false OR phone matches 10-digit pattern
367
+ condition: {
368
+ or: [
369
+ { "!": { var: "hasPhone" } },
370
+ {
371
+ and: [
372
+ { var: "hasPhone" },
373
+ { regex_match: ["^[0-9]{10}$", { var: "phone" }] },
374
+ ],
375
+ },
376
+ ],
377
+ },
378
+ message: "Please enter a valid 10-digit phone number",
379
+ },
380
+ },
381
+ {
382
+ type: "boolean",
383
+ name: "acceptTerms",
384
+ label: "I accept the terms and conditions",
385
+ validation: {
386
+ // Checkbox must be checked
387
+ condition: { var: "acceptTerms" },
388
+ message: "You must accept the terms and conditions",
389
+ },
390
+ },
391
+ ],
392
+ };
393
+ ```
394
+
395
+ **Available JSON Logic Operations:**
396
+ - Standard operators: `var`, `and`, `or`, `!`, `==`, `!=`, `>`, `<`, `>=`, `<=`, `if`
397
+ - Custom: `regex_match` - `["pattern", { var: "fieldName" }]`
398
+
399
+ ## API Reference
400
+
401
+ ### DynamicForm Props
402
+
403
+ ```typescript
404
+ interface DynamicFormProps {
405
+ // Required
406
+ config: FormConfiguration; // Form configuration
407
+ fieldComponents: FieldComponentRegistry; // Component implementations
408
+ onSubmit: (data: FormData) => void; // Submit handler
409
+
410
+ // Optional - Validation (priority order: resolver > schema > config-driven)
411
+ resolver?: Resolver<FormData>; // Custom react-hook-form resolver (Yup, Joi, etc.)
412
+ schema?: ZodSchema; // External Zod schema (wrapped with visibility-aware resolver)
413
+
414
+ // Optional - Components
415
+ initialData?: FormData; // Pre-fill form values
416
+ customComponents?: CustomComponentRegistry; // Custom field components
417
+ customContainers?: CustomContainerRegistry; // Custom layout containers
418
+
419
+ // Optional - Event handlers
420
+ onChange?: (data: FormData, field: string) => void;
421
+ onError?: (errors: unknown) => void;
422
+ onReset?: () => void;
423
+ onValidationChange?: (errors: unknown, isValid: boolean) => void;
424
+
425
+ // Optional - Form behavior
426
+ mode?: "onChange" | "onBlur" | "onSubmit" | "onTouched" | "all";
427
+ invisibleFieldValidation?: "skip" | "validate" | "warn";
428
+ fieldWrapper?: FieldWrapperFunction; // Wrap each field with custom component
429
+
430
+ // Optional - HTML attributes
431
+ className?: string;
432
+ style?: CSSProperties;
433
+ id?: string;
434
+ children?: React.ReactNode; // Submit button, etc.
435
+ }
436
+ ```
437
+
438
+ ### Validation Options
439
+
440
+ The library supports three approaches to validation:
441
+
442
+ ```tsx
443
+ // Option 1: External resolver (full control - Yup, Joi, Vest, custom)
444
+ import { yupResolver } from '@hookform/resolvers/yup';
445
+ <DynamicForm resolver={yupResolver(yupSchema)} ... />
446
+
447
+ // Option 2: External Zod schema (wrapped with visibility-aware resolver)
448
+ <DynamicForm schema={myZodSchema} invisibleFieldValidation="skip" ... />
449
+
450
+ // Option 3: Config-driven (auto-generated from field validation configs)
451
+ <DynamicForm config={configWithValidation} ... />
452
+
453
+ // Option 4: No validation (omit resolver, schema, and validation in config)
454
+ <DynamicForm config={simpleConfig} ... />
455
+ ```
456
+
457
+ ### Hooks
458
+
459
+ ```typescript
460
+ // Access form context inside nested components
461
+ const { config, form } = useDynamicFormContext();
462
+
463
+ // Safe version that returns null outside form context
464
+ const context = useDynamicFormContextSafe();
465
+ ```
466
+
467
+ ### Field Component Props
468
+
469
+ All field components receive these props:
470
+
471
+ ```typescript
472
+ interface BaseFieldProps {
473
+ field: ControllerRenderProps; // react-hook-form: value, onChange, onBlur, ref
474
+ fieldState: ControllerFieldState; // error, invalid, isTouched, isDirty
475
+ config: FieldElement; // Field configuration
476
+ }
477
+ ```
478
+
479
+ ### Exports
480
+
481
+ ```typescript
482
+ // Components
483
+ export { DynamicForm } from 'dynamic-forms';
484
+
485
+ // Hooks
486
+ export { useDynamicFormContext, useDynamicFormContextSafe } from 'dynamic-forms';
487
+
488
+ // Custom Components
489
+ export {
490
+ defineCustomComponent, // Type-safe component definition helper
491
+ ConfigurationError, // Error class for invalid configurations
492
+ } from 'dynamic-forms';
493
+
494
+ // Types
495
+ export type {
496
+ FormConfiguration,
497
+ FormElement,
498
+ FieldElement,
499
+ ContainerElement,
500
+ ColumnElement,
501
+ ValidationConfig,
502
+ FieldComponentRegistry,
503
+ CustomComponentRegistry,
504
+ CustomContainerRegistry,
505
+ FormData,
506
+ ZodSchema,
507
+ // Custom component types
508
+ CustomComponentDefinition,
509
+ CustomComponentRenderProps,
510
+ // Field component types
511
+ TextFieldComponent,
512
+ EmailFieldComponent,
513
+ BooleanFieldComponent,
514
+ PhoneFieldComponent,
515
+ DateFieldComponent,
516
+ SelectFieldComponent,
517
+ ArrayFieldComponent,
518
+ CustomFieldComponent,
519
+ // Field element types
520
+ SelectFieldElement,
521
+ ArrayFieldElement,
522
+ SelectOption,
523
+ } from 'dynamic-forms';
524
+
525
+ // Utilities
526
+ export {
527
+ parseConfiguration,
528
+ safeParseConfiguration,
529
+ generateZodSchema,
530
+ createVisibilityAwareResolver,
531
+ calculateVisibility,
532
+ flattenFields,
533
+ getFieldNames,
534
+ mergeDefaults,
535
+ applyJsonLogic,
536
+ evaluateCondition,
537
+ isFieldElement,
538
+ isContainerElement,
539
+ isColumnElement,
540
+ isCustomFieldElement,
541
+ isArrayFieldElement,
542
+ } from 'dynamic-forms';
543
+ ```
544
+
545
+ ## Creating Field Components
546
+
547
+ Field components are React components that render form inputs. They receive react-hook-form controller props for state management.
548
+
549
+ ```tsx
550
+ import type { TextFieldComponent } from 'dynamic-forms';
551
+
552
+ const TextField: TextFieldComponent = ({ field, fieldState, config }) => {
553
+ return (
554
+ <div className="field">
555
+ {config.label && (
556
+ <label htmlFor={field.name}>
557
+ {config.label}
558
+ {config.validation?.required && <span>*</span>}
559
+ </label>
560
+ )}
561
+
562
+ <input
563
+ id={field.name}
564
+ type="text"
565
+ placeholder={config.placeholder}
566
+ aria-invalid={fieldState.invalid}
567
+ aria-describedby={fieldState.error ? `${field.name}-error` : undefined}
568
+ {...field}
569
+ />
570
+
571
+ {fieldState.error && (
572
+ <span id={`${field.name}-error`} role="alert">
573
+ {fieldState.error.message}
574
+ </span>
575
+ )}
576
+ </div>
577
+ );
578
+ };
579
+ ```
580
+
581
+ ## Development
582
+
583
+ ### Scripts
584
+
585
+ ```bash
586
+ pnpm dev # Start dev server (localhost:3000)
587
+ pnpm build # Build library
588
+ pnpm test # Run tests
589
+ pnpm test:watch # Run tests in watch mode
590
+ pnpm typecheck # TypeScript type checking
591
+ pnpm lint # Check for lint errors
592
+ pnpm lint:fix # Auto-fix lint errors
593
+ ```
594
+
595
+ ### Project Structure
596
+
597
+ ```text
598
+ src/
599
+ ├── components/ # React components
600
+ │ ├── FormRenderer # Renders all elements
601
+ │ ├── ElementRenderer # Routes to field/container
602
+ │ ├── FieldRenderer # Renders fields via registry
603
+ │ └── ContainerRenderer # Renders layouts
604
+ ├── context/ # React context
605
+ ├── hooks/ # useDynamicFormContext
606
+ ├── parser/ # Config parsing
607
+ ├── schema/ # Zod schema generation
608
+ ├── resolver/ # Visibility-aware resolver
609
+ ├── validation/ # JSON Logic evaluation
610
+ ├── types/ # TypeScript definitions
611
+ └── utils/ # Utilities
612
+
613
+ sample/ # Sample application
614
+ ├── App.tsx # Demo form
615
+ ├── fields/ # Sample field components
616
+ └── containers/ # Sample containers
617
+ ```
618
+
619
+ ### Running the Sample App
620
+
621
+ ```bash
622
+ pnpm dev
623
+ # Open http://localhost:3000
624
+ ```
625
+
626
+ ## Tech Stack
627
+
628
+ - **React 19** - UI framework
629
+ - **react-hook-form** - Form state management
630
+ - **Zod v4** - Schema validation
631
+ - **TypeScript** - Type safety
632
+ - **Vitest** - Testing
633
+ - **tsdown** - Library bundling (ESM + CJS)
634
+ - **Vite** - Dev server
635
+
636
+ ## Contributing
637
+
638
+ ### Branch Naming Convention
639
+
640
+ Create branches using the format: `type/description`
641
+
642
+ | Type | Purpose | Example |
643
+ |------|---------|---------|
644
+ | `feat/` | New features | `feat/custom-validators` |
645
+ | `fix/` | Bug fixes | `fix/nested-path-resolution` |
646
+ | `refactor/` | Code refactoring | `refactor/schema-generation` |
647
+ | `docs/` | Documentation | `docs/api-reference` |
648
+ | `chore/` | Maintenance tasks | `chore/update-dependencies` |
649
+ | `test/` | Test additions/fixes | `test/array-field-coverage` |
650
+
651
+ ### Commit Messages
652
+
653
+ This project uses [Conventional Commits](https://www.conventionalcommits.org/) for automated versioning and changelog generation.
654
+
655
+ **Format:**
656
+ ```text
657
+ type(scope): description
658
+
659
+ [optional body]
660
+ ```
661
+
662
+ **Examples:**
663
+ ```bash
664
+ feat(schema): add support for custom validators
665
+ fix(components): resolve visibility calculation bug
666
+ refactor(parser): simplify configuration parsing logic
667
+ docs(readme): add contributing guidelines
668
+ chore(deps): update react-hook-form to v7.72
669
+ test(utils): add edge case tests for flattenFields
670
+ ```
671
+
672
+ | Type | Description | Version Bump |
673
+ |------|-------------|--------------|
674
+ | `feat` | New feature | Minor (1.0.0 → 1.1.0) |
675
+ | `fix` | Bug fix | Patch (1.0.0 → 1.0.1) |
676
+ | `feat!` or `BREAKING CHANGE` | Breaking change | Major (1.0.0 → 2.0.0) |
677
+ | `docs`, `chore`, `refactor`, `test` | Non-release changes | None |
678
+
679
+ For detailed release workflow, see [docs/release-workflow.md](docs/release-workflow.md).
680
+
681
+ ### Pull Request Process
682
+
683
+ 1. Create a branch from `main` using the naming convention above
684
+ 2. Make your changes with conventional commit messages
685
+ 3. Ensure all checks pass: `pnpm test && pnpm typecheck && pnpm lint`
686
+ 4. Open a pull request to `main`
687
+ 5. Address review feedback
688
+ 6. Squash and merge (or rebase) when approved
689
+
690
+ ## License
691
+
692
+ MIT