Working with DOM Events
A few different scenarios you may encounter while working with JavaScript events in the DOM.
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.