Skip to content

Commit 464073d

Browse files
authored
feat(component): add ngrxPush pipe and ngrxLet directive to @ngrx/component package (#2046)
1 parent b68fa67 commit 464073d

30 files changed

+1577
-18
lines changed

modules/component/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
{
22
"name": "@ngrx/component",
33
"version": "0.0.0-PLACEHOLDER",
4-
"description": "Reactive utilities for components",
4+
"description": "Reactive Extensions for Angular Components",
55
"repository": {
66
"type": "git",
77
"url": "https://github.com/ngrx/platform.git"
88
},
99
"keywords": [
1010
"Angular",
11-
"Redux",
11+
"RxJS",
1212
"NgRx",
13-
"Schematics",
13+
"Components",
1414
"Angular CLI"
1515
],
1616
"author": "NgRx",
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import {
2+
ChangeDetectorRef,
3+
EmbeddedViewRef,
4+
Injector,
5+
NgZone,
6+
OnDestroy,
7+
Type,
8+
} from '@angular/core';
9+
import { CdAware, createCdAware, getGlobalThis } from '../../src/core';
10+
import {
11+
concat,
12+
EMPTY,
13+
NEVER,
14+
NextObserver,
15+
Observable,
16+
of,
17+
PartialObserver,
18+
Unsubscribable,
19+
} from 'rxjs';
20+
import { tap } from 'rxjs/operators';
21+
22+
class CdAwareImplementation<U> implements OnDestroy {
23+
public renderedValue: any = undefined;
24+
public error: any = undefined;
25+
public completed: boolean = false;
26+
private readonly subscription: Unsubscribable;
27+
public cdAware: CdAware<U | undefined | null>;
28+
resetContextObserver: NextObserver<any> = {
29+
next: _ => (this.renderedValue = undefined),
30+
error: e => (this.error = e),
31+
complete: () => (this.completed = true),
32+
};
33+
updateViewContextObserver: PartialObserver<U | undefined | null> = {
34+
next: (n: U | undefined | null) => (this.renderedValue = n),
35+
error: e => (this.error = e),
36+
complete: () => (this.completed = true),
37+
};
38+
configurableBehaviour = <T>(
39+
o$: Observable<Observable<T>>
40+
): Observable<Observable<T>> => o$.pipe(tap());
41+
42+
constructor() {
43+
this.cdAware = createCdAware<U>({
44+
work: () => {},
45+
resetContextObserver: this.resetContextObserver,
46+
updateViewContextObserver: this.updateViewContextObserver,
47+
configurableBehaviour: this.configurableBehaviour,
48+
});
49+
this.subscription = this.cdAware.subscribe();
50+
}
51+
52+
ngOnDestroy(): void {
53+
this.subscription.unsubscribe();
54+
}
55+
}
56+
57+
let cdAwareImplementation: CdAwareImplementation<any>;
58+
const setupCdAwareImplementation = () => {
59+
cdAwareImplementation = new CdAwareImplementation();
60+
cdAwareImplementation.renderedValue = undefined;
61+
cdAwareImplementation.error = undefined;
62+
cdAwareImplementation.completed = false;
63+
};
64+
65+
describe('CdAware', () => {
66+
beforeEach(() => {
67+
setupCdAwareImplementation();
68+
});
69+
70+
it('should be implementable', () => {
71+
expect(cdAwareImplementation).toBeDefined();
72+
});
73+
74+
describe('next value', () => {
75+
it('should do nothing if initialized (as no value ever was emitted)', () => {
76+
expect(cdAwareImplementation.renderedValue).toBe(undefined);
77+
});
78+
79+
it('should render undefined as value when initially undefined was passed (as no value ever was emitted)', () => {
80+
cdAwareImplementation.cdAware.next(undefined);
81+
expect(cdAwareImplementation.renderedValue).toBe(undefined);
82+
});
83+
84+
it('should render null as value when initially null was passed (as no value ever was emitted)', () => {
85+
cdAwareImplementation.cdAware.next(null);
86+
expect(cdAwareImplementation.renderedValue).toBe(null);
87+
});
88+
89+
it('should render undefined as value when initially of(undefined) was passed (as undefined was emitted)', () => {
90+
cdAwareImplementation.cdAware.next(of(undefined));
91+
expect(cdAwareImplementation.renderedValue).toBe(undefined);
92+
});
93+
94+
it('should render null as value when initially of(null) was passed (as null was emitted)', () => {
95+
cdAwareImplementation.cdAware.next(of(null));
96+
expect(cdAwareImplementation.renderedValue).toBe(null);
97+
});
98+
99+
it('should render undefined as value when initially EMPTY was passed (as no value ever was emitted)', () => {
100+
cdAwareImplementation.cdAware.next(EMPTY);
101+
expect(cdAwareImplementation.renderedValue).toBe(undefined);
102+
});
103+
104+
it('should render undefined as value when initially NEVER was passed (as no value ever was emitted)', () => {
105+
cdAwareImplementation.cdAware.next(NEVER);
106+
expect(cdAwareImplementation.renderedValue).toBe(undefined);
107+
});
108+
// Also: 'should keep last emitted value in the view until a new observable NEVER was passed (as no value ever was emitted from new observable)'
109+
it('should render emitted value from passed observable without changing it', () => {
110+
cdAwareImplementation.cdAware.next(of(42));
111+
expect(cdAwareImplementation.renderedValue).toBe(42);
112+
});
113+
114+
it('should render undefined as value when a new observable NEVER was passed (as no value ever was emitted from new observable)', () => {
115+
cdAwareImplementation.cdAware.next(of(42));
116+
expect(cdAwareImplementation.renderedValue).toBe(42);
117+
cdAwareImplementation.cdAware.next(NEVER);
118+
expect(cdAwareImplementation.renderedValue).toBe(undefined);
119+
});
120+
});
121+
122+
describe('observable context', () => {
123+
it('next handling running observable', () => {
124+
cdAwareImplementation.cdAware.next(concat(of(42), NEVER));
125+
expect(cdAwareImplementation.renderedValue).toBe(42);
126+
expect(cdAwareImplementation.error).toBe(undefined);
127+
expect(cdAwareImplementation.completed).toBe(false);
128+
});
129+
130+
it('next handling completed observable', () => {
131+
cdAwareImplementation.cdAware.next(of(42));
132+
expect(cdAwareImplementation.renderedValue).toBe(42);
133+
expect(cdAwareImplementation.error).toBe(undefined);
134+
expect(cdAwareImplementation.completed).toBe(true);
135+
});
136+
137+
it('error handling', () => {
138+
expect(cdAwareImplementation.renderedValue).toBe(undefined);
139+
cdAwareImplementation.cdAware.subscribe({
140+
error: (e: Error) => expect(e).toBeDefined(),
141+
});
142+
expect(cdAwareImplementation.renderedValue).toBe(undefined);
143+
// @TODO use this line
144+
// expect(cdAwareImplementation.error).toBe(ArgumentNotObservableError);
145+
expect(cdAwareImplementation.completed).toBe(false);
146+
});
147+
148+
it('completion handling', () => {
149+
cdAwareImplementation.cdAware.next(EMPTY);
150+
expect(cdAwareImplementation.renderedValue).toBe(undefined);
151+
expect(cdAwareImplementation.error).toBe(undefined);
152+
expect(cdAwareImplementation.completed).toBe(true);
153+
});
154+
});
155+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { EMPTY, isObservable, Observable, of } from 'rxjs';
2+
import { toObservableValue } from '../../../src/core/projections';
3+
4+
describe('toObservableValue', () => {
5+
describe('used as RxJS creation function', () => {
6+
it('should take observables', () => {
7+
const observable: Observable<any> = toObservableValue(EMPTY);
8+
expect(isObservable(observable)).toBe(true);
9+
});
10+
11+
it('should take a promise', () => {
12+
const observable: Observable<any> = toObservableValue(
13+
new Promise(() => {})
14+
);
15+
expect(isObservable(observable)).toBe(true);
16+
});
17+
18+
it('should take undefined', () => {
19+
const observable: Observable<any> = toObservableValue(undefined);
20+
expect(isObservable(observable)).toBe(true);
21+
});
22+
23+
it('should take a null', () => {
24+
const observable: Observable<any> = toObservableValue(null);
25+
expect(isObservable(observable)).toBe(true);
26+
});
27+
28+
it('throw if no observable, promise, undefined or null is passed', () => {
29+
const observable: Observable<any> = toObservableValue(null);
30+
observable.subscribe({
31+
error(e) {
32+
expect(e).toBeDefined();
33+
},
34+
});
35+
});
36+
});
37+
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import {
2+
getChangeDetectionHandler,
3+
getGlobalThis,
4+
} from '../../../src/core/utils';
5+
import { Injector } from '@angular/core';
6+
7+
class NgZone {}
8+
class NoopNgZone {}
9+
class ChangeDetectorRef {
10+
public markForCheck(): void {}
11+
public detectChanges(): void {}
12+
}
13+
14+
let noopNgZone: any;
15+
let ngZone: any;
16+
let changeDetectorRef: any;
17+
18+
beforeAll(() => {
19+
const injector = Injector.create([
20+
{ provide: NgZone, useClass: NgZone, deps: [] },
21+
{ provide: NoopNgZone, useClass: NoopNgZone, deps: [] },
22+
{ provide: ChangeDetectorRef, useClass: ChangeDetectorRef, deps: [] },
23+
]);
24+
noopNgZone = injector.get(NoopNgZone) as NgZone;
25+
ngZone = injector.get(NgZone);
26+
changeDetectorRef = injector.get(ChangeDetectorRef);
27+
});
28+
29+
describe('getChangeDetectionHandler', () => {
30+
describe('in ViewEngine', () => {
31+
beforeAll(() => {
32+
getGlobalThis().ng = { probe: true };
33+
});
34+
35+
it('should return markForCheck in zone-full mode', () => {
36+
const markForCheckSpy = jasmine.createSpy('markForCheck');
37+
changeDetectorRef.markForCheck = markForCheckSpy;
38+
getChangeDetectionHandler(ngZone, changeDetectorRef)();
39+
expect(markForCheckSpy).toHaveBeenCalled();
40+
});
41+
42+
it('should return detectChanges in zone-less mode', () => {
43+
const detectChangesSpy = jasmine.createSpy('detectChanges');
44+
changeDetectorRef.detectChanges = detectChangesSpy;
45+
getChangeDetectionHandler(noopNgZone, changeDetectorRef)();
46+
expect(detectChangesSpy).toHaveBeenCalled();
47+
});
48+
});
49+
50+
describe('in Ivy', () => {
51+
beforeEach(() => {
52+
getGlobalThis().ng = undefined;
53+
});
54+
55+
it('should return markDirty in zone-full mode', () => {
56+
expect(getChangeDetectionHandler(ngZone, changeDetectorRef).name).toBe(
57+
'markDirty'
58+
);
59+
});
60+
61+
it('should return detectChanges in zone-less mode', () => {
62+
expect(
63+
getChangeDetectionHandler(noopNgZone, changeDetectorRef).name
64+
).toBe('detectChanges');
65+
});
66+
});
67+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { getGlobalThis } from '../../../src/core';
2+
3+
describe('getGlobalThis', () => {
4+
it('should return global this', () => {
5+
getGlobalThis().prop = 42;
6+
const globalThis = getGlobalThis();
7+
8+
expect(globalThis).toBeDefined();
9+
expect(globalThis.prop).toBe(42);
10+
});
11+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { hasZone } from '../../../src/core/utils';
2+
import { NgZone } from '@angular/core';
3+
4+
class NoopNgZone {}
5+
6+
describe('isZoneLess', () => {
7+
it('should return false if something else than noop zone is passed', () => {
8+
expect(!hasZone({} as NgZone)).toBe(false);
9+
});
10+
11+
it('should return true if a noop zone is passed', () => {
12+
expect(!hasZone(new NoopNgZone() as NgZone)).toBe(true);
13+
});
14+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { getGlobalThis, isIvy } from '../../../src/core';
2+
3+
describe('isIvy', () => {
4+
describe('in ViewEngine Angular 8 + 9', () => {
5+
it('should return false if ng is defined with probe', () => {
6+
getGlobalThis().ng = { probe: true };
7+
expect(isIvy()).toBe(false);
8+
});
9+
});
10+
describe('in Ivy Angular 9', () => {
11+
it('should return true if ng is undefined', () => {
12+
getGlobalThis().ng = undefined;
13+
expect(isIvy()).toBe(true);
14+
});
15+
16+
it('should return true if ng.probe is set', () => {
17+
getGlobalThis().ng = { probe: undefined };
18+
expect(isIvy()).toBe(true);
19+
});
20+
});
21+
});

0 commit comments

Comments
 (0)