I was just asked:
Hello desandro! I'm a new developer.
elem.addEventListener( startEvent, this );
I wonder what is this means. I'm confused. [edited]
Confusing, indeed! this in JavaScript is difficult to learn, and especially tricky to understand when used in event listeners.
Let's take a step back and look at a simplified example. Here is a demo for a mouse-draggable element.
var dragElem = document.querySelector('.draggable');
var x = 0;
var y = 0;
var dragStartX, dragStartY, pointerDownX, pointerDownY;
dragElem.addEventListener( 'mousedown', function( event ) {
dragStartX = x;
dragStartY = y;
pointerDownX = event.pageX;
pointerDownY = event.pageY;
window.addEventListener( 'mousemove', onmousemove );
window.addEventListener( 'mouseup', onmouseup );
});
function onmousemove( event ) {
var moveX = event.pageX - pointerDownX;
var moveY = event.pageY - pointerDownY;
x = dragStartX + moveX;
y = dragStartY + moveY;
dragElem.style.left = x + 'px';
dragElem.style.top = y + 'px';
}
function onmouseup() {
window.removeEventListener( 'mousemove', onmousemove );
window.removeEventListener( 'mouseup', onmouseup );
}
See the Pen Draggable, single element by Dave DeSandro (@desandro) on CodePen.
To start dragging, I first add a mousedown event listener. When triggered, I then add listeners for mousemove and mouseup. In onmousemove is where I calculate and set the element's position. In onmouseup, I remove mousemove and mouseup listeners to stop dragging.
This works just fine for a single element. Metafizzy plugins are designed to handle multiple instances on the same page.
var dragElems = document.querySelectorAll('.draggable');
for ( var i=0; i < dragElems.length; i++ ) {
var dragElem = dragElems[i];
}
One way to approach this is to wrap up the draggable code into its own big function and call that for each element. But I like to use classes with prototype to handle multiple instances of the same behavior.
function Dragger( element ) {
this.element = element;
this.x = 0;
this.y = 0;
this.element.addEventListener( 'mousedown', this.onmousedown );
}
Dragger.prototype.onmousedown = function( event ) {
this.dragStartX = this.x;
this.dragStartY = this.y;
this.pointerDownX = event.pageX;
this.pointerDownY = event.pageY;
window.addEventListener( 'mousemove', this.onmousemove );
window.addEventListener( 'mouseup', this.onmouseup );
};
Now I need to add an event listener within the Dragger class functions. But there is a problem. I want to use this to reference the instance of the Dragger class within the event handler functions. But functions added with addEventListener will reference the bound element as this, not the function or object.
dragElem.addEventListener( 'mousedown', function() {
console.log( this );
});
handleEvent
One solution is to use a little-known feature of browser JavaScript, handleEvent. An object with an handleEvent method will be triggered with when added with addEventListener. Within the handleEvent method, this will refer to the listener object, not the element.
var listener = {
greeting: 'Hello ',
handleEvent: function( event ) {
console.log( this.greeting + event.type );
},
};
dragElem.addEventListener( 'mousedown', listener );
See simple handleEvent demo on CodePen.
The handleEvent method can be used for multiple events. You can specify logic by using event.type.
var listener = {
greeting: 'Hello ',
handleEvent: function( event ) {
console.log( this.greeting + event.type );
},
};
dragElem.addEventListener( 'mousedown', listener );
dragElem.addEventListener( 'mousemove', listener );
dragElem.addEventListener( 'mouseup', listener );
Back to the Dragger class. So now I add the handleEvent method to cooridate which event method to trigger. Then I can add this as the event listener.
function Dragger( element ) {
this.element = element;
this.x = 0;
this.y = 0;
this.element.addEventListener( 'mousedown', this );
}
Dragger.prototype.handleEvent = function( event ) {
var method = 'on' + event.type;
if ( this[ method ] ) {
this[ method ]( event );
}
};
Dragger.prototype.onmousedown = function( event ) {
this.dragStartX = this.x;
this.dragStartY = this.y;
this.pointerDownX = event.pageX;
this.pointerDownY = event.pageY;
window.addEventListener( 'mousemove', this );
window.addEventListener( 'mouseup', this );
};
See the Pen Draggable event listeners, handleEvent by Dave DeSandro (@desandro) on CodePen.
So to the original question: this can be used as an event listener as it has a handleEvent method. That method then triggers other methods that match event.type, like onmousedown.
I learned the handleEvent technique back in 2010. I use it in all the Metafizzy plugins. But JavaScript has come a long way in that time.
bind this
Instead of adding circuitry through handleEvent, you can [specify this with .bind()](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Specifying_this_using_bind().
The Function.prototype.bind() method lets you specify the value that should be used as this for all calls to a given function.
Using bind() has an additional benefit in that you can add multiple event listeners for the same event name.
this.element.addEventListener( 'click',
this.onElementClick.bind( this ) );
this.button.addEventListener( 'click',
this.onButtonClick.bind( this ) );
But, because bind() returns a new function, you will need to keep track of that function if you want to remove it later.
this.handleElementClick = this.onElementClick.bind( this );
this.handleButtonClick = this.onButtonClick.bind( this );
this.element.addEventListener( 'click', this.handleElementClick );
this.button.addEventListener( 'click', this.handleButtonClick );
this.element.removeEventListener( 'click', this.handleElementClick );
this.button.removeEventListener( 'click', this.handleButtonClick );
Here's what the Dragger class looks like using bind().
function Dragger( element ) {
this.element = element;
this.x = 0;
this.y = 0;
this.mousedownHandler = this.onmousedown.bind( this );
this.mousemoveHandler = this.onmousemove.bind( this );
this.mouseupHandler = this.onmouseup.bind( this );
this.element.addEventListener( 'mousedown', this.mousedownHandler );
}
Dragger.prototype.onmousedown = function( event ) {
this.dragStartX = this.x;
this.dragStartY = this.y;
this.pointerDownX = event.pageX;
this.pointerDownY = event.pageY;
window.addEventListener( 'mousemove', this.mousemoveHandler );
window.addEventListener( 'mouseup', this.mouseupHandler );
};
See the Pen Draggable bind this by Dave DeSandro (@desandro) on CodePen.
Arrow functions
With the new ES6 hotness, you can specify this using arrow functions. Within an arrow function, this will refer to the enclosing object.
function Dragger( element ) {
this.element = element;
this.x = 0;
this.y = 0;
this.onmousedown = ( event ) => {
this.dragStartX = this.x;
this.dragStartY = this.y;
this.pointerDownX = event.pageX;
this.pointerDownY = event.pageY;
window.addEventListener( 'mousemove', this.onmousemove );
window.addEventListener( 'mouseup', this.onmouseup );
};
this.onmousemove = ( event ) => {
var moveX = event.pageX - this.pointerDownX;
var moveY = event.pageY - this.pointerDownY;
this.x = this.dragStartX + moveX;
this.y = this.dragStartY + moveY;
this.element.style.left = this.x + 'px';
this.element.style.top = this.y + 'px';
};
this.onmouseup = () => {
window.removeEventListener( 'mousemove', this.onmousemove );
window.removeEventListener( 'mouseup', this.onmouseup );
};
this.element.addEventListener( 'mousedown', this.onmousedown );
}
See the Pen Draggable event listeners, arrow functions by Dave DeSandro (@desandro) on CodePen.
Personally, I'm not a fan of the arrow function technique for this scenario. It puts the method code inside another function, rather than outside on prototype. But I'm including it for completeness.
Each technique has its own pros and cons. handleEvent has served me well over the years, but I'm finding that I run into event name conflicts with big plugins like Flickity. So I'm starting to use out bind() a bit more. But then I miss the elegance of adding just this and not having to deal with extra event handler functions. Arrow functions, meanwhile, are just not for me.