event bubbles

Working with DOM Events

A few different scenarios you may encounter while working with JavaScript events in the DOM.

Gideon Kreitzer
2020/09/07
  1. Event handlers and method contexts
  2. Passing arguments to an event handler reference
  3. Passing two distinct handlers to an event listener

1. Event handlers and method contexts

Let's suppose we're building a “Mailbox” class that will provide the service of sending form data to a mail server. This class has an event handler method on its prototype. Inside the constructor, we attach an event to this method as one of the bootstrapping steps.

We'd like to use the validate method inside our send method by referencing it with this.validate(), thinking that it will find it on the prototype chain of our instance. However, addEventListener will redefine the context of the send method during execution by assigning its this reference to the DOM node to which the event was attached. In this case this will end up referencing the browser's form object which does not have our validate method on its prototype chain.

While we'd otherwise expect this behaviour from addEventListener, it can be overlooked when your mind is deep into the weeds of creating a new class and naturally assumes that since this is being used inside a method that is explicitly notated on the Mailbox class it should also continue to be bound to it.

The following snippet shows the relevant code excerpts from our hypothetical class.

// our example class
function Mailbox(options) {
    this.form = options.formElement || 'form';
    this.method = options.httpMethod || 'POST';
    //...

    // attach submission event to the "send" handler
    this.form.addEventListener('submit', this.send);
}

// a basic field validation method that
// we intend to use in the send() handler
Mailbox.prototype.validate = function() {
    // the validation code
}

// the event handler
Mailbox.prototype.send = function(event) {
    // Normally this method of referencing a method
    // on the same class will work, but since send()
    // had its context changed by addEventListener()
    // it has lost access to Mailbox's prototype and 
    // will throw an error.
    this.validate();
    //...
}

We can avoid the error by explicitly referencing the validate() method with Mailbox.prototype.validate(). This will work, but you'll have to do this for every other property of this class that you would like to access within the send method.

Mailbox.prototype.send = function(event) {
    // one solution is to use an explicit reference
    Mailbox.prototype.validate();
    //...
}

A better solution would be to keep the context of the class consistent by locking in this when executing send.

function Mailbox(options) {
    // persist the desired (class) context by using .bind()
    this.form.addEventListener('submit', this.send.bind(this));
}

Mailbox.prototype.send = function(event) {
    // works again!
    this.validate();
    //...
}

2. Passing arguments to an event handler reference

Often times you'll find yourself needing to pass arbitrary arguments to an event handler function that you're referencing from within a listener. There are a few ways to meet this need, but I'll cover one way that I use from time to time. Consider the following sample code.

export default (arg1, arg2) => {

    // 'theHandler' is expecting 'arg1' and 'arg2' 
    element.addEventListener('click', theHandler);

    //...
};

function theHandler(arg1, arg2) {
    //...
}

In the above example we have a module that receives two arguments. It adds an event listener who's handler depends on them. How would we feed these variables to the event handler?

export default (arg1, arg2) => {

    element.addEventListener(
        'click', theHandler.bind(null, arg1, arg2)
    );

};

function theHandler(arg1, arg2, event) {
    //...
}

In the solution above we use the bind() method to essentially partially apply our desired arguments. bind() affixes the comma-separated arguments it receives as the first arguments of the function it was called on. Take note of the order of arguments in the handler's signature — that is, the event object passed from addEventListener() is positioned last. By the time the event listener is executed, bind() has already prepended our arguments to the handler, so any subsequent arguments applied at execution time will be postfixed in sequence.

Note that in our particular case, theHandler is not concerned with the context lent by this, so we just pass null as the first argument to bind().

3. Passing two distinct handlers to an event listener

We have a single event listener that should trigger two separate handlers. It might feel a little verbose and inefficient to attach two separate event listeners that is listening for the same event on the same element just to be able to assign a distinct handler to each.

elementX.addEventListener('click', handlerA);
    //...
};

elementX.addEventListener('click', handlerB);
    //...
};

Above is the somewhat longwinded approach.

elementX.addEventListener('click', function(event) {

        handlerA(arg1, event);
        handlerB(arg1, arg2, event);

    });
};

So, all we had to do was make use of an intermediary function to refactor the old solution into something a little cleaner. In this case however, be sure to explicitly pass down the event object.

Credits
Photo by Drew Beamer
Tags
events DOM JavaScript