Anuglar2 lifecycle events as rxjs Observable

后端 未结 1 1971
一向
一向 2021-02-20 06:07

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

1条回答
  •  爱一瞬间的悲伤
    2021-02-20 06:33

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

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

    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

    0 讨论(0)
提交回复
热议问题