-
-
Save ThomasBurleson/37653f1f273149e6f6bc07e5b29ea313 to your computer and use it in GitHub Desktop.
/** | |
* 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; | |
}; | |
} |
@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.
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.
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.
I love this idea. How did I miss it?
You have been super busy @aaronfrost!
npm version coming soon
Hello Thomas, thanks for your contribution!
Wondering when autoUnsubscribe is called?
Cheers!
@AustinMatherne - The Angular team is considering exposing component lifecycle events via an observable stream. ;-)