Created
December 15, 2016 06:46
-
-
Save studds/3ac4749d17dc77c7d22bd80790f8030c to your computer and use it in GitHub Desktop.
SubscribeTo decorator: a async resolution for angular 2 components
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Created by daniel on 12/12/16. | |
*/ | |
import { OnDestroy, OnInit } from '@angular/core'; | |
import { Subscription } from 'rxjs'; | |
export interface ISubscribeTo { | |
// array of functions which create the subscriptions | |
subscribers: Function[]; | |
// array of subscriptions we need to unsubscribe from OnDestroy | |
subscriptions: Subscription[]; | |
subscriptionPropertyKeys: string[]; | |
// boolean flag per subscription property key, set to true the first time each subscription fires | |
subscriptionResolvedMap: { [ index: string ]: boolean }; | |
// true when all subscriptions have fired at least once | |
subscriptionsResolved: boolean; | |
} | |
/** | |
* | |
* Make sure all the required state-tracking properties have been added to the prototype before we try to add any subscriptions. | |
* Also wrap ngOnInit and ngOnDestroy to subscribe / unsubscribe. | |
* | |
* @param {Object & OnInit & OnDestroy & ISubscribeTo} proto | |
*/ | |
function initialisePrototype(proto: Object & OnInit & OnDestroy & ISubscribeTo): void { | |
if (!proto.subscribers) { | |
proto.subscribers = []; | |
proto.subscriptions = []; | |
proto.subscriptionPropertyKeys = []; | |
proto.subscriptionResolvedMap = {}; | |
const ngOnInit = proto.ngOnInit || ((): void => undefined); | |
const ngOnDestroy = proto.ngOnDestroy || ((): void => undefined); | |
// call all subscriber functions during ngOnInit and then call the original ngOnInit | |
proto.ngOnInit = function (): void { | |
proto.subscribers | |
.forEach((subscriber: Function): void => { | |
subscriber.call(this); | |
}); | |
ngOnInit.call(this); | |
}; | |
// unsubscribe from everything and then call the original ngOnDestroy | |
proto.ngOnDestroy = function (): void { | |
proto.subscriptions | |
.forEach((subscription: Subscription): void => { | |
subscription.unsubscribe(); | |
}); | |
proto.subscriptions = []; | |
ngOnDestroy.call(this); | |
}; | |
/** | |
* returns true when all subscriptions have fired at least once | |
*/ | |
Object.defineProperty(proto, 'subscriptionsResolved', { | |
get: function (): boolean { | |
return proto.subscriptionPropertyKeys | |
.every((subscriptionPropertyKey: string): boolean => { | |
return proto.subscriptionResolvedMap[subscriptionPropertyKey] === true; | |
}); | |
} | |
}); | |
} | |
} | |
/** | |
* | |
* Create a decorator function which will subscribe to the observable defined by `this[options.observableKey]` and return the | |
* latest value via a getter at `proto[propertyName]`. The observable must be defined before ngOnInit is called or will be | |
* ignored. | |
* | |
* @param {{ observableKey: string }} options | |
* @returns {(proto:(Object&OnInit&OnDestroy&ISubscribeTo), propertyName:string)=>void} | |
*/ | |
export function SubscribeTo<T>(options: { observableKey: string }): Function { | |
return function (proto: Object & OnInit & OnDestroy & ISubscribeTo, propertyName: string): void { | |
initialisePrototype(proto); | |
let latestValue: T; | |
proto.subscribers.push(function (): void { | |
const observable$ = this[options.observableKey]; | |
// we entirely ignore any observables which are undefined when ngInit is called | |
if (observable$) { | |
proto.subscriptionPropertyKeys.push(propertyName); | |
const subscription = observable$ | |
.subscribe((value: T): void => { | |
latestValue = value; | |
this.subscriptionResolvedMap[propertyName] = true; | |
}); | |
// register the subscription so that it will be unsubscribed in ngOnDestroy | |
proto.subscriptions.push(subscription); | |
} | |
}); | |
// getter to return the last value from the subscription above. | |
Object.defineProperty(proto, propertyName, { | |
get: function (): T { | |
return latestValue; | |
} | |
}); | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Created by daniel on 12/12/16. | |
*/ | |
import assert = require('assert'); | |
import { Observable, Subject } from 'rxjs'; | |
import sinon = require('sinon'); | |
import { SubscribeTo } from './subscribe-to.decorator'; | |
import { OnDestroy, OnInit } from '@angular/core'; | |
describe('SubscribeTo', function (): void { | |
let unsub: sinon.SinonSpy; | |
let sub: sinon.SinonSpy; | |
class TestSubscribeTo<T, T2> implements OnInit, OnDestroy { | |
observable$: Observable<T>; | |
@SubscribeTo({ observableKey: 'observable$' }) observable: T; | |
initSpy; | |
destroySpy; | |
subscriptionsResolved: boolean; | |
observable2$: Observable<T2> | undefined; | |
@SubscribeTo({ observableKey: 'observable2$' }) observable2: T2; | |
constructor(observable$: Observable<T>, observable2$?: Observable<T2>) { | |
this.observable$ = observable$; | |
this.observable2$ = observable2$; | |
} | |
ngOnInit(): void { | |
if (this.initSpy) { | |
this.initSpy(); | |
} | |
} | |
ngOnDestroy(): void { | |
if (this.destroySpy) { | |
this.destroySpy(); | |
} | |
} | |
} | |
let observable: Observable<void>; | |
beforeEach(function (): void { | |
unsub = sinon.spy(); | |
sub = sinon.spy((): Function => { | |
return unsub; | |
}); | |
observable = Observable.create(sub); | |
}); | |
it('should subscribe to the observable on init & unsub on destroy', function (): void { | |
const testSubscribeTo = new TestSubscribeTo(observable); | |
testSubscribeTo.ngOnInit(); | |
testSubscribeTo.ngOnDestroy(); | |
assert(sub.callCount === 1); | |
assert(unsub.callCount === 1); | |
}); | |
it('should wrap existing ngOnInit & ngOnDestroy', function (): void { | |
const testSubscribeTo = new TestSubscribeTo(observable); | |
testSubscribeTo.initSpy = sinon.spy(); | |
testSubscribeTo.destroySpy = sinon.spy(); | |
testSubscribeTo.ngOnInit(); | |
testSubscribeTo.ngOnDestroy(); | |
assert(testSubscribeTo.initSpy.callCount === 1); | |
assert(testSubscribeTo.destroySpy.callCount === 1); | |
}); | |
it('set the last value received', function (): void { | |
const subject = new Subject<string>(); | |
const testSubscribeTo = new TestSubscribeTo(subject); | |
testSubscribeTo.ngOnInit(); | |
subject.next('hello'); | |
assert(testSubscribeTo.observable === 'hello'); | |
}); | |
it('should set resolved = true when single observable receives a value', function (): void { | |
const subject = new Subject<string>(); | |
const testSubscribeTo = new TestSubscribeTo(subject); | |
testSubscribeTo.ngOnInit(); | |
subject.next('hello'); | |
assert(testSubscribeTo.subscriptionsResolved === true); | |
}); | |
it('should subscribe to the observable on init & unsub on destroy - multiple', function (): void { | |
const testSubscribeTo = new TestSubscribeTo(observable, observable); | |
testSubscribeTo.ngOnInit(); | |
testSubscribeTo.ngOnDestroy(); | |
assert(sub.callCount === 2); | |
assert(unsub.callCount === 2); | |
}); | |
it('should set resolved = true when single observable receives a value - multiple', function (): void { | |
const subject = new Subject<string>(); | |
const subject2 = new Subject<string>(); | |
const testSubscribeTo = new TestSubscribeTo(subject, subject2); | |
testSubscribeTo.ngOnInit(); | |
subject.next('hello'); | |
assert(testSubscribeTo.subscriptionsResolved === false); | |
subject2.next('world'); | |
assert(testSubscribeTo.subscriptionsResolved === true); | |
}); | |
it('should set resolved = true when single observable receives a value - multiple', function (): void { | |
const subject = new Subject<string>(); | |
const subject2 = new Subject<string>(); | |
const testSubscribeTo = new TestSubscribeTo(subject, subject2); | |
testSubscribeTo.ngOnInit(); | |
subject.next('hello'); | |
subject2.next('world'); | |
assert(testSubscribeTo.observable === 'hello'); | |
assert(testSubscribeTo.observable2 === 'world'); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment