Ajouter dynamiquement un écouteur d'événements

143

Je commence tout juste à m'amuser avec Angular 2 et je me demande si quelqu'un peut me dire la meilleure façon d'ajouter et de supprimer dynamiquement des écouteurs d'événements des éléments.

J'ai un composant mis en place. Lorsqu'un certain élément du modèle est cliqué, je souhaite ajouter un écouteur mousemoveà un autre élément du même modèle. Je souhaite ensuite supprimer cet auditeur lorsqu'un troisième élément est cliqué.

J'ai en quelque sorte fait fonctionner cela en utilisant simplement du Javascript pour saisir les éléments, puis en appelant le standard, addEventListener()mais je me suis demandé s'il y avait une manière plus " Angular2.0 " de faire cela que je devrais examiner.

popClingwrap
la source

Réponses:

262

Renderer est obsolète dans Angular 4.0.0-rc.1, lisez la mise à jour ci-dessous

La méthode angular2 consiste à utiliser listenou à listenGlobalpartir de Renderer

Par exemple, si vous souhaitez ajouter un événement de clic à un composant, vous devez utiliser Renderer et ElementRef (cela vous donne également la possibilité d'utiliser ViewChild, ou tout ce qui récupère le nativeElement)

constructor(elementRef: ElementRef, renderer: Renderer) {

    // Listen to click events in the component
    renderer.listen(elementRef.nativeElement, 'click', (event) => {
      // Do something with 'event'
    })
);

Vous pouvez utiliser listenGlobalqui vous donnera accès à document, bodyetc.

renderer.listenGlobal('document', 'click', (event) => {
  // Do something with 'event'
});

Notez que depuis la version bêta.2 les deux listenet listenGlobalretournent une fonction pour supprimer l'écouteur (voir la section des changements de rupture du journal des modifications pour la version bêta.2). C'est pour éviter les fuites de mémoire dans les grosses applications (voir # 6686 ).

Donc, pour supprimer l'écouteur que nous avons ajouté dynamiquement, nous devons affecter listenou listenGlobalà une variable qui contiendra la fonction retournée, puis nous l'exécutons.

// listenFunc will hold the function returned by "renderer.listen"
listenFunc: Function;

// globalListenFunc will hold the function returned by "renderer.listenGlobal"
globalListenFunc: Function;

constructor(elementRef: ElementRef, renderer: Renderer) {
    
    // We cache the function "listen" returns
    this.listenFunc = renderer.listen(elementRef.nativeElement, 'click', (event) => {
        // Do something with 'event'
    });

    // We cache the function "listenGlobal" returns
    this.globalListenFunc = renderer.listenGlobal('document', 'click', (event) => {
        // Do something with 'event'
    });
}

ngOnDestroy() {
    // We execute both functions to remove the respectives listeners

    // Removes "listen" listener
    this.listenFunc();
    
    // Removs "listenGlobal" listener
    this.globalListenFunc();
}

Voici un plnkr avec un exemple de travail. L'exemple contient l'utilisation de listenet listenGlobal.

Utilisation de RendererV2 avec Angular 4.0.0-rc.1 + (Renderer2 depuis 4.0.0-rc.3)

  • 25/02/2017 : Rendererest obsolète, nous devrions maintenant l'utiliser RendererV2(voir ligne ci-dessous). Voir le commit .

  • 10/03/2017 : a RendererV2été renommé en Renderer2. Voir les changements de rupture .

RendererV2n'a plus de listenGlobalfonction pour les événements globaux (document, corps, fenêtre). Il n'a qu'une listenfonction qui réalise les deux fonctionnalités.

Pour référence, je copie et colle le code source de l'implémentation du DOM Renderer car il peut changer (oui, c'est angulaire!).

listen(target: 'window'|'document'|'body'|any, event: string, callback: (event: any) => boolean):
      () => void {
    if (typeof target === 'string') {
      return <() => void>this.eventManager.addGlobalEventListener(
          target, event, decoratePreventDefault(callback));
    }
    return <() => void>this.eventManager.addEventListener(
               target, event, decoratePreventDefault(callback)) as() => void;
  }

Comme vous pouvez le voir, il vérifie maintenant si nous passons une chaîne (document, corps ou fenêtre), auquel cas il utilisera une addGlobalEventListenerfonction interne . Dans tous les autres cas, lorsque nous passons un élément (nativeElement), il utilisera un simpleaddEventListener

Pour supprimer l'auditeur, c'est la même chose qu'avec Rendererangular 2.x. listenrenvoie une fonction, puis appelez cette fonction.

Exemple

// Add listeners
let global = this.renderer.listen('document', 'click', (evt) => {
  console.log('Clicking the document', evt);
})

let simple = this.renderer.listen(this.myButton.nativeElement, 'click', (evt) => {
  console.log('Clicking the button', evt);
});

// Remove listeners
global();
simple();

plnkr avec Angular 4.0.0-rc.1 en utilisant RendererV2

plnkr avec Angular 4.0.0-rc.3 en utilisant Renderer2

Eric Martinez
la source
Ce n'est que mon deuxième jour avec Angular2 et j'avais à peine commencé à me familiariser avec la v1, donc beaucoup de choses sont plutôt nouvelles. Vous m'avez donné une bonne quantité de choses à lire, alors je ferme celui-ci et je reviendrai sans aucun doute bientôt avec BEAUCOUP de questions plus liées. Vive la réponse détaillée :)
popClingwrap
3
@popClingwrap, vous pouvez également vérifier HostListener . Dans la documentation, vérifiez les directives d'attribut sous Répondre à l'action de l'utilisateur pour voir comment hostest également utilisé.
Eric Martinez
@EricMartinez existe-t-il un moyen d'arrêter d'écouter pour écouter ou écouterGlobal? (idem removeEventListener)
Nik
3
@ user1394625 oui, comme vous pouvez le voir dans la réponse le ngOnDestroycode, les deux listenet listenGlobalretournent une fonction qui, lorsqu'elle est appelée / exécutée, l'écouteur est supprimée. Donc, comme vous le voyez, il this.funccontient la fonction retournée par renderer.listenet quand je le fais, this.func()je supprime l'écouteur. Il en va de même listenGlobal.
Eric Martinez
@EricMartinez a encore une question pour vous ... comment puis-je accéder à `` l'événement '' dans la fonction pour preventDefault () ou stopPropagation ()
Nik
5

Je trouve également cela extrêmement déroutant. comme @EricMartinez le souligne Renderer2 listen () renvoie la fonction pour supprimer l'écouteur:

ƒ () { return element.removeEventListener(eventName, /** @type {?} */ (handler), false); }

Si j'ajoute un auditeur

this.listenToClick = this.renderer.listen('document', 'click', (evt) => {
    alert('Clicking the document');
})

Je m'attendrais à ce que ma fonction exécute ce que je voulais, pas le contraire total qui est de supprimer l'auditeur.

// I´d expect an alert('Clicking the document'); 
this.listenToClick();
// what you actually get is removing the listener, so nothing...

Dans le scénario donné, il serait en fait plus logique de le nommer comme suit:

// Add listeners
let unlistenGlobal = this.renderer.listen('document', 'click', (evt) => {
    console.log('Clicking the document', evt);
})

let removeSimple = this.renderer.listen(this.myButton.nativeElement, 'click', (evt) => {
    console.log('Clicking the button', evt);
});

Il doit y avoir une bonne raison à cela mais à mon avis c'est très trompeur et pas intuitif.

Tahiche
la source
3
Si vous ajoutiez un écouteur, pourquoi vous attendriez-vous à ce que la fonction retournée en ajoutant cet écouteur appelle cet écouteur? Cela n'a pas beaucoup de sens pour moi. L'intérêt d'ajouter un écouteur est de répondre à des événements que vous ne pouvez pas nécessairement déclencher par programme. Je pense que si vous vous attendiez à ce que cette fonction appelle votre auditeur, vous ne comprenez peut-être pas complètement les auditeurs.
Willwsharp
@tahiche mate c'est vraiment déroutant, merci de l'avoir signalé!
godblessstrawberry
Il renvoie ceci afin que vous puissiez également supprimer à nouveau l'écouteur lorsque vous détruirez votre composant ultérieurement. Lors de l'ajout d'auditeurs, il est recommandé de les supprimer plus tard lorsque vous n'en avez plus besoin. Stockez donc cette valeur de retour et appelez-la dans votre ngOnDestroyméthode. J'avoue que cela peut sembler déroutant au début, mais c'est en fait une fonctionnalité très utile. Sinon, comment nettoyer après vous-même?
Wilt
1

J'ajouterai un exemple StackBlitz et un commentaire à la réponse de @tahiche.

La valeur de retour est une fonction pour supprimer l'écouteur d'événements après l'avoir ajouté. Il est recommandé de supprimer les écouteurs d'événements lorsque vous n'en avez plus besoin. Vous pouvez donc stocker cette valeur de retour et l'appeler dans votre ngOnDestroyméthode.

J'avoue que cela peut sembler déroutant au début, mais c'est en fait une fonctionnalité très utile. Sinon, comment pouvez-vous nettoyer après vous-même?

export class MyComponent implements OnInit, OnDestroy {

  public removeEventListener: () => void;

  constructor(
    private renderer: Renderer2, 
    private elementRef: ElementRef
  ) {
  }

  public ngOnInit() {
    this.removeEventListener = this.renderer.listen(this.elementRef.nativeElement, 'click', (event) => {
      if (event.target instanceof HTMLAnchorElement) {
        // Prevent opening anchors the default way
        event.preventDefault();
        // Your custom anchor click event handler
        this.handleAnchorClick(event);
      }
    });
  }

  public ngOnDestroy() {
    this.removeEventListener();
  }
}

Vous pouvez trouver un StackBlitz ici pour montrer comment cela pourrait fonctionner pour capturer les clics sur les éléments d'ancrage.

J'ai ajouté un corps avec une image comme suit:
<img src="x" onerror="alert(1)"></div>
pour montrer que le désinfectant fait son travail.

Ici, dans ce violon, vous trouvez le même corps attaché à un innerHTMLsans le désinfecter et cela démontrera le problème.

Se flétrir
la source
0

Voici ma solution de contournement:

J'ai créé une bibliothèque avec Angular 6. J'ai ajouté un composant commun commonlib-headerqui est utilisé comme ça dans une application externe.

Notez serviceReferencequelle est la classe (injectée dans le composant constructor(public serviceReference: MyService)qui utilise le commonlib-header) qui contient la stringFunctionNameméthode:

<commonlib-header
    [logo]="{ src: 'assets/img/logo.svg', alt: 'Logo', href: '#' }"
    [buttons]="[{ index: 0, innerHtml: 'Button', class: 'btn btn-primary', onClick: [serviceReference, 'stringFunctionName', ['arg1','arg2','arg3']] }]">
    </common-header>

Le composant de bibliothèque est programmé comme ceci. L'événement dynamique est ajouté dans la onClick(fn: any)méthode:

export class HeaderComponent implements OnInit {

 _buttons: Array<NavItem> = []

 @Input()
  set buttons(buttons: Array<any>) {
    buttons.forEach(navItem => {
      let _navItem = new NavItem(navItem.href, navItem.innerHtml)

      _navItem.class = navItem.class

      _navItem.onClick = navItem.onClick // this is the array from the component @Input properties above

      this._buttons[navItem.index] = _navItem
    })
  }

  constructor() {}

  ngOnInit() {}

  onClick(fn: any){
    let ref = fn[0]
    let fnName = fn[1]
    let args = fn[2]

    ref[fnName].apply(ref, args)
  }

Le réutilisable header.component.html :

<div class="topbar-right">
  <button *ngFor="let btn of _buttons"
    class="{{ btn.class }}"
    (click)="onClick(btn.onClick)"
    [innerHTML]="btn.innerHtml | keepHtml"></button>
</div>
Gus
la source