Skip to content

Commit 00329f5

Browse files
authored
Merge pull request #156 from jafaircl/add-valid-invalid
2 parents 974f46b + 2a0358f commit 00329f5

File tree

8 files changed

+158
-52
lines changed

8 files changed

+158
-52
lines changed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,28 @@ const control = new FormControl('');
179179
control.enabled$.subscribe(isEnabled => ...);
180180
```
181181

182+
### `invalid$`
183+
184+
Observes the control's `invalid` status.
185+
186+
```ts
187+
import { FormControl } from '@ngneat/reactive-forms';
188+
189+
const control = new FormControl('');
190+
control.invalid$.subscribe(isInvalid => ...);
191+
```
192+
193+
### `valid$`
194+
195+
Observes the control's `valid` status.
196+
197+
```ts
198+
import { FormControl } from '@ngneat/reactive-forms';
199+
200+
const control = new FormControl('');
201+
control.valid$.subscribe(isValid => ...);
202+
```
203+
182204
### `status$`
183205

184206
Observes the control's `status`.

libs/reactive-forms/src/lib/core.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ export function controlValueChanges$<T>(
2323

2424
export type ControlState = 'VALID' | 'INVALID' | 'PENDING' | 'DISABLED';
2525

26-
export function controlStatus$<K extends 'disabled' | 'enabled' | 'status'>(
26+
export function controlStatus$<
27+
K extends 'disabled' | 'enabled' | 'invalid' | 'valid' | 'status'
28+
>(
2729
control: AbstractControl,
2830
type: K
2931
): Observable<K extends 'status' ? ControlState : boolean> {

libs/reactive-forms/src/lib/form-array.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Validators } from '@angular/forms';
12
import { expectTypeOf } from 'expect-type';
23
import { Observable, of, Subject, Subscription } from 'rxjs';
34
import { FormControl, FormGroup } from '..';
@@ -33,6 +34,8 @@ describe('FormArray Types', () => {
3334

3435
expectTypeOf(arr.disabled$).toEqualTypeOf<Observable<boolean>>();
3536
expectTypeOf(arr.enabled$).toEqualTypeOf<Observable<boolean>>();
37+
expectTypeOf(arr.invalid$).toEqualTypeOf<Observable<boolean>>();
38+
expectTypeOf(arr.valid$).toEqualTypeOf<Observable<boolean>>();
3639
expectTypeOf(arr.status$).toEqualTypeOf<Observable<ControlState>>();
3740

3841
const first$ = arr.select((state) => {
@@ -207,6 +210,28 @@ describe('FormArray Functionality', () => {
207210
expect(spy).toHaveBeenCalledTimes(2);
208211
});
209212

213+
it('should invalidChanges$', () => {
214+
const control = new FormArray([new FormControl(null, Validators.required)]);
215+
const spy = jest.fn();
216+
control.invalid$.subscribe(spy);
217+
expect(spy).toHaveBeenCalledWith(true);
218+
control.setValue(['abc']);
219+
expect(spy).toHaveBeenCalledWith(false);
220+
control.setValue([null]);
221+
expect(spy).toHaveBeenCalledTimes(3);
222+
});
223+
224+
it('should validChanges$', () => {
225+
const control = new FormArray([new FormControl(null, Validators.required)]);
226+
const spy = jest.fn();
227+
control.valid$.subscribe(spy);
228+
expect(spy).toHaveBeenCalledWith(false);
229+
control.setValue(['abc']);
230+
expect(spy).toHaveBeenCalledWith(true);
231+
control.setValue([null]);
232+
expect(spy).toHaveBeenCalledTimes(3);
233+
});
234+
210235
it('should statusChanges$', () => {
211236
const control = createArray();
212237
const spy = jest.fn();

libs/reactive-forms/src/lib/form-array.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ import { DeepPartial } from './types';
2424
export class FormArray<
2525
T,
2626
Control extends AbstractControl = T extends Record<any, any>
27-
? FormGroup<ControlsOf<T>>
28-
: FormControl<T>
29-
> extends NgFormArray {
27+
? FormGroup<ControlsOf<T>>
28+
: FormControl<T>
29+
> extends NgFormArray {
3030
readonly value!: T[];
3131
readonly valueChanges!: Observable<T[]>;
3232

@@ -43,6 +43,8 @@ export class FormArray<
4343
readonly value$ = controlValueChanges$<T[]>(this);
4444
readonly disabled$ = controlStatus$(this, 'disabled');
4545
readonly enabled$ = controlStatus$(this, 'enabled');
46+
readonly invalid$ = controlStatus$(this, 'invalid');
47+
readonly valid$ = controlStatus$(this, 'valid');
4648
readonly status$ = controlStatus$(this, 'status');
4749
readonly errors$ = controlErrorChanges$(
4850
this,

libs/reactive-forms/src/lib/form-control.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,28 @@ describe('FormControl Functionality', () => {
3636
expect(spy).toHaveBeenCalledTimes(2);
3737
});
3838

39+
it('should invalidChanges$', () => {
40+
const control = new FormControl<string | null>(null, Validators.required);
41+
const spy = jest.fn();
42+
control.invalid$.subscribe(spy);
43+
expect(spy).toHaveBeenCalledWith(true);
44+
control.setValue('abc');
45+
expect(spy).toHaveBeenCalledWith(false);
46+
control.setValue(null);
47+
expect(spy).toHaveBeenCalledTimes(3);
48+
});
49+
50+
it('should validChanges$', () => {
51+
const control = new FormControl<string | null>(null, Validators.required);
52+
const spy = jest.fn();
53+
control.valid$.subscribe(spy);
54+
expect(spy).toHaveBeenCalledWith(false);
55+
control.setValue('abc');
56+
expect(spy).toHaveBeenCalledWith(true);
57+
control.setValue(null);
58+
expect(spy).toHaveBeenCalledTimes(3);
59+
});
60+
3961
it('should statusChanges$', () => {
4062
const control = new FormControl<string>();
4163
const spy = jest.fn();

libs/reactive-forms/src/lib/form-control.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
} from './core';
2020
import { BoxedValue } from './types';
2121

22-
2322
export class FormControl<T> extends NgFormControl {
2423
readonly value!: T;
2524
readonly valueChanges!: Observable<T>;
@@ -37,6 +36,8 @@ export class FormControl<T> extends NgFormControl {
3736
readonly value$ = controlValueChanges$<T>(this);
3837
readonly disabled$ = controlStatus$(this, 'disabled');
3938
readonly enabled$ = controlStatus$(this, 'enabled');
39+
readonly invalid$ = controlStatus$(this, 'invalid');
40+
readonly valid$ = controlStatus$(this, 'valid');
4041
readonly status$ = controlStatus$(this, 'status');
4142
readonly errors$ = controlErrorChanges$(
4243
this,

libs/reactive-forms/src/lib/form-group.spec.ts

Lines changed: 77 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { expectTypeOf } from 'expect-type';
22
import { FormGroup } from './form-group';
33
import { FormControl } from './form-control';
44
import { FormArray } from './form-array';
5-
import { AbstractControl } from '@angular/forms';
5+
import { AbstractControl, Validators } from '@angular/forms';
66
import { Observable, of, Subject, Subscription } from 'rxjs';
77
import { ControlsOf } from '..';
88
import { ValuesOf } from './types';
@@ -68,6 +68,32 @@ describe('FormGroup Functionality', () => {
6868
expect(spy).toHaveBeenCalledTimes(2);
6969
});
7070

71+
it('should invalidChanges$', () => {
72+
const control = new FormGroup({
73+
name: new FormControl<string | null>(null, Validators.required),
74+
});
75+
const spy = jest.fn();
76+
control.invalid$.subscribe(spy);
77+
expect(spy).toHaveBeenCalledWith(true);
78+
control.setValue({ name: 'abc' });
79+
expect(spy).toHaveBeenCalledWith(false);
80+
control.setValue({ name: null });
81+
expect(spy).toHaveBeenCalledTimes(3);
82+
});
83+
84+
it('should validChanges$', () => {
85+
const control = new FormGroup({
86+
name: new FormControl<string | null>(null, Validators.required),
87+
});
88+
const spy = jest.fn();
89+
control.valid$.subscribe(spy);
90+
expect(spy).toHaveBeenCalledWith(false);
91+
control.setValue({ name: 'abc' });
92+
expect(spy).toHaveBeenCalledWith(true);
93+
control.setValue({ name: null });
94+
expect(spy).toHaveBeenCalledTimes(3);
95+
});
96+
7197
it('should statusChanges$', () => {
7298
const control = createGroup();
7399
const spy = jest.fn();
@@ -198,7 +224,9 @@ describe('FormGroup Functionality', () => {
198224

199225
function areAllAllChildrenDirty(control: AbstractControl) {
200226
expect(control.dirty).toBe(true);
201-
(control as any)._forEachChild((control: AbstractControl) => areAllAllChildrenDirty(control));
227+
(control as any)._forEachChild((control: AbstractControl) =>
228+
areAllAllChildrenDirty(control)
229+
);
202230
}
203231

204232
it('should markAllAsDirty', () => {
@@ -331,6 +359,8 @@ describe('FormGroup Types', () => {
331359

332360
expectTypeOf(group.disabled$).toEqualTypeOf<Observable<boolean>>();
333361
expectTypeOf(group.enabled$).toEqualTypeOf<Observable<boolean>>();
362+
expectTypeOf(group.invalid$).toEqualTypeOf<Observable<boolean>>();
363+
expectTypeOf(group.valid$).toEqualTypeOf<Observable<boolean>>();
334364
expectTypeOf(group.status$).toEqualTypeOf<Observable<ControlState>>();
335365

336366
const name$ = group.select((state) => {
@@ -467,22 +497,19 @@ describe('FormGroup Types', () => {
467497
});
468498
});
469499

470-
471-
472500
describe('ControlsOf', () => {
473-
474501
it('should infer the type', () => {
475502
interface Foo {
476503
str: string;
477504
nested: {
478505
one: string;
479-
two: number,
506+
two: number;
480507
deep: {
481508
id: number;
482-
arr: string[]
483-
}
484-
},
485-
arr: string[]
509+
arr: string[];
510+
};
511+
};
512+
arr: string[];
486513
}
487514

488515
const group = new FormGroup<ControlsOf<Foo>>({
@@ -492,56 +519,57 @@ describe('ControlsOf', () => {
492519
two: new FormControl(),
493520
deep: new FormGroup({
494521
id: new FormControl(1),
495-
arr: new FormArray([])
496-
})
522+
arr: new FormArray([]),
523+
}),
497524
}),
498-
arr: new FormArray([])
525+
arr: new FormArray([]),
499526
});
500527

501528
expectTypeOf(group.value).toEqualTypeOf<Foo>();
502529

503530
expectTypeOf(group.get('str')).toEqualTypeOf<FormControl<string>>();
504-
expectTypeOf(group.get('nested')).toEqualTypeOf<FormGroup<ControlsOf<Foo['nested']>>>();
505-
expectTypeOf(group.get('arr')).toEqualTypeOf<FormArray<string, FormControl<string>>>();
531+
expectTypeOf(group.get('nested')).toEqualTypeOf<
532+
FormGroup<ControlsOf<Foo['nested']>>
533+
>();
534+
expectTypeOf(group.get('arr')).toEqualTypeOf<
535+
FormArray<string, FormControl<string>>
536+
>();
506537

507538
expectTypeOf(group.get('nested').value).toEqualTypeOf<Foo['nested']>();
508539
expectTypeOf(group.get('arr').value).toEqualTypeOf<Foo['arr']>();
509540

510-
511541
new FormGroup<ControlsOf<Foo>>({
512542
// @ts-expect-error - should be typed
513543
str: new FormControl(1),
514544
// @ts-expect-error - should be typed
515545
nested: new FormGroup({
516546
// one: new FormControl(''),
517-
two: new FormControl()
547+
two: new FormControl(),
518548
}),
519549
// @ts-expect-error - should be typed
520-
arr: new FormArray([new FormControl(1)])
521-
})
522-
})
550+
arr: new FormArray([new FormControl(1)]),
551+
});
552+
});
523553

524554
it('should allow FormControls as objects or arrays', () => {
525-
526555
interface Bar {
527556
str: string;
528557
controlGroup: FormControl<{
529558
one: string;
530-
two: number
531-
}>,
532-
controlArr: FormControl<string[]>,
559+
two: number;
560+
}>;
561+
controlArr: FormControl<string[]>;
533562
group: {
534563
id: string;
535564
deep: {
536565
id: number;
537-
arr: FormControl<string[]>
538-
}
539-
}
540-
arr: string[],
541-
arrGroup: Array<{ name: string, count: number }>;
566+
arr: FormControl<string[]>;
567+
};
568+
};
569+
arr: string[];
570+
arrGroup: Array<{ name: string; count: number }>;
542571
}
543572

544-
545573
const group = new FormGroup<ControlsOf<Bar>>({
546574
str: new FormControl(''),
547575
controlGroup: new FormControl({ one: '', two: 1 }),
@@ -550,31 +578,32 @@ describe('ControlsOf', () => {
550578
id: new FormControl(),
551579
deep: new FormGroup({
552580
id: new FormControl(),
553-
arr: new FormControl([])
554-
})
581+
arr: new FormControl([]),
582+
}),
555583
}),
556584
arr: new FormArray([]),
557-
arrGroup: new FormArray([])
585+
arrGroup: new FormArray([]),
558586
});
559587

560588
expectTypeOf(group.value).toEqualTypeOf<ValuesOf<ControlsOf<Bar>>>();
561589

562590
new FormGroup<ControlsOf<Bar>>({
563591
str: new FormControl(''),
564592
// @ts-expect-error - should be FormControl
565-
controlGroup: new FormGroup({ one: new FormControl(''), two: new FormControl() }),
593+
controlGroup: new FormGroup({
594+
one: new FormControl(''),
595+
two: new FormControl(),
596+
}),
566597
// @ts-expect-error - should be FormControl
567598
controlArr: new FormArray([]),
568599
// @ts-expect-error - should be FormGroup
569600
group: new FormControl(),
570601
// @ts-expect-error - should be FormArray
571602
arr: new FormControl([]),
572603
// @ts-expect-error - should be FormArray
573-
arrGroup: new FormControl([])
604+
arrGroup: new FormControl([]),
574605
});
575-
576-
})
577-
606+
});
578607

579608
it('should work with optional fields', () => {
580609
type Foo = {
@@ -583,29 +612,30 @@ describe('ControlsOf', () => {
583612
baz: null | string;
584613
arr?: string[];
585614
nested: {
586-
id: string
587-
}
588-
}
615+
id: string;
616+
};
617+
};
589618

590619
const group = new FormGroup<ControlsOf<Foo>>({
591620
foo: new FormControl(''),
592621
name: new FormControl(''),
593622
baz: new FormControl(null),
594623
arr: new FormArray([]),
595624
nested: new FormGroup({
596-
id: new FormControl('')
597-
})
598-
})
625+
id: new FormControl(''),
626+
}),
627+
});
599628

600629
// @ts-expect-error - should be a string
601630
group.get('name')?.patchValue(1);
602631

603-
expectTypeOf(group.get('name')).toEqualTypeOf<FormControl<string | undefined> | undefined>();
632+
expectTypeOf(group.get('name')).toEqualTypeOf<
633+
FormControl<string | undefined> | undefined
634+
>();
604635

605636
expectTypeOf(group.value.name).toEqualTypeOf<string | undefined>();
606637
expectTypeOf(group.value.arr).toEqualTypeOf<string[] | undefined>();
607638
expectTypeOf(group.value.baz).toEqualTypeOf<string | null>();
608639
expectTypeOf(group.value.nested).toEqualTypeOf<{ id: string }>();
609-
})
610-
640+
});
611641
});

0 commit comments

Comments
 (0)