Skip to main content
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Oguz Demirkapi
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Oguz Demirkapi

Sanity Check: Manipulating Event Handlers And Timers Outside Of NgZone In Angular 5.2.10

By
Published in

CAUTION: This is primarily a "note to self" post.

Yesterday, as I was recording the video for my Medium-inspired (textSelect) Angular directive, it occurred to me that I didn't have a great mental model for Angular Zone / NgZone / zone.js. I knew that I could bind an event-handler outside of the Angular Zone; and, that its handler would run outside of the zone; but, I wasn't 100% confident that I knew how subsequent bindings would operate. As such, I wanted to perform a quick sanity check to see how bindings within bindings would interact with the Angular Zone service provider.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

This sanity check exploration only has one component - the App component. Within the App component, I setup a "mousedown" event handler outside of the Angular Zone. This means that the mousedown handler will execute outside of Angular's change-detection life-cycle. Within the mousedown handler, though, I'm also setting up a subsequent "mouseup" event handler. And, within the mouseup handler, I'm setting up a Timer. The goal here is check / confirm that the mouseup and Timer functions also operate outside of Angular Zone - just as the root mousedown handler does. And, that they, therefore, operate outside of the Angular change-detection life-cycle.

// Import the core angular services.
import { Component } from "@angular/core";
import { DoCheck } from "@angular/core";
import { ElementRef } from "@angular/core";
import { OnInit } from "@angular/core";
import { NgZone } from "@angular/core";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@Component({
	selector: "my-app",
	styleUrls: [ "./app.component.less" ],
	template:
	`
		<p>
			<a (click)="removeMousedown()">Remove mousedown</a>.
			<a (click)="setupMousedown()">Add mousedown</a>.<br />
			<br />

			<em>You can click around to trigger console logging...</em>
		</p>
	`
})
export class AppComponent implements DoCheck, OnInit {

	private element: Element;
	private zone: NgZone;

	// I initialize the app-component.
	constructor( elementRef: ElementRef, zone: NgZone ) {

		this.element = elementRef.nativeElement;
		this.zone = zone;

	}

	// ---
	// PUBLIC METHODS.
	// ---

	// I get called when the directive needs to be dirty-checked. In other words, this
	// method will give us insight into how often change-detection is triggered in the
	// Angular application.
	public ngDoCheck() : void {

		console.log( "ngDoCheck() - Change detection running." );

	}


	// I get called after the inputs have been bound for the first time.
	public ngOnInit() : void {

		this.setupMousedown();

	}


	// I remove the mousedown event-handler.
	// --
	// NOTE: Because this is getting called from the VIEW BINDINGS, it is being called
	// from WITHIN the Angular Zone. As such, it will lead to change-detection.
	public removeMousedown() : void {

		// TEST: Here, I'm looking to sanity check the idea that an event handler that
		// was bound OUTSIDE of the Angular Zone can be removed from WITHIN the Angular
		// Zone.
		this.element.removeEventListener( "mousedown", this.handleMousedown, false );

	}


	// I setup the mousedown event-handler.
	// --
	// NOTE: Because this is getting called from the VIEW BINDINGS and ngOnInit(), it
	// is being called from WITHIN the Angular Zone. As such, it will lead to change-
	// detection.
	public setupMousedown() : void {

		// TEST: Here, I'm setting up the mousedown event binding OUTSIDE of the Angular
		// Zone. As such, the "mousedown" event should NOT lead to change-detection.
		this.zone.runOutsideAngular(
			() : void => {

				this.element.addEventListener( "mousedown", this.handleMousedown, false );

			}
		);

	}

	// ---
	// PRIVATE METHODS.
	// ---

	// I handle mousedown events.
	private handleMousedown = ( event: MouseEvent ) : void => {

		console.log( "mousedown" );
		// TEST: The current method is running OUTSIDE of the Angular Zone. As such, I'm
		// testing to see if subsequent event bindings, configured OUTSIDE of the Angular
		// Zone, will continue to operate OUTSIDE of the Angular Zone.
		this.element.addEventListener( "mouseup", this.handleMouseup, false );

	}


	// I handle mouseup events.
	private handleMouseup = ( event: MouseEvent ) : void => {

		console.log( "mouseup" );

		this.element.removeEventListener( "mouseup", this.handleMouseup, false );
		// TEST: If the current event handler is running outside of the Angular Zone
		// (something we're actively testing), this checks to see if a timer that is
		// initiated here will also operate OUTSIDE of the Angular Zone.
		setTimeout(
			() : void => {

				console.log( "timer" );

			},
			100
		);

	}

}

As you can see, I'm logging calls to the ngDoCheck() Directive life-cycle method. This method will get called during Angular's change-detection digest, which should give us insight into when our interactions trigger change-detection (ie, which actions interact with handlers that were bound inside the Angular Zone). And, when we run this in the browser and click around, we get the following output:

Sanity Check of Angular Zone / zone.js bindings in Angular 5.2.10.

As you can see, none of the mousedown handlers trigger Angular's change-detection, which is what we would expect given that the mousedown handler was explicitly bound inside the zone.runOutsideAngular() method. But, what you can also see is that the subsequent handlers, bound inside the mousedown handler, also operate outside of Angular's change-detection life-cycle even without an explicit zone.runOutsideAngular() call. As such, we can be confident that handlers bound inside handlers will use the same zone.js instance as the original binding.

To be clear, this is what is supposed to happen. But, I wasn't 100% confident in the cascading feature of handlers bound within a given Zone.js instance. This clears it up nicely.

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

Reader Comments

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