Angular директива с подпиской на положение DOM элемента относительно view port (попадание\выход)
В данной статье разберу работу директивы которая отслеживает DOM элемент относительно вью порта пользователя.
Код директивы:
import {Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output} from '@angular/core';
/**
* Удалять подписку когда
* VIEW элемент попал первый раз во вью порт
* VIEW элемент вышел перыцй раз из вью порта
* ALL в любом из случав при первом тригере
* DEFAULT только при уничтожении родителя
* */
export enum ViewObserverMode {
VIEW = 'view',
HIDE = 'hide',
ALL = 'all',
DEFAULT = 'default'
}
/**
* Директива которая трегерит евент при попадании или выходе элемента во вью порт
* */
@Directive({
selector: '[prepareViewObserver]'
})
export class PrepareViewObserverDirective implements OnInit, OnDestroy {
private readonly options: IntersectionObserverInit = {
root: null,
rootMargin: '0px',
threshold: 0.1
}
private intersectionObserver: IntersectionObserver | undefined;
@Input()
item: unknown = {};
@Input()
mode: ViewObserverMode = ViewObserverMode.DEFAULT;
@Output()
prepareViewObserver = new EventEmitter();
constructor(private elementRef: ElementRef) {}
ngOnInit(): void {
this.prepareViewObserverIntercept();
}
prepareViewObserverIntercept(): void {
this.intersectionObserver = new IntersectionObserver(this.viewCallback.bind(this), this.options);
this.intersectionObserver.observe(this.elementRef.nativeElement);
}
/**
* при появлении элемента в зоне видимости срабатывает колбек prepareViewObserver
* */
viewCallback(entries: IntersectionObserverEntry[]): void {
entries.forEach(entry => {
this.prepareViewObserverEmitter(entry.isIntersecting);
this.viewObserverUnobserve(entry.isIntersecting, this.mode);
});
}
prepareViewObserverEmitter(isIntersecting: boolean): void {
this.prepareViewObserver.emit({visible: isIntersecting, item: this.item});
}
viewObserverUnobserve(isIntersecting: boolean, mode: ViewObserverMode): void {
if (mode === ViewObserverMode.DEFAULT) return;
if (mode === ViewObserverMode.ALL) return this.observerUnobserveAction();
if (isIntersecting && mode === ViewObserverMode.VIEW) return this.observerUnobserveAction();
if (!isIntersecting && mode === ViewObserverMode.HIDE) return this.observerUnobserveAction();
}
observerUnobserveAction(): void {
this.intersectionObserver?.unobserve(this.elementRef.nativeElement);
}
ngOnDestroy(): void {
this.observerUnobserveAction();
}
}
В item можно передать объект что бы вернуть его и использовать в функции родителя при тригере эмита с дерективы.
Шаблон:
<div class="examples__cards">
<div class="examples__card"
[item]="card"
(prepareViewObserver) = changeDiscoveryElement($event);
*ngFor="let card of searchCards">
<example-card *ngIf="!card.hide"
[title]="card.title"
[subTitle]="card.subTitle">
<pre>
{{ card.script }}
</pre>
</example-card>
</div>
</div>
Тут пример применения в качестве бесконечного скрола где changeDiscoveryElement($event)
управляет hide ключом итема card и убирает (сверху скролла) или отрисовывает (снизу) в зависимости от положения DOM элемента относительно view port.
компонент:
changeDiscoveryElement(event: {visible: boolean, item: Card}): void {
event.item.hide = !event.visible;
}
Эта функция просто меняет ключ hide на булев тип visible который отдает директива и указывает зашел ли элемент во вью (visible = true) порт или нет (visible = false)
так же прокидывается текущий item в качестве ссылки который можно мутировать что бы отрисовать или нет компонент через ngIf.
Проблемы данного примера:
Надо иметь оберточный блок что бы высота блока была не 0 и тригер не отработал на всех элементах до отрисовки
Надо прокидывать item
Подписка на каждый элемент если mode = 'default' до полного уничтожения элемента
Итог: адаптивная директива позволяющая манипулировать и вставлять скрипты в момент выхода\входа отслеживаемого DOM элемента относительно экрана пользователя (view port).
Открыт к критике. Спасибо за внимание!