Skip to main content
Ben Nadel at cf.Objective() 2012 (Minneapolis, MN) with: Alec Irwin
Ben Nadel at cf.Objective() 2012 (Minneapolis, MN) with: Alec Irwin

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

By
Published in Comments (5)

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.

Want to use code from this post? Check out the license.

Reader Comments

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

15,848 Comments

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

15,848 Comments

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

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.

1 Comments

I wonder if this has to do with zone.js? It does put off certain behaviors until the next "tick". Totally conjecture and not sure how one would test this. The overall conjecture is that maybe it adds all event handlers to the queue to run after the next tick, and simply runs them all at that point "outside"/regardless of what Angular says.

If you can think of a way to test this I would love to see it!

15,848 Comments

@Zachary,

To be honest, a bunch of the Zone.js is over my head. As is some of the DOM-events plug-in code that I was seeing that was taking or not-taking the Zone into account. Furthermore, I see some behaviors in Angular that I can't really explain at all. For example, when you execute a Navigation, there is call in the Router that creates a Promise. The resolution of that Promise seems to happens in between local-element event handlers and the eventual propagation of the event up the DOM tree .... which makes no sense since the propagation up the DOM tree should happen synchronously before the Promise ever resolves.

I know none of that makes sense without code and context. But, I say that all to say that I do think that Angular is doing some tricky / unexpected things under the hood to coordinate all of the events. It's some very complex code under the hood.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel