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

Ryan Silva picture Ryan Silva · Mar 21, 2018 · Viewed 19.7k times · Source

I'm creating an Angular component that wraps a native <button> element with some additional features. Buttons do not fire a click event if they're disabled and I want to replicate the same functionality. i.e., given:

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

Is there a way for my-button to prevent onClick() from getting called?

In Angular you can listen to the host click event this way, and stop propagation of the event:

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

This prevents the event from bubbling to ancestor elements, but it does not stop the built-in (click) output from firing on the same host element.

Is there a way to accomplish this?


Edit 1: the way I'm solving this now is by using a different output called "onClick", and consumers have to know to use "onClick" instead of "click". It's not ideal.

Edit 2: Click events that originate on the <button> element are successfully stopped. But if you put elements inside the button tag as I have, click events on those targets do propagate up to the host. Hm, it should be possible to wrap the button in another element which stops propagation...

Answer

ConnorsFan picture ConnorsFan · Mar 21, 2018

You could do the following:

  • Redefine the click event of the component, and emit this event when the button is clicked
  • Set the CSS style pointer-events: none on the component host
  • Set the CSS style pointer-events: auto on the button
  • Call event.stopPropagation() on the button click event handler

If you need to process the click event of other elements inside of your component, set the style attribute pointer-events: auto on them, and call event.stopPropagation() in their click event handler.

You can test the code in 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();
  }
}

UPDATE:

Since the button can contain HTML child elements (span, img, etc.), you can add the following CSS style to prevent the click from being propagated to the parent:

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

Thanks to @ErikWitkowski for his comment on this special case. See this stackblitz for a demo.