From 2fb4e72e9e266e401e57ad68bd36c4e944600be8 Mon Sep 17 00:00:00 2001 From: Andrew Seguin Date: Thu, 25 Jan 2018 14:50:53 -0800 Subject: [PATCH 1/3] core(initialized): add mixin for initialized --- src/lib/core/common-behaviors/index.ts | 1 + .../core/common-behaviors/initialized.spec.ts | 50 ++++++++++++ src/lib/core/common-behaviors/initialized.ts | 79 +++++++++++++++++++ tools/package-tools/rollup-globals.ts | 1 + 4 files changed, 131 insertions(+) create mode 100644 src/lib/core/common-behaviors/initialized.spec.ts create mode 100644 src/lib/core/common-behaviors/initialized.ts diff --git a/src/lib/core/common-behaviors/index.ts b/src/lib/core/common-behaviors/index.ts index bfc3317731b7..a6eec481e0b8 100644 --- a/src/lib/core/common-behaviors/index.ts +++ b/src/lib/core/common-behaviors/index.ts @@ -12,3 +12,4 @@ export {CanColor, mixinColor, ThemePalette} from './color'; export {CanDisableRipple, mixinDisableRipple} from './disable-ripple'; export {HasTabIndex, mixinTabIndex} from './tabindex'; export {CanUpdateErrorState, mixinErrorState} from './error-state'; +export {OnInitialized, mixinInitialized} from './initialized'; diff --git a/src/lib/core/common-behaviors/initialized.spec.ts b/src/lib/core/common-behaviors/initialized.spec.ts new file mode 100644 index 000000000000..e569216059e3 --- /dev/null +++ b/src/lib/core/common-behaviors/initialized.spec.ts @@ -0,0 +1,50 @@ +import {mixinInitialized} from './initialized'; +import {OnInitialized} from '@angular/material/core'; + +describe('MixinOnInitialized', () => { + class EmptyClass { } + let instance: OnInitialized; + + beforeEach(() => { + + const classWithOnInitialized = mixinInitialized(EmptyClass); + instance = new classWithOnInitialized(); + }); + + it('should emit for subscriptions made before the directive was marked as initialized', done => { + // Listen for an event from the initialized stream and mark the test as done when it emits. + instance.initialized.subscribe(() => done()); + + // Mark the class as initialized so that the stream emits and the test completes. + instance._markInitialized(); + }); + + it('should emit for subscriptions made after the directive was marked as initialized', done => { + // Mark the class as initialized so the stream emits when subscribed and the test completes. + instance._markInitialized(); + + // Listen for an event from the initialized stream and mark the test as done when it emits. + instance.initialized.subscribe(() => done()); + }); + + it('should emit for multiple subscriptions made before and after marked as initialized', done => { + // Should expect the number of notifications to match the number of subscriptions. + const expectedNotificationCount = 4; + let currentNotificationCount = 0; + + // Function that completes the test when the number of notifications meets the expectation. + function onNotified() { + if (++currentNotificationCount === expectedNotificationCount) { + done(); + } + } + + instance.initialized.subscribe(onNotified); // Subscription 1 + instance.initialized.subscribe(onNotified); // Subscription 2 + + instance._markInitialized(); + + instance.initialized.subscribe(onNotified); // Subscription 3 + instance.initialized.subscribe(onNotified); // Subscription 4 + }); +}); diff --git a/src/lib/core/common-behaviors/initialized.ts b/src/lib/core/common-behaviors/initialized.ts new file mode 100644 index 000000000000..8f0c58469470 --- /dev/null +++ b/src/lib/core/common-behaviors/initialized.ts @@ -0,0 +1,79 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Constructor} from './constructor'; +import {Observable} from 'rxjs/Observable'; +import {Subscriber} from 'rxjs/Subscriber'; + +/** + * Mixin that adds an initialized property to a directive which, when subscribed to, will emit a + * value once markInitialized has been called, which should be done during the ngOnInit function. + * If the subscription is made after it has already been marked as initialized, then it will trigger + * an emit immediately. + * @docs-private + */ +export interface OnInitialized { + /** Stream that emits once during the directive/component's ngOnInit. */ + initialized: Observable; + + /** + * Sets the state as initialized and must be called during ngOnInit to notify subscribers that + * the directive has been initialized. + * @docs-private + */ + _markInitialized: () => void; +} + +/** Mixin to augment a directive with an initialized property that will emits when ngOnInit ends. */ +export function mixinInitialized>(base: T): + Constructor & T { + return class extends base { + /** Whether this directive has been marked as initialized. */ + _isInitialized = false; + + /** + * List of subscribers that subscribed before the directive was initialized. Should be notified + * during _markInitialized. + */ + _pendingSubscribers: Subscriber[] = []; + + /** + * Observable stream that emits when the directive initializes. If already initialized, the + * subscriber is stored to be notified once _markInitialized is called. + */ + initialized = new Observable(subscriber => { + // If initialized, immediately notify the subscriber. Otherwise store the subscriber to notify + // when _markInitialized is called. + if (this._isInitialized) { + this._notifySubscriber(subscriber); + } else { + this._pendingSubscribers.push(subscriber); + } + }); + + constructor(...args: any[]) { super(...args); } + + /** + * Marks the state as initialized and notifies pending subscribers. Should be called at the end + * of ngOnInit. + * @docs-private + */ + _markInitialized(): void { + this._isInitialized = true; + + this._pendingSubscribers.forEach(this._notifySubscriber); + this._pendingSubscribers = []; + } + + /** Emits and completes the subscriber stream (should only emit once). */ + _notifySubscriber(subscriber: Subscriber): void { + subscriber.next(); + subscriber.complete(); + } + }; +} diff --git a/tools/package-tools/rollup-globals.ts b/tools/package-tools/rollup-globals.ts index 0707dfac8d40..af1624542ddc 100644 --- a/tools/package-tools/rollup-globals.ts +++ b/tools/package-tools/rollup-globals.ts @@ -54,6 +54,7 @@ export const rollupGlobals = { ...rollupMatEntryPoints, 'rxjs/BehaviorSubject': 'Rx', + 'rxjs/ReplaySubject': 'Rx', 'rxjs/Observable': 'Rx', 'rxjs/Subject': 'Rx', 'rxjs/Subscription': 'Rx', From 742019c9a916e1e5a2fdac4685008bd3c457c328 Mon Sep 17 00:00:00 2001 From: Andrew Seguin Date: Fri, 26 Jan 2018 09:44:01 -0800 Subject: [PATCH 2/3] comments --- src/lib/core/common-behaviors/index.ts | 2 +- .../core/common-behaviors/initialized.spec.ts | 13 ++++++------ src/lib/core/common-behaviors/initialized.ts | 20 ++++++++++++------- tools/package-tools/rollup-globals.ts | 1 - 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/lib/core/common-behaviors/index.ts b/src/lib/core/common-behaviors/index.ts index a6eec481e0b8..5e963bda2d85 100644 --- a/src/lib/core/common-behaviors/index.ts +++ b/src/lib/core/common-behaviors/index.ts @@ -12,4 +12,4 @@ export {CanColor, mixinColor, ThemePalette} from './color'; export {CanDisableRipple, mixinDisableRipple} from './disable-ripple'; export {HasTabIndex, mixinTabIndex} from './tabindex'; export {CanUpdateErrorState, mixinErrorState} from './error-state'; -export {OnInitialized, mixinInitialized} from './initialized'; +export {HasInitialized, mixinInitialized} from './initialized'; diff --git a/src/lib/core/common-behaviors/initialized.spec.ts b/src/lib/core/common-behaviors/initialized.spec.ts index e569216059e3..c441921eb80f 100644 --- a/src/lib/core/common-behaviors/initialized.spec.ts +++ b/src/lib/core/common-behaviors/initialized.spec.ts @@ -1,14 +1,14 @@ import {mixinInitialized} from './initialized'; -import {OnInitialized} from '@angular/material/core'; +import {HasInitialized} from '@angular/material/core'; -describe('MixinOnInitialized', () => { +describe('MixinHasInitialized', () => { class EmptyClass { } - let instance: OnInitialized; + let instance: HasInitialized; beforeEach(() => { - const classWithOnInitialized = mixinInitialized(EmptyClass); - instance = new classWithOnInitialized(); + const classWithHasInitialized = mixinInitialized(EmptyClass); + instance = new classWithHasInitialized(); }); it('should emit for subscriptions made before the directive was marked as initialized', done => { @@ -34,7 +34,8 @@ describe('MixinOnInitialized', () => { // Function that completes the test when the number of notifications meets the expectation. function onNotified() { - if (++currentNotificationCount === expectedNotificationCount) { + currentNotificationCount++; + if (currentNotificationCount === expectedNotificationCount) { done(); } } diff --git a/src/lib/core/common-behaviors/initialized.ts b/src/lib/core/common-behaviors/initialized.ts index 8f0c58469470..166e08ed4c66 100644 --- a/src/lib/core/common-behaviors/initialized.ts +++ b/src/lib/core/common-behaviors/initialized.ts @@ -17,7 +17,7 @@ import {Subscriber} from 'rxjs/Subscriber'; * an emit immediately. * @docs-private */ -export interface OnInitialized { +export interface HasInitialized { /** Stream that emits once during the directive/component's ngOnInit. */ initialized: Observable; @@ -31,16 +31,17 @@ export interface OnInitialized { /** Mixin to augment a directive with an initialized property that will emits when ngOnInit ends. */ export function mixinInitialized>(base: T): - Constructor & T { + Constructor & T { return class extends base { /** Whether this directive has been marked as initialized. */ _isInitialized = false; /** * List of subscribers that subscribed before the directive was initialized. Should be notified - * during _markInitialized. + * during _markInitialized. Set to null after pending subscribers are notified, and should + * not expect to be populated after. */ - _pendingSubscribers: Subscriber[] = []; + _pendingSubscribers: Subscriber[] | null = []; /** * Observable stream that emits when the directive initializes. If already initialized, the @@ -52,7 +53,7 @@ export function mixinInitialized>(base: T): if (this._isInitialized) { this._notifySubscriber(subscriber); } else { - this._pendingSubscribers.push(subscriber); + this._pendingSubscribers!.push(subscriber); } }); @@ -64,10 +65,15 @@ export function mixinInitialized>(base: T): * @docs-private */ _markInitialized(): void { + if (this._isInitialized) { + throw Error('This directive has already been marked as initialized and ' + + 'should not be called twice.'); + } + this._isInitialized = true; - this._pendingSubscribers.forEach(this._notifySubscriber); - this._pendingSubscribers = []; + this._pendingSubscribers!.forEach(this._notifySubscriber); + this._pendingSubscribers = null; } /** Emits and completes the subscriber stream (should only emit once). */ diff --git a/tools/package-tools/rollup-globals.ts b/tools/package-tools/rollup-globals.ts index af1624542ddc..0707dfac8d40 100644 --- a/tools/package-tools/rollup-globals.ts +++ b/tools/package-tools/rollup-globals.ts @@ -54,7 +54,6 @@ export const rollupGlobals = { ...rollupMatEntryPoints, 'rxjs/BehaviorSubject': 'Rx', - 'rxjs/ReplaySubject': 'Rx', 'rxjs/Observable': 'Rx', 'rxjs/Subject': 'Rx', 'rxjs/Subscription': 'Rx', From 90198716693591326824246d19e7de5baa70e8de Mon Sep 17 00:00:00 2001 From: Andrew Seguin Date: Fri, 26 Jan 2018 09:44:30 -0800 Subject: [PATCH 3/3] remove newline --- src/lib/core/common-behaviors/initialized.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/core/common-behaviors/initialized.spec.ts b/src/lib/core/common-behaviors/initialized.spec.ts index c441921eb80f..723bc44b414f 100644 --- a/src/lib/core/common-behaviors/initialized.spec.ts +++ b/src/lib/core/common-behaviors/initialized.spec.ts @@ -6,7 +6,6 @@ describe('MixinHasInitialized', () => { let instance: HasInitialized; beforeEach(() => { - const classWithHasInitialized = mixinInitialized(EmptyClass); instance = new classWithHasInitialized(); });