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 +326 -1
- package/fesm2022/ngx-vest-forms.mjs +492 -38
- package/fesm2022/ngx-vest-forms.mjs.map +1 -1
- package/index.d.ts +326 -7
- package/package.json +11 -2
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
|
+
````
|