Skip to content

Instantly share code, notes, and snippets.

@endash
Last active January 25, 2016 01:36
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save endash/ef51f00650cf75e6e8ac to your computer and use it in GitHub Desktop.
Save endash/ef51f00650cf75e6e8ac to your computer and use it in GitHub Desktop.
Angular 2 component observable subscription decorators ***THIS IS AN EXPERIMENT/PROOF OF CONCEPT*** Feedback welcome. Note that some code may appear superfluous but is there for defensive purposes.

Note: This implementation is based on RxJS 4.0

⚠️ potentially buggy do not use in production ⚠️

Use case

Heavy use of observables can result in many components with duplicate boilerplate code for subscribing to an @Input observable and assigning the value(s) to another property for use in the template. This might also involve juggling property names if you want to use the same semantic naming in both the component's API and template. Additionally, some defensive boilerplate is necessary to guard against the possibility of differing synchronous and asynchronous behaviour.

Decorators

@Subscribe() sets up a combo get/set on the decorated property. Observables get assigned to the property, and the latest value is gotten from it. The observable reference is closed over and not otherwise accessible.

@SubscribeTo(property) overrides the observable property with a setter to trap and subscribe to the new observable. The observable remains accessible via a getter. Values are assigned to the property that is decorated.

Both will look for an instance of ChangeDetectorRef on the component and call markForCheck if found.

Update: These are both combined into the same decorator, hinging simply on whether a property name is passed as an argument.

Alternatives

  1. Handle subscribe for observables on an observable-by-observable and component-by-component basis.
  2. Use the async pipe in conjunction with the ? operator for each use of the property in the template.
  3. The same technique could probably be implemented as a factory method rather than a decorator.

Zone

Zone.run is needed to account for the fact that an asynchronous observable will execute its callback outside of the angular zone, making changes that won't get automatically picked up since no CD cycle is kicked off as a consequence. Because zone.run causes an error if the observable is not asynchronous, subscriptions are made uniformly asynchronous with observeOn. See https://gist.github.com/endash/1f961830d0c5b744598a for futher discussion of the issue.

Drawbacks

  1. Using window.zone to wrap the subscriber callbacks. This turns out to be the appropriate zone, but that may only be because we're in a setter for a property binding.
  2. Can only have one @SubscribeTo per observable.
  3. Must inject ChangeDetectorRef Because the observable callback is a "purely internal" change (i.e. not a binding) we have to manually trigger the check. IF all parent components are set to the default CD strategy CD will work without it, but if there's an OnPush component anywhere above the component it will break.
@Component({
selector: 'stories',
inputs: ['stories'],
})
export class Stories {
@Subscribe() private stories: ListOfStoryPresentations;
}
@Component({
selector: 'stories',
inputs: ['observable:stories'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class Stories {
@SubscribeTo('observable') private stories: ListOfStoryPresentations = List<Observable<StoryPresentation>>();
constructor(private _changes: ChangeDetectorRef, private _zone: NgZone) {}
}
import {NgZone, ChangeDetectorRef} from 'angular2/angular2';
import {Observable, Subscription, Scheduler} from '@reactivex/rxjs';
function findChangeDetectorRef(object: any): ChangeDetectorRef {
for (var i in object) {
if (object[i] instanceof ChangeDetectorRef) {
return object[i];
}
}
return null;
}
interface SubscribePropertyMap {
observable: Observable<any>;
observableValue: any;
changeDetector: ChangeDetectorRef;
subscriber: Subscription<any>
}
/*
This decorator subscribes to an observable in another property,
(which is overwritten with a setter to capture the assignment)
and sets the resulting value(s) to the property that is being
decorated.
The property that is being decorated is unaltered.
If ChangeDetectorRef is injected into the class it will be
found and markForCheck will be called on it. This is not
necessary with the default change detection strategy.
If NgZone is injected into the class the subscriber will be
set up asynchronously. This safeguard against receive an
asynchronous observable, callbacks for which will not run
inside the angular zone.
NOTE: Injecting NgZone is optional but if it is NOT injected
AND an asynchronous observable is used an exception will
be thrown.
*/
export function SubscribeTo(observablePropertyName: string = null) {
return function (proto: any, prop: string) {
var closureMap = new WeakMap();
function getMap(object): SubscribePropertyMap {
if (closureMap.has(object)) {
return <SubscribePropertyMap>closureMap.get(object);
} else {
let ret = {};
closureMap.set(object, ret);
return <SubscribePropertyMap>ret;
}
}
Object.defineProperty(proto, (observablePropertyName || prop) , {
get() {
var map = getMap(this);
if(!!observablePropertyName) {
return map.observable;
} else {
return map.observableValue;
}
},
set (value: any) {
var map = getMap(this);
if (value instanceof Observable) {
if (undefined === map.changeDetector) {
map.changeDetector = findChangeDetectorRef(this);
}
if (map.subscriber) {
map.subscriber.unsubscribe();
}
map.observable = value;
// This is the best way to get the zone unfortunately.
// Because we're in a property binding setter the zone
// is the correct zone.
let zone = window['zone'];
map.subscriber = map.observable.observeOn(Scheduler.nextTick).subscribe((res) => {
zone.run(() => {
if (!!observablePropertyName) {
this[prop] = res;
} else {
map.observableValue = res;
}
if (map.changeDetector) {
map.changeDetector.markForCheck();
}
});
});
} else {
map.observableValue = value;
}
}
});
}
}
/*
This decorator subscribes to an observable in this same property.
The decorated property is overwritten with a setter to capture
the assignment of the observable, and a getter to provide the
value(s).
The property that is being decorated is overwritten. That means
default value assignments will cause errors.
The observable will not be accessible, as the variable in which
it is stored is closed over. If access to the observable is
desired, use SubscribeTo in conjunction with a second property.
If ChangeDetectorRef is injected into the class it will be
found and markForCheck will be called on it. This is not
necessary with the default change detection strategy.
If NgZone is injected into the class the subscriber will be
set up asynchronously. This safeguard against receive an
asynchronous observable, callbacks for which will not run
inside the angular zone.
NOTE: Injecting NgZone is optional but if it is NOT injected
AND an asynchronous observable is used an exception will
be thrown.
*/
export var Subscribe = SubscribeTo;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment