-
-
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. ;-)