Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Anuglar2 lifecycle events as rxjs Observable

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
    });
 }
like image 406
Kali Avatar asked Nov 22 '16 09:11

Kali


1 Answers

There 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.com/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 complete on component 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.component';
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 component 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:

  1. 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.
  2. 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.
  3. 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

like image 106
bygrace Avatar answered Oct 17 '22 19:10

bygrace