Как стать автором
Обновить

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.

Проблемы данного примера:

  1. Надо иметь оберточный блок что бы высота блока была не 0 и тригер не отработал на всех элементах до отрисовки

  2. Надо прокидывать item

  3. Подписка на каждый элемент если mode = 'default' до полного уничтожения элемента

Итог: адаптивная директива позволяющая манипулировать и вставлять скрипты в момент выхода\входа отслеживаемого DOM элемента относительно экрана пользователя (view port).

Открыт к критике. Спасибо за внимание!

Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.