Last active
April 18, 2023 07:47
-
-
Save ThomasBurleson/37653f1f273149e6f6bc07e5b29ea313 to your computer and use it in GitHub Desktop.
Using untilViewDestroyed to link component ngOnDestroy to observable unsubscribe.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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; | |
}; | |
} |
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!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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