Skip to content

Commit 5290bd8

Browse files
andrewseguintinayuangao
authored andcommitted
core(initialized): add mixin for initialized (#9609)
* core(initialized): add mixin for initialized * comments * remove newline
1 parent 7e352ce commit 5290bd8

File tree

3 files changed

+136
-0
lines changed

3 files changed

+136
-0
lines changed

src/lib/core/common-behaviors/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ export {CanColor, mixinColor, ThemePalette} from './color';
1212
export {CanDisableRipple, mixinDisableRipple} from './disable-ripple';
1313
export {HasTabIndex, mixinTabIndex} from './tabindex';
1414
export {CanUpdateErrorState, mixinErrorState} from './error-state';
15+
export {HasInitialized, mixinInitialized} from './initialized';
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {mixinInitialized} from './initialized';
2+
import {HasInitialized} from '@angular/material/core';
3+
4+
describe('MixinHasInitialized', () => {
5+
class EmptyClass { }
6+
let instance: HasInitialized;
7+
8+
beforeEach(() => {
9+
const classWithHasInitialized = mixinInitialized(EmptyClass);
10+
instance = new classWithHasInitialized();
11+
});
12+
13+
it('should emit for subscriptions made before the directive was marked as initialized', done => {
14+
// Listen for an event from the initialized stream and mark the test as done when it emits.
15+
instance.initialized.subscribe(() => done());
16+
17+
// Mark the class as initialized so that the stream emits and the test completes.
18+
instance._markInitialized();
19+
});
20+
21+
it('should emit for subscriptions made after the directive was marked as initialized', done => {
22+
// Mark the class as initialized so the stream emits when subscribed and the test completes.
23+
instance._markInitialized();
24+
25+
// Listen for an event from the initialized stream and mark the test as done when it emits.
26+
instance.initialized.subscribe(() => done());
27+
});
28+
29+
it('should emit for multiple subscriptions made before and after marked as initialized', done => {
30+
// Should expect the number of notifications to match the number of subscriptions.
31+
const expectedNotificationCount = 4;
32+
let currentNotificationCount = 0;
33+
34+
// Function that completes the test when the number of notifications meets the expectation.
35+
function onNotified() {
36+
currentNotificationCount++;
37+
if (currentNotificationCount === expectedNotificationCount) {
38+
done();
39+
}
40+
}
41+
42+
instance.initialized.subscribe(onNotified); // Subscription 1
43+
instance.initialized.subscribe(onNotified); // Subscription 2
44+
45+
instance._markInitialized();
46+
47+
instance.initialized.subscribe(onNotified); // Subscription 3
48+
instance.initialized.subscribe(onNotified); // Subscription 4
49+
});
50+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Constructor} from './constructor';
10+
import {Observable} from 'rxjs/Observable';
11+
import {Subscriber} from 'rxjs/Subscriber';
12+
13+
/**
14+
* Mixin that adds an initialized property to a directive which, when subscribed to, will emit a
15+
* value once markInitialized has been called, which should be done during the ngOnInit function.
16+
* If the subscription is made after it has already been marked as initialized, then it will trigger
17+
* an emit immediately.
18+
* @docs-private
19+
*/
20+
export interface HasInitialized {
21+
/** Stream that emits once during the directive/component's ngOnInit. */
22+
initialized: Observable<void>;
23+
24+
/**
25+
* Sets the state as initialized and must be called during ngOnInit to notify subscribers that
26+
* the directive has been initialized.
27+
* @docs-private
28+
*/
29+
_markInitialized: () => void;
30+
}
31+
32+
/** Mixin to augment a directive with an initialized property that will emits when ngOnInit ends. */
33+
export function mixinInitialized<T extends Constructor<{}>>(base: T):
34+
Constructor<HasInitialized> & T {
35+
return class extends base {
36+
/** Whether this directive has been marked as initialized. */
37+
_isInitialized = false;
38+
39+
/**
40+
* List of subscribers that subscribed before the directive was initialized. Should be notified
41+
* during _markInitialized. Set to null after pending subscribers are notified, and should
42+
* not expect to be populated after.
43+
*/
44+
_pendingSubscribers: Subscriber<void>[] | null = [];
45+
46+
/**
47+
* Observable stream that emits when the directive initializes. If already initialized, the
48+
* subscriber is stored to be notified once _markInitialized is called.
49+
*/
50+
initialized = new Observable<void>(subscriber => {
51+
// If initialized, immediately notify the subscriber. Otherwise store the subscriber to notify
52+
// when _markInitialized is called.
53+
if (this._isInitialized) {
54+
this._notifySubscriber(subscriber);
55+
} else {
56+
this._pendingSubscribers!.push(subscriber);
57+
}
58+
});
59+
60+
constructor(...args: any[]) { super(...args); }
61+
62+
/**
63+
* Marks the state as initialized and notifies pending subscribers. Should be called at the end
64+
* of ngOnInit.
65+
* @docs-private
66+
*/
67+
_markInitialized(): void {
68+
if (this._isInitialized) {
69+
throw Error('This directive has already been marked as initialized and ' +
70+
'should not be called twice.');
71+
}
72+
73+
this._isInitialized = true;
74+
75+
this._pendingSubscribers!.forEach(this._notifySubscriber);
76+
this._pendingSubscribers = null;
77+
}
78+
79+
/** Emits and completes the subscriber stream (should only emit once). */
80+
_notifySubscriber(subscriber: Subscriber<void>): void {
81+
subscriber.next();
82+
subscriber.complete();
83+
}
84+
};
85+
}

0 commit comments

Comments
 (0)