您能否阻止触发 Angular 组件的主机点击?

Can you prevent an Angular component's host click from firing?

我正在创建一个 Angular 组件,它包装了具有一些附加功能的原生 <button> 元素。如果按钮被禁用并且我想复制相同的功能,则按钮不会触发点击事件。即,给定:

<my-button (click)="onClick()" [isDisabled]="true">Save</my-button>

my-button 有没有办法防止 onClick() 被调用?

在Angular中可以这样监听宿主点击事件,并停止传播事件:

//Inside my-button component
@HostListener('click', ['$event'])
onHostClick(event: MouseEvent) {
  event.stopPropagation();
}

这可以防止事件冒泡到祖先元素,但不会阻止内置 (click) 输出在同一宿主元素上触发。

有办法做到这一点吗?


编辑 1: 我现在解决这个问题的方法是使用一个名为 "onClick" 的不同输出,消费者必须知道使用 "onClick" 而不是 "click"。不太理想。

编辑 2: 源自 <button> 元素的单击事件已成功停止。但是,如果您像我一样将元素放入按钮标记内,则这些目标上的单击事件会向上传播到主机。嗯,应该可以将按钮包装在另一个停止传播的元素中......

我不相信有一种本机方法可以防止事件触发,正如 this git issue 在 2016 年所支持的那样:

The order of execution is red herring - the order in which an event on the same element is propagated to multiple listeners is currently undefined. this is currently by design.

Your problem is that the event exposed to the listeners is the real DOM event and calling stopImmediatePropagation() on the provided event stops execution of other listeners registered on this element. However since all the the listeners registered via Angular are proxied by just a single dom listener (for performance reasons) calling stopImmediatePropagation on this event has no effect.

您可以执行以下操作:

  • 重新定义组件的click事件,点击按钮时触发该事件
  • 在组件主机上设置 CSS 样式 pointer-events: none
  • 在按钮上设置 CSS 样式 pointer-events: auto
  • 在按钮单击事件处理程序上调用 event.stopPropagation()

如果您需要处理组件内部其他元素的点击事件,请为它们设置样式属性pointer-events: auto,并在它们的点击事件处理程序中调用event.stopPropagation()

您可以测试this stackblitz中的代码。

import { Component, HostListener, Input, Output, ElementRef, EventEmitter } from '@angular/core';

@Component({
  selector: 'my-button',
  host: {
    "[style.pointer-events]": "'none'"
  },
  template: `
    <button (click)="onButtonClick($event)" [disabled]="isDisabled" >...</button>
    <span (click)="onSpanClick($event)">Span element</span>`,
  styles: [`button, span { pointer-events: auto; }`]
})
export class MyCustomComponent {

  @Input() public isDisabled: boolean = false;
  @Output() public click: EventEmitter<MouseEvent> = new EventEmitter();

  onButtonClick(event: MouseEvent) {
    event.stopPropagation();
    this.click.emit(event);
  }

  onSpanClick(event: MouseEvent) {
    event.stopPropagation();
  }
}

更新:

由于按钮可以包含HTML个子元素(spanimg等),可以添加如下CSS样式来防止点击正在传播到父级:

:host ::ng-deep button * { 
  pointer-events: none; 
}

感谢 @ErikWitkowski for on this special case. See this stackblitz 的演示。

您可以使用本机添加和删除 EventListeners。从 angular 的角度考虑,这绝不是一个好的解决方案。此外,如果将 disabled 属性放入 button 中,这将不起作用,因为它将覆盖附加的 eventListeners。需要使用 disabled class 代替。 (或者将 button 包装在 span 中并使用其中的模板引用 #btn。)

StackBlitz

import { Component, OnInit, OnChanges, HostListener, Input, Output, EventEmitter, SimpleChanges, ElementRef, ViewChild } from '@angular/core';

@Component({
  selector: 'app-my-button',
  template: `<button [class.disabled]="isDisabled" #btn><span>hey</span></button>`,
  styles: [`button.disabled { opacity:0.5 }`]
})
export class MyButtonComponent implements OnInit, OnChanges {
  disableClick = e => e.stopPropagation();
  @Input() isDisabled: boolean;
  @ViewChild('btn') btn: ElementRef;
  constructor() { }

  ngOnChanges(changes: SimpleChanges) {
    if(this.isDisabled) {
      this.btn.nativeElement.addEventListener('click', this.disableClick);
    } else {
      this.btn.nativeElement.removeEventListener('click', this.disableClick);
    }
  }
  ngOnInit() {
  }

}

您可以尝试在捕获模式下停止传播事件(查看 true 作为 addEventListener 调用的最后一个参数和 window作为监听对象)。

window.addEventListener('click', (event) => {
    let clickDisallowed = true; // detect if this click is disallowed

    if (event.target === this.elementRef.nativeElement && clickDisallowed) {
        event.stopImmediatePropagation();
    }
}, true);

别忘了退订这个听众。