Agregue dinámicamente oyente de eventos

143

Estoy empezando a perder el tiempo con Angular 2 y me pregunto si alguien puede decirme la mejor manera de agregar y eliminar dinámicamente los oyentes de eventos de los elementos.

Tengo un componente configurado. Cuando se hace clic en cierto elemento de la plantilla, quiero agregar un oyente mousemovea otro elemento de la misma plantilla. Luego quiero eliminar este oyente cuando se hace clic en un tercer elemento.

De alguna manera conseguí que esto funcionara simplemente usando Javascript simple para tomar los elementos y luego llamar al estándar, addEventListener()pero me preguntaba si debería estar buscando una forma más " Angular2.0 " de hacer esto.

popClingwrap
fuente

Respuestas:

262

El procesador ha quedado en desuso en Angular 4.0.0-rc.1, lea la actualización a continuación

La forma angular2 es usar listeno listenGlobaldesde Renderer

Por ejemplo, si desea agregar un evento de clic a un Componente, debe usar Renderer y ElementRef (esto también le da la opción de usar ViewChild o cualquier cosa que recupere el nativeElement)

constructor(elementRef: ElementRef, renderer: Renderer) {

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

Se puede utilizar listenGlobalque le dará acceso a document, body, etc.

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

Tenga en cuenta que dado que tanto beta.2 listeny listenGlobaldevuelve una función para eliminar el oyente (ver cambios rompiendo la sección de cambios para beta.2). Esto es para evitar pérdidas de memoria en grandes aplicaciones (ver # 6686 ).

Entonces, para eliminar el oyente que agregamos dinámicamente, debemos asignar listeno listenGlobala una variable que contendrá la función devuelta, y luego la ejecutamos.

// 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();
}

Aquí hay un plnkr con un ejemplo funcionando. El ejemplo contiene el uso de listeny listenGlobal.

Uso de RendererV2 con Angular 4.0.0-rc.1 + (Renderer2 desde 4.0.0-rc.3)

  • 25/02/2017 : Rendererha quedado en desuso, ahora deberíamos usarlo RendererV2(ver línea a continuación). Ver el compromiso .

  • 10/03/2017 : RendererV2fue renombrado a Renderer2. Ver los cambios de última hora .

RendererV2no tiene más listenGlobalfunciones para eventos globales (documento, cuerpo, ventana). Solo tiene una listenfunción que logra ambas funcionalidades.

Como referencia, copio y pego el código fuente de la implementación del DOM Renderer ya que puede cambiar (¡sí, es angular!).

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;
  }

Como puede ver, ahora verifica si estamos pasando una cadena (documento, cuerpo o ventana), en cuyo caso utilizará una addGlobalEventListenerfunción interna . En cualquier otro caso, cuando pasamos un elemento (nativeElement) usará un simpleaddEventListener

Para eliminar el oyente es lo mismo que con Rendererangular 2.x. listendevuelve una función, luego llama a esa función.

Ejemplo

// 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 con Angular 4.0.0-rc.1 usando RendererV2

plnkr con Angular 4.0.0-rc.3 usando Renderer2

Eric Martinez
fuente
Este es solo mi segundo día con Angular2 y apenas había comenzado a entender v1, por lo que mucho de esto es bastante nuevo y confuso. Sin embargo, me ha dado una buena cantidad de cosas para leer, así que estoy cerrando esto y, sin duda, volveré pronto con MUCHAS preguntas más relacionadas. Saludos por la respuesta detallada :)
popClingwrap
3
@popClingwrap también puedes consultar HostListener . En los documentos, verifique las directivas de Atributo en Responder a la acción del usuario para ver cómo hostse usa también.
Eric Martinez
@EricMartinez, ¿hay alguna manera de dejar de escuchar ya sea escuchar o escuchar Global? (igual que removeEventListener)
Nik
3
@ user1394625 sí, como se puede ver en la respuesta del ngOnDestroycódigo, tanto listeny listenGlobaldevolver una función que cuando se le llama / ejecuta el oyente se retira. Entonces, como ve, this.funcmantiene la función devuelta por renderer.listeny cuando lo hago this.func(), estoy eliminando el oyente. Lo mismo vale para listenGlobal.
Eric Martinez
@EricMartinez tiene una pregunta más para usted ... ¿cómo puedo acceder al 'evento' dentro de la función para prevenirDefault () o stopPropagation ()
Nik
5

A mí esto me parece extremadamente confuso. como @EricMartinez señala Renderer2 listen () devuelve la función para eliminar el oyente:

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

Si estoy agregando un oyente

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

Esperaría que mi función ejecute lo que pretendía, no el opuesto total que es eliminar el oyente.

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

En el escenario dado, en realidad tendría más sentido nombrarlo así:

// 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);
});

Debe haber una buena razón para esto, pero en mi opinión es muy engañoso y no intuitivo.

tahiche
fuente
3
Si estuviera agregando un oyente, ¿por qué esperaría que la función devuelta al agregar ese oyente invocaría a ese oyente? Eso no tiene mucho sentido para mí. El objetivo de agregar un oyente es responder a eventos que no necesariamente se pueden activar programáticamente. Creo que si espera que esa función invoque a su oyente, es posible que no esté entendiendo a los oyentes por completo.
Willwsharp
@tahiche compañero esto es realmente confuso, ¡gracias por señalar esto!
godblessstrawberry
Devuelve esto para que también pueda eliminar el oyente nuevamente cuando destruya su componente más adelante. Al agregar oyentes, se considera una buena práctica eliminarlos más tarde cuando ya no los necesite. Así que almacene este valor de retorno y llámelo dentro de su ngOnDestroymétodo. Admito que puede parecer confuso al principio, pero en realidad es una característica muy útil. ¿De qué otra manera limpiar después de ti?
marchita el
1

Agregaré un ejemplo de StackBlitz y un comentario a la respuesta de @tahiche.

El valor de retorno es una función para eliminar el detector de eventos después de haberlo agregado. Se considera una buena práctica eliminar oyentes de eventos cuando ya no los necesita. Entonces puede almacenar este valor de retorno y llamarlo dentro de su ngOnDestroymétodo.

Admito que puede parecer confuso al principio, pero en realidad es una característica muy útil. ¿De qué otra manera puedes limpiar después de ti?

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();
  }
}

Puede encontrar un StackBlitz aquí para mostrar cómo podría funcionar esto para atrapar al hacer clic en los elementos de anclaje.

Agregué un cuerpo con una imagen de la siguiente manera:
<img src="x" onerror="alert(1)"></div>
para mostrar que el desinfectante está haciendo su trabajo.

Aquí en este violín encontrarás el mismo cuerpo unido a un innerHTMLsin desinfectarlo y demostrará el problema.

Marchitar
fuente
0

Aquí está mi solución:

Creé una biblioteca con Angular 6. Agregué un componente común commonlib-header que se usa así en una aplicación externa.

Tenga en cuenta serviceReferencecuál es la clase (inyectada en el componente constructor(public serviceReference: MyService)que usa commonlib-header) que contiene el stringFunctionNamemétodo:

<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>

El componente de la biblioteca se programa así. El evento dinámico se agrega en el onClick(fn: any)método:

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)
  }

Lo reutilizable 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
fuente