"Si un ouvrier veut bien faire son travail, il doit d'abord affûter ses outils." - Confucius, "Les Entretiens de Confucius. Lu Linggong"
Page de garde > La programmation > Angular LAB : créons une directive de visibilité

Angular LAB : créons une directive de visibilité

Publié le 2024-11-03
Parcourir:866

Angular LAB: let

Dans cet article, je vais illustrer comment créer une directive angulaire très simple qui garde une trace de l'état de visibilité d'un élément, ou en d'autres termes, quand il entre et sort de la fenêtre. J'espère que ce sera un exercice intéressant et peut-être utile !

Pour ce faire, nous allons utiliser l'API JavaScript IntersectionObserver qui est disponible dans les navigateurs modernes.

Ce que nous voulons réaliser

Nous souhaitons utiliser la directive comme ceci :

I'm being observed! Can you see me yet?

  • la visibilité est le sélecteur de notre directive personnalisée
  • visibilitéMonitor est une entrée facultative qui spécifie s'il faut continuer ou non à observer l'élément (si faux, arrêtez la surveillance lorsqu'il entre dans la fenêtre)
  • visibilitéChange nous en informera

La sortie aura cette forme :

type VisibilityChange =
  | {
      isVisible: true;
      target: HTMLElement;
    }
  | {
      isVisible: false;
      target: HTMLElement | undefined;
    };

Avoir une cible non définie signifiera que l'élément a été supprimé du DOM (par exemple, par un @if).

Création de la directive

Notre directive surveillera simplement un élément, elle ne changera pas la structure du DOM : ce sera une Directive d'Attribut.

@Directive({
  selector: "[visibility]",
  standalone: true
})
export class VisibilityDirective implements OnInit, OnChanges, AfterViewInit, OnDestroy {
  private element = inject(ElementRef);

  /**
   * Emits after the view is initialized.
   */
  private afterViewInit$ = new Subject();

  /**
   * The IntersectionObserver for this element.
   */
  private observer: IntersectionObserver | undefined;

  /**
   * Last known visibility for this element.
   * Initially, we don't know.
   */
  private isVisible: boolean = undefined;

  /**
   * If false, once the element becomes visible there will be one emission and then nothing.
   * If true, the directive continuously listens to the element and emits whenever it becomes visible or not visible.
   */
  visibilityMonitor = input(false);

  /**
   * Notifies the listener when the element has become visible.
   * If "visibilityMonitor" is true, it continuously notifies the listener when the element goes in/out of view.
   */
  visibilityChange = output();
}

Dans le code ci-dessus, vous voyez :

  • l'entrée et la sortie dont nous avons parlé plus tôt
  • une propriété appelée afterViewInit$ (un observable) qui agira comme une contrepartie réactive au hook de cycle de vie ngAfterViewInit
  • une propriété appelée observer qui stockera l'IntersectionObserver en charge de surveiller notre élément
  • une propriété appelée isVisibile qui stockera le dernier état de visibilité, afin d'éviter de réémettre le même état deux fois de suite

Et naturellement, on injecte le ElementRef afin de récupérer l'élément DOM sur lequel on applique notre directive.

Avant d'écrire la méthode main, occupons-nous du cycle de vie de la directive.

ngOnInit(): void {
  this.reconnectObserver();
}

ngOnChanges(): void {
  this.reconnectObserver();
}

ngAfterViewInit(): void {
  this.afterViewInit$.next();
}

ngOnDestroy(): void {
  // Disconnect and if visibilityMonitor is true, notify the listener
  this.disconnectObserver();
  if (this.visibilityMonitor) {
    this.visibilityChange.emit({
      isVisible: false,
      target: undefined
    });
  }
}

private reconnectObserver(): void {}
private disconnectObserver(): void {}

Voici maintenant ce qui se passe :

  • Dans ngOnInit et ngOnChanges, nous redémarrons l'observateur. Ceci afin de rendre la directive réactive : si l'entrée change, la directive commencera à se comporter différemment. Notez que, même si ngOnChanges s'exécute également avant ngOnInit, nous avons toujours besoin de ngOnInit car ngOnChanges ne s'exécute pas s'il n'y a aucune entrée dans le modèle !
  • Lorsque la vue est initialisée, nous déclenchons le sujet, nous y reviendrons dans quelques secondes
  • Nous déconnectons notre observateur lorsque la directive est détruite afin d'éviter les fuites mémoire. Enfin, si le développeur le demande, nous notifions que l'élément a été supprimé du DOM en émettant un élément non défini.

IntersectionObservateur

C'est le cœur de notre directive. Notre méthode reconnectObserver sera celle qui permettra de commencer à observer ! Ce sera quelque chose comme ceci :

private reconnectObserver(): void {
    // Disconnect an existing observer
    this.disconnectObserver();
    // Sets up a new observer
    this.observer = new IntersectionObserver((entries, observer) => {
      entries.forEach(entry => {
        const { isIntersecting: isVisible, target } = entry;
        const hasChangedVisibility = isVisible !== this.isVisible;
        const shouldEmit = isVisible || (!isVisible && this.visibilityMonitor);
        if (hasChangedVisibility && shouldEmit) {
          this.visibilityChange.emit({
            isVisible,
            target: target as HTMLElement
          });
          this.isVisible = isVisible;
        }
        // If visilibilyMonitor is false, once the element is visible we stop.
        if (isVisible && !this.visibilityMonitor) {
          observer.disconnect();
        }
      });
    });
    // Start observing once the view is initialized
    this.afterViewInit$.subscribe(() => {
        this.observer?.observe(this.element.nativeElement);
    });
  }

Faites-moi confiance, ce n'est pas aussi compliqué qu'il y paraît ! Voici le mécanisme :

  • Nous déconnectons d'abord l'observateur s'il était déjà en cours d'exécution
  • Nous créons un IntersectionObserver et définissons son comportement. Les entrées contiendront les éléments surveillés, elles contiendront donc notre élément. La propriété isIntersecting indiquera si la visibilité de l'élément a changé : on le compare à l'état précédent (notre propriété) et si c'est dû, on émet. Ensuite, nous stockons le nouvel état dans notre propriété pour plus tard.
  • Si visibilitéMonitor est faux, dès que l'élément devient visible on déconnecte l'observateur : son travail est fait !
  • Ensuite, nous devons démarrer l'observateur en passant notre élément, nous attendons donc que notre vue soit initialisée pour ce faire.

Enfin, implémentons la méthode qui déconnecte l'observateur, c'est simple :

 private disconnectObserver(): void {
    if (this.observer) {
      this.observer.disconnect();
      this.observer = undefined;
    }
  }

Code final

Voici la directive complète. Ce n'était qu'un exercice, alors soyez libre de le modifier comme bon vous semble !

type VisibilityChange =
  | {
      isVisible: true;
      target: HTMLElement;
    }
  | {
      isVisible: false;
      target: HTMLElement | undefined;
    };

@Directive({
  selector: "[visibility]",
  standalone: true
})
export class VisibilityDirective
  implements OnChanges, OnInit, AfterViewInit, OnDestroy {
  private element = inject(ElementRef);

  /**
   * Emits after the view is initialized.
   */
  private afterViewInit$ = new Subject();

  /**
   * The IntersectionObserver for this element.
   */
  private observer: IntersectionObserver | undefined;

  /**
   * Last known visibility for this element.
   * Initially, we don't know.
   */
  private isVisible: boolean = undefined;

  /**
   * If false, once the element becomes visible there will be one emission and then nothing.
   * If true, the directive continuously listens to the element and emits whenever it becomes visible or not visible.
   */
  visibilityMonitor = input(false);

  /**
   * Notifies the listener when the element has become visible.
   * If "visibilityMonitor" is true, it continuously notifies the listener when the element goes in/out of view.
   */
  visibilityChange = output();

  ngOnInit(): void {
    this.reconnectObserver();
  }

  ngOnChanges(): void {
    this.reconnectObserver();
  }

  ngAfterViewInit(): void {
    this.afterViewInit$.next(true);
  }

  ngOnDestroy(): void {
    // Disconnect and if visibilityMonitor is true, notify the listener
    this.disconnectObserver();
    if (this.visibilityMonitor) {
      this.visibilityChange.emit({
        isVisible: false,
        target: undefined
      });
    }
  }

  private reconnectObserver(): void {
    // Disconnect an existing observer
    this.disconnectObserver();
    // Sets up a new observer
    this.observer = new IntersectionObserver((entries, observer) => {
      entries.forEach(entry => {
        const { isIntersecting: isVisible, target } = entry;
        const hasChangedVisibility = isVisible !== this.isVisible;
        const shouldEmit = isVisible || (!isVisible && this.visibilityMonitor);
        if (hasChangedVisibility && shouldEmit) {
          this.visibilityChange.emit({
            isVisible,
            target: target as HTMLElement
          });
          this.isVisible = isVisible;
        }
        // If visilibilyMonitor is false, once the element is visible we stop.
        if (isVisible && !this.visibilityMonitor) {
          observer.disconnect();
        }
      });
    });
    // Start observing once the view is initialized
    this.afterViewInit$.subscribe(() => {
        this.observer?.observe(this.element.nativeElement);
    });
  }

  private disconnectObserver(): void {
    if (this.observer) {
      this.observer.disconnect();
      this.observer = undefined;
    }
  }
}
Déclaration de sortie Cet article est reproduit sur : https://dev.to/this-is-angular/angular-lab-lets-create-a-visibility-directive-5dpp?1 En cas de violation, veuillez contacter [email protected] pour le supprimer
Dernier tutoriel Plus>

Clause de non-responsabilité: Toutes les ressources fournies proviennent en partie d'Internet. En cas de violation de vos droits d'auteur ou d'autres droits et intérêts, veuillez expliquer les raisons détaillées et fournir une preuve du droit d'auteur ou des droits et intérêts, puis l'envoyer à l'adresse e-mail : [email protected]. Nous nous en occuperons pour vous dans les plus brefs délais.

Copyright© 2022 湘ICP备2022001581号-3