Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at TechCrunch Disrupt (New York, NY) with: Danielle Morrill
Ben Nadel at TechCrunch Disrupt (New York, NY) with: Danielle Morrill@DanielleMORRILL )

Host Bindings Don't Prevent Default Event Behavior Until After All Event Handlers Have Executed In Angular 7.1.1

By Ben Nadel on

The title of this post may be a little bit misleading - this post is about an event-binding caveat on Angular elements, so the details don't all fit nicely into a one-line summary. But, essentially, with the way host bindings work in Angular, if you return "false" from an event-binding handler, Angular will automatically call the event.preventDefault() method on the associated event. However, if you have multiple event-bindings listening to the same event on the same element, the implicit call to event.preventDefault() does not get executed until after all of the local event-handlers have been run. As such, one event-handler won't be able to detect whether or not its sibling event-handler is attempting to prevent an event's default behavior.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

To see this in action, we can setup multiple "click" bindings on the same element in an Angular template. In the first click handler, we can prevent the default behavior. And, in the second click handler, we can check to see if the event's default behavior has been prevented.

In the following code, I'm using two different techniques to prevent an events default behavior. In the first test, I'm returning "false" so that Angular will implicitly prevent the default. And, in the second test, I'm explicitly calling event.preventDefault():

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @Component({
  • selector: "my-app",
  • styleUrls: [ "./app.component.less" ],
  • template:
  • `
  • <div (click)="handleContainer( $event )">
  •  
  • <div class="child">
  • Use nothing.
  • </div>
  •  
  • <div
  • (click)="handleChild( $event, false )"
  • (click)="handleSibling( $event )"
  • class="child">
  • Use "return false" to prevent default.
  • </div>
  •  
  • <div
  • (click)="handleChild( $event, true )"
  • (click)="handleSibling( $event )"
  • class="child">
  • Use "event.preventDefault()" to prevent default.
  • </div>
  •  
  • </div>
  • `
  • })
  • export class AppComponent {
  •  
  • // I log the click event at the container level (one level up in the DOM).
  • public handleContainer( event: MouseEvent ) : void {
  •  
  • console.group( "Container" );
  • console.log( "event.returnValue:", event.returnValue );
  • console.log( "event.defaultPrevented:", event.defaultPrevented );
  • console.groupEnd();
  •  
  • }
  •  
  •  
  • // I am the first click-handler - I prevent the default behavior using two
  • // different approaches.
  • public handleChild( event: MouseEvent, explicit: boolean ) : false | void {
  •  
  • if ( explicit ) {
  •  
  • console.group( "First Click Handler In handleChild()" );
  • console.warn( "Using event.preventDefault() to prevent default." );
  • console.groupEnd();
  •  
  • // Prevent default using explicit event method.
  • event.preventDefault();
  •  
  • } else {
  •  
  • console.group( "First Click Handler In handleChild()" );
  • console.warn( "Using return( false ) to prevent default." );
  •  
  • // Prevent default using the implicit understanding that returning "false"
  • // from a host-binding will automatically prevent the default behavior on
  • // the associated event object.
  • console.groupEnd();
  • return( false );
  •  
  • }
  •  
  • }
  •  
  •  
  • // I am the second click-handler on the child element. I am a sibling to the handler
  • // that is preventing the default click-event behavior.
  • public handleSibling( event: MouseEvent ) : void {
  •  
  • console.group( "Sibling" );
  • console.log( "event.returnValue:", event.returnValue );
  • console.log( "event.defaultPrevented:", event.defaultPrevented );
  • console.groupEnd();
  •  
  • }
  •  
  • }

As you can see, the first click-handler method, handleChild(), takes the event object and prevents the default behavior. The second click-handler on the same element, handleSibling(), then inspects the event object to see if the behavior change can be detected.

Now, if we run this in the browser and click on the three different links (one control and two tests), we get the following output:


 
 
 

 
 Using Angular to prevent default event behavior in a host binding in Angular 7.1.1. 
 
 
 

As you can see, if we return "false" in order to leverage the implicit logic in Angular's host bindings, the sibling click-handler does not see that the event's default behavior has been prevented. Of course, if we explicitly call event.preventDefault(), the sibling click-handler will see the altered behavior. And, in all cases, the event's default behavior will be modified by the time the event bubbles up to the parent container in the Document Object Model (DOM).

Now, to a large degree, this behavior may be moot as I don't believe that Angular makes any explicit guarantees about the order in which host bindings on a given element will be invoked. But, if you're down in the weeds, really trying to hack something together (as I sometimes am), this caveat is important to understand. Especially if you're piggy-backing on an Angular provided directive.



Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

Interesting! I'm wondering if the same thing would happen if the events are called from the same attribute? For example:

ng-click="handleChild($event, false);handleSibling($event)"

My assumption would be yes, since you're passing the same $event to each, and the issue only occurs when you're using return(false) to invoke it.

Good stuff, Ben!

Reply to this Comment

@Steve,

Interesting question -- to be honest, I completely forgot you could even put more than one expression inside the evaluated attribute :D

Ultimately, what brought this to my attention is that I am trying to tap into the [routerLink][fragment] directive that is provided by the RouterModule. It uses the return( false ) approach. And, if I also provide a similar directive (on the same element), I cannot see if that event has been altered.

I think my approach, in that case, will be to go up one level in the DOM and capture the click event there. We'll see.

Reply to this Comment

@All,

Understanding this event.defaultPrevented behavior was useful in my polyfill for the fragment portion of the RouterLink directive. Right now, as of Angular 7.1.1, second clicks of a fragment link are still ignored. So, I polyfilled the logic:

https://www.bennadel.com/blog/3537-polyfilling-the-second-click-of-a-routerlink-fragment-in-angular-7-1-1.htm

This creates a (click) handler on the a[routerLink][fragment] selector; then, tracks the click event as it bubbles up the DOM. Once it gets the parent element, I can check to see if the defaultPrevented property is set.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.