0
# Form Control Utilities
1
2
Utility classes and functions for form control manipulation, validation state management, and specialized validation scenarios.
3
4
## Capabilities
5
6
### AbstractControlUtil
7
8
Utility class providing helper methods for Angular AbstractControl manipulation and validation state management.
9
10
#### Control Presence Check
11
12
Determines if a control has a meaningful value (not null, undefined, or empty string).
13
14
```typescript { .api }
15
/**
16
* Checks if control has a meaningful value
17
* @param control - AbstractControl to check
18
* @returns true if control value is null, undefined, or empty string
19
*/
20
static isNotPresent(control: AbstractControl): boolean;
21
```
22
23
**Usage Example:**
24
25
```typescript
26
import { AbstractControlUtil } from "ngx-validators";
27
import { FormControl } from "@angular/forms";
28
29
const control1 = new FormControl(null);
30
const control2 = new FormControl('');
31
const control3 = new FormControl('value');
32
const control4 = new FormControl(0);
33
34
console.log(AbstractControlUtil.isNotPresent(control1)); // true
35
console.log(AbstractControlUtil.isNotPresent(control2)); // true
36
console.log(AbstractControlUtil.isNotPresent(control3)); // false
37
console.log(AbstractControlUtil.isNotPresent(control4)); // false
38
```
39
40
**Implementation Logic:**
41
- Returns `true` if value is `undefined` or `null`
42
- Returns `true` if value is empty string `""`
43
- Returns `false` for all other values (including `0`, `false`, empty arrays/objects)
44
45
#### Error Management
46
47
##### Add Error
48
49
Programmatically adds a validation error to a control.
50
51
```typescript { .api }
52
/**
53
* Adds a validation error to the control
54
* @param control - AbstractControl to add error to (can be null)
55
* @param errorId - String identifier for the error
56
* @param value - Error value/details
57
*/
58
static addError(control: AbstractControl | null, errorId: string, value: any): void;
59
```
60
61
**Usage Example:**
62
63
```typescript
64
import { AbstractControlUtil } from "ngx-validators";
65
import { FormControl } from "@angular/forms";
66
67
const control = new FormControl('invalid-value');
68
69
// Add custom validation error
70
AbstractControlUtil.addError(control, 'customRule', {
71
message: 'Value does not meet custom criteria',
72
expected: 'valid-format',
73
actual: 'invalid-value'
74
});
75
76
console.log(control.errors);
77
// Output: { customRule: { message: '...', expected: '...', actual: '...' } }
78
79
// Add simple boolean error
80
AbstractControlUtil.addError(control, 'required', true);
81
82
console.log(control.errors);
83
// Output: { customRule: {...}, required: true }
84
```
85
86
**Behavior:**
87
- If control is `null`, the method does nothing
88
- If control has no existing errors, creates new errors object with the specified error
89
- If control has existing errors but not the specified `errorId`, adds the new error
90
- If the error already exists, does nothing (doesn't overwrite)
91
92
##### Remove Error
93
94
Programmatically removes a specific validation error from a control.
95
96
```typescript { .api }
97
/**
98
* Removes a specific validation error from the control
99
* @param control - AbstractControl to remove error from (can be null)
100
* @param errorId - String identifier of the error to remove
101
*/
102
static removeError(control: AbstractControl | null, errorId: string): void;
103
```
104
105
**Usage Example:**
106
107
```typescript
108
const control = new FormControl('', {
109
validators: [/* some validators */]
110
});
111
112
// Assume control has multiple errors
113
console.log(control.errors);
114
// Output: { required: true, minlength: { requiredLength: 5, actualLength: 0 } }
115
116
// Remove specific error
117
AbstractControlUtil.removeError(control, 'required');
118
119
console.log(control.errors);
120
// Output: { minlength: { requiredLength: 5, actualLength: 0 } }
121
122
// Remove the last error
123
AbstractControlUtil.removeError(control, 'minlength');
124
125
console.log(control.errors);
126
// Output: null (errors object is cleared when no errors remain)
127
```
128
129
**Behavior:**
130
- If control is `null`, the method does nothing
131
- If control has no errors or doesn't have the specified error, does nothing
132
- If removing the error would leave other errors, removes only the specified error
133
- If removing the error would leave no errors, sets control errors to `null`
134
135
### EqualToValidator
136
137
Specialized validator for ensuring two form controls have equal values, commonly used for password confirmation scenarios.
138
139
#### Equal To Validation
140
141
Creates a validator function that compares two controls within the same form group.
142
143
```typescript { .api }
144
/**
145
* Creates validator that ensures two controls have equal values
146
* @param c1Name - Name of the first control to compare
147
* @param c2Name - Name of the second control to compare
148
* @returns ValidatorFn that validates equality at form group level
149
*/
150
static equalTo(c1Name: string, c2Name: string): ValidatorFn;
151
```
152
153
**Usage Example:**
154
155
```typescript
156
import { EqualToValidator } from "ngx-validators";
157
import { FormBuilder, FormGroup } from "@angular/forms";
158
159
export class RegistrationComponent {
160
registrationForm: FormGroup;
161
162
constructor(private fb: FormBuilder) {
163
this.registrationForm = this.fb.group({
164
password: [''],
165
confirmPassword: [''],
166
email: [''],
167
confirmEmail: ['']
168
}, {
169
validators: [
170
EqualToValidator.equalTo('password', 'confirmPassword'),
171
EqualToValidator.equalTo('email', 'confirmEmail')
172
]
173
});
174
}
175
}
176
```
177
178
**Advanced Usage with Custom Control Names:**
179
180
```typescript
181
const userForm = this.fb.group({
182
newPassword: [''],
183
passwordVerification: [''],
184
primaryEmail: [''],
185
emailConfirmation: ['']
186
}, {
187
validators: [
188
EqualToValidator.equalTo('newPassword', 'passwordVerification'),
189
EqualToValidator.equalTo('primaryEmail', 'emailConfirmation')
190
]
191
});
192
```
193
194
**Error Handling:**
195
196
The validator manages errors at two levels:
197
198
1. **Form Group Level**: Returns validation error object if controls don't match
199
2. **Control Level**: Directly adds/removes `notEqualTo` error on the second control
200
201
```typescript
202
// Form group level error (returned by the validator)
203
if (form.errors?.equalTo) {
204
console.log('Form has equality validation errors');
205
}
206
207
// Control level error (added directly to the second control)
208
const confirmControl = form.get('confirmPassword');
209
if (confirmControl?.errors?.notEqualTo) {
210
console.log('Confirmation password does not match');
211
}
212
```
213
214
**Template Usage:**
215
216
```html
217
<form [formGroup]="registrationForm">
218
<input formControlName="password" type="password" placeholder="Password">
219
220
<input formControlName="confirmPassword" type="password" placeholder="Confirm Password">
221
222
<!-- Show error from the control level -->
223
<div *ngIf="registrationForm.get('confirmPassword')?.errors?.notEqualTo">
224
Passwords do not match.
225
</div>
226
227
<!-- Show error from the form level -->
228
<div *ngIf="registrationForm.errors?.equalTo">
229
Please ensure all fields match their confirmations.
230
</div>
231
</form>
232
```
233
234
## Integration with Other Validators
235
236
### Using Utilities in Custom Validators
237
238
```typescript
239
import { AbstractControlUtil } from "ngx-validators";
240
import { AbstractControl, ValidatorFn, ValidationErrors } from "@angular/forms";
241
242
export function customValidator(): ValidatorFn {
243
return (control: AbstractControl): ValidationErrors | null => {
244
// Use utility to check if control has meaningful value
245
if (AbstractControlUtil.isNotPresent(control)) {
246
return null; // Don't validate empty controls
247
}
248
249
const value = control.value;
250
251
// Custom validation logic
252
if (value !== 'expected-value') {
253
// Use utility to add error to related control
254
const relatedControl = control.parent?.get('relatedField');
255
AbstractControlUtil.addError(relatedControl, 'relatedFieldError', {
256
message: 'Related field must be updated when this field changes'
257
});
258
259
return { customValidation: { expected: 'expected-value', actual: value } };
260
} else {
261
// Use utility to remove error from related control
262
const relatedControl = control.parent?.get('relatedField');
263
AbstractControlUtil.removeError(relatedControl, 'relatedFieldError');
264
265
return null;
266
}
267
};
268
}
269
```
270
271
### Combining with Password Validators
272
273
```typescript
274
import { PasswordValidators, AbstractControlUtil, EqualToValidator } from "ngx-validators";
275
276
const passwordForm = this.fb.group({
277
password: ['', [
278
PasswordValidators.digitCharacterRule(1),
279
PasswordValidators.lowercaseCharacterRule(1),
280
PasswordValidators.uppercaseCharacterRule(1)
281
]],
282
confirmPassword: ['']
283
}, {
284
validators: [EqualToValidator.equalTo('password', 'confirmPassword')]
285
});
286
287
// Manual error management example
288
const handlePasswordChange = (newPassword: string) => {
289
const confirmControl = passwordForm.get('confirmPassword');
290
291
if (AbstractControlUtil.isNotPresent(confirmControl)) {
292
return;
293
}
294
295
// Clear previous custom errors
296
AbstractControlUtil.removeError(confirmControl, 'passwordStrengthMismatch');
297
298
// Add custom validation if needed
299
if (newPassword.length > 0 && confirmControl.value !== newPassword) {
300
AbstractControlUtil.addError(confirmControl, 'passwordStrengthMismatch', {
301
message: 'Confirmation password must match the new password'
302
});
303
}
304
};
305
```
306
307
### EqualToDirective
308
309
Template-driven forms directive that validates two form controls have equal values, with reactive subscription management and automatic validation updates.
310
311
```typescript { .api }
312
@Directive({
313
selector: "[equalTo][ngModel], [equalTo][formControlName], [equalTo][formControl]"
314
})
315
export class EqualToDirective implements Validator, OnDestroy, OnChanges {
316
@Input() equalTo: string | AbstractControl;
317
318
validate(c: AbstractControl): ValidationErrors | null;
319
ngOnDestroy(): void;
320
ngOnChanges(changes: SimpleChanges): void;
321
registerOnValidatorChange(fn: () => void): void;
322
}
323
```
324
325
**Usage Examples:**
326
327
```html
328
<!-- Basic usage with ngModel -->
329
<form>
330
<input
331
type="password"
332
name="password"
333
[(ngModel)]="user.password"
334
#password="ngModel">
335
336
<input
337
type="password"
338
name="confirmPassword"
339
[(ngModel)]="user.confirmPassword"
340
#confirmPassword="ngModel"
341
[equalTo]="'password'">
342
343
<div *ngIf="confirmPassword.errors?.notEqualTo">
344
Passwords do not match.
345
</div>
346
</form>
347
348
<!-- Usage with reactive forms in template -->
349
<form [formGroup]="myForm">
350
<input formControlName="email" type="email">
351
352
<input
353
formControlName="confirmEmail"
354
type="email"
355
[equalTo]="'email'">
356
357
<div *ngIf="myForm.get('confirmEmail')?.errors?.notEqualTo">
358
Email addresses do not match.
359
</div>
360
</form>
361
```
362
363
**Advanced Usage with AbstractControl Reference:**
364
365
```typescript
366
export class MyComponent {
367
@ViewChild('passwordControl') passwordControl!: NgModel;
368
369
// Can pass control reference directly
370
getPasswordControl(): AbstractControl {
371
return this.passwordControl.control;
372
}
373
}
374
```
375
376
```html
377
<input
378
type="password"
379
name="password"
380
[(ngModel)]="user.password"
381
#passwordControl="ngModel">
382
383
<input
384
type="password"
385
name="confirmPassword"
386
[(ngModel)]="user.confirmPassword"
387
#confirmPassword="ngModel"
388
[equalTo]="getPasswordControl()">
389
```
390
391
**Key Features:**
392
393
1. **Reactive Subscription Management**: Automatically subscribes to target control's value changes
394
2. **Memory Leak Prevention**: Implements `OnDestroy` to unsubscribe from observables
395
3. **Circular Update Prevention**: Uses `delay(1)` operator to prevent validation loops
396
4. **Flexible Input**: Accepts either control name (string) or AbstractControl reference
397
5. **Error Management**: Returns `{ notEqualTo: true }` when values don't match
398
399
**Error Object:**
400
401
```typescript
402
{
403
notEqualTo: true
404
}
405
```
406
407
**Implementation Notes:**
408
409
- The directive creates a subscription to the target control's `valueChanges` observable
410
- When the target control value changes, it triggers validation on the current control
411
- The `delay(1)` operator prevents infinite validation loops
412
- The directive properly cleans up subscriptions to prevent memory leaks
413
- Works with both template-driven forms (`ngModel`) and reactive forms (`formControlName`, `formControl`)
414
415
## Error State Management Best Practices
416
417
### Conditional Error Display
418
419
```typescript
420
export class FormUtilities {
421
/**
422
* Checks if a control should display errors
423
* @param control - FormControl to check
424
* @returns true if control has errors and has been touched or is dirty
425
*/
426
static shouldShowErrors(control: AbstractControl): boolean {
427
return !!(control && control.errors && (control.touched || control.dirty));
428
}
429
430
/**
431
* Gets user-friendly error message for a control
432
* @param control - FormControl with potential errors
433
* @returns formatted error message or empty string
434
*/
435
static getErrorMessage(control: AbstractControl): string {
436
if (!control || !control.errors) return '';
437
438
if (control.errors['required']) return 'This field is required';
439
if (control.errors['notEqualTo']) return 'Values do not match';
440
if (control.errors['minlength']) {
441
const error = control.errors['minlength'];
442
return `Minimum length is ${error.requiredLength} characters`;
443
}
444
445
return 'Please check this field';
446
}
447
}
448
```
449
450
**Template Usage:**
451
452
```html
453
<div class="form-field">
454
<input formControlName="confirmPassword" type="password">
455
456
<div class="error-message"
457
*ngIf="shouldShowErrors(form.get('confirmPassword'))">
458
{{ getErrorMessage(form.get('confirmPassword')) }}
459
</div>
460
</div>
461
```