When I started using RxJs with Angular 2+ I found myself manually tracking subscriptions in places where I couldn’t use the async pipe. I also found myself setting up internal observables for @Input fields so that they could mix with streams from the NgRx store.

import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { Store } from '@ngrx/store';
import { Subscription } from 'rxjs/Subscription';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/withLatestFrom';

import { RootState } from '../state/models/root-state.model';

export class TestComponent implements OnInit, OnDestroy {
    private subscription: Subscription;
    private inputSource: Subject = new Subject();

    @Input()
    public set input(value: string) {
        this.inputSource.next(value);
    }

    constructor(private store: Store) {
    }

    ngOnInit(): void {
        this.subscription = this.inputSource.asObservable()
            .withLatestFrom(this.store.select(x => x.data))
            .subscribe((x) => {
                // TODO
            });
    }

    ngOnDestroy(): void {
        if (this.subscription) {
            this.subscription.unsubscribe();
        }
    }
}

At first I wasn’t checking if the subscription existed in the ngOnDestroy. Then I found out the hard way that ngOnInit doesn’t always fire but the ngOnDestroy will. Good to know!

This got old after a while so I put together a way to expose the lifecycle events as observables.

Decorator

The first solution was to use decorators to mark an observable property as the receiver for a specific life-cycle event. It would be similar to how @HostListener worked and I figured it would be the least obtrusive. Behind the curtains the decorator would add the ngX method for the corresponding life-cycle event to the prototype. If it already existed then it would duck punch it. Bellow is the decorator and its usage.

The Magic

/**
 * 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())).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);
}

The 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;
    @OnInitObservable
    onInit: Observable;
    @DoCheckObservable
    doCheck: Observable;
    @AfterContentInitObservable
    afterContentInit: Observable;
    @AfterContentCheckedObservable
    afterContentChecked: Observable;
    @AfterViewInitObservable
    afterViewInit: Observable;
    @AfterViewCheckedObservable
    afterViewChecked: Observable;
    @OnDestroyObservable
    onDestroy: Observable;

    @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) => {
            });
    }
}

This worked fine till I ran an AOT build and realized that I was adding it to the prototype too late. Despite taking a while to wander through the angular source code I couldn’t find a hacky way to make it work.

Base Class

The second solution was to use a base class. Clearly this was the simpler option but I didn’t love the idea of modifying the prototype chain to make this happen. Thankfully I didn’t have a lot of inheritance in my components already. Bellow is what I ended up with.

The Magic

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 { return this.getObservable(onChangesKey).takeUntil(this.onDestroy); }
    protected get onInit(): Observable { return this.getObservable(onInitKey).takeUntil(this.onDestroy).take(1); }
    protected get doCheck(): Observable { return this.getObservable(doCheckKey).takeUntil(this.onDestroy); }
    protected get afterContentInit(): Observable { return this.getObservable(afterContentInitKey).takeUntil(this.onDestroy).take(1); }
    protected get afterContentChecked(): Observable { return this.getObservable(afterContentCheckedKey).takeUntil(this.onDestroy); }
    protected get afterViewInit(): Observable { return this.getObservable(afterViewInitKey).takeUntil(this.onDestroy).take(1); }
    protected get afterViewChecked(): Observable { return this.getObservable(afterViewCheckedKey).takeUntil(this.onDestroy); }
    protected get onDestroy(): Observable { 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 {
        return (this[key] || (this[key] = new Subject())).asObservable();
    }

    private emit(key: symbol, value?: any): void {
        const subject = this[key];
        if (!subject) return;
        subject.next(value);
    }
}

The Usage

import { Component, OnInit, Input } from '@angular/core';

import { LifeCycleComponent } from './life-cycle.component';
import { MyService } from './my.service'

@Component({
  template: ''
})
export class TestBaseComponent extends LifeCycleComponent implements OnInit {
  @Input()
  public name: string;

  constructor(private myService: MyService) {
    super();
  }

  ngOnInit() {
    super.ngOnInit();
    this.myService.takeUntil(this.onDestroy).subscribe(() => { ... });
    this.onChanges.filter(x => x.name != null).map(x => x.name.currentValue).subscribe(() => { ... });
  }
}

The biggest drawback is that you have to remember to call the shadowed method on the base class if you decide to implement the standard life-cycle callback. Since I have it as an observable I tend to not implement them that way but for example I did here. Doing takeUntil(this.onDestroy) is much nicer than tracking subscriptions. Getting the @Input as observable is a bit wordy but I could hide some of that in the base class if desired.

In the end it isn’t as tight of an integration as if the Angular team had added the functionality and I’m still not sure it is the best approach but I have liked using it so far.

Thanks for reading and here are some links for further investigation:

Tags:
  1. RxJs
  2. Angular

Erik Murphy

Erik is an agile software developer in Charlotte, NC. He enjoys working full-stack (CSS, JS, C#, SQL) as each layer presents new challenges. His experience involves a variety of applications ranging from developing brochure sites to high-performance streaming applications. He has worked in many domains including military, healthcare, finance, and energy.

Copyright © 2024 Induro, LLC All Rights Reserved.