Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Using untilViewDestroyed to link component ngOnDestroy to observable unsubscribe.
/**
* When manually subscribing to an observable in a view component, developers are traditionally required
* to unsubscribe during ngOnDestroy. This utility method auto-configures and manages that relationship
* by watching the DOM with a MutationObserver and internally using the takeUntil RxJS operator.
*
* Angular 7 has stricter enforcements and throws errors with monkey-patching of view component life-cycle methods.
* Here is an updated version that uses MutationObserver to accomplish the same goal.
*
* @code
*
* import {untilViewDestroyed} from 'utils/untilViewDestroyed.ts'
*
* @Component({})
* export class TicketDetails {
* search: FormControl;
*
* constructor(private ticketService: TicketService, private elRef: ElementRef){}
* ngOnInit() {
* this.search.valueChanges.pipe(
* untilViewDestroyed(elRef),
* switchMap(()=> this.ticketService.loadAll()),
* map(ticket=> ticket.name)
* )
* .subscribe( tickets => this.tickets = tickets );
* }
*
* }
*
* Utility method to hide complexity of bridging a view component instance to a manual observable subs
*/
import { ElementRef } from '@angular/core';
import { Observable, ReplaySubject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
/**
* Wait until the DOM element has been removed (destroyed) and then
* use `takeUntil()` to complete the source subscription.
*
* If the `pipe(untilViewDestroyed(element.nativeEl))` is used in the constructor
* we must delay until the new view has been inserted into the DOM.
*/
export function untilViewDestroyed<T>(element: ElementRef): (source: Observable<T>) => Observable<T> {
const destroyed$ = (element && element.nativeElement) ? watchElementDestroyed(element.nativeElement) : null;
return (source$: Observable<T>) => destroyed$ ? source$.pipe(takeUntil(destroyed$)) : source$;
}
/**
* Auto-unsubscribe when the element is removed from the DOM
*/
export function autoUnsubscribe<T>(subscription: Subscription, element: ElementRef) {
if (typeof MutationObserver !== 'undefined') {
const stop$ = new ReplaySubject<boolean>();
const hasBeenRemoved = isElementRemoved(element.nativeElement);
setTimeout(() => {
const domObserver = new MutationObserver((records: MutationRecord[]) => {
if (records.some(hasBeenRemoved)) {
subscription.unsubscribe();
domObserver.disconnect();
}
});
domObserver.observe(element.nativeElement.parentNode as Node, { childList: true });
}, 20);
}
}
/**
* Unique hashkey
*/
const destroy$ = 'destroy$';
/**
* Use MutationObserver to watch for Element being removed from the DOM: destroyed
* When destroyed, stop subscriptions upstream.
*/
function watchElementDestroyed(nativeEl: Element, delay: number = 20): Observable<boolean> {
const parentNode = nativeEl.parentNode as Node;
if (!nativeEl[destroy$] && parentNode ) {
if (typeof MutationObserver !== 'undefined') {
const stop$ = new ReplaySubject<boolean>();
const hasBeenRemoved = isElementRemoved(nativeEl);
nativeEl[destroy$] = stop$.asObservable();
setTimeout(() => {
const domObserver = new MutationObserver((records: MutationRecord[]) => {
if (records.some(hasBeenRemoved)) {
stop$.next(true);
stop$.complete();
domObserver.disconnect();
nativeEl[destroy$] = null;
}
});
domObserver.observe(parentNode, { childList: true });
}, delay);
}
}
return nativeEl[destroy$];
}
function isElementRemoved(nativeEl) {
return (record: MutationRecord) => {
return Array.from(record.removedNodes).indexOf(nativeEl) > -1;
};
}
@Yonet

This comment has been minimized.

Copy link

commented Mar 6, 2018

For the comparison, this is the current implementation on MandaApp:

export class SomeSmartComponent implements OnDestroy, OnInit {

  public tableHasSelections = false;


  private _ngUnsubscribe$: Subject<void> = new Subject<void>();

  constructor(
    private _contentService: ContentService,
    private _cd: ChangeDetectorRef,
  ) {}

  ngOnDestroy(): void {
    this._ngUnsubscribe$.next();
    this._ngUnsubscribe$.complete();
  }

  ngOnInit(): void {
    this._contentService.contentDataTableSelectionAnnounced$
      .pipe(takeUntil(this._ngUnsubscribe$))
      .subscribe(selections => {
        this.tableHasSelections = selections.length > 0;
        this._cd.markForCheck();
      });
  }
...
}
@ThomasBurleson

This comment has been minimized.

Copy link
Owner Author

commented Mar 6, 2018

@asyegul - right, your app version requires four (4) things:

  • the import of takeUntil pipeable.
  • a private member _ngUnsubscribe$,
  • must remember to call next/complete in the ngOnDestroy()or manifest a memory leak.
  • Often developers must create a 'faux' ngOnDestroy(); simply to call announce component shutdown to trigger subscription cleanups/disconnects.

All of these issues are hidden with my version. Here is the revised version:

export class SomeSmartComponent implements OnDestroy, OnInit {
  public tableHasSelections = false;

  constructor( private srv: ContentService, private cd: ChangeDetectorRef ) {}

  ngOnInit(): void {
    const selectionChange$ = this.srv.contentDataTableSelectionAnnounced$;

    selectionChange$.pipe( untilViewDestroyed(this) ).subscribe( selections => {
       this.tableHasSelections = selections.length > 0;
       this.cd.markForCheck();
    });
}
@petebacondarwin

This comment has been minimized.

Copy link

commented Mar 19, 2018

I believe you could simply the untilViewDestroyed function:

/**
 *  Monkey-patch ngOnDestroy and then use `takeUntil()` to complete the source subscription.
 */
export function untilViewDestroyed<T>(component: OnDestroyLike): (source: Observable<T>) => Observable<T> {
   return takeUntil<T>(componentDestroyed(component));
}
@ThomasBurleson

This comment has been minimized.

Copy link
Owner Author

commented Mar 21, 2018

I do like your version better @petebacondarwin.

@AustinMatherne

This comment has been minimized.

Copy link

commented May 9, 2018

@ThomasBurleson I like this approach more than the current our pattern. It's unfortunate that life cycle hooks need to be implemented at compile time. In our app, we would end up with a bunch of empty ngOnDestory methods.

export class SomeSmartComponent implements OnInit {

  constructor(private srv: SomeService) {}

  ngOnInit(): void {
    // Argument of type 'this' is not assignable to parameter of type 'OnDestroyLike'.
    this.srv.someObservable.pipe(untilViewDestroyed(this)).subscribe(...);
  }
}
export class SomeSmartComponent implements OnInit, OnDestroy {

  constructor(private srv: SomeService) {}

  ngOnInit(): void {
    this.srv.someObservable.pipe(untilViewDestroyed(this)).subscribe(...);
  }

  ngOnDestroy(): void {
    //  empty method
  }
}

We could extend an abstract class with an empty ngOnDestroy to clean that up

export abstract class DestroyableComponent implements OnDestroy {
  ngOnDestroy(): void {}
}

but that got me thinking. An observable could be exposed by the base class which wraps ngOnDestroy, or taken even further to expose the entire lifecycle of the component as an observable. An observable of OnChanges that can be filtered, mapped, etc, would be handy.

@ThomasBurleson

This comment has been minimized.

Copy link
Owner Author

commented Aug 8, 2018

@AustinMatherne - The Angular team is considering exposing component lifecycle events via an observable stream. ;-)

@ThomasBurleson

This comment has been minimized.

Copy link
Owner Author

commented Oct 31, 2018

@AustinMatherne, @asyegul, @matsko,

Angular 7 has stricter enforcements and throws errors with monkey-patching of view component life-cycle methods.
This gist is an updated version that uses MutationObserver to accomplish the same goal.

Note: this version requires that the component inject the ElementRef into the constructor. Thanks to @matsko for he great tip to use MutationObserver.

@wilgert

This comment has been minimized.

Copy link

commented Feb 7, 2019

I think this is very useful an worthy of being a package published on npm! Is it possible to do that?

I've previously tried to use ngx-take-until-destroy but unfortunately ran into some issues that we've not been able to solve yet.

@un33k

This comment has been minimized.

Copy link

commented May 10, 2019

Nice take on auto unsubscribe. I still like a service that does that. The most deterministic it has been for me. Even though decorators are easier.

https://github.com/neekware/nwx-unsub

@aaronfrost

This comment has been minimized.

Copy link

commented May 21, 2019

I love this idea. How did I miss it?

@ThomasBurleson

This comment has been minimized.

Copy link
Owner Author

commented May 27, 2019

You have been super busy @aaronfrost!

npm version coming soon

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.