Last active
February 9, 2023 17:45
-
-
Save th0r/11b4069230a475870ca64d309f0cf646 to your computer and use it in GitHub Desktop.
Angular `InView` directives
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
<!-- wInViewRoot directive is needed to specify the `root` for `IntersectionObserver` and some other it's options e.g. `margin` --> | |
<div class="container" wInViewRoot="viewport"> | |
Any content can be here | |
<w-in-view-item> | |
<!-- Content will be replaced by a placeholder <div> with the same height as original content. | |
Also `InViewItemComponent`s change detector will be detached when it become invisible which means | |
all the content's change detectors won't be reachable and will be inactive as well. --> | |
</w-in-view-item> | |
...or any other content can be here | |
<w-in-view-item> | |
<!-- ... --> | |
</w-in-view-item> | |
...or here | |
</div> |
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
<div> | |
<ng-content></ng-content> | |
</div> |
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
import { | |
AfterViewInit, | |
ChangeDetectorRef, | |
Component, | |
ElementRef, | |
NgZone, | |
OnDestroy, | |
OnInit, | |
Optional, | |
SkipSelf | |
} from '@angular/core'; | |
import {InViewRootDirective} from '../in-view-root/in-view-root.directive'; | |
@Component({ | |
selector: 'w-in-view-item', | |
templateUrl: './in-view-item.component.html' | |
}) | |
export class InViewItemComponent implements OnInit, AfterViewInit, OnDestroy { | |
visible = true; | |
elem: HTMLElement; | |
private contentElem: HTMLElement; | |
private placeholderElem: HTMLElement; | |
constructor( | |
private root: InViewRootDirective, | |
private elemRef: ElementRef<HTMLElement>, | |
private changeDetector: ChangeDetectorRef, | |
private ngZone: NgZone, | |
@Optional() @SkipSelf() private parentInViewItem: InViewItemComponent | |
) { | |
} | |
ngOnInit() { | |
this.elem = this.elemRef.nativeElement; | |
this.root.registerItem(this); | |
} | |
ngAfterViewInit() { | |
this.contentElem = this.elem.firstElementChild as HTMLElement; | |
} | |
ngOnDestroy() { | |
this.root.unregisterItem(this); | |
} | |
toggleVisibility(flag: boolean) { | |
if (this.visible === flag) { | |
return; | |
} | |
if (flag) { | |
if (this.placeholderElem && this.placeholderElem.parentNode === this.elem) { | |
this.elem.replaceChild(this.contentElem, this.placeholderElem); | |
} | |
this.changeDetector.reattach(); | |
this.changeDetector.detectChanges(); | |
} else { | |
this.changeDetector.detach(); | |
if (document.documentElement.contains(this.elem)) { | |
this.placeholderElem = this.placeholderElem || document.createElement('div'); | |
this.placeholderElem.style.height = `${this.contentElem.getBoundingClientRect().height}px`; | |
this.elem.replaceChild(this.placeholderElem, this.contentElem); | |
} | |
} | |
this.visible = flag; | |
} | |
forceShow() { | |
let inViewItem: InViewItemComponent = this; | |
do { | |
inViewItem.toggleVisibility(true); | |
inViewItem = inViewItem.parentInViewItem; | |
} while (inViewItem); | |
} | |
} |
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
import { | |
AfterViewInit, | |
Directive, | |
ElementRef, | |
Input, | |
NgZone, | |
OnDestroy | |
} from '@angular/core'; | |
import {InViewItemComponent} from '../in-view-item/in-view-item.component'; | |
export const isSupported = 'IntersectionObserver' in window; | |
@Directive({ | |
selector: '[wInViewRoot]' | |
}) | |
export class InViewRootDirective implements OnDestroy, AfterViewInit { | |
@Input('wInViewRoot') root?: 'viewport' | HTMLElement; | |
@Input('wInViewMargin') margin = '100%'; | |
private items = new Map<Element, InViewItemComponent>(); | |
private intersectionObserver: IntersectionObserver; | |
constructor( | |
private elemRef: ElementRef<HTMLElement>, | |
private ngZone: NgZone | |
) {} | |
ngAfterViewInit() { | |
if (!isSupported) { | |
return; | |
} | |
let root: HTMLElement; | |
if (this.root === 'viewport') { | |
root = null; | |
} else { | |
root = this.root || this.elemRef.nativeElement; | |
} | |
this.ngZone.runOutsideAngular(() => { | |
this.intersectionObserver = new IntersectionObserver(this.handleIntersectionChange, { | |
root, | |
rootMargin: this.margin, | |
threshold: 0 | |
}); | |
this.items.forEach(item => | |
this.intersectionObserver.observe(item.elem) | |
); | |
}); | |
} | |
ngOnDestroy() { | |
if (this.intersectionObserver) { | |
this.intersectionObserver.disconnect(); | |
} | |
} | |
registerItem(item: InViewItemComponent) { | |
if (isSupported) { | |
this.items.set(item.elem, item); | |
if (this.intersectionObserver) { | |
this.ngZone.runOutsideAngular(() => { | |
this.intersectionObserver.observe(item.elem); | |
}); | |
} | |
} | |
} | |
unregisterItem(item: InViewItemComponent) { | |
if (isSupported) { | |
this.items.delete(item.elem); | |
this.intersectionObserver.unobserve(item.elem); | |
} | |
} | |
private handleIntersectionChange = (entries: IntersectionObserverEntry[]) => { | |
entries.forEach(entry => { | |
this.items.get(entry.target).toggleVisibility(entry.isIntersecting); | |
}); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment