ngx-vest-forms 1.2.0 → 1.3.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 CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  ⭐ If you like this project, star it on GitHub — it helps a lot!
15
15
 
16
- [Overview](#overview) • [Getting Started](#getting-started) • [Features](#features) • [Basic Usage](#basic-usage) • [Examples](#examples) • [Resources](#resources) • [Developer Resources](#developer-resources) • [Acknowledgments](#acknowledgments)
16
+ [Overview](#overview) • [Getting Started](#getting-started) • [Features](#features) • [Basic Usage](#basic-usage) • [Examples](#examples) • [Form Structure Changes](#handling-form-structure-changes) • [Field State Utilities](#field-state-utilities) • [Documentation](#documentation) • [Resources](#resources) • [Developer Resources](#developer-resources) • [Acknowledgments](#acknowledgments)
17
17
 
18
18
  </div>
19
19
 
@@ -163,12 +163,14 @@ Your form automatically creates FormGroups and FormControls with type-safe, unid
163
163
  - **Form Arrays** - Dynamic lists with add/remove functionality
164
164
  - **Reactive Disabling** - Disable fields based on computed signals
165
165
  - **State Management** - Preserve field state across conditional rendering
166
+ - **Structure Change Detection** - Manual trigger for validation updates when form structure changes
166
167
 
167
168
  ### Developer Experience
168
169
 
169
170
  - **Runtime Shape Checking** - Catch typos in `name` attributes early
170
171
  - **Built-in Error Display** - `sc-control-wrapper` component for consistent UX
171
172
  - **Validation Config** - Declare field dependencies for complex scenarios
173
+ - **Field State Utilities** - Helper functions for managing dynamic form state
172
174
  - **Modern Angular** - Built for Angular 18+ with standalone components
173
175
 
174
176
  ## Basic Usage
@@ -209,6 +211,12 @@ The `scVestForm` directive offers these outputs:
209
211
  | `validChange` | Emits when the form becomes valid or invalid |
210
212
  | `errorsChange` | Emits the complete list of errors for the form and all its controls |
211
213
 
214
+ ### Public Methods
215
+
216
+ | Method | Description |
217
+ | ------------------------- | --------------------------------------------------------------------------------------------------------------------- |
218
+ | `triggerFormValidation()` | Manually triggers form validation update when form structure changes without value changes (e.g., conditional fields) |
219
+
212
220
  ### Avoiding typo's
213
221
 
214
222
  Template-driven forms are type-safe, but not in the `name` attributes or `ngModelGroup` attributes.
@@ -377,6 +385,302 @@ We can bind the computed signal to the `disabled` directive of Angular.
377
385
  />
378
386
  ```
379
387
 
388
+ ### Handling Form Structure Changes
389
+
390
+ When form structure changes dynamically in combination with _NON_ form elements (e.g., conditional fields are shown/hidden), the validation state may not update automatically since no control values change. For these scenarios, use the `triggerFormValidation()` method:
391
+
392
+ #### The Problem
393
+
394
+ ```typescript
395
+ // Form structure changes based on selection
396
+ @if (procedureType() === 'typeA') {
397
+ <input name="fieldA" [ngModel]="formValue().fieldA" />
398
+ }
399
+ @else if (procedureType() === 'typeB') {
400
+ <input name="fieldB" [ngModel]="formValue().fieldB" />
401
+ }
402
+ @else if (procedureType() === 'typeC') {
403
+ <!-- NON-FORM ELEMENT -->
404
+ <p>No additional input required for this procedure type.</p>
405
+ }
406
+ ```
407
+
408
+ **Issue**: When switching from `typeA` to `typeC`, the input field is removed but no control values change, so validation doesn't update automatically.
409
+
410
+ #### The Solution
411
+
412
+ ```typescript
413
+ @Component({
414
+ template: `
415
+ <form
416
+ scVestForm
417
+ [suite]="validationSuite"
418
+ (formValueChange)="formValue.set($event)"
419
+ #vestForm="scVestForm"
420
+ >
421
+ <select
422
+ name="procedureType"
423
+ [ngModel]="formValue().procedureType"
424
+ (ngModelChange)="onProcedureTypeChange($event)"
425
+ >
426
+ <option value="typeA">Type A</option>
427
+ <option value="typeB">Type B</option>
428
+ <option value="typeC">Type C (No input)</option>
429
+ </select>
430
+
431
+ @if (formValue().procedureType === 'typeA') {
432
+ <input name="fieldA" [ngModel]="formValue().fieldA" />
433
+ } @else if (formValue().procedureType === 'typeB') {
434
+ <input name="fieldB" [ngModel]="formValue().fieldB" />
435
+ } @else if (formValue().procedureType === 'typeC') {
436
+ <p>No additional input required.</p>
437
+ }
438
+ </form>
439
+ `,
440
+ })
441
+ export class MyFormComponent {
442
+ @ViewChild('vestForm') vestForm!: FormDirective<MyFormModel>;
443
+
444
+ protected readonly formValue = signal<MyFormModel>({});
445
+ protected readonly validationSuite = myValidationSuite;
446
+
447
+ onProcedureTypeChange(newType: string) {
448
+ // Update the form value
449
+ this.formValue.update((current) => ({
450
+ ...current,
451
+ procedureType: newType,
452
+ // Clear fields that are no longer relevant
453
+ ...(newType !== 'typeA' && { fieldA: undefined }),
454
+ ...(newType !== 'typeB' && { fieldB: undefined }),
455
+ }));
456
+
457
+ // ✅ CRITICAL: Trigger validation update after structure change
458
+ this.vestForm.triggerFormValidation();
459
+ }
460
+ }
461
+ ```
462
+
463
+ #### When to Use `triggerFormValidation()`
464
+
465
+ Call this method in these scenarios:
466
+
467
+ - **After changing form structure** - When conditional fields are shown/hidden
468
+ - **After clearing form sections** - When resetting parts of the form
469
+ - **After dynamic field addition/removal** - When programmatically modifying form structure
470
+ - **After switching form modes** - When toggling between different form layouts
471
+
472
+ #### Field Clearing Pattern for Component State Consistency
473
+
474
+ ##### When Field Clearing is Required
475
+
476
+ Field clearing utilities are **specifically needed** when conditional logic switches between:
477
+
478
+ - **Form inputs** (`<input>`, `<select>`, `<textarea>`) ↔ **NON-form elements** (`<p>`, `<div>`, informational content)
479
+
480
+ **Primary Use Case - The Problem Scenario:**
481
+
482
+ ```typescript
483
+ // This template structure REQUIRES manual field clearing:
484
+ @if (procedureType === 'typeA') {
485
+ <input name="fieldA" [ngModel]="formValue().fieldA" /> // Form input
486
+ } @else if (procedureType === 'typeB') {
487
+ <input name="fieldB" [ngModel]="formValue().fieldB" /> // Form input
488
+ } @else if (procedureType === 'typeC') {
489
+ <p>No additional input required for this procedure.</p> // NON-form element!
490
+ }
491
+ ```
492
+
493
+ **Why This Creates State Inconsistency:**
494
+
495
+ 1. **Switching FROM form input TO non-form content:** Angular removes FormControl, but component signal retains old value
496
+ 2. **Result:** `ngForm.form.value` becomes clean, but `formValue()` signal remains stale
497
+ 3. **Problem:** State inconsistency between Angular's form state and your component state
498
+
499
+ ```typescript
500
+ // Before switching from typeA to typeC:
501
+ formValue() = { procedureType: 'typeA', fieldA: 'some-value' };
502
+ ngForm.form.value = { procedureType: 'typeA', fieldA: 'some-value' };
503
+
504
+ // After switching (WITHOUT manual clearing):
505
+ formValue() = { procedureType: 'typeC', fieldA: 'some-value' }; // ❌ Stale fieldA!
506
+ ngForm.form.value = { procedureType: 'typeC' }; // ✅ Clean
507
+
508
+ // After switching (WITH manual clearing):
509
+ formValue() = { procedureType: 'typeC' }; // ✅ Consistent
510
+ ngForm.form.value = { procedureType: 'typeC' }; // ✅ Consistent
511
+ ```
512
+
513
+ ##### When Field Clearing is NOT Required
514
+
515
+ Pure form-to-form conditionals usually don't need manual field clearing:
516
+
517
+ ```typescript
518
+ // This template structure usually DOES NOT require manual field clearing:
519
+ @if (inputType === 'text') {
520
+ <input name="field" [ngModel]="formValue().field" type="text" /> // Form input
521
+ } @else if (inputType === 'number') {
522
+ <input name="field" [ngModel]="formValue().field" type="number" /> // Form input
523
+ } @else if (inputType === 'email') {
524
+ <input name="field" [ngModel]="formValue().field" type="email" /> // Form input
525
+ }
526
+ ```
527
+
528
+ **Why:** All branches contain form inputs with the same `name` attribute, so Angular maintains the FormControl and your component state naturally stays consistent.
529
+
530
+ **The Pattern:**
531
+
532
+ ```typescript
533
+ onStructureChange(newValue: string) {
534
+ this.formValue.update((current) => ({
535
+ ...current,
536
+ procedureType: newValue,
537
+ // Clear fields that are no longer relevant
538
+ ...(newValue !== 'typeA' && { fieldA: undefined }),
539
+ ...(newValue !== 'typeB' && { fieldB: undefined }),
540
+ }));
541
+
542
+ // Trigger validation update after structure change
543
+ this.vestFormRef.triggerFormValidation();
544
+ }
545
+ ```
546
+
547
+ #### Alternative: Utility Helper
548
+
549
+ For cleaner code, you can use the built-in utility functions from ngx-vest-forms:
550
+
551
+ ```typescript
552
+ import { clearFieldsWhen } from 'ngx-vest-forms';
553
+
554
+ // In your component method
555
+ onStructureChange(newValue: string) {
556
+ this.formValue.update((current) =>
557
+ clearFieldsWhen(current, {
558
+ fieldA: newValue !== 'typeA',
559
+ fieldB: newValue !== 'typeB',
560
+ })
561
+ );
562
+
563
+ // Trigger validation update after structure change
564
+ this.vestFormRef.triggerFormValidation();
565
+ }
566
+ ```
567
+
568
+ **Additional Utility Functions:**
569
+
570
+ ````typescript
571
+ import { clearFields, keepFieldsWhen } from 'ngx-vest-forms';
572
+
573
+ // Clear specific fields unconditionally
574
+ const cleanedState = clearFields(currentFormValue, ['fieldA', 'fieldB']);
575
+
576
+ // Keep only fields that meet conditions
577
+ const filteredState = keepFieldsWhen(currentFormValue, {
578
+ procedureType: true, // always keep
579
+ fieldA: procedureType === 'typeA',
580
+ fieldB: procedureType === 'typeB',
581
+ });
582
+ ```#### Validation Suite Pattern for Conditional Fields
583
+
584
+ ```typescript
585
+ import { staticSuite, test, enforce, omitWhen, only } from 'vest';
586
+
587
+ export const myValidationSuite = staticSuite(
588
+ (model: MyFormModel, field?: string) => {
589
+ if (field) {
590
+ only(field); // Performance optimization
591
+ }
592
+
593
+ // Always validate procedure type
594
+ test('procedureType', 'Procedure type is required', () => {
595
+ enforce(model.procedureType).isNotBlank();
596
+ });
597
+
598
+ // Conditional validations
599
+ omitWhen(model.procedureType !== 'typeA', () => {
600
+ test('fieldA', 'Field A is required for Type A', () => {
601
+ enforce(model.fieldA).isNotBlank();
602
+ });
603
+ });
604
+
605
+ omitWhen(model.procedureType !== 'typeB', () => {
606
+ test('fieldB', 'Field B is required for Type B', () => {
607
+ enforce(model.fieldB).isNotBlank();
608
+ });
609
+ });
610
+
611
+ // Note: No validation needed for typeC as it has no input fields
612
+ }
613
+ );
614
+ ```
615
+
616
+ ## Field State Utilities
617
+
618
+ ngx-vest-forms provides utility functions specifically designed for the scenario where conditional logic switches between **form inputs** and **non-form elements** (like informational text, paragraphs, or other non-input content).
619
+
620
+ > **Key Use Case:** These utilities are primarily needed when your template conditionally renders form inputs in some branches and non-form content in others. They ensure your component state stays consistent with the actual form structure.
621
+
622
+ ### `clearFieldsWhen`
623
+
624
+ Conditionally clears fields when switching from form inputs to non-form content.
625
+
626
+ ```typescript
627
+ import { clearFieldsWhen } from 'ngx-vest-forms';
628
+
629
+ // EXAMPLE: Clear fields when switching to non-form content (typeC shows info text, not input)
630
+ const updatedState = clearFieldsWhen(currentFormValue, {
631
+ fieldA: procedureType !== 'typeA', // Clear when NOT showing fieldA input
632
+ fieldB: procedureType !== 'typeB', // Clear when NOT showing fieldB input
633
+ // When procedureType === 'typeC', both fields are cleared (typeC shows <p> text, not inputs)
634
+ shippingAddress: !useShippingAddress, // Clear when showing "No shipping needed" message
635
+ });
636
+ ```
637
+
638
+ ### `clearFields`
639
+
640
+ Unconditionally clears specific fields. Useful for form reset operations or cleanup tasks.
641
+
642
+ ```typescript
643
+ import { clearFields } from 'ngx-vest-forms';
644
+
645
+ // Clear specific fields unconditionally
646
+ const cleanedState = clearFields(currentFormValue, [
647
+ 'temporaryData',
648
+ 'draftSaved',
649
+ ]);
650
+ ```
651
+
652
+ ### `keepFieldsWhen`
653
+
654
+ Creates a new state containing only fields that meet specified conditions. Takes a "whitelist" approach instead of clearing unwanted fields.
655
+
656
+ ```typescript
657
+ import { keepFieldsWhen } from 'ngx-vest-forms';
658
+
659
+ // Keep only relevant fields
660
+ const filteredState = keepFieldsWhen(currentFormValue, {
661
+ basicInfo: true, // always keep
662
+ addressInfo: needsAddress,
663
+ paymentInfo: requiresPayment,
664
+ });
665
+ ```
666
+
667
+ ### Why These Utilities Are Needed
668
+
669
+ The utilities are specifically needed when conditionally switching between **form inputs** and **non-form elements**:
670
+
671
+ **The Core Problem:**
672
+
673
+ 1. **Template structure:** `@if (condition) { <input> } @else { <p>Info text</p> }`
674
+ 2. **Angular's behavior:** Automatically removes FormControls when switching to non-form content
675
+ 3. **Your component signals:** Retain old field values from when input was present
676
+ 4. **Result:** State inconsistency between `ngForm.form.value` (clean) and `formValue()` (stale)
677
+
678
+ **The Solution:**
679
+
680
+ These utilities synchronize your component state with Angular's form state, ensuring consistency when form structure changes involve non-form content.
681
+
682
+ > **Note:** Pure form-to-form conditionals (e.g., switching between different input types with the same `name`) typically don't require these utilities as Angular maintains the FormControl throughout.
683
+
380
684
  ## Examples
381
685
 
382
686
  ### Simple Form with Validation
@@ -504,6 +808,8 @@ export class ConditionalFormComponent {
504
808
  - **[Purchase Form Demo](https://github.com/ngx-vest-forms/ngx-vest-forms/tree/master/projects/examples/src/app/components/smart/purchase-form)** - Complex form with nested objects, validation dependencies, and conditional logic
505
809
  - **[Business Hours Demo](https://github.com/ngx-vest-forms/ngx-vest-forms/tree/master/projects/examples/src/app/components/smart/business-hours-form)** - Dynamic form arrays with complex validation rules
506
810
 
811
+ > **💡 Pro Tip**: Check out our detailed [Structure Change Detection Guide](./docs/STRUCTURE_CHANGE_DETECTION.md) for advanced handling of conditional form scenarios, alternative approaches, and performance considerations.
812
+
507
813
  ### Validations
508
814
 
509
815
  The absolute gem in ngx-vest-forms is the flexibility in validations without writing any boilerplate.
@@ -1083,6 +1389,24 @@ export class AddressComponent {
1083
1389
  }
1084
1390
  ```
1085
1391
 
1392
+ ## Documentation
1393
+
1394
+ ### Detailed Guides
1395
+
1396
+ For comprehensive documentation beyond this README, check out our detailed guides:
1397
+
1398
+ - **[Structure Change Detection Guide](./docs/STRUCTURE_CHANGE_DETECTION.md)** - Advanced handling of conditional form scenarios
1399
+ - Alternative approaches and their trade-offs
1400
+ - Performance considerations and best practices
1401
+ - Detailed API reference with examples
1402
+ - When and why to use `triggerFormValidation()`
1403
+
1404
+ ### Coming Soon
1405
+
1406
+ - **Advanced Form Arrays Guide** - Dynamic lists, nested arrays, and complex scenarios
1407
+ - **Custom Validation Guide** - Building reusable validation suites and complex rules
1408
+ - **Performance Optimization Guide** - Tips and techniques for large-scale forms
1409
+
1086
1410
  ## Resources
1087
1411
 
1088
1412
  ### Documentation & Tutorials
@@ -1181,3 +1505,4 @@ These pioneers laid the groundwork that made ngx-vest-forms possible, combining
1181
1505
  ## License
1182
1506
 
1183
1507
  This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
1508
+ ````