diff --git a/modules/component/spec/core/cd-aware/cd-aware.abstract.spec.ts b/modules/component/spec/core/cd-aware/cd-aware_creator.spec.ts similarity index 62% rename from modules/component/spec/core/cd-aware/cd-aware.abstract.spec.ts rename to modules/component/spec/core/cd-aware/cd-aware_creator.spec.ts index ce69eab5ab..87bbd94971 100644 --- a/modules/component/spec/core/cd-aware/cd-aware.abstract.spec.ts +++ b/modules/component/spec/core/cd-aware/cd-aware_creator.spec.ts @@ -1,16 +1,18 @@ import { OnDestroy } from '@angular/core'; -import { CdAware, createCdAware } from '../../../src/core'; +import { CdAware, createCdAware, createRender } from '../../../src/core'; import { concat, EMPTY, NEVER, NextObserver, - Observable, + Observer, of, - PartialObserver, Unsubscribable, } from 'rxjs'; -import { tap } from 'rxjs/operators'; +import { + manualInstanceNgZone, + MockChangeDetectorRef, +} from '../../fixtures/fixtures'; class CdAwareImplementation implements OnDestroy { public renderedValue: any = undefined; @@ -20,24 +22,21 @@ class CdAwareImplementation implements OnDestroy { public cdAware: CdAware; resetContextObserver: NextObserver = { next: _ => (this.renderedValue = undefined), - error: e => (this.error = e), - complete: () => (this.completed = true), }; - updateViewContextObserver: PartialObserver = { + updateViewContextObserver: Observer = { next: (n: U | undefined | null) => (this.renderedValue = n), error: e => (this.error = e), complete: () => (this.completed = true), }; - configurableBehaviour = ( - o$: Observable> - ): Observable> => o$.pipe(tap()); constructor() { this.cdAware = createCdAware({ - work: () => {}, - resetContextObserver: this.resetContextObserver, + render: createRender({ + ngZone: manualInstanceNgZone, + cdRef: new MockChangeDetectorRef(), + }), updateViewContextObserver: this.updateViewContextObserver, - configurableBehaviour: this.configurableBehaviour, + resetContextObserver: this.resetContextObserver, }); this.subscription = this.cdAware.subscribe(); } @@ -69,59 +68,61 @@ describe('CdAware', () => { 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); + it('should render_creator undefined as value when initially undefined was passed (as no value ever was emitted)', () => { + cdAwareImplementation.cdAware.nextPotentialObservable(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); + it('should render_creator null as value when initially null was passed (as no value ever was emitted)', () => { + cdAwareImplementation.cdAware.nextPotentialObservable(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)); + it('should render_creator undefined as value when initially of(undefined) was passed (as undefined was emitted)', () => { + cdAwareImplementation.cdAware.nextPotentialObservable(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)); + it('should render_creator null as value when initially of(null) was passed (as null was emitted)', () => { + cdAwareImplementation.cdAware.nextPotentialObservable(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); + it('should render_creator undefined as value when initially EMPTY was passed (as no value ever was emitted)', () => { + cdAwareImplementation.cdAware.nextPotentialObservable(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); + it('should render_creator undefined as value when initially NEVER was passed (as no value ever was emitted)', () => { + cdAwareImplementation.cdAware.nextPotentialObservable(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)); + it('should render_creator emitted value from passed observable without changing it', () => { + cdAwareImplementation.cdAware.nextPotentialObservable(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)); + it('should render_creator undefined as value when a new observable NEVER was passed (as no value ever was emitted from new observable)', () => { + cdAwareImplementation.cdAware.nextPotentialObservable(of(42)); expect(cdAwareImplementation.renderedValue).toBe(42); - cdAwareImplementation.cdAware.next(NEVER); + cdAwareImplementation.cdAware.nextPotentialObservable(NEVER); expect(cdAwareImplementation.renderedValue).toBe(undefined); }); }); describe('observable context', () => { it('next handling running observable', () => { - cdAwareImplementation.cdAware.next(concat(of(42), NEVER)); + cdAwareImplementation.cdAware.nextPotentialObservable( + 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)); + cdAwareImplementation.cdAware.nextPotentialObservable(of(42)); expect(cdAwareImplementation.renderedValue).toBe(42); expect(cdAwareImplementation.error).toBe(undefined); expect(cdAwareImplementation.completed).toBe(true); @@ -139,7 +140,7 @@ describe('CdAware', () => { }); it('completion handling', () => { - cdAwareImplementation.cdAware.next(EMPTY); + cdAwareImplementation.cdAware.nextPotentialObservable(EMPTY); expect(cdAwareImplementation.renderedValue).toBe(undefined); expect(cdAwareImplementation.error).toBe(undefined); expect(cdAwareImplementation.completed).toBe(true); diff --git a/modules/component/spec/core/cd-aware/get-change-detection-handling.spec.ts b/modules/component/spec/core/cd-aware/get-change-detection-handling.spec.ts deleted file mode 100644 index 2a5f9a096e..0000000000 --- a/modules/component/spec/core/cd-aware/get-change-detection-handling.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { getGlobalThis } from '../../../src/core/utils'; -import { Injector } from '@angular/core'; -import { getChangeDetectionHandler } from '../../../src/core/cd-aware'; - -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'); - }); - }); -}); diff --git a/modules/component/spec/core/cd-aware/render_creator.spec.ts b/modules/component/spec/core/cd-aware/render_creator.spec.ts new file mode 100644 index 0000000000..5ca780ebfd --- /dev/null +++ b/modules/component/spec/core/cd-aware/render_creator.spec.ts @@ -0,0 +1,32 @@ +import { createRender } from '../../../src/core/cd-aware'; +import { + manualInstanceNgZone, + MockChangeDetectorRef, + manualInstanceNoopNgZone, +} from '../../fixtures/fixtures'; + +describe('renderCreator', () => { + it('should create', () => { + const render = createRender({ + ngZone: manualInstanceNgZone, + cdRef: new MockChangeDetectorRef(), + }); + expect(render).toBeDefined(); + }); + + it('should call markForCheck', () => { + const cdRef = new MockChangeDetectorRef(); + const render = createRender({ ngZone: manualInstanceNgZone, cdRef }); + render(); + expect(cdRef.detectChanges).toHaveBeenCalledTimes(0); + expect(cdRef.markForCheck).toHaveBeenCalledTimes(1); + }); + + it('should call detectChanges', () => { + const cdRef = new MockChangeDetectorRef(); + const render = createRender({ ngZone: manualInstanceNoopNgZone, cdRef }); + render(); + expect(cdRef.detectChanges).toHaveBeenCalledTimes(1); + expect(cdRef.markForCheck).toHaveBeenCalledTimes(0); + }); +}); diff --git a/modules/component/spec/core/projections/toObservableValue.spec.ts b/modules/component/spec/core/projections/toObservableValue.spec.ts deleted file mode 100644 index 13f15827a1..0000000000 --- a/modules/component/spec/core/projections/toObservableValue.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { EMPTY, isObservable, Observable } from 'rxjs'; -import { toObservableValue } from '../../../src/core/projections'; - -describe('toObservableValue', () => { - describe('used as RxJS creation function', () => { - // NOTE: (benlesh) These tests are probably all redundant, as you're just - // testing `rxjs` from in every case but `null` and `undefined`. - - it('should take observables', () => { - const observable: Observable = toObservableValue(EMPTY); - expect(isObservable(observable)).toBe(true); - }); - - it('should take a promise', () => { - const observable: Observable = toObservableValue( - new Promise(() => {}) - ); - expect(isObservable(observable)).toBe(true); - }); - - it('should take an iterable', () => { - const set = new Set([1, 2, 3]); - const observable: Observable = toObservableValue(set.values()); - expect(isObservable(observable)).toBe(true); - }); - - it('should take undefined', () => { - const observable: Observable = toObservableValue(undefined); - expect(isObservable(observable)).toBe(true); - }); - - it('should take a null', () => { - const observable: Observable = toObservableValue(null); - expect(isObservable(observable)).toBe(true); - }); - - // NOTE: (benlesh) - AFIACT this test would never have passed with the existing code - // `toObservableValue(null)` was made to return `of(null)` - xit('throw if no observable, promise, undefined or null is passed', () => { - const observable: Observable = toObservableValue(null); - observable.subscribe({ - error(e) { - expect(e).toBeDefined(); - }, - }); - }); - }); -}); diff --git a/modules/component/spec/core/utils/has-zone.spec.ts b/modules/component/spec/core/utils/has-zone.spec.ts deleted file mode 100644 index 293a4583b8..0000000000 --- a/modules/component/spec/core/utils/has-zone.spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -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); - }); -}); diff --git a/modules/component/spec/core/utils/zone-check.spec.ts b/modules/component/spec/core/utils/zone-check.spec.ts new file mode 100644 index 0000000000..d9ed62fb73 --- /dev/null +++ b/modules/component/spec/core/utils/zone-check.spec.ts @@ -0,0 +1,15 @@ +import { isNgZone } from '../../../src/core/utils'; +import { + manualInstanceNgZone, + manualInstanceNoopNgZone, +} from '../../fixtures/fixtures'; + +describe('envZonePatched', () => { + it('should return true if `zone.js` did patch the global API', () => { + expect(isNgZone(manualInstanceNgZone)).toBe(true); + }); + + it('should return false if `zone.js` did not patch the global API', () => { + expect(isNgZone(manualInstanceNoopNgZone)).toBe(false); + }); +}); diff --git a/modules/component/spec/fixtures/fixtures.ts b/modules/component/spec/fixtures/fixtures.ts new file mode 100644 index 0000000000..eec051efd7 --- /dev/null +++ b/modules/component/spec/fixtures/fixtures.ts @@ -0,0 +1,49 @@ +import createSpy = jasmine.createSpy; +import { ChangeDetectorRef } from '@angular/core'; +import { MockNgZone } from './mock-ng-zone'; +import { MockNoopNgZone } from './mock-noop-ng-zone'; + +/** + * this is not exposed as NgZone should never be exposed to get miss matched with the real one + */ +class NgZone extends MockNgZone {} + +/** + * this is not exposed as NgZone should never be exposed to get miss matched with the real one + */ +class NoopNgZone extends MockNoopNgZone {} + +export const manualInstanceNgZone = new NgZone({ + enableLongStackTrace: false, + shouldCoalesceEventChangeDetection: false, +}); +export const manualInstanceNoopNgZone = new NoopNgZone({ + enableLongStackTrace: false, + shouldCoalesceEventChangeDetection: false, +}); + +export class MockChangeDetectorRef { + markForCheck = createSpy('markForCheck'); + detectChanges = createSpy('detectChanges'); + checkNoChanges = createSpy('checkNoChanges'); + detach = createSpy('detach'); + reattach = createSpy('reattach'); +} + +export const mockPromise = { + then: () => {}, +}; + +export function getMockOptimizedStrategyConfig() { + return { + component: {}, + cdRef: (new MockChangeDetectorRef() as any) as ChangeDetectorRef, + }; +} + +export function getMockNoopStrategyConfig() { + return { + component: {}, + cdRef: (new MockChangeDetectorRef() as any) as ChangeDetectorRef, + }; +} diff --git a/modules/component/spec/fixtures/mock-event-emitter.ts b/modules/component/spec/fixtures/mock-event-emitter.ts new file mode 100644 index 0000000000..91890a8f5f --- /dev/null +++ b/modules/component/spec/fixtures/mock-event-emitter.ts @@ -0,0 +1,8 @@ +import { EventEmitter } from '@angular/core'; + +export class MockEventEmitter extends EventEmitter { + next(value: any) {} + error(error: any) {} + complete() {} + emit() {} +} diff --git a/modules/component/spec/fixtures/mock-ng-zone.ts b/modules/component/spec/fixtures/mock-ng-zone.ts new file mode 100644 index 0000000000..57d1ed9b79 --- /dev/null +++ b/modules/component/spec/fixtures/mock-ng-zone.ts @@ -0,0 +1,54 @@ +import { MockEventEmitter } from './mock-event-emitter'; + +/** + * source: https://github.com/angular/angular/blob/master/packages/core/src/zone/ng_zone.ts#L88 + */ +export class MockNgZone { + readonly hasPendingMacrotasks: boolean = false; + readonly hasPendingMicrotasks: boolean = false; + readonly isStable: boolean = true; + readonly onUnstable: MockEventEmitter = new MockEventEmitter(false); + readonly onMicrotaskEmpty: MockEventEmitter = new MockEventEmitter( + false + ); + readonly onStable: MockEventEmitter = new MockEventEmitter(false); + readonly onError: MockEventEmitter = new MockEventEmitter(false); + + static isInAngularZone(): boolean { + return true; + } + + static assertInAngularZone(): void {} + + static assertNotInAngularZone(): void {} + + constructor({ + enableLongStackTrace = false, + shouldCoalesceEventChangeDetection = false, + }) {} + + run(fn: Function): any { + return fn(); + } + + runTask( + fn: (...args: any[]) => T, + applyThis?: any, + applyArgs?: any[], + name?: string + ): T { + return undefined as any; + } + + runGuarded( + fn: (...args: any[]) => T, + applyThis?: any, + applyArgs?: any[] + ): T { + return undefined as any; + } + + runOutsideAngular(fn: Function): any { + return fn(); + } +} diff --git a/modules/component/spec/fixtures/mock-noop-ng-zone.ts b/modules/component/spec/fixtures/mock-noop-ng-zone.ts new file mode 100644 index 0000000000..64169e98ff --- /dev/null +++ b/modules/component/spec/fixtures/mock-noop-ng-zone.ts @@ -0,0 +1,54 @@ +import { MockEventEmitter } from './mock-event-emitter'; + +/** + * source: https://github.com/angular/angular/blob/master/packages/core/src/zone/ng_zone.ts#L88 + */ +export class MockNoopNgZone { + readonly hasPendingMacrotasks: boolean = false; + readonly hasPendingMicrotasks: boolean = false; + readonly isStable: boolean = true; + readonly onUnstable: MockEventEmitter = new MockEventEmitter(false); + readonly onMicrotaskEmpty: MockEventEmitter = new MockEventEmitter( + false + ); + readonly onStable: MockEventEmitter = new MockEventEmitter(false); + readonly onError: MockEventEmitter = new MockEventEmitter(false); + + static isInAngularZone(): boolean { + return true; + } + + static assertInAngularZone(): void {} + + static assertNotInAngularZone(): void {} + + constructor({ + enableLongStackTrace = false, + shouldCoalesceEventChangeDetection = false, + }) {} + + run(fn: Function): any { + return fn(); + } + + runTask( + fn: (...args: any[]) => T, + applyThis?: any, + applyArgs?: any[], + name?: string + ): T { + return {} as any; + } + + runGuarded( + fn: (...args: any[]) => T, + applyThis?: any, + applyArgs?: any[] + ): T { + return {} as any; + } + + runOutsideAngular(fn: Function): any { + return fn(); + } +} diff --git a/modules/component/spec/let/let.directive.spec.ts b/modules/component/spec/let/let.directive.spec.ts index 83d725c5dc..f8c5cd6618 100644 --- a/modules/component/spec/let/let.directive.spec.ts +++ b/modules/component/spec/let/let.directive.spec.ts @@ -1,37 +1,29 @@ import { ChangeDetectorRef, Component, - NgZone as OriginalNgZone, TemplateRef, ViewContainerRef, } from '@angular/core'; import { EMPTY, interval, NEVER, Observable, of, throwError } from 'rxjs'; import { async, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { LetDirective } from '@ngrx/component'; +import { LetDirective } from '../../src/let'; import { take } from 'rxjs/operators'; +import { MockChangeDetectorRef } from '../fixtures/fixtures'; let letDirective: any; -class NgZone extends OriginalNgZone { - constructor() { - super({ enableLongStackTrace: false }); - } -} - -class NoopNgZone { - constructor() { - // super({enableLongStackTrace: false}); - } -} - -class MockChangeDetectorRef { - public markForCheck(): string { - return 'markForCheck'; - } - - public detectChanges(): string { - return 'detectChanges'; - } +@Component({ + template: ` + {{ + value === null ? 'null' : (value | json) || 'undefined' + }} + `, +}) +class LetDirectiveTestComponent { + value$: Observable = of(42); } @Component({ @@ -42,8 +34,13 @@ class MockChangeDetectorRef { > `, }) -class LetDirectiveTestComponent { - cfg: any = { optimized: false }; +class LetDirectiveTestComponentStrategy { + numRenders = 0; + + renders() { + return ++this.numRenders; + } + value$: Observable = of(42); } @@ -53,7 +50,6 @@ class LetDirectiveTestComponent { `, }) class LetDirectiveTestErrorComponent { - cfg: any = { optimized: false }; value$: Observable = of(42); } @@ -65,13 +61,11 @@ class LetDirectiveTestErrorComponent { `, }) class LetDirectiveTestCompleteComponent { - cfg: any = { optimized: false }; value$: Observable = of(42); } let fixtureLetDirectiveTestComponent: any; let letDirectiveTestComponent: { - cfg: any; value$: Observable | undefined | null; }; let componentNativeElement: any; @@ -80,7 +74,6 @@ const setupLetDirectiveTestComponent = (): void => { TestBed.configureTestingModule({ declarations: [LetDirectiveTestComponent, LetDirective], providers: [ - NgZone, { provide: ChangeDetectorRef, useClass: MockChangeDetectorRef }, TemplateRef, ViewContainerRef, @@ -93,11 +86,26 @@ const setupLetDirectiveTestComponent = (): void => { fixtureLetDirectiveTestComponent.componentInstance; componentNativeElement = fixtureLetDirectiveTestComponent.nativeElement; }; +const setupLetDirectiveTestComponentStrategy = (): void => { + TestBed.configureTestingModule({ + declarations: [LetDirectiveTestComponentStrategy, LetDirective], + providers: [ + { provide: ChangeDetectorRef, useClass: MockChangeDetectorRef }, + TemplateRef, + ViewContainerRef, + ], + }); + fixtureLetDirectiveTestComponent = TestBed.createComponent( + LetDirectiveTestComponentStrategy + ); + letDirectiveTestComponent = + fixtureLetDirectiveTestComponent.componentInstance; + componentNativeElement = fixtureLetDirectiveTestComponent.nativeElement; +}; const setupLetDirectiveTestComponentError = (): void => { TestBed.configureTestingModule({ declarations: [LetDirectiveTestErrorComponent, LetDirective], providers: [ - NgZone, { provide: ChangeDetectorRef, useClass: MockChangeDetectorRef }, TemplateRef, ViewContainerRef, @@ -115,7 +123,6 @@ const setupLetDirectiveTestComponentComplete = (): void => { TestBed.configureTestingModule({ declarations: [LetDirectiveTestCompleteComponent, LetDirective], providers: [ - NgZone, { provide: ChangeDetectorRef, useClass: MockChangeDetectorRef }, TemplateRef, ViewContainerRef, @@ -140,49 +147,49 @@ describe('LetDirective', () => { expect(componentNativeElement).toBeDefined(); }); - it('should render undefined as value when initially undefined was passed (as no value ever was emitted)', () => { + it('should render_creator undefined as value when initially undefined was passed (as no value ever was emitted)', () => { letDirectiveTestComponent.value$ = undefined; fixtureLetDirectiveTestComponent.detectChanges(); expect(componentNativeElement.textContent).toBe('undefined'); }); - it('should render null as value when initially null was passed (as no value ever was emitted)', () => { + it('should render_creator null as value when initially null was passed (as no value ever was emitted)', () => { letDirectiveTestComponent.value$ = null; fixtureLetDirectiveTestComponent.detectChanges(); expect(componentNativeElement.textContent).toBe('null'); }); - it('should render undefined as value when initially of(undefined) was passed (as undefined was emitted)', () => { + it('should render_creator undefined as value when initially of(undefined) was passed (as undefined was emitted)', () => { letDirectiveTestComponent.value$ = of(undefined); fixtureLetDirectiveTestComponent.detectChanges(); expect(componentNativeElement.textContent).toBe('undefined'); }); - it('should render null as value when initially of(null) was passed (as null was emitted)', () => { + it('should render_creator null as value when initially of(null) was passed (as null was emitted)', () => { letDirectiveTestComponent.value$ = of(null); fixtureLetDirectiveTestComponent.detectChanges(); expect(componentNativeElement.textContent).toBe('null'); }); - it('should render undefined as value when initially EMPTY was passed (as no value ever was emitted)', () => { + it('should render_creator undefined as value when initially EMPTY was passed (as no value ever was emitted)', () => { letDirectiveTestComponent.value$ = EMPTY; fixtureLetDirectiveTestComponent.detectChanges(); expect(componentNativeElement.textContent).toBe('undefined'); }); - it('should render nothing as value when initially NEVER was passed (as no value ever was emitted)', () => { + it('should render_creator nothing as value when initially NEVER was passed (as no value ever was emitted)', () => { letDirectiveTestComponent.value$ = NEVER; fixtureLetDirectiveTestComponent.detectChanges(); expect(componentNativeElement.textContent).toBe(''); }); - it('should render emitted value from passed observable without changing it', () => { + it('should render_creator emitted value from passed observable without changing it', () => { letDirectiveTestComponent.value$ = of(42); fixtureLetDirectiveTestComponent.detectChanges(); expect(componentNativeElement.textContent).toBe('42'); }); - it('should render undefined as value when a new observable NEVER was passed (as no value ever was emitted from new observable)', () => { + it('should render_creator undefined as value when a new observable NEVER was passed (as no value ever was emitted from new observable)', () => { letDirectiveTestComponent.value$ = of(42); fixtureLetDirectiveTestComponent.detectChanges(); expect(componentNativeElement.textContent).toBe('42'); @@ -191,7 +198,7 @@ describe('LetDirective', () => { expect(componentNativeElement.textContent).toBe('undefined'); }); - it('should render new value as value when a new observable was passed', () => { + it('should render_creator new value as value when a new observable was passed', () => { letDirectiveTestComponent.value$ = of(42); fixtureLetDirectiveTestComponent.detectChanges(); expect(componentNativeElement.textContent).toBe('42'); @@ -200,46 +207,77 @@ describe('LetDirective', () => { expect(componentNativeElement.textContent).toBe('45'); }); + it('should render_creator the last value when a new observable was passed', () => { + letDirectiveTestComponent.value$ = of(42, 45); + fixtureLetDirectiveTestComponent.detectChanges(); + expect(componentNativeElement.textContent).toBe('45'); + }); + + it('should render_creator values over time when a new observable was passed', fakeAsync(() => { + letDirectiveTestComponent.value$ = interval(1000).pipe(take(3)); + fixtureLetDirectiveTestComponent.detectChanges(); + expect(componentNativeElement.textContent).toBe(''); + tick(1000); + fixtureLetDirectiveTestComponent.detectChanges(); + expect(componentNativeElement.textContent).toBe('0'); + tick(1000); + fixtureLetDirectiveTestComponent.detectChanges(); + expect(componentNativeElement.textContent).toBe('1'); + tick(1000); + fixtureLetDirectiveTestComponent.detectChanges(); + expect(componentNativeElement.textContent).toBe('2'); + + tick(1000); + fixtureLetDirectiveTestComponent.detectChanges(); + // Remains at 2, since that was the last value. + expect(componentNativeElement.textContent).toBe('2'); + })); + + it('should render new value as value when a new observable was passed', () => { + letDirectiveTestComponent.value$ = of(42); + fixtureLetDirectiveTestComponent.detectChanges(); + expect(componentNativeElement.textContent).toBe('42'); + letDirectiveTestComponent.value$ = of(45); + fixtureLetDirectiveTestComponent.detectChanges(); + expect(componentNativeElement.textContent).toBe('45'); + }); it('should render the last value when a new observable was passed', () => { letDirectiveTestComponent.value$ = of(42, 45); fixtureLetDirectiveTestComponent.detectChanges(); expect(componentNativeElement.textContent).toBe('45'); }); - it( - 'should render values over time when a new observable was passed', - fakeAsync(() => { - letDirectiveTestComponent.value$ = interval(1000).pipe(take(3)); - fixtureLetDirectiveTestComponent.detectChanges(); - expect(componentNativeElement.textContent).toBe(''); - tick(1000); - fixtureLetDirectiveTestComponent.detectChanges(); - expect(componentNativeElement.textContent).toBe('0'); - tick(1000); - fixtureLetDirectiveTestComponent.detectChanges(); - expect(componentNativeElement.textContent).toBe('1'); - tick(1000); - fixtureLetDirectiveTestComponent.detectChanges(); - expect(componentNativeElement.textContent).toBe('2'); - - tick(1000); - fixtureLetDirectiveTestComponent.detectChanges(); - // Remains at 2, since that was the last value. - expect(componentNativeElement.textContent).toBe('2'); - }) - ); + it('should render values over time when a new observable was passed', fakeAsync(() => { + letDirectiveTestComponent.value$ = interval(1000).pipe(take(3)); + fixtureLetDirectiveTestComponent.detectChanges(); + expect(componentNativeElement.textContent).toBe(''); + tick(1000); + fixtureLetDirectiveTestComponent.detectChanges(); + expect(componentNativeElement.textContent).toBe('0'); + tick(1000); + fixtureLetDirectiveTestComponent.detectChanges(); + expect(componentNativeElement.textContent).toBe('1'); + tick(1000); + fixtureLetDirectiveTestComponent.detectChanges(); + expect(componentNativeElement.textContent).toBe('2'); + + tick(1000); + fixtureLetDirectiveTestComponent.detectChanges(); + // Remains at 2, since that was the last value. + expect(componentNativeElement.textContent).toBe('2'); + })); }); describe('when error', () => { beforeEach(async(setupLetDirectiveTestComponentError)); - it('should render the error to false if next or complete', () => { + it('should render_creator the error to false if next or complete', () => { letDirectiveTestComponent.value$ = of(1); fixtureLetDirectiveTestComponent.detectChanges(); expect(componentNativeElement.textContent).toBe('false'); }); - it('should render the error to true if one occurs', () => { + it('should render_creator the error to true if one occurs', () => { letDirectiveTestComponent.value$ = throwError(new Error('error message')); fixtureLetDirectiveTestComponent.detectChanges(); expect(componentNativeElement.textContent).toBe('true'); @@ -249,7 +287,7 @@ describe('LetDirective', () => { describe('when complete', () => { beforeEach(async(setupLetDirectiveTestComponentComplete)); - it('should render true if completed', () => { + it('should render_creator true if completed', () => { letDirectiveTestComponent.value$ = EMPTY; fixtureLetDirectiveTestComponent.detectChanges(); expect(componentNativeElement.textContent).toBe('true'); diff --git a/modules/component/spec/push/push.pipe.spec.ts b/modules/component/spec/push/push.pipe.spec.ts index 9b34c613dc..f9906777df 100644 --- a/modules/component/spec/push/push.pipe.spec.ts +++ b/modules/component/spec/push/push.pipe.spec.ts @@ -1,13 +1,9 @@ import { PushPipe } from '../../src/push'; import { async, TestBed } from '@angular/core/testing'; -import { - ChangeDetectorRef, - Component, - NgZone as OriginalNgZone, -} from '@angular/core'; -import { getGlobalThis, isIvy, hasZone } from '../../src/core/utils'; +import { ChangeDetectorRef, Component } from '@angular/core'; +import { getGlobalThis } from '../../src/core/utils'; import { EMPTY, NEVER, Observable, of } from 'rxjs'; -import { CoalescingConfig } from '../../src/core'; +import { MockChangeDetectorRef } from '../fixtures/fixtures'; let pushPipe: PushPipe; @@ -15,52 +11,25 @@ function wrapWithSpace(str: string): string { return ' ' + str + ' '; } -class NgZone extends OriginalNgZone { - constructor() { - super({ enableLongStackTrace: false }); - } -} - -class NoopNgZone { - constructor() { - // super({enableLongStackTrace: false}); - } -} - -class MockChangeDetectorRef { - public markForCheck(): string { - return 'markForCheck'; - } - - public detectChanges(): string { - return 'detectChanges'; - } -} - @Component({ template: ` - {{ (value$ | ngrxPush: cfg | json) || 'undefined' }} + {{ (value$ | ngrxPush | json) || 'undefined' }} `, }) class PushPipeTestComponent { - cfg: CoalescingConfig = { optimized: false }; value$: Observable = of(42); } let fixturePushPipeTestComponent: any; let pushPipeTestComponent: { - cfg: CoalescingConfig; value$: Observable | undefined | null; }; let componentNativeElement: any; -let noopNgZone: NoopNgZone; -let ngZone: NgZone; const setupPushPipeComponent = () => { TestBed.configureTestingModule({ providers: [ PushPipe, - NgZone, { provide: ChangeDetectorRef, useClass: MockChangeDetectorRef }, ], }); @@ -71,17 +40,15 @@ const setupPushPipeComponentZoneLess = () => { TestBed.configureTestingModule({ providers: [ - { provide: NgZone, useClass: NoopNgZone }, { provide: ChangeDetectorRef, useClass: MockChangeDetectorRef }, { provide: PushPipe, useClass: PushPipe, - depths: [ChangeDetectorRef, NgZone], + depths: [ChangeDetectorRef], }, ], }); - pushPipe = TestBed.inject(PushPipe); - noopNgZone = TestBed.inject(NgZone); + pushPipe = TestBed.get(PushPipe); }; const setupPushPipeComponentZoneFull = () => { @@ -89,13 +56,12 @@ const setupPushPipeComponentZoneFull = () => { TestBed.configureTestingModule({ providers: [ PushPipe, - NgZone, { provide: ChangeDetectorRef, useClass: MockChangeDetectorRef }, ], }); pushPipe = TestBed.inject(PushPipe); - ngZone = TestBed.inject(NgZone); }; + describe('PushPipe', () => { describe('used as a Service', () => { beforeEach(async(setupPushPipeComponent)); @@ -204,7 +170,7 @@ describe('PushPipe', () => { ); }); - it('should return emitted value from passed observable without changing it', () => { + it('should emitted value from passed observable without changing it', () => { pushPipeTestComponent.value$ = of(42); fixturePushPipeTestComponent.detectChanges(); expect(componentNativeElement.textContent).toBe(wrapWithSpace('42')); @@ -222,47 +188,4 @@ describe('PushPipe', () => { }); }); }); - - xdescribe('when used in zone-less', () => { - beforeEach(async(setupPushPipeComponentZoneLess)); - - it('should call dcRef.detectChanges in ViewEngine', () => { - getGlobalThis().ng = { probe: true }; - const ngZone = (pushPipe as any).ngZone; - expect(!hasZone(noopNgZone as NgZone)).toBe(true); - expect(noopNgZone).toBe(ngZone); - expect(!hasZone(ngZone)).toBe(true); - expect(isIvy()).toBe(false); - // TODO(LayZeeDK) no method by that name, @BioPhoton - // expect(pushPipe.handleChangeDetection.name).toBe('detectChanges'); - }); - - it('should call detectChanges in Ivy', () => { - getGlobalThis().ng = undefined; - expect(!hasZone(noopNgZone as NgZone)).toBe(true); - expect(isIvy()).toBe(true); - // @TODO - expect(false).toBe('detectChanges'); - }); - }); - - xdescribe('when used in zone-full mode', () => { - beforeEach(async(setupPushPipeComponentZoneFull)); - - it('should call dcRef.markForCheck in ViewEngine', () => { - getGlobalThis().ng = { probe: true }; - expect(!hasZone(ngZone)).toBe(false); - expect(isIvy()).toBe(false); - // TODO(LayZeeDK) no method by that name, @BioPhoton - // expect(pushPipe.handleChangeDetection()).toBe('markForCheck'); - }); - - it('should call markDirty in Ivy', () => { - getGlobalThis().ng = undefined; - expect(!hasZone(ngZone)).toBe(false); - expect(isIvy()).toBe(true); - // @TODO - expect(false).toBe('markDirty'); - }); - }); }); diff --git a/modules/component/spec/rx-marbles/index.ts b/modules/component/spec/rx-marbles/index.ts new file mode 100644 index 0000000000..6608627077 --- /dev/null +++ b/modules/component/spec/rx-marbles/index.ts @@ -0,0 +1,2 @@ +export * from './jest.observable-matcher'; +export * from './observableMatcher'; diff --git a/modules/component/spec/rx-marbles/jest.observable-matcher.ts b/modules/component/spec/rx-marbles/jest.observable-matcher.ts new file mode 100644 index 0000000000..979e575a85 --- /dev/null +++ b/modules/component/spec/rx-marbles/jest.observable-matcher.ts @@ -0,0 +1,5 @@ +import { defaultAssert, observableMatcher } from './observableMatcher'; + +export const jestMatcher = observableMatcher(defaultAssert, (a, e) => + expect(a).toStrictEqual(e) +); diff --git a/modules/component/spec/rx-marbles/observableMatcher.ts b/modules/component/spec/rx-marbles/observableMatcher.ts new file mode 100644 index 0000000000..b745fc24ad --- /dev/null +++ b/modules/component/spec/rx-marbles/observableMatcher.ts @@ -0,0 +1,63 @@ +import { isEqual } from 'lodash'; + +function stringify(x: any): string { + return JSON.stringify(x, function(key: string, value: any) { + if (Array.isArray(value)) { + return ( + '[' + + value.map(function(i) { + return '\n\t' + stringify(i); + }) + + '\n]' + ); + } + return value; + }) + .replace(/\\"/g, '"') + .replace(/\\t/g, '\t') + .replace(/\\n/g, '\n'); +} + +function deleteErrorNotificationStack(marble: any) { + const { notification } = marble; + if (notification) { + const { kind, error } = notification; + if (kind === 'E' && error instanceof Error) { + notification.error = { name: error.name, message: error.message }; + } + } + return marble; +} + +export function defaultAssert(value: any, message: string): void { + if (value) { + return; + } + throw new Error(message); +} + +export function observableMatcher( + assert: (a: any, e: any) => void, + assertDeepEqual: (a: any, e: any) => void +): (actual: any, expected: any) => void { + return (actual: any, expected: any) => { + if (Array.isArray(actual) && Array.isArray(expected)) { + actual = actual.map(deleteErrorNotificationStack); + expected = expected.map(deleteErrorNotificationStack); + + const passed = isEqual(actual, expected); + if (passed) { + return; + } + + let message = '\nExpected \n'; + actual.forEach((x: any) => (message += `\t${stringify(x)}\n`)); + + message += '\t\nto deep equal \n'; + expected.forEach((x: any) => (message += `\t${stringify(x)}\n`)); + assert(passed, message); + } else { + assertDeepEqual(actual, expected); + } + }; +} diff --git a/modules/component/src/core/cd-aware/cd-aware_creator.ts b/modules/component/src/core/cd-aware/cd-aware_creator.ts index 31b7304275..a8c4f8293d 100644 --- a/modules/component/src/core/cd-aware/cd-aware_creator.ts +++ b/modules/component/src/core/cd-aware/cd-aware_creator.ts @@ -1,36 +1,21 @@ -import { ChangeDetectorRef, NgZone } from '@angular/core'; import { + EMPTY, NextObserver, Observable, - PartialObserver, Subject, Subscribable, Subscription, } from 'rxjs'; -import { distinctUntilChanged, map, switchAll, tap } from 'rxjs/operators'; -import { toObservableValue } from '../projections'; -import { getChangeDetectionHandler } from './get-change-detection-handling'; - -export interface CoalescingConfig { - optimized: boolean; -} +import { + catchError, + distinctUntilChanged, + filter, + switchMap, + tap, +} from 'rxjs/operators'; export interface CdAware extends Subscribable { - next: (value: Observable | Promise | null | undefined) => void; -} - -export interface WorkConfig { - context: any; - ngZone: NgZone; - cdRef: ChangeDetectorRef; -} - -export function setUpWork(cfg: WorkConfig): () => void { - const render: (component?: any) => void = getChangeDetectionHandler( - cfg.ngZone, - cfg.cdRef - ); - return () => render(cfg.context); + nextPotentialObservable: (value: any) => void; } /** @@ -43,43 +28,55 @@ export function setUpWork(cfg: WorkConfig): () => void { * Also custom behaviour is something you need to implement in the extending class */ export function createCdAware(cfg: { - work: () => void; - resetContextObserver: NextObserver; - configurableBehaviour: ( - o: Observable> - ) => Observable>; - updateViewContextObserver: PartialObserver; + render: () => void; + resetContextObserver: NextObserver; + updateViewContextObserver: NextObserver; }): CdAware { - const observablesSubject = new Subject< - Observable | Promise | null | undefined + const potentialObservablesSubject = new Subject< + Observable | undefined | null >(); - const observables$: Observable< - U | undefined | null - > = observablesSubject.pipe( - distinctUntilChanged(), - // Try to convert it to values, throw if not possible - map((v) => toObservableValue(v)), - tap((v: any) => { - cfg.resetContextObserver.next(v); - cfg.work(); - }), - map(value$ => - value$.pipe( + const observablesFromTemplate$ = potentialObservablesSubject.pipe( + distinctUntilChanged() + ); + + const rendering$ = observablesFromTemplate$.pipe( + // Compose the observables from the template and the strategy + switchMap((observable$) => { + // If the passed observable is: + // - undefined - No value set + // - null - null passed directly or no value set over `async` pipe + if (observable$ == null) { + // Update the value to render_creator with null/undefined + cfg.updateViewContextObserver.next(observable$ as any); + // Render the view + cfg.render(); + // Stop further processing + return EMPTY; + } + + // If a new Observable arrives, reset the value to render_creator + // We do this because we don't know when the next value arrives and want to get rid of the old value + cfg.resetContextObserver.next(); + cfg.render(); + + return observable$.pipe( distinctUntilChanged(), - tap(cfg.updateViewContextObserver) - ) - ), - cfg.configurableBehaviour, - switchAll(), - tap(() => cfg.work()) + tap(cfg.updateViewContextObserver), + tap(() => cfg.render()), + catchError((e) => { + console.error(e); + return EMPTY; + }) + ); + }) ); return { - next(value: any): void { - observablesSubject.next(value); + nextPotentialObservable(value: Observable | undefined | null): void { + potentialObservablesSubject.next(value); }, subscribe(): Subscription { - return observables$.subscribe(); + return rendering$.subscribe(); }, } as CdAware; } diff --git a/modules/component/src/core/cd-aware/creator_render.ts b/modules/component/src/core/cd-aware/creator_render.ts new file mode 100644 index 0000000000..467c7bb541 --- /dev/null +++ b/modules/component/src/core/cd-aware/creator_render.ts @@ -0,0 +1,19 @@ +import { ChangeDetectorRef, NgZone } from '@angular/core'; +import { isNgZone } from '../utils'; + +export interface RenderConfig { + ngZone: NgZone; + cdRef: ChangeDetectorRef; +} + +export function createRender(config: RenderConfig): () => void { + function render() { + if (isNgZone(config.ngZone)) { + config.cdRef.markForCheck(); + } else { + config.cdRef.detectChanges(); + } + } + + return render; +} diff --git a/modules/component/src/core/cd-aware/get-change-detection-handling.ts b/modules/component/src/core/cd-aware/get-change-detection-handling.ts deleted file mode 100644 index 91d0d716d1..0000000000 --- a/modules/component/src/core/cd-aware/get-change-detection-handling.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { - ChangeDetectorRef, - NgZone, - ɵdetectChanges as detectChanges, - ɵmarkDirty as markDirty, -} from '@angular/core'; - -import { isIvy } from '../utils/is-ivy'; -import { hasZone } from '../utils/has-zone'; - -export function getChangeDetectionHandler( - ngZone: NgZone, - cdRef: ChangeDetectorRef -): (component?: T) => void { - if (isIvy()) { - return hasZone(ngZone) ? markDirty : detectChanges; - } else { - return hasZone(ngZone) - ? cdRef.markForCheck.bind(cdRef) - : cdRef.detectChanges.bind(cdRef); - } -} diff --git a/modules/component/src/core/cd-aware/index.ts b/modules/component/src/core/cd-aware/index.ts index 2586045752..5b0d97c7c5 100644 --- a/modules/component/src/core/cd-aware/index.ts +++ b/modules/component/src/core/cd-aware/index.ts @@ -1,2 +1,2 @@ -export * from './get-change-detection-handling'; +export * from './creator_render'; export * from './cd-aware_creator'; diff --git a/modules/component/src/core/index.ts b/modules/component/src/core/index.ts index 89ff3544b4..2edbe35c01 100644 --- a/modules/component/src/core/index.ts +++ b/modules/component/src/core/index.ts @@ -1,3 +1,2 @@ export * from './utils'; -export * from './projections'; export * from './cd-aware'; diff --git a/modules/component/src/core/projections/index.ts b/modules/component/src/core/projections/index.ts deleted file mode 100644 index 23b688d4d3..0000000000 --- a/modules/component/src/core/projections/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './toObservableValue'; diff --git a/modules/component/src/core/projections/toObservableValue.ts b/modules/component/src/core/projections/toObservableValue.ts deleted file mode 100644 index 97435e9db6..0000000000 --- a/modules/component/src/core/projections/toObservableValue.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { from, of, Observable, ObservableInput } from 'rxjs'; - -/** - * @description - * - * This operator ensures the passed value is of the right type for `CdAware`. - * It takes `null`, `undefined` or `Observable` and returns `Observable`. - * Every other value throws an error. - * - * ```ts - * import { toObservableValue } from `projections/toObservableValue`; - * - * const toObservableValue() - * .pipe(switchAll()) - * .subscribe((n) => console.log(n);); - * ``` - */ -export function toObservableValue(p: any): Observable { - return p ? from(p) : of(p); -} diff --git a/modules/component/src/core/utils/get-global-this.ts b/modules/component/src/core/utils/get-global-this.ts index 95773c3cfd..aebf053f1d 100644 --- a/modules/component/src/core/utils/get-global-this.ts +++ b/modules/component/src/core/utils/get-global-this.ts @@ -1,18 +1,14 @@ /** * @description * - * This function returns a reference to globalThis in the following environments: - * - Browser - * - SSR (Server Side Rendering) - * - Tests + * A fallback for the new `globalThis` reference. * - * The function can be just imported and used everywhere. + * It should be used to replace `window` due to different environments in: + * - SSR (Server Side Rendering) + * - Tests + * - Browser * - * ```ts - * import { getGlobalThis } from `utils/get-global-this`; - * - * console.log(getGlobalThis()); - * ``` + * @return - A reference to globalThis. `window` in the Browser. */ export function getGlobalThis(): any { return ((globalThis as any) || (self as any) || (window as any)) as any; diff --git a/modules/component/src/core/utils/index.ts b/modules/component/src/core/utils/index.ts index a478ad7046..d08bb80332 100644 --- a/modules/component/src/core/utils/index.ts +++ b/modules/component/src/core/utils/index.ts @@ -1,3 +1,3 @@ -export { getGlobalThis } from './get-global-this'; -export { isIvy } from './is-ivy'; -export { hasZone } from './has-zone'; +export * from './get-global-this'; +export * from './zone-checks'; +export * from './is-ivy'; diff --git a/modules/component/src/core/utils/zone-checks.ts b/modules/component/src/core/utils/zone-checks.ts new file mode 100644 index 0000000000..f1438f5373 --- /dev/null +++ b/modules/component/src/core/utils/zone-checks.ts @@ -0,0 +1,22 @@ +/** + * isNgZone + * + * @description + * + * This function takes any instance of a class and checks + * if the constructor name is equal to `NgZone`. + * This means the Angular application that instantiated this service assumes it runs in a ZuneLess environment, + * and therefor it's change detection will not be triggered by zone related logic. + * + * However, keep in mind this does not mean `zone.js` is not present. + * The environment could still run in ZoneFull mode even if Angular turned it off. + * Consider the situation of a Angular element configured for ZoneLess + * environments is used in an Angular application relining on the zone mechanism. + * + * @param instance - The instance to check for constructor name of `NgZone`. + * @return boolean - true if instance is of type `NgZone`. + * + */ +export function isNgZone(instance: any): boolean { + return instance?.constructor?.name === 'NgZone'; +} diff --git a/modules/component/src/let/let.directive.ts b/modules/component/src/let/let.directive.ts index 32a53e25da..a9a6bb78a2 100644 --- a/modules/component/src/let/let.directive.ts +++ b/modules/component/src/let/let.directive.ts @@ -1,37 +1,15 @@ import { ChangeDetectorRef, Directive, - EmbeddedViewRef, Input, NgZone, OnDestroy, TemplateRef, - Type, ViewContainerRef, } from '@angular/core'; -import { - EMPTY, - NextObserver, - Observable, - PartialObserver, - ReplaySubject, - Unsubscribable, -} from 'rxjs'; -import { - catchError, - distinctUntilChanged, - filter, - map, - startWith, - withLatestFrom, -} from 'rxjs/operators'; -import { - CdAware, - CoalescingConfig as NgRxLetConfig, - createCdAware, - setUpWork, -} from '../core'; +import { NextObserver, ObservableInput, Observer, Unsubscribable } from 'rxjs'; +import { CdAware, createCdAware, createRender } from '../core'; export interface LetViewContext { // to enable `let` syntax we have to use $implicit (var; let v = var) @@ -120,17 +98,11 @@ export class LetDirective implements OnDestroy { $complete: false, }; - private readonly configSubject = new ReplaySubject(); - private readonly config$ = this.configSubject.pipe( - filter(v => v !== undefined && v !== null), - distinctUntilChanged(), - startWith({ optimized: true }) - ); - protected readonly subscription: Unsubscribable; private readonly cdAware: CdAware; - private readonly resetContextObserver: NextObserver = { + private readonly resetContextObserver: NextObserver = { next: () => { + // if not initialized no need to set undefined if (this.embeddedView) { this.ViewContext.$implicit = undefined; this.ViewContext.ngrxLet = undefined; @@ -139,10 +111,9 @@ export class LetDirective implements OnDestroy { } }, }; - private readonly updateViewContextObserver: PartialObserver< - U | null | undefined - > = { + private readonly updateViewContextObserver: Observer = { next: (value: U | null | undefined) => { + // to have init lazy if (!this.embeddedView) { this.createEmbeddedView(); } @@ -150,12 +121,14 @@ export class LetDirective implements OnDestroy { this.ViewContext.ngrxLet = value; }, error: (error: Error) => { + // to have init lazy if (!this.embeddedView) { this.createEmbeddedView(); } this.ViewContext.$error = true; }, complete: () => { + // to have init lazy if (!this.embeddedView) { this.createEmbeddedView(); } @@ -165,31 +138,16 @@ export class LetDirective implements OnDestroy { static ngTemplateContextGuard( dir: LetDirective, - ctx: unknown + ctx: unknown | null | undefined ): ctx is LetViewContext { return true; } - private readonly configurableBehaviour = ( - o$: Observable> - ): Observable> => - o$.pipe( - withLatestFrom(this.config$), - map(([value$, config]) => { - return value$.pipe(catchError(e => EMPTY)); - }) - ); - - @Input() - set ngrxLet( - potentialObservable: Observable | Promise | null | undefined - ) { - this.cdAware.next(potentialObservable); - } + static ngTemplateGuard_ngrxLet: 'binding'; @Input() - set ngrxLetConfig(config: NgRxLetConfig) { - this.configSubject.next(config || { optimized: true }); + set ngrxLet(potentialObservable: ObservableInput | null | undefined) { + this.cdAware.nextPotentialObservable(potentialObservable); } constructor( @@ -199,14 +157,9 @@ export class LetDirective implements OnDestroy { private readonly viewContainerRef: ViewContainerRef ) { this.cdAware = createCdAware({ - work: setUpWork({ - cdRef, - ngZone, - context: (cdRef as EmbeddedViewRef>).context, - }), + render: createRender({ cdRef, ngZone }), resetContextObserver: this.resetContextObserver, updateViewContextObserver: this.updateViewContextObserver, - configurableBehaviour: this.configurableBehaviour, }); this.subscription = this.cdAware.subscribe(); } diff --git a/modules/component/src/push/push.pipe.ts b/modules/component/src/push/push.pipe.ts index 6b91e95a5b..f74a839eb3 100644 --- a/modules/component/src/push/push.pipe.ts +++ b/modules/component/src/push/push.pipe.ts @@ -1,26 +1,12 @@ import { ChangeDetectorRef, - EmbeddedViewRef, NgZone, OnDestroy, Pipe, PipeTransform, - Type, } from '@angular/core'; -import { - NextObserver, - Observable, - PartialObserver, - Subject, - Unsubscribable, -} from 'rxjs'; -import { distinctUntilChanged, map, withLatestFrom } from 'rxjs/operators'; -import { - CdAware, - CoalescingConfig as PushPipeConfig, - createCdAware, - setUpWork, -} from '../core'; +import { NextObserver, ObservableInput, Unsubscribable } from 'rxjs'; +import { CdAware, createCdAware, createRender } from '../core'; /** * @Pipe PushPipe @@ -39,7 +25,8 @@ import { * ``` * * The problem is `async` pipe just marks the component and all its ancestors as dirty. - * It needs zone.js microtask queue to exhaust until `ApplicationRef.tick` is called to render all dirty marked components. + * It needs zone.js microtask queue to exhaust until `ApplicationRef.tick` is called to render_creator all dirty marked + * components. * * Heavy dynamic and interactive UIs suffer from zones change detection a lot and can * lean to bad performance or even unusable applications, but the `async` pipe does not work in zone-less mode. @@ -47,7 +34,7 @@ import { * `ngrxPush` pipe solves that problem. * * Included Features: - * - Take observables or promises, retrieve their values and render the value to the template + * - Take observables or promises, retrieve their values and render_creator the value to the template * - Handling null and undefined values in a clean unified/structured way * - Triggers change-detection differently if `zone.js` is present or not (`detectChanges` or `markForCheck`) * - Distinct same values in a row to increase performance @@ -68,58 +55,34 @@ import { export class PushPipe implements PipeTransform, OnDestroy { private renderedValue: S | null | undefined; - private readonly configSubject = new Subject(); - private readonly config$ = this.configSubject - .asObservable() - .pipe(distinctUntilChanged()); - private readonly subscription: Unsubscribable; private readonly cdAware: CdAware; - private readonly updateViewContextObserver: PartialObserver< + private readonly resetContextObserver: NextObserver = { + next: () => (this.renderedValue = undefined), + }; + private readonly updateViewContextObserver: NextObserver< S | null | undefined > = { next: (value: S | null | undefined) => (this.renderedValue = value), }; - private readonly resetContextObserver: NextObserver = { - next: (value: unknown) => (this.renderedValue = undefined), - }; - private readonly configurableBehaviour = ( - o$: Observable> - ): Observable> => - o$.pipe( - withLatestFrom(this.config$), - map(([value$, config]) => { - return value$.pipe(); - }) - ); constructor(cdRef: ChangeDetectorRef, ngZone: NgZone) { this.cdAware = createCdAware({ - work: setUpWork({ - ngZone, - cdRef, - context: (cdRef as EmbeddedViewRef>).context, - }), + render: createRender({ cdRef, ngZone }), updateViewContextObserver: this.updateViewContextObserver, resetContextObserver: this.resetContextObserver, - configurableBehaviour: this.configurableBehaviour, }); this.subscription = this.cdAware.subscribe(); } - transform(potentialObservable: null, config?: PushPipeConfig): null; - transform(potentialObservable: undefined, config?: PushPipeConfig): undefined; - transform( - potentialObservable: Observable | Promise, - config?: PushPipeConfig - ): S; - transform( - potentialObservable: Observable | Promise | null | undefined, - config: PushPipeConfig = { optimized: true } - ): S | null | undefined { - this.configSubject.next(config); - this.cdAware.next(potentialObservable); - return this.renderedValue; + transform(potentialObservable: null): null; + transform(potentialObservable: undefined): undefined; + transform(potentialObservable: ObservableInput): T; + transform( + potentialObservable: ObservableInput | null | undefined + ): T | null | undefined { + this.cdAware.nextPotentialObservable(potentialObservable); + return this.renderedValue as any; } ngOnDestroy(): void { diff --git a/modules/component/tsconfig-build.json b/modules/component/tsconfig-build.json index e61990cf6d..369636fce6 100644 --- a/modules/component/tsconfig-build.json +++ b/modules/component/tsconfig-build.json @@ -6,19 +6,18 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "strictPropertyInitialization": false, - "strict": false, "module": "es2015", "moduleResolution": "node", "noEmitOnError": false, "noImplicitAny": true, "noImplicitReturns": true, "downlevelIteration": true, - "outDir": "../../dist/modules/component", + "outDir": "../../dist/packages/component", "paths": {}, "rootDir": ".", "sourceMap": true, "inlineSources": true, - "lib": ["es2018", "dom"], + "lib": ["es2015", "dom"], "target": "es2015", "skipLibCheck": true }, diff --git a/projects/ngrx.io/content/guide/component/push.md b/projects/ngrx.io/content/guide/component/push.md index 9840f843eb..a117b052c3 100644 --- a/projects/ngrx.io/content/guide/component/push.md +++ b/projects/ngrx.io/content/guide/component/push.md @@ -19,7 +19,7 @@ The current way of binding an observable to the view looks like that: ``` The problem is `async` pipe just marks the component and all its ancestors as dirty. -It needs zone.js microtask queue to exhaust until `ApplicationRef.tick` is called to render all dirty marked components. +It needs zone.js microtask queue to exhaust until `ApplicationRef.tick` is called to render_creator all dirty marked components. Heavy dynamic and interactive UIs suffer from zones change detection a lot and can lean to bad performance or even unusable applications, but the `async` pipe does not work in zone-less mode. @@ -36,7 +36,7 @@ lean to bad performance or even unusable applications, but the `async` pipe does ## Included Features - - Take observables or promises, retrieve their values and render the value to the template + - Take observables or promises, retrieve their values and render_creator the value to the template - Handling null and undefined values in a clean unified/structured way - Triggers change-detection differently if `zone.js` is present or not (`detectChanges` or `markForCheck`) - Distinct same values in a row to increase performance