Search code examples
javascriptangularangular-template

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


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


Solution

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