Is there a build in way to get angular2 lifecycle events like OnDestroy
as rxjs Observable
?
I would like to subscribe to a observable like that:
ngOnInit() {
MyService.myCustomFunction()
.takeUntil(NgOnDestroy) //NgOnDestroy would be the lifecycle observable
.subscribe(() => {
//any code
});
}
Which seems to be intuitive and better to read than:
private customObservable: Observable;
ngOnDestroy() {
this.customObservable.unsubscribe();
}
ngOnInit() {
this.customObservable = MyService.myCustomFunction()
.subscribe(() => {
//any code
});
}
Is there a build in way to get angular2 lifecycle events like OnDestroy
as rxjs Observable
?
I would like to subscribe to a observable like that:
ngOnInit() {
MyService.myCustomFunction()
.takeUntil(NgOnDestroy) //NgOnDestroy would be the lifecycle observable
.subscribe(() => {
//any code
});
}
Which seems to be intuitive and better to read than:
private customObservable: Observable;
ngOnDestroy() {
this.customObservable.unsubscribe();
}
ngOnInit() {
this.customObservable = MyService.myCustomFunction()
.subscribe(() => {
//any code
});
}
Share
Improve this question
asked Nov 22, 2016 at 9:10
KaliKali
9491 gold badge8 silver badges18 bronze badges
1
-
4
Currently not, but the Angular team is considering better observable integration (for example for
@HostListener()
) – Günter Zöchbauer Commented Nov 22, 2016 at 9:12
1 Answer
Reset to default 8There isn't a built in way, but you could set up a decorator or base class to do it if you don't want to wait.
Base Class
This solution works with AOT. However, in older versions of Angular, there was a bug where lifecycle events on base classes weren't getting registered when using AOT. It at least seems to work in 4.4.x+. You can get more information here to see if your version will be affected: https://github./angular/angular/issues/12922
Example
import { SimpleChanges, OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/takeUntil';
import 'rxjs/add/operator/take';
const onChangesKey = Symbol('onChanges');
const onInitKey = Symbol('onInit');
const doCheckKey = Symbol('doCheck');
const afterContentInitKey = Symbol('afterContentInit');
const afterContentCheckedKey = Symbol('afterContentChecked');
const afterViewInitKey = Symbol('afterViewInit');
const afterViewCheckedKey = Symbol('afterViewChecked');
const onDestroyKey = Symbol('onDestroy');
export abstract class LifeCycleComponent implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {
// all observables will plete on ponent destruction
protected get onChanges(): Observable<SimpleChanges> { return this.getObservable(onChangesKey).takeUntil(this.onDestroy); }
protected get onInit(): Observable<void> { return this.getObservable(onInitKey).takeUntil(this.onDestroy).take(1); }
protected get doCheck(): Observable<void> { return this.getObservable(doCheckKey).takeUntil(this.onDestroy); }
protected get afterContentInit(): Observable<void> { return this.getObservable(afterContentInitKey).takeUntil(this.onDestroy).take(1); }
protected get afterContentChecked(): Observable<void> { return this.getObservable(afterContentCheckedKey).takeUntil(this.onDestroy); }
protected get afterViewInit(): Observable<void> { return this.getObservable(afterViewInitKey).takeUntil(this.onDestroy).take(1); }
protected get afterViewChecked(): Observable<void> { return this.getObservable(afterViewCheckedKey).takeUntil(this.onDestroy); }
protected get onDestroy(): Observable<void> { return this.getObservable(onDestroyKey).take(1); }
ngOnChanges(changes: SimpleChanges): void { this.emit(onChangesKey, changes); };
ngOnInit(): void { this.emit(onInitKey); };
ngDoCheck(): void { this.emit(doCheckKey); };
ngAfterContentInit(): void { this.emit(afterContentInitKey); };
ngAfterContentChecked(): void { this.emit(afterContentCheckedKey); };
ngAfterViewInit(): void { this.emit(afterViewInitKey); };
ngAfterViewChecked(): void { this.emit(afterViewCheckedKey); };
ngOnDestroy(): void { this.emit(onDestroyKey); };
private getObservable(key: symbol): Observable<any> {
return (this[key] || (this[key] = new Subject<any>())).asObservable();
}
private emit(key: symbol, value?: any): void {
const subject = this[key];
if (!subject) return;
subject.next(value);
}
}
Usage
import { Component, OnInit } from '@angular/core';
import { LifeCycleComponent } from './life-cycle.ponent';
import { MyService } from './my.service'
@Component({
template: ''
})
export class TestBaseComponent extends LifeCycleComponent implements OnInit {
constructor(private myService: MyService) {
super();
}
ngOnInit() {
super.ngOnInit();
this.myService.takeUntil(this.onDestroy).subscribe(() => {});
}
}
Since you are inheriting make sure that if you feel inclined to implement one of the life-cycle interfaces that you also invoke the base class method (e.g. ngOnInit() { super.ngOnInit(); }
).
Decorator
This solution does not work with AOT. Personally I like this approach better but it not working with AOT is kind of a deal breaker for some projects.
Example
/**
* Creates an observable property on an object that will
* emit when the corresponding life-cycle event occurs.
* The main rules are:
* 1. Don't name the property the same as the angular interface method.
* 2. If a class inherits from another ponent where the parent uses this decorator
* and the child implements the corresponding interface then it needs to call the parent method.
* @param {string} lifeCycleMethodName name of the function that angular calls for the life-cycle event
* @param {object} target class that contains the decorated property
* @param {string} propertyKey name of the decorated property
*/
function applyLifeCycleObservable(
lifeCycleMethodName: string,
target: object,
propertyKey: string
): void {
// Save a reference to the original life-cycle callback so that we can call it if it exists.
const originalLifeCycleMethod = target.constructor.prototype[lifeCycleMethodName];
// Use a symbol to make the observable for the instance unobtrusive.
const instanceSubjectKey = Symbol(propertyKey);
Object.defineProperty(target, propertyKey, {
get: function() {
// Get the observable for this instance or create it.
return (this[instanceSubjectKey] || (this[instanceSubjectKey] = new Subject<any>())).asObservable();
}
});
// Add or override the life-cycle callback.
target.constructor.prototype[lifeCycleMethodName] = function() {
// If it hasn't been created then there no subscribers so there is no need to emit
if (this[instanceSubjectKey]) {
// Emit the life-cycle event.
// We pass the first parameter because onChanges has a SimpleChanges parameter.
this[instanceSubjectKey].next.call(this[instanceSubjectKey], arguments[0]);
}
// If the object already had a life-cycle callback then invoke it.
if (originalLifeCycleMethod && typeof originalLifeCycleMethod === 'function') {
originalLifeCycleMethod.apply(this, arguments);
}
};
}
// Property Decorators
export function OnChangesObservable(target: any, propertyKey: string) {
applyLifeCycleObservable('ngOnChanges', target, propertyKey);
}
export function OnInitObservable(target: any, propertyKey: string) {
applyLifeCycleObservable('ngOnInit', target, propertyKey);
}
export function DoCheckObservable(target: any, propertyKey: string) {
applyLifeCycleObservable('ngDoCheck', target, propertyKey);
}
export function AfterContentInitObservable(target: any, propertyKey: string) {
applyLifeCycleObservable('ngAfterContentInit', target, propertyKey);
}
export function AfterContentCheckedObservable(target: any, propertyKey: string) {
applyLifeCycleObservable('ngAfterContentChecked', target, propertyKey);
}
export function AfterViewInitObservable(target: any, propertyKey: string) {
applyLifeCycleObservable('ngAfterViewInit', target, propertyKey);
}
export function AfterViewCheckedObservable(target: any, propertyKey: string) {
applyLifeCycleObservable('ngAfterViewChecked', target, propertyKey);
}
export function OnDestroyObservable(target: any, propertyKey: string) {
applyLifeCycleObservable('ngOnDestroy', target, propertyKey);
}
Usage
import { Component, OnInit, Input, SimpleChange } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import {
OnChangesObservable,
OnInitObservable,
DoCheckObservable,
AfterContentInitObservable,
AfterContentCheckedObservable,
AfterViewInitObservable,
AfterViewCheckedObservable,
OnDestroyObservable
} from './life-cycle.decorator';
import { MyService } from './my.service'
@Component({
template: ''
})
export class TestDecoratorComponent implements OnInit {
@OnChangesObservable
onChanges: Observable<SimpleChanges>;
@OnInitObservable
onInit: Observable<void>;
@DoCheckObservable
doCheck: Observable<void>;
@AfterContentInitObservable
afterContentInit: Observable<void>;
@AfterContentCheckedObservable
afterContentChecked: Observable<void>;
@AfterViewInitObservable
afterViewInit: Observable<void>;
@AfterViewCheckedObservable
afterViewChecked: Observable<void>;
@OnDestroyObservable
onDestroy: Observable<void>;
@Input()
input: string;
constructor(private myService: MyService) {
}
ngOnInit() {
this.myService.takeUntil(this.onDestroy).subscribe(() => {});
this.onChanges
.map(x => x.input)
.filter(x => x != null)
.takeUntil(this.onDestroy)
.subscribe((change: SimpleChange) => {
});
}
}
There are a few rules about this solution that I think are reasonable to follow:
- Name your property anything but the name of the method angular will call to notify your object of the life-cycle event (e.g. don't name the property ngOnInit). This is because the decorator will create the property as a getter and will have to create that method on the class to intercept the life-cycle event. If you ignore this then you will get a runtime error.
- If you inherit from a class that uses the life-cycle property decorator and the child class implements the angular interface for the corresponding event then the child class must call the method on the parent class (e.g.
ngOnInit() { super.ngOnInit(); }
). If you ignore this then your observable wont emit because the method on the parent class is shadowed. - You may be tempted to do something like this instead of implementing the angular interface:
this.onInit.subscribe(() => this.ngOnInit())
. Don't. Its not magic. Angular just checks for the presence of the function. So name the method you call in subscribe something other than what the angular interface would have you do. If you ignore this then you will create an infinite loop.
You can still implement the standard angular interfaces for the lifecycle events if you want to. The decorator will overwrite it but it will emit on the observable and then invoke your original implementation. Alternatively you could just subscribe to the corresponding observable.
--
One benefit to note is that it basically allows your @Input properties to be observable since ngOnChanges is now observable. You could setup a filter with a map to create a stream on the property's value (e.g. this.onChanges.map(x => x.myInput).filter(x => x != null).subscribe(x => { ... });
).
A lot of the code above was typed in this editor for an example so there may be syntax errors. Here is a running example I setup when playing around with it. Open the console to see the events fire.
https://codepen.io/bygrace1986/project/editor/AogqjM