Skip to content
This repository was archived by the owner on May 3, 2024. It is now read-only.

Commit b49b82f

Browse files
feat: 🎸 dependentValidator
1 parent 618b4cb commit b49b82f

File tree

11 files changed

+270
-20
lines changed

11 files changed

+270
-20
lines changed

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ The design of this library promotes less boilerplate code, which keeps your temp
3535
- [Handling form submission](#handling_form_submission)
3636
- [Getting error details](#getting_error_details)
3737
- [Styling](#styling)
38+
- [Miscellaneous](#miscellaneous)
3839
- [Development](#development)
3940

4041
## How it works
@@ -236,6 +237,64 @@ Include something similar to the following in global CSS file:
236237
}
237238
```
238239

240+
## Miscellaneous
241+
242+
ngx-errors library provides a couple of misc function that ease your work with forms.
243+
244+
### **dependentValidator**
245+
Makes it easy to trigger validation on the control, that depends on a value of a different control
246+
247+
Example with using `FormBuilder`:
248+
```ts
249+
import { dependentValidator } from '@ngspot/ngx-errors';
250+
251+
export class LazyComponent {
252+
constructor(fb: FormBuilder) {
253+
this.form = fb.group({
254+
password: ['', Validators.required],
255+
confirmPassword: ['', dependentValidator<string>({
256+
watchControl: f => f!.get('password')!,
257+
validator: (passwordValue) => isEqualToValidator(passwordValue)
258+
})],
259+
});
260+
}
261+
}
262+
263+
function isEqualToValidator<T>(compareVal: T): ValidatorFn {
264+
return function(control: AbstractControl): ValidationErrors | null {
265+
return control.value === compareVal
266+
? null
267+
: { match: { expected: compareVal, actual: control.value } };
268+
}
269+
}
270+
```
271+
272+
The `dependentValidator` may also take `condition`. If provided, it needs to return true for the validator to be used.
273+
274+
```ts
275+
const controlA = new FormControl('');
276+
const controlB = new FormControl('', dependentValidator<string>({
277+
watchControl: () => controlA,
278+
validator: () => Validators.required,
279+
condition: (val) => val === 'fire'
280+
}));
281+
```
282+
In the example above, the `controlB` will only be required when `controlA` value is `'fire'`
283+
284+
### **extractTouchedChanges**
285+
As of today, the FormControl does not provide a way to subscribe to the changes of `touched` status. This function lets you do just that:
286+
287+
```ts
288+
* const touchedChanged$ = extractTouchedChanges(formControl);
289+
```
290+
291+
### **markDescendantsAsDirty**
292+
As of today, the FormControl does not provide a way to mark the control and all its children as `dirty`. This function lets you do just that:
293+
294+
```ts
295+
markDescendantsAsDirty(formControl);
296+
```
297+
239298
## Development
240299

241300
### Basic Workflow

projects/ngx-errors/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"name": "@ngspot/ngx-errors",
3-
"version": "2.0.0",
3+
"version": "2.0.1",
44
"description": "Handle error messages in Angular forms with ease",
55
"peerDependencies": {
6-
"@angular/core": "^9.0.0",
6+
"@angular/core": ">= 9.0.0",
77
"tslib": "^1.10.0"
88
},
99
"author": {

projects/ngx-errors/src/lib/misc.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AbstractControl } from '@angular/forms';
1+
import { AbstractControl, FormArray, FormGroup } from '@angular/forms';
22
import { Observable, Subject } from 'rxjs';
33

44
/**
@@ -48,3 +48,31 @@ export const extractTouchedChanges = (
4848

4949
return touchedChanges$.asObservable();
5050
};
51+
52+
/**
53+
* Marks the provided control as well as all of its children as dirty
54+
* @param options to be passed into control.markAsDirty() call
55+
*/
56+
export function markDescendantsAsDirty(
57+
control: AbstractControl,
58+
options?: {
59+
onlySelf?: boolean;
60+
emitEvent?: boolean;
61+
}
62+
) {
63+
control.markAsDirty(options);
64+
65+
if (control instanceof FormGroup || control instanceof FormArray) {
66+
let controls = Object.keys(control.controls).map(
67+
(controlName) => control.get(controlName)!
68+
);
69+
70+
controls.forEach((control) => {
71+
control.markAsDirty(options);
72+
73+
if ((control as FormGroup | FormArray).controls) {
74+
markDescendantsAsDirty(control, options);
75+
}
76+
});
77+
}
78+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import {
2+
AbstractControl,
3+
FormControl,
4+
ValidationErrors,
5+
ValidatorFn,
6+
} from '@angular/forms';
7+
import { dependentValidator } from './validators';
8+
9+
function matchValidator<T>(compareVal: T): ValidatorFn {
10+
return function (control: AbstractControl): ValidationErrors | null {
11+
return control.value === compareVal
12+
? null
13+
: { match: { expected: compareVal, actual: control.value } };
14+
};
15+
}
16+
17+
describe('dependentValidator', () => {
18+
let controlA: FormControl;
19+
let controlB: FormControl;
20+
21+
let controlAValue: string;
22+
let controlBValue: string;
23+
24+
// let matchValidatorSpy: jasmine.Spy<typeof matchValidator>;
25+
let condition: ((val?: any) => boolean) | undefined;
26+
27+
Given(() => {
28+
controlAValue = '';
29+
controlBValue = '';
30+
condition = undefined;
31+
// matchValidatorSpy = jasmine.createSpy('matchValidator', matchValidator).and.callThrough();
32+
});
33+
34+
When(() => {
35+
controlA = new FormControl(controlAValue);
36+
controlB = new FormControl(
37+
controlBValue,
38+
dependentValidator<string>({
39+
watchControl: () => controlA,
40+
validator: (val) => matchValidator(val),
41+
condition,
42+
})
43+
);
44+
});
45+
46+
describe('controlA.value === controlB.value', () => {
47+
Given(() => (controlAValue = ''));
48+
Then('Control B is valid', () => expect(controlB.valid).toBe(true));
49+
});
50+
51+
describe('controlA.value !== controlB.value', () => {
52+
Given(() => (controlAValue = 'asd'));
53+
Then('Control B is invalid', () => expect(controlB.valid).toBe(false));
54+
});
55+
56+
describe('controlA.value !== controlB.value, then updated to match', () => {
57+
Given(() => {
58+
controlAValue = 'asd';
59+
controlBValue = 'qwe';
60+
});
61+
62+
Then('Control B is valid', () => {
63+
controlA.setValue(controlBValue);
64+
expect(controlB.valid).toBe(true);
65+
});
66+
});
67+
68+
describe('condition is provided', () => {
69+
describe('GIVEN: condition returns false', () => {
70+
Given(() => {
71+
controlAValue = 'not Dima';
72+
controlBValue = 'two';
73+
condition = (val) => val === 'Dima';
74+
});
75+
76+
Then('Control B is valid', () => {
77+
expect(controlB.valid).toBe(true);
78+
});
79+
});
80+
81+
describe('GIVEN: condition returns true', () => {
82+
Given(() => {
83+
controlAValue = 'Dima';
84+
controlBValue = 'two';
85+
condition = (val) => val === 'Dima';
86+
});
87+
88+
Then('Control B is invalid', () => {
89+
expect(controlB.valid).toBe(false);
90+
});
91+
});
92+
});
93+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { isDevMode } from '@angular/core';
2+
import { AbstractControl, ValidatorFn } from '@angular/forms';
3+
4+
export interface DependentValidatorOptions<T> {
5+
/**
6+
* Function that returns AbstractControl to watch
7+
* @param form - the root FormGroup of the control being validated
8+
*/
9+
watchControl: (form?: AbstractControl) => AbstractControl;
10+
/**
11+
* @param watchControlValue - the value of the control being watched
12+
* @returns ValidatorFn. Ex: Validators.required
13+
*/
14+
validator: (watchControlValue?: T) => ValidatorFn;
15+
/**
16+
* If the condition is provided, it must return true in order for the
17+
* validator to be applied.
18+
* @param watchControlValue - the value of the control being watched
19+
*/
20+
condition?: (watchControlValue?: T) => boolean;
21+
}
22+
23+
/**
24+
* Makes it easy to trigger validation on the control, that depends on
25+
* a value of a different control
26+
*/
27+
export function dependentValidator<T = any>(
28+
opts: DependentValidatorOptions<T>
29+
) {
30+
let subscribed = false;
31+
32+
return (formControl: AbstractControl) => {
33+
const form = formControl.root;
34+
const { watchControl, condition, validator } = opts;
35+
const controlToWatch = watchControl(form);
36+
37+
if (!controlToWatch) {
38+
if (isDevMode()) {
39+
console.warn(
40+
`dependentValidator could not find specified watchControl`
41+
);
42+
}
43+
return null;
44+
}
45+
46+
if (!subscribed) {
47+
subscribed = true;
48+
49+
controlToWatch.valueChanges.subscribe(() => {
50+
formControl.updateValueAndValidity();
51+
});
52+
}
53+
54+
if (condition === undefined || condition(controlToWatch.value)) {
55+
const validatorFn = validator(controlToWatch.value);
56+
return validatorFn(formControl);
57+
}
58+
59+
return null;
60+
};
61+
}

projects/ngx-errors/src/public-api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ export * from './lib/errors-configuration';
77
export * from './lib/errors.directive';
88
export * from './lib/errors.module';
99
export * from './lib/ngx-errors';
10+
export * from './lib/validators';
11+
export * from './lib/misc';

projects/playground/src/app/app.component.spec.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,8 @@ import { AppComponent } from './app.component';
55
describe('AppComponent', () => {
66
beforeEach(async(() => {
77
TestBed.configureTestingModule({
8-
imports: [
9-
RouterTestingModule
10-
],
11-
declarations: [
12-
AppComponent
13-
],
8+
imports: [RouterTestingModule],
9+
declarations: [AppComponent],
1410
}).compileComponents();
1511
}));
1612

@@ -25,11 +21,4 @@ describe('AppComponent', () => {
2521
const app = fixture.componentInstance;
2622
expect(app.title).toEqual('playground');
2723
});
28-
29-
it('should render title', () => {
30-
const fixture = TestBed.createComponent(AppComponent);
31-
fixture.detectChanges();
32-
const compiled = fixture.nativeElement;
33-
expect(compiled.querySelector('.content span').textContent).toContain('playground app is running!');
34-
});
3524
});

projects/playground/src/app/lazy/lazy.component.html

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
<form [formGroup]="form">
2-
<input type="text" formControlName="firstName" />
2+
<label>
3+
First Name
4+
<input type="text" formControlName="firstName" />
5+
</label>
36

47
<pre>
58
Dirty: {{ form.controls.firstName.dirty }}
@@ -11,7 +14,12 @@
1114
</div>
1215

1316
<div formGroupName="address">
14-
<input type="text" formControlName="street" />
17+
<label>
18+
Street
19+
<input type="text" formControlName="street" />
20+
</label>
21+
22+
<pre>{{ form.get('address.street')?.errors | json }}</pre>
1523

1624
<div ngxErrors="street">
1725
<div ngxError="required">Street name is required</div>

projects/playground/src/app/lazy/lazy.component.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { ReactiveFormsModule } from '@angular/forms';
23

34
import { LazyComponent } from './lazy.component';
45

@@ -8,6 +9,7 @@ describe('LazyComponent', () => {
89

910
beforeEach(async(() => {
1011
TestBed.configureTestingModule({
12+
imports: [ReactiveFormsModule],
1113
declarations: [LazyComponent],
1214
}).compileComponents();
1315
}));

projects/playground/src/app/lazy/lazy.component.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
22
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
3+
import { dependentValidator } from '@ngspot/ngx-errors';
34

45
@Component({
56
selector: 'app-lazy',
@@ -13,7 +14,14 @@ export class LazyComponent implements OnInit {
1314
this.form = fb.group({
1415
firstName: ['', Validators.required],
1516
address: fb.group({
16-
street: ['', Validators.required],
17+
street: [
18+
'',
19+
dependentValidator<string>({
20+
watchControl: (f) => f!.get('firstName')!,
21+
condition: (val) => !!val,
22+
validator: () => Validators.required,
23+
}),
24+
],
1725
}),
1826
});
1927
}

0 commit comments

Comments
 (0)