Skip to content

Instantly share code, notes, and snippets.

@vijayksingh
Forked from ThomasBurleson/untilDestroyed.ts
Created October 14, 2020 06:06
Show Gist options
  • Save vijayksingh/688947131100871d1a1466a4b6bc447c to your computer and use it in GitHub Desktop.
Save vijayksingh/688947131100871d1a1466a4b6bc447c to your computer and use it in GitHub Desktop.
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;
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment