Skip to content

Instantly share code, notes, and snippets.

@studds
Created December 15, 2016 06:46
Show Gist options
  • Save studds/3ac4749d17dc77c7d22bd80790f8030c to your computer and use it in GitHub Desktop.
Save studds/3ac4749d17dc77c7d22bd80790f8030c to your computer and use it in GitHub Desktop.
SubscribeTo decorator: a async resolution for angular 2 components
/**
* 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;
}
});
};
}
/**
* 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