Skip to content

Instantly share code, notes, and snippets.

@th0r
Last active February 9, 2023 17:45
Show Gist options
  • Save th0r/11b4069230a475870ca64d309f0cf646 to your computer and use it in GitHub Desktop.
Save th0r/11b4069230a475870ca64d309f0cf646 to your computer and use it in GitHub Desktop.
Angular `InView` directives
<!-- 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>
<div>
<ng-content></ng-content>
</div>
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);
}
}
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