Skip to content

Commit 8cbd20b

Browse files
authored
Merge pull request #76 from KamilSocha91/feat/valueChangesDiffOperator
2 parents 9ed32ab + 249d243 commit 8cbd20b

File tree

6 files changed

+381
-0
lines changed

6 files changed

+381
-0
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Let's take a look at all the neat things we provide:
2323

2424
## 🔮 Features
2525

26+
27+
2628
✅ Offers (almost) seamless `FormControl`, `FormGroup`, `FormArray` Replacement<br>
2729
✅ Allows Typed Forms! <br>
2830
✅ Provides Reactive Queries <br>
@@ -41,6 +43,7 @@ Let's take a look at all the neat things we provide:
4143
- [Control Queries](#control-queries)
4244
- [Control Methods](#control-methods)
4345
- [Control Errors](#control-errors)
46+
- [Control Operators](#control-operators)
4447
- [ControlValueAccessor](#controlvalueaccessor)
4548
- [Form Builder](#form-builder)
4649
- [Persist Form](#persist-form)
@@ -469,6 +472,27 @@ import { FormControl, NgValidatorsErrors } from '@ngneat/reactive-forms';
469472

470473
const control = new FormControl<string, NgValidatorsErrors>();
471474
```
475+
## Control Operators
476+
477+
Each `valueChanges` or `values$` takes an operator `diff()`, which emits only changed parts of form:
478+
479+
```ts
480+
import { FormGroup, FormControl } from '@ngneat/reactive-forms';
481+
482+
const control = new FormGroup<string>({
483+
name: new FormControl(''),
484+
phone: new FormGroup({
485+
num: new FormControl(),
486+
prefix: new FormControl()
487+
}),
488+
skills: new FormArray([])
489+
});
490+
control.value$
491+
.pipe(diff())
492+
.subscribe(value => {
493+
// value is emitted only if it has been changed, and only the changed parts.
494+
});
495+
```
472496

473497
## ControlValueAccessor
474498

projects/ngneat/reactive-forms/src/lib/formArray.spec.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { of, Subject } from 'rxjs';
22
import { FormArray } from './formArray';
33
import { FormControl } from './formControl';
44
import { FormGroup } from './formGroup';
5+
import { diff } from './operators/diff';
56

67
const errorFn = group => {
78
return { isInvalid: true };
@@ -11,6 +12,57 @@ const createArray = (withError = false) => {
1112
return new FormArray<string>([new FormControl(''), new FormControl('')], withError ? errorFn : []);
1213
};
1314

15+
describe('FormArray valueChanges$ diff() operator', () => {
16+
const control = createArray();
17+
const spy = jest.fn();
18+
control.value$.pipe(diff()).subscribe(spy);
19+
20+
it('should be initialized', () => {
21+
expect(spy).toHaveBeenCalledWith(['', '']);
22+
expect(spy).toHaveBeenCalledTimes(1);
23+
});
24+
25+
it('should filter duplicated calls', () => {
26+
control.patchValue(['1', '2']);
27+
expect(spy).toHaveBeenCalledWith(['1', '2']);
28+
expect(spy).toHaveBeenCalledTimes(2);
29+
control.patchValue(['1', '2']);
30+
expect(spy).toHaveBeenCalledTimes(2);
31+
});
32+
33+
it('should push new value', () => {
34+
control.push(new FormControl('3'));
35+
expect(spy).toHaveBeenCalledWith(['1', '2', '3']);
36+
expect(spy).toHaveBeenCalledTimes(3);
37+
control.patchValue(['1', '2', '3']);
38+
expect(spy).toHaveBeenCalledTimes(3);
39+
});
40+
41+
it('should override previous values', () => {
42+
control.patchValue(['2', '3', '4']);
43+
expect(spy).toHaveBeenCalledWith(['2', '3', '4']);
44+
expect(spy).toHaveBeenCalledTimes(4);
45+
});
46+
47+
it('should clear control', () => {
48+
control.removeAt(1);
49+
expect(spy).toHaveBeenCalledWith(['2', '4']);
50+
expect(spy).toHaveBeenCalledTimes(5);
51+
control.removeAt(0);
52+
expect(spy).toHaveBeenCalledWith(['4']);
53+
expect(spy).toHaveBeenCalledTimes(6);
54+
control.removeAt(0);
55+
expect(spy).toHaveBeenCalledWith([]);
56+
expect(spy).toHaveBeenCalledTimes(7);
57+
});
58+
59+
it('should push empty value', () => {
60+
control.push(new FormControl(''));
61+
expect(spy).toHaveBeenCalledWith(['']);
62+
expect(spy).toHaveBeenCalledTimes(8);
63+
});
64+
});
65+
1466
describe('FormArray', () => {
1567
it('should valueChanges$', () => {
1668
const control = createArray();
@@ -19,6 +71,12 @@ describe('FormArray', () => {
1971
expect(spy).toHaveBeenCalledWith(['', '']);
2072
control.patchValue(['1', '2']);
2173
expect(spy).toHaveBeenCalledWith(['1', '2']);
74+
control.push(new FormControl('3'));
75+
expect(spy).toHaveBeenCalledWith(['1', '2', '3']);
76+
control.push(new FormControl(''));
77+
expect(spy).toHaveBeenCalledWith(['1', '2', '3', '']);
78+
control.removeAt(1);
79+
expect(spy).toHaveBeenCalledWith(['1', '3', '']);
2280
});
2381

2482
it('should disabledChanges$', () => {

projects/ngneat/reactive-forms/src/lib/formControl.spec.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { of, Subject } from 'rxjs';
22
import { FormControl } from './formControl';
33
import { NgValidatorsErrors } from './types';
44
import { Validators } from '@angular/forms';
5+
import { diff } from './operators/diff';
56

67
const validatorExample = new FormControl<string, NgValidatorsErrors>('', {
78
validators(control: FormControl<string>) {
@@ -14,6 +15,89 @@ const validatorExample = new FormControl<string, NgValidatorsErrors>('', {
1415
}
1516
});
1617

18+
describe('FormControl valueChanges$ diff() operator', () => {
19+
const control = new FormControl<string>();
20+
const spy = jest.fn();
21+
control.value$.pipe(diff()).subscribe(spy);
22+
23+
it('should be initialized', () => {
24+
expect(spy).toHaveBeenCalledWith(null);
25+
expect(spy).toHaveBeenCalledTimes(1);
26+
});
27+
28+
it('should filter duplicated calls', () => {
29+
control.patchValue('patched');
30+
expect(spy).toHaveBeenCalledWith('patched');
31+
expect(spy).toHaveBeenCalledTimes(2);
32+
control.patchValue('patched');
33+
expect(spy).toHaveBeenCalledTimes(2);
34+
});
35+
36+
it('should push new value', () => {
37+
control.patchValue('updated');
38+
expect(spy).toHaveBeenCalledWith('updated');
39+
expect(spy).toHaveBeenCalledTimes(3);
40+
});
41+
42+
it('should push null value', () => {
43+
control.patchValue(null);
44+
expect(spy).toHaveBeenCalledWith(null);
45+
expect(spy).toHaveBeenCalledTimes(4);
46+
});
47+
48+
it('should push empty value', () => {
49+
control.patchValue('');
50+
expect(spy).toHaveBeenCalledWith('');
51+
expect(spy).toHaveBeenCalledTimes(5);
52+
});
53+
54+
it('should push number value', () => {
55+
control.patchValue('0');
56+
expect(spy).toHaveBeenCalledWith('0');
57+
expect(spy).toHaveBeenCalledTimes(6);
58+
});
59+
});
60+
61+
describe('FormControl valueChanges$ diff() operator Array input', () => {
62+
const control = new FormControl<(string | number)[]>();
63+
const spy = jest.fn();
64+
control.value$.pipe(diff()).subscribe(spy);
65+
66+
it('should be initialized', () => {
67+
expect(spy).toHaveBeenCalledWith(null);
68+
expect(spy).toHaveBeenCalledTimes(1);
69+
});
70+
71+
it('should push array of strings', () => {
72+
control.patchValue(['1', '2', '3']);
73+
expect(spy).toHaveBeenCalledWith(['1', '2', '3']);
74+
expect(spy).toHaveBeenCalledTimes(2);
75+
});
76+
77+
it('should push array of numbers', () => {
78+
control.patchValue([1, 2, 3]);
79+
expect(spy).toHaveBeenCalledWith([1, 2, 3]);
80+
expect(spy).toHaveBeenCalledTimes(3);
81+
});
82+
});
83+
84+
describe('FormControl valueChanges$ diff() operator Object input', () => {
85+
const control = new FormControl<object>();
86+
const spy = jest.fn();
87+
control.value$.pipe(diff()).subscribe(spy);
88+
89+
it('should be initialized', () => {
90+
expect(spy).toHaveBeenCalledWith(null);
91+
expect(spy).toHaveBeenCalledTimes(1);
92+
});
93+
94+
it('should push object', () => {
95+
control.patchValue({ a: 1, b: 2 });
96+
expect(spy).toHaveBeenCalledWith({ a: 1, b: 2 });
97+
expect(spy).toHaveBeenCalledTimes(2);
98+
});
99+
});
100+
17101
describe('FormControl', () => {
18102
it('should valueChanges$', () => {
19103
const control = new FormControl<string>();

projects/ngneat/reactive-forms/src/lib/formGroup.spec.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { FormGroup } from './formGroup';
55
import { FormArray } from './formArray';
66
import { switchMap } from 'rxjs/operators';
77
import { wrapIntoObservable } from './utils';
8+
import { diff } from './operators/diff';
9+
import { AbstractControlOf } from './types';
810

911
type Person = {
1012
name: string;
@@ -33,6 +35,144 @@ const createGroup = (withError = false) => {
3335
);
3436
};
3537

38+
const createArray = <T>(elements: T[], withError = false): FormArray<T> => {
39+
const controlList = elements.map(element => new FormControl<T>(element)) as Array<AbstractControlOf<T>>;
40+
return new FormArray<T>(controlList, withError ? errorFn : []);
41+
};
42+
43+
describe('FormGroup valueChanges$ diff() operator', () => {
44+
const control = createGroup();
45+
const spy = jest.fn();
46+
control.value$.pipe(diff()).subscribe(spy);
47+
48+
it('should be initialized', () => {
49+
expect(spy).toHaveBeenCalledWith({ name: null, phone: { num: null, prefix: null }, skills: [] });
50+
expect(spy).toHaveBeenCalledTimes(1);
51+
});
52+
53+
it('should filter duplicated calls', () => {
54+
control.patchValue({ name: 'changed' });
55+
expect(spy).toHaveBeenCalledWith({ name: 'changed' });
56+
expect(spy).toHaveBeenCalledTimes(2);
57+
control.patchValue({ name: 'changed' });
58+
expect(spy).toHaveBeenCalledTimes(2);
59+
});
60+
61+
it('should allow deep FormGroup duplicated calls filtering', () => {
62+
control.patchValue({ phone: { num: 1, prefix: 1 } });
63+
expect(spy).toHaveBeenCalledWith({ phone: { num: 1, prefix: 1 } });
64+
expect(spy).toHaveBeenCalledTimes(3);
65+
control.patchValue({ phone: { num: 1, prefix: 1 } });
66+
expect(spy).toHaveBeenCalledTimes(3);
67+
});
68+
69+
it('should allow deep FormArray duplicated calls filtering', () => {
70+
control.setControl('skills', createArray(['driving']));
71+
expect(spy).toHaveBeenCalledWith({ skills: ['driving'] });
72+
expect(spy).toHaveBeenCalledTimes(4);
73+
control.setControl('skills', createArray(['driving']));
74+
expect(spy).toHaveBeenCalledTimes(4);
75+
});
76+
77+
it('should allow deep FormArray of numbers duplicated calls filtering', () => {
78+
control.setControl('skills', createArray([1, 2] as any));
79+
expect(spy).toHaveBeenCalledWith({ skills: [1, 2] });
80+
expect(spy).toHaveBeenCalledTimes(5);
81+
control.setControl('skills', createArray([1, 2] as any));
82+
expect(spy).toHaveBeenCalledTimes(5);
83+
});
84+
85+
it('should allow deep FormControl null value use', () => {
86+
control.patchValue({ name: null });
87+
expect(spy).toHaveBeenCalledWith({ name: null });
88+
expect(spy).toHaveBeenCalledTimes(6);
89+
});
90+
91+
it('should allow deep FormArray of null values use', () => {
92+
control.setControl('skills', createArray([null, null]));
93+
expect(spy).toHaveBeenCalledWith({ skills: [null, null] });
94+
expect(spy).toHaveBeenCalledTimes(7);
95+
});
96+
97+
it('should allow deep FormGroup null value use', () => {
98+
control.patchValue({ phone: { num: null } });
99+
expect(spy).toHaveBeenCalledWith({ phone: { num: null } });
100+
expect(spy).toHaveBeenCalledTimes(8);
101+
});
102+
103+
it('should allow push new value deep in to FormArray', () => {
104+
const arrayControl = <FormArray>control.get('skills');
105+
arrayControl.push(new FormControl('3'));
106+
expect(spy).toHaveBeenCalledWith({ skills: [null, null, '3'] });
107+
expect(spy).toHaveBeenCalledTimes(9);
108+
});
109+
110+
it('should allow removing value deep from FormArray', () => {
111+
const arrayControl = <FormArray>control.get('skills');
112+
arrayControl.removeAt(0);
113+
expect(spy).toHaveBeenCalledWith({ skills: [null, '3'] });
114+
expect(spy).toHaveBeenCalledTimes(10);
115+
});
116+
117+
it('should perform advanced/deep form input', () => {
118+
const group = new FormGroup({
119+
a: new FormControl(),
120+
b: new FormGroup({
121+
c: new FormControl(),
122+
d: new FormControl()
123+
}),
124+
e: new FormGroup({
125+
f: new FormControl(),
126+
g: new FormControl()
127+
})
128+
});
129+
group.value$.pipe(diff()).subscribe(spy);
130+
group.patchValue({
131+
b: {
132+
c: 'new'
133+
},
134+
e: {
135+
g: 'new'
136+
}
137+
});
138+
expect(spy).toHaveBeenCalledWith({
139+
b: {
140+
c: 'new'
141+
},
142+
e: {
143+
g: 'new'
144+
}
145+
});
146+
expect(spy).toHaveBeenCalledTimes(12);
147+
});
148+
149+
it('should perform advanced/deep form input with special form type', () => {
150+
const deep = new FormGroup({
151+
a: new FormControl(),
152+
b: new FormGroup({
153+
c: new FormGroup({
154+
e: new FormArray([])
155+
}),
156+
d: new FormControl()
157+
})
158+
});
159+
deep.value$.pipe(diff()).subscribe(spy);
160+
const arrayControl = <FormArray>deep
161+
.get('b')
162+
.get('c')
163+
.get('e');
164+
arrayControl.push(new FormControl('3'));
165+
expect(spy).toHaveBeenCalledWith({
166+
b: {
167+
c: {
168+
e: ['3']
169+
}
170+
}
171+
});
172+
expect(spy).toHaveBeenCalledTimes(14);
173+
});
174+
});
175+
36176
describe('FormGroup', () => {
37177
it('should valueChanges$', () => {
38178
const control = createGroup();

0 commit comments

Comments
 (0)