Skip to main content
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Brian Chouteau
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Brian Chouteau

Using Public Class Fields To Bind Event Handlers In JavaScript

By
Published in

When I started using Angular with TypeScript, I was delighted to see that TypeScript offered a way to bind Functions to a Class instance as part of the Class definition. It worked by moving the "Function definition" into the constructor() method during compilation. This feature of TypeScript is amazing; and, I'm thrilled to see that this feature has also been adopted by the JavaScript specification. This allows us to easily bind event handlers without having to use .bind() or pass-in inline Fat-Arrow functions within our JavaScript classes.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

When you pass a Function reference out-of-context as part of a callback mechanism, you often lose the this binding associated with the Function. This is why the Fat-Arrow syntax was such a huge step forward for the JavaScript language: it allowed us to pass-around Functions that retained their original this binding regardless of how they were eventually invoked.

The Fat-Arrow function, combined with Public Class Fields, bring that same level of elegance into our class-based event-handlers. When defining a class method that will act as a callback, instead of defining it as a Function Declaration, we can define it as a Function Expression that is assigned to a class property.

So, instead of doing this:

class MyClass {

	callback() {
		// ... THIS reference may be broken in some cases ...
	}

}

... where callback() is being defined on the Class prototype chain, we can do this:

class MyClass {

	callback = () => {
		// ... THIS reference will ALWAYS be bound to MyClass instance ...
	};

}

Here, the callback property of the class is being initialized to point to a per-instance Fat-arrow function. This way, the callback Function is always bound to a this reference which refers to our instantiated MyClass instance.

To see this in action, let's create a Class that manages click events for a simple counter. The class has an internal method, handleClick, which is being defined as a public class property using a Fat-Arrow function:

<!doctype html>
<html lang="en">
<head>
	<meta charset="utf-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1" />
	<link rel="stylesheet" type="text/css" href="./main.css" />
</head>
<body>

	<h1>
		Using Public Class Fields To Bind Event Handlers In JavaScript
	</h1>

	<button id="buttonTarget">
		Click me &rarr;
		( <span id="spanTarget"><!-- Counter value reconciles here. --></span> )
	</button>

	<script type="text/javascript">

		// Since CLASS definitions don't get hoisted in JavaScript, I'm queuing the class
		// instantiation in a micro-task so that it will run after the JavaScript compiler
		// knows about our class specification.
		queueMicrotask(() => {

			new CounterController( buttonTarget, spanTarget );

		});

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

		class CounterController {

			/**
			* I initialize the counter and attach it to the given targets.
			*/
			constructor( hostTarget, valueTarget ) {

				this.hostTarget = hostTarget;
				this.valueTarget = valueTarget;
				this.value = 0;

				// NOTE: The Function reference that we're passing-in here has been pre-
				// bound to the THIS context (thanks to the Fat-Arrow syntax down below)
				// during class instantiation, but BEFORE the constructor has run.
				this.hostTarget.addEventListener( "click", this.handleClick );
				this.valueTarget.textContent = this.value;

			}

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

			// NOTE: We using the FAT-ARROW syntax here to define a PUBLIC FIELD that
			// happens to reference a Function. As such, this is creating a per-instance
			// version of the Function rather than one defined on the class Prototype.
			// When this Fat-Arrow function is invoked, it's bound to the THIS reference
			// associated with the class instance under construction.
			handleClick = ( event ) => {

				this.value++;
				this.valueTarget.textContent = this.value;

			};

		}

	</script>

</body>
</html>

As you can see, in my CounterController class definition, the handleClick "method" is actually just a "public class field" that's being initialized with a Fat-Arrow function. And, by defining the class in such a way, it allows us to attach our a click handler by passing around a "naked" Function reference:

.addEventListener( "click", this.handleClick );

Notice that we're not calling anything like .bind(this) on the Function reference. Instead, we're allowing our Fat-Arrow function to manage the this binding within our callback.

Now, if we run this code in the browser, we can see that all the bindings between the callbacks and the class properties work perfectly:

Clicking the button increments the counter and updates the view rendering.

As you can see, when the button is clicked, our handleClick callback clearly has no problem referencing the .value and .valueTarget properties bound to the CounterController instance. This is because the Fat-Arrow syntax retains its binding even when the Function is passed out-of-scope.

Gone are the days of calling .bind() on Functions in our Class constructors! Now, by combining Fat-Arrow functions with public class fields, we can easily create bound-methods as part of our class specification. Handling events (and event callbacks) has never been so sweet!

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

Reader Comments

Post A Comment — I'd Love To Hear From You!

Post a Comment

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