Skip to content

Commit

Permalink
feat(component): add ngrxPush pipe and ngrxLet directive to @ngrx/com…
Browse files Browse the repository at this point in the history
…ponent package (#2046)
  • Loading branch information
BioPhoton authored Mar 19, 2020
1 parent b68fa67 commit 464073d
Show file tree
Hide file tree
Showing 30 changed files with 1,577 additions and 18 deletions.
6 changes: 3 additions & 3 deletions modules/component/package.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
{
"name": "@ngrx/component",
"version": "0.0.0-PLACEHOLDER",
"description": "Reactive utilities for components",
"description": "Reactive Extensions for Angular Components",
"repository": {
"type": "git",
"url": "https://github.com/ngrx/platform.git"
},
"keywords": [
"Angular",
"Redux",
"RxJS",
"NgRx",
"Schematics",
"Components",
"Angular CLI"
],
"author": "NgRx",
Expand Down
155 changes: 155 additions & 0 deletions modules/component/spec/core/cd-aware.abstract.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import {
ChangeDetectorRef,
EmbeddedViewRef,
Injector,
NgZone,
OnDestroy,
Type,
} from '@angular/core';
import { CdAware, createCdAware, getGlobalThis } from '../../src/core';
import {
concat,
EMPTY,
NEVER,
NextObserver,
Observable,
of,
PartialObserver,
Unsubscribable,
} from 'rxjs';
import { tap } from 'rxjs/operators';

class CdAwareImplementation<U> implements OnDestroy {
public renderedValue: any = undefined;
public error: any = undefined;
public completed: boolean = false;
private readonly subscription: Unsubscribable;
public cdAware: CdAware<U | undefined | null>;
resetContextObserver: NextObserver<any> = {
next: _ => (this.renderedValue = undefined),
error: e => (this.error = e),
complete: () => (this.completed = true),
};
updateViewContextObserver: PartialObserver<U | undefined | null> = {
next: (n: U | undefined | null) => (this.renderedValue = n),
error: e => (this.error = e),
complete: () => (this.completed = true),
};
configurableBehaviour = <T>(
o$: Observable<Observable<T>>
): Observable<Observable<T>> => o$.pipe(tap());

constructor() {
this.cdAware = createCdAware<U>({
work: () => {},
resetContextObserver: this.resetContextObserver,
updateViewContextObserver: this.updateViewContextObserver,
configurableBehaviour: this.configurableBehaviour,
});
this.subscription = this.cdAware.subscribe();
}

ngOnDestroy(): void {
this.subscription.unsubscribe();
}
}

let cdAwareImplementation: CdAwareImplementation<any>;
const setupCdAwareImplementation = () => {
cdAwareImplementation = new CdAwareImplementation();
cdAwareImplementation.renderedValue = undefined;
cdAwareImplementation.error = undefined;
cdAwareImplementation.completed = false;
};

describe('CdAware', () => {
beforeEach(() => {
setupCdAwareImplementation();
});

it('should be implementable', () => {
expect(cdAwareImplementation).toBeDefined();
});

describe('next value', () => {
it('should do nothing if initialized (as no value ever was emitted)', () => {
expect(cdAwareImplementation.renderedValue).toBe(undefined);
});

it('should render undefined as value when initially undefined was passed (as no value ever was emitted)', () => {
cdAwareImplementation.cdAware.next(undefined);
expect(cdAwareImplementation.renderedValue).toBe(undefined);
});

it('should render null as value when initially null was passed (as no value ever was emitted)', () => {
cdAwareImplementation.cdAware.next(null);
expect(cdAwareImplementation.renderedValue).toBe(null);
});

it('should render undefined as value when initially of(undefined) was passed (as undefined was emitted)', () => {
cdAwareImplementation.cdAware.next(of(undefined));
expect(cdAwareImplementation.renderedValue).toBe(undefined);
});

it('should render null as value when initially of(null) was passed (as null was emitted)', () => {
cdAwareImplementation.cdAware.next(of(null));
expect(cdAwareImplementation.renderedValue).toBe(null);
});

it('should render undefined as value when initially EMPTY was passed (as no value ever was emitted)', () => {
cdAwareImplementation.cdAware.next(EMPTY);
expect(cdAwareImplementation.renderedValue).toBe(undefined);
});

it('should render undefined as value when initially NEVER was passed (as no value ever was emitted)', () => {
cdAwareImplementation.cdAware.next(NEVER);
expect(cdAwareImplementation.renderedValue).toBe(undefined);
});
// 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)'
it('should render emitted value from passed observable without changing it', () => {
cdAwareImplementation.cdAware.next(of(42));
expect(cdAwareImplementation.renderedValue).toBe(42);
});

it('should render undefined as value when a new observable NEVER was passed (as no value ever was emitted from new observable)', () => {
cdAwareImplementation.cdAware.next(of(42));
expect(cdAwareImplementation.renderedValue).toBe(42);
cdAwareImplementation.cdAware.next(NEVER);
expect(cdAwareImplementation.renderedValue).toBe(undefined);
});
});

describe('observable context', () => {
it('next handling running observable', () => {
cdAwareImplementation.cdAware.next(concat(of(42), NEVER));
expect(cdAwareImplementation.renderedValue).toBe(42);
expect(cdAwareImplementation.error).toBe(undefined);
expect(cdAwareImplementation.completed).toBe(false);
});

it('next handling completed observable', () => {
cdAwareImplementation.cdAware.next(of(42));
expect(cdAwareImplementation.renderedValue).toBe(42);
expect(cdAwareImplementation.error).toBe(undefined);
expect(cdAwareImplementation.completed).toBe(true);
});

it('error handling', () => {
expect(cdAwareImplementation.renderedValue).toBe(undefined);
cdAwareImplementation.cdAware.subscribe({
error: (e: Error) => expect(e).toBeDefined(),
});
expect(cdAwareImplementation.renderedValue).toBe(undefined);
// @TODO use this line
// expect(cdAwareImplementation.error).toBe(ArgumentNotObservableError);
expect(cdAwareImplementation.completed).toBe(false);
});

it('completion handling', () => {
cdAwareImplementation.cdAware.next(EMPTY);
expect(cdAwareImplementation.renderedValue).toBe(undefined);
expect(cdAwareImplementation.error).toBe(undefined);
expect(cdAwareImplementation.completed).toBe(true);
});
});
});
37 changes: 37 additions & 0 deletions modules/component/spec/core/projections/toObservableValue.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { EMPTY, isObservable, Observable, of } from 'rxjs';
import { toObservableValue } from '../../../src/core/projections';

describe('toObservableValue', () => {
describe('used as RxJS creation function', () => {
it('should take observables', () => {
const observable: Observable<any> = toObservableValue(EMPTY);
expect(isObservable(observable)).toBe(true);
});

it('should take a promise', () => {
const observable: Observable<any> = toObservableValue(
new Promise(() => {})
);
expect(isObservable(observable)).toBe(true);
});

it('should take undefined', () => {
const observable: Observable<any> = toObservableValue(undefined);
expect(isObservable(observable)).toBe(true);
});

it('should take a null', () => {
const observable: Observable<any> = toObservableValue(null);
expect(isObservable(observable)).toBe(true);
});

it('throw if no observable, promise, undefined or null is passed', () => {
const observable: Observable<any> = toObservableValue(null);
observable.subscribe({
error(e) {
expect(e).toBeDefined();
},
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
getChangeDetectionHandler,
getGlobalThis,
} from '../../../src/core/utils';
import { Injector } from '@angular/core';

class NgZone {}
class NoopNgZone {}
class ChangeDetectorRef {
public markForCheck(): void {}
public detectChanges(): void {}
}

let noopNgZone: any;
let ngZone: any;
let changeDetectorRef: any;

beforeAll(() => {
const injector = Injector.create([
{ provide: NgZone, useClass: NgZone, deps: [] },
{ provide: NoopNgZone, useClass: NoopNgZone, deps: [] },
{ provide: ChangeDetectorRef, useClass: ChangeDetectorRef, deps: [] },
]);
noopNgZone = injector.get(NoopNgZone) as NgZone;
ngZone = injector.get(NgZone);
changeDetectorRef = injector.get(ChangeDetectorRef);
});

describe('getChangeDetectionHandler', () => {
describe('in ViewEngine', () => {
beforeAll(() => {
getGlobalThis().ng = { probe: true };
});

it('should return markForCheck in zone-full mode', () => {
const markForCheckSpy = jasmine.createSpy('markForCheck');
changeDetectorRef.markForCheck = markForCheckSpy;
getChangeDetectionHandler(ngZone, changeDetectorRef)();
expect(markForCheckSpy).toHaveBeenCalled();
});

it('should return detectChanges in zone-less mode', () => {
const detectChangesSpy = jasmine.createSpy('detectChanges');
changeDetectorRef.detectChanges = detectChangesSpy;
getChangeDetectionHandler(noopNgZone, changeDetectorRef)();
expect(detectChangesSpy).toHaveBeenCalled();
});
});

describe('in Ivy', () => {
beforeEach(() => {
getGlobalThis().ng = undefined;
});

it('should return markDirty in zone-full mode', () => {
expect(getChangeDetectionHandler(ngZone, changeDetectorRef).name).toBe(
'markDirty'
);
});

it('should return detectChanges in zone-less mode', () => {
expect(
getChangeDetectionHandler(noopNgZone, changeDetectorRef).name
).toBe('detectChanges');
});
});
});
11 changes: 11 additions & 0 deletions modules/component/spec/core/utils/get-global-this.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { getGlobalThis } from '../../../src/core';

describe('getGlobalThis', () => {
it('should return global this', () => {
getGlobalThis().prop = 42;
const globalThis = getGlobalThis();

expect(globalThis).toBeDefined();
expect(globalThis.prop).toBe(42);
});
});
14 changes: 14 additions & 0 deletions modules/component/spec/core/utils/has-zone.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { hasZone } from '../../../src/core/utils';
import { NgZone } from '@angular/core';

class NoopNgZone {}

describe('isZoneLess', () => {
it('should return false if something else than noop zone is passed', () => {
expect(!hasZone({} as NgZone)).toBe(false);
});

it('should return true if a noop zone is passed', () => {
expect(!hasZone(new NoopNgZone() as NgZone)).toBe(true);
});
});
21 changes: 21 additions & 0 deletions modules/component/spec/core/utils/is-ivy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { getGlobalThis, isIvy } from '../../../src/core';

describe('isIvy', () => {
describe('in ViewEngine Angular 8 + 9', () => {
it('should return false if ng is defined with probe', () => {
getGlobalThis().ng = { probe: true };
expect(isIvy()).toBe(false);
});
});
describe('in Ivy Angular 9', () => {
it('should return true if ng is undefined', () => {
getGlobalThis().ng = undefined;
expect(isIvy()).toBe(true);
});

it('should return true if ng.probe is set', () => {
getGlobalThis().ng = { probe: undefined };
expect(isIvy()).toBe(true);
});
});
});
Loading

0 comments on commit 464073d

Please sign in to comment.