Adicionar dinamicamente ouvinte de eventos

143

Estou apenas começando a mexer no Angular 2 e me pergunto se alguém pode me dizer a melhor maneira de adicionar e remover dinamicamente ouvintes de eventos dos elementos.

Eu tenho um componente configurado. Quando um determinado elemento do modelo é clicado, desejo adicionar um ouvinte para mousemoveoutro elemento do mesmo modelo. Desejo remover esse ouvinte quando um terceiro elemento for clicado.

Eu meio que consegui isso trabalhando apenas usando Javascript simples para pegar os elementos e depois chamando o padrão, addEventListener()mas me perguntei se havia uma maneira mais " Angular2.0 " de fazer isso que eu deveria examinar.

popClingwrap
fonte

Respostas:

262

O renderizador foi descontinuado no Angular 4.0.0-rc.1, leia a atualização abaixo

A maneira angular2 é usar listenou listenGlobaldo Renderer

Por exemplo, se você deseja adicionar um evento de clique a um Componente, é necessário usar o Renderer e ElementRef (isso também oferece a opção de usar o ViewChild ou qualquer coisa que recupere o nativeElement)

constructor(elementRef: ElementRef, renderer: Renderer) {

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

Você pode usar listenGlobalque lhe dará acesso a document, body, etc.

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

Observe que, desde o beta.2 ambos listene listenGlobalretorne uma função para remover o ouvinte (consulte a seção de alterações recentes do changelog para o beta.2). Isso é para evitar vazamentos de memória em grandes aplicativos (consulte # 6686 ).

Portanto, para remover o ouvinte que adicionamos dinamicamente, devemos atribuir listenou listenGlobala uma variável que reterá a função retornada e depois a executamos.

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

Aqui está um plnkr com um exemplo de trabalho. O exemplo contém o uso de listene listenGlobal.

Usando o RendererV2 com Angular 4.0.0-rc.1 + (Renderer2 desde 4.0.0-rc.3)

  • 25/02/2017 : Rendererfoi descontinuado, agora devemos usar RendererV2(veja a linha abaixo). Veja o commit .

  • 10/03/2017 : RendererV2renomeado para Renderer2. Veja as mudanças mais recentes .

RendererV2não tem mais listenGlobalfunção para eventos globais (documento, corpo, janela). Ele tem apenas uma listenfunção que atinge ambas as funcionalidades.

Para referência, estou copiando e colando o código fonte da implementação do DOM Renderer, pois ele pode mudar (sim, é 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 você pode ver, agora verifica se estamos passando uma string (documento, corpo ou janela); nesse caso, ele usará uma addGlobalEventListenerfunção interna . Em qualquer outro caso, quando passarmos um elemento (nativeElement), ele usará um simplesaddEventListener

Para remover o ouvinte, é igual Rendererao angular 2.x. listenretorna uma função e chame essa função.

Exemplo

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

plnkr com Angular 4.0.0-rc.3 usando o Renderer2

Eric Martinez
fonte
Este é apenas o meu segundo dia com o Angular2 e eu mal havia começado a entender a v1, então muito disso é um pouco confuso. Você me deu uma boa quantidade de coisas para ler, por isso estou encerrando esta e, sem dúvida, voltarei em breve com MUITAS perguntas mais relacionadas. Felicidades para a resposta detalhada :)
popClingwrap
3
@popClingwrap, você também pode verificar o HostListener . Nos documentos, verifique as diretrizes de Atributo em Responder à ação do usuário para ver como hosté usado também.
Eric Martinez
@EricMartinez, existe uma maneira de parar de ouvir ou listenGlobal? (o mesmo que removeEventListener)
Nik
3
@ user1394625 sim, como você pode ver na resposta do ngOnDestroycódigo, ambos listene listenGlobalretornam uma função que, quando chamada / executada, o ouvinte é removido. Então, como você vê, this.funcestá mantendo a função retornada por renderer.listene, quando o faço this.func(), estou removendo o ouvinte. O mesmo vale para listenGlobal.
Eric Martinez
@EricMartinez tem mais uma pergunta para você ... como posso acessar o 'evento' dentro da função para preventDefault () ou stopPropagation ()
Nik
5

Eu também acho isso extremamente confuso. como @EricMartinez aponta Renderer2 listen () retorna a função para remover o ouvinte:

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

Se estou adicionando um ouvinte

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

Eu esperaria que minha função execute o que eu pretendia, não o total oposto ao qual é remover o ouvinte.

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

No cenário fornecido, faria mais sentido nomear como:

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

Deve haver uma boa razão para isso, mas, na minha opinião, é muito enganador e não intuitivo.

tahiche
fonte
3
Se você estivesse adicionando um ouvinte, por que esperaria que a função retornada adicionando esse ouvinte invocasse esse ouvinte? Isso não faz muito sentido para mim. O objetivo de adicionar um ouvinte é responder a eventos que você não pode necessariamente acionar programaticamente. Acho que se você esperava que essa função chamasse seu ouvinte, talvez não entendesse completamente os ouvintes.
Willwsharp 29/03/19
@ tahiche companheiro isso é realmente confuso, obrigado por apontar isso!
godblessstrawberry
Ele retorna isso para que você também possa remover o ouvinte novamente quando destruir seu componente posteriormente. Ao adicionar ouvintes, é uma boa prática removê-los mais tarde, quando você não precisar mais deles. Portanto, armazene esse valor de retorno e chame-o dentro do seu ngOnDestroymétodo. Admito que possa parecer confuso a princípio, mas na verdade é um recurso muito útil. De que outra forma você se limpa?
Wilt
1

Vou adicionar um exemplo StackBlitz e um comentário à resposta de @tahiche.

O valor de retorno é uma função para remover o ouvinte de evento após a adição. É considerado uma boa prática remover ouvintes de eventos quando você não precisar mais deles. Portanto, você pode armazenar esse valor de retorno e chamá-lo dentro do seu ngOnDestroymétodo.

Admito que possa parecer confuso a princípio, mas na verdade é um recurso muito útil. De que outra forma você pode se limpar?

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

Você pode encontrar um StackBlitz aqui para mostrar como isso poderia funcionar para capturar cliques em elementos âncora.

Adicionei um corpo com uma imagem da seguinte forma:
<img src="x" onerror="alert(1)"></div>
para mostrar que o desinfetante está fazendo seu trabalho.

Aqui neste violino, você encontra o mesmo corpo ligado a um innerHTMLsem higienizá-lo e ele demonstrará o problema.

Wilt
fonte
0

Aqui está minha solução alternativa:

Criei uma biblioteca com o Angular 6. Adicionei um componente comum commonlib-headerque é usado assim em um aplicativo externo.

Observe serviceReferencequal é a classe (injetada no componente constructor(public serviceReference: MyService)que usa o commonlib-header) que contém o 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>

O componente da biblioteca é programado assim. O evento dinâmico é adicionado no 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)
  }

O reutilizável 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
fonte