动态添加事件监听器

Dynamically add event listener

我刚开始弄乱 Angular 2,我想知道是否有人可以告诉我从元素中动态添加和删除事件侦听器的最佳方法。

我设置了一个组件。单击模板中的某个元素时,我想将 mousemove 的侦听器添加到同一模板的另一个元素。然后我想在单击第三个元素时删除此侦听器。

我只是使用简单的 Javascript 来获取元素然后调用标准的 addEventListener() 来让这个工作正常,但我想知道是否有更多的“Angular2.0" 我应该研究的方法。

渲染器已在 Angular 4.0.0-rc.1 中弃用,请阅读下面的更新

angular2方式是使用Renderer

中的listenlistenGlobal

例如,如果您想向组件添加点击事件,则必须使用 Renderer 和 ElementRef(这也为您提供了使用 ViewChild 或任何检索 nativeElement 的选项)

constructor(elementRef: ElementRef, renderer: Renderer) {

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

您可以使用 listenGlobal 来访问 documentbody

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

请注意,自 beta.2 以来,listenlistenGlobal return 都有一个删除侦听器的函数(请参阅 breaking changes section from changelog for beta.2). This is to avoid memory leaks in big applications (see #6686)。

因此,要删除我们动态添加的侦听器,我们必须将 listenlistenGlobal 分配给将保存函数 returned 的变量,然后我们执行它。

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

这是一个 plnkr 的示例。该示例包含 listenlistenGlobal.

的用法

将 RendererV2 与 Angular 4.0.0-rc.1+ 一起使用 (Renderer2 自 4.0.0-rc.3 起)

  • 25/02/2017Renderer 已被弃用,现在我们应该使用 RendererV2(见下行)。见 commit.

  • 10/03/2017RendererV2 已重命名为 Renderer2。见 breaking changes.

RendererV2 不再有全局事件(文档、正文、window)的 listenGlobal 功能。它只有一个 listen 函数可以实现这两种功能。

作为参考,我复制并粘贴了 DOM 渲染器实现的 source code,因为它可能会改变(是的,它是 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;
  }

如您所见,现在它会验证我们是否正在传递字符串(文档、正文或 window),在这种情况下它将使用内部 addGlobalEventListener 函数。在任何其他情况下,当我们传递一个元素 (nativeElement) 时,它将使用简单的 addEventListener

删除侦听器与 angular 2.x 中的 Renderer 相同。 listen return 是一个函数,然后调用该函数。

例子

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

plnkrAngular 4.0.0-rc.1 使用 RendererV2

plnkrAngular 4.0.0-rc.3 使用 Renderer2

我觉得这非常令人困惑。 正如@EricMartinez 指出 Renderer2 listen() returns 移除监听器的函数:

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

如果我要添加一个监听器

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

我希望我的函数执行我想要的,而不是完全相反,即删除监听器。

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

在给定的场景中,将其命名为更有意义:

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

这一定有充分的理由,但在我看来,这是非常具有误导性且不直观的。

这是我的解决方法:

我用 Angular 6 创建了一个库。我添加了一个公共组件 commonlib-header,它在外部应用程序中是这样使用的。

请注意 serviceReference,它是 class(注入到使用 commonlib-header 的组件 constructor(public serviceReference: MyService) 中),它包含 stringFunctionName 方法:

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

库组件是这样编程的。 onClick(fn: any)方法中添加动态事件:

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

可重复使用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>

我将在@tahiche 的回答中添加 StackBlitz example 和评论。

return 值是一个函数,用于在添加事件侦听器后删除它。当您不再需要事件侦听器时,删除事件侦听器被认为是一种很好的做法。所以你可以存储这个 return 值并在你的 ngOnDestroy 方法中调用它。

我承认一开始它可能看起来很混乱,但它实际上是一个非常有用的功能。你还能怎么清理自己?

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

您可以找到 a StackBlitz here 来展示如何捕捉锚元素的点击。

我添加了一个图片如下:
<img src="x" onerror="alert(1)"></div>
以表明消毒剂正在发挥作用。

Here in this fiddle 你会发现同一具尸体附在了一个 innerHTML 上,但没有对其进行消毒,它会证明这个问题。