How to add DOM Events Listeners
… simply having the ability to remove them later on and, as simple as it sounds, you won’t believe how many developers are still doing it wrong!
All you can Anti-Pattern!
Unless all you do is trashing the entire DOM every time you update it, these are all things you might do wrong already:
node.addEventListener('type', (arrow)=>{function})
is wrong, you’ll never be able to remove that arrow function listenernode.addEventListener('type', obj.method.bind(obj))
is wrong, for the very same reasonnode.addEventListener('type', this.method.bind(this))
is wrong too, you are trapping yourself in a place where you won’t have control over your own listeners. It’s a very bad habit you should probably refactor already!node.addEventListener('type', function (e) {})
is wrong as well, is anonymous, no way you can drop thatnode.addEventListener('type', {handleEvent: (e) => {}})
is wrong, you won’t have the object reference within that arrow functionnode.addEventListener('type', {handleEvent: function (e) {}})
is wrong if there’s no mechanism within the object handler to dropthis
, which will point to the objectnode.addEventListener('type', function named(e) {})
is also wrong if there’s no mechanism within that function to remove such listener
All the solutions you want!
The easiest way to solve any trouble is deadly simple: address that handler in a way you can remove it later on.
// arrow function for a click
let click = (evt)=>{evt.preventDefault(); alert('CLICK')}
// arrow function add or remove
node.addEventListener('click', click);
node.removeEventListener('click', click);
// generic prototype method to retrieve always same bound one
// lazy evaluated on demand, creates bound methods ONCE!
Class.prototype.boundTo = function (name) {
// I bet you don't use '\x01' as prefix that often ... right?
return this['\x01' + name] || Object.defineProperty(
this, '\x01' + name, {value: this[name].bind(this)}
)['\x01' + name];
};
// anytime you need, within a method
node.addEventListener('click', this.boundTo('clicker'));
// anytime you want to remove it
node.removeEventListener('click', this.boundTo('clicker'));
// anonymous bound (WeakMap between DOM nodes and objects)
node.addEventListener('click', {
// YES, handleEvent is standard and compatible with
// every browser since about ever!
handleEvent: function (e) {
if (node.getAttribute('last-click')) return this.drop(e);
alert('CLICKER');
},
// removes pretty much any listener through any event
drop: function (e) {
e.currentTarget.removeEventListener(
e.type, this, e.eventPhase === e.CAPTURING_PHASE
);
}
});
// named function expression that removes itself
node.addEventListener('click', function click(e) {
if (someConditionIsFalse) {
return e.currentTarget.removeEventListener(e.type, click);
}
});
Are you curious about that object or class prototype pattern so you can forget altogether about binding everything and focus just on what your methods do, instead of which this
reference they have?
There is a library for that, that takes care of everything for you, but today we stick just with standards and handleEvent
is untouchable legacy standard.
Which this
in your handler?
There is so much confusion about the context in the JavaScript community, that arrow functions, actually context-less, are misused in many possible ways and don’t actually help much solving the confusion about this
.
Nitpick first, I really hope I didn’t have to tell you again that if you write a loop that sets always the same listener to a list of nodes, you should really declare such listener once instead of N times per iteration, but the most common problem is that many developers apparently don’t know about the existence of the event.currentTarget property, possibly fooled by years of bad tutorials based on target
and target
only.
In November 2000 the W3C described evt.currentTarget
as following:
Used to indicate the EventTarget whose EventListeners are currently being processed. This is particularly useful during capturing and bubbling.
The developer readable version of that sentence is that given an event you can always retrieve the node in which the current handler was set simply accessing the read-only property currentTarget
.
document.body.addEventListener('click', function click(evt) {
// true, it doesn't matter where we clicked, it will be true!
alert(evt.currentTarget === document.body);
evt.currentTarget.removeEventListener(evt.type, click);
});
If there’s some API out there that doesn’t expose the currentTarget
is because the API author probably forgot about it, or probably read same tutorials every other developer read online instead of reading long standing standards.
In that case please file a bug and ask to implement currentTarget
there too, since it’s in specs by the year 2000 as EventInterface recommendation and it should be the universal way to always retrieve what developers think the this
reference usually points at.
… and about that this
reference …
Not only it’s wrong to think that this
within a handler is the element we attached the listener to, an assumption full of surprise since the Function.bind
era which is years ago, using objects as handlers, which is actually a common pattern used in popular production sites too, will automatically point at the very same object handler used to listen to that event.
var myClicker = {
handleEvent: function (e) {
// 1 to 3, and always true
alert([++this.clicks, this === myClicker]);
if (this.clicks === 3) {
// auto easy clean up at any time, what could you ask for more?
e.currentTarget.removeEventListener(e.type, this);
}
},
clicks: 0
};
document.body.addEventListener('click', myClicker);
The handleEvent
can be a prototype
method too and I’ve no idea why many are not using this pattern already: it basically makes every this.method.bind(this)
in your code completely redundant, superfluous, and boring to write … even more error prone!
15 years DOM Level 2 and 11 years of DOM Level 3
As summary, what’s amusing me is that DOM events are one of the most rock-solid and cross browsers specifications that since version 3, April 2004 haven’t been touched much!
You can guess it’s not because nobody uses them, we’ve all used them since ever, rather because these are pretty useful, well thought, and never needed many amends on specs!
Accordingly, shall we start getting them right, specially now that we are 1 day after that future we’ve dreamed about many years ago? I hope so, and I’m sure Doc would tell you so too!