The value of "this" within the handler using addEventListener
Asked Answered
P

13

111

I've created a Javascript object via prototyping. I'm trying to render a table dynamically. While the rendering part is simple and works fine, I also need to handle certain client side events for the dynamically rendered table. That, also is easy. Where I'm having issues is with the "this" reference inside of the function that handles the event. Instead of "this" references the object, it's referencing the element that raised the event.

See code. The problematic area is in ticketTable.prototype.handleCellClick = function():

function ticketTable(ticks)
{
    // tickets is an array
    this.tickets = ticks;
} 

ticketTable.prototype.render = function(element)
    {
        var tbl = document.createElement("table");
        for ( var i = 0; i < this.tickets.length; i++ )
        {
            // create row and cells
            var row = document.createElement("tr");
            var cell1 = document.createElement("td");
            var cell2 = document.createElement("td");

            // add text to the cells
            cell1.appendChild(document.createTextNode(i));
            cell2.appendChild(document.createTextNode(this.tickets[i]));

            // handle clicks to the first cell.
            // FYI, this only works in FF, need a little more code for IE
            cell1.addEventListener("click", this.handleCellClick, false);

            // add cells to row
            row.appendChild(cell1);
            row.appendChild(cell2);


            // add row to table
            tbl.appendChild(row);            
        }

        // Add table to the page
        element.appendChild(tbl);
    }

    ticketTable.prototype.handleCellClick = function()
    {
        // PROBLEM!!!  in the context of this function, 
        // when used to handle an event, 
        // "this" is the element that triggered the event.

        // this works fine
        alert(this.innerHTML);

        // this does not.  I can't seem to figure out the syntax to access the array in the object.
        alert(this.tickets.length);
    }
Particularly answered 27/8, 2009 at 2:44 Comment(0)
R
51

You need to "bind" handler to your instance.

var _this = this;
function onClickBound(e) {
  _this.handleCellClick.call(cell1, e || window.event);
}
if (cell1.addEventListener) {
  cell1.addEventListener("click", onClickBound, false);
}
else if (cell1.attachEvent) {
  cell1.attachEvent("onclick", onClickBound);
}

Note that event handler here normalizes event object (passed as a first argument) and invokes handleCellClick in a proper context (i.e. referring to an element that was attached event listener to).

Also note that context normalization here (i.e. setting proper this in event handler) creates a circular reference between function used as event handler (onClickBound) and an element object (cell1). In some versions of IE (6 and 7) this can, and probably will, result in a memory leak. This leak in essence is browser failing to release memory on page refresh due to circular reference existing between native and host object.

To circumvent it, you would need to either a) drop this normalization; b) employ alternative (and more complex) normalization strategy; c) "clean up" existing event listeners on page unload, i.e. by using removeEventListener, detachEvent and elements nulling (which unfortunately would render browsers' fast history navigation useless).

You could also find a JS library that takes care of this. Most of them (e.g.: jQuery, Prototype.js, YUI, etc.) usually handle cleanups as described in (c).

Reverso answered 27/8, 2009 at 2:53 Comment(5)
Where does var _this = this; in the context of my code go? Do I need to add onClickBound(e) to the prototype?Particularly
In render, right before attaching event listener. You can pretty much just replace addEventListener line from original example with this snippet.Reverso
It's interesting that you mention clean up. I'll actually be destroying these objects at some point in the process as well. I had originally planned on just doing .innerHTML = ""; My guess is that is bad in this context. How would I destroy this tables and avoid the mentioned leak?Particularly
As I said before, look into removeEventListener/detachEvent and breaking circular references. Here's a good explanation of a leak - jibbering.com/faq/faq_notes/closures.html#clMemReverso
I don't know why, but this self = this tricks always seem wrong to me.Wood
W
118

You can use bind which lets you specify the value that should be used as this for all calls to a given function.

   var Something = function(element) {
      this.name = 'Something Good';
      this.onclick1 = function(event) {
        console.log(this.name); // undefined, as this is the element
      };
      this.onclick2 = function(event) {
        console.log(this.name); // 'Something Good', as this is the binded Something object
      };
      element.addEventListener('click', this.onclick1, false);
      element.addEventListener('click', this.onclick2.bind(this), false); // Trick
    }

A problem in the example above is that you cannot remove the listener with bind. Another solution is using a special function called handleEvent to catch any events:

var Something = function(element) {
  this.name = 'Something Good';
  this.handleEvent = function(event) {
    console.log(this.name); // 'Something Good', as this is the Something object
    switch(event.type) {
      case 'click':
        // some code here...
        break;
      case 'dblclick':
        // some code here...
        break;
    }
  };

  // Note that the listeners in this case are this, not this.handleEvent
  element.addEventListener('click', this, false);
  element.addEventListener('dblclick', this, false);

  // You can properly remove the listners
  element.removeEventListener('click', this, false);
  element.removeEventListener('dblclick', this, false);
}

Like always mdn is the best :). I just copy pasted the part than answer this question.

Wood answered 22/10, 2013 at 0:31 Comment(1)
handleEvent() method... that's like a little nugget of gold! Worked like a charm.Auditory
R
51

You need to "bind" handler to your instance.

var _this = this;
function onClickBound(e) {
  _this.handleCellClick.call(cell1, e || window.event);
}
if (cell1.addEventListener) {
  cell1.addEventListener("click", onClickBound, false);
}
else if (cell1.attachEvent) {
  cell1.attachEvent("onclick", onClickBound);
}

Note that event handler here normalizes event object (passed as a first argument) and invokes handleCellClick in a proper context (i.e. referring to an element that was attached event listener to).

Also note that context normalization here (i.e. setting proper this in event handler) creates a circular reference between function used as event handler (onClickBound) and an element object (cell1). In some versions of IE (6 and 7) this can, and probably will, result in a memory leak. This leak in essence is browser failing to release memory on page refresh due to circular reference existing between native and host object.

To circumvent it, you would need to either a) drop this normalization; b) employ alternative (and more complex) normalization strategy; c) "clean up" existing event listeners on page unload, i.e. by using removeEventListener, detachEvent and elements nulling (which unfortunately would render browsers' fast history navigation useless).

You could also find a JS library that takes care of this. Most of them (e.g.: jQuery, Prototype.js, YUI, etc.) usually handle cleanups as described in (c).

Reverso answered 27/8, 2009 at 2:53 Comment(5)
Where does var _this = this; in the context of my code go? Do I need to add onClickBound(e) to the prototype?Particularly
In render, right before attaching event listener. You can pretty much just replace addEventListener line from original example with this snippet.Reverso
It's interesting that you mention clean up. I'll actually be destroying these objects at some point in the process as well. I had originally planned on just doing .innerHTML = ""; My guess is that is bad in this context. How would I destroy this tables and avoid the mentioned leak?Particularly
As I said before, look into removeEventListener/detachEvent and breaking circular references. Here's a good explanation of a leak - jibbering.com/faq/faq_notes/closures.html#clMemReverso
I don't know why, but this self = this tricks always seem wrong to me.Wood
M
16

This arrow syntax works for me:

document.addEventListener('click', (event) => {
  // do stuff with event
  // do stuff with this 
});

this will be the parent context and not the document context.

Messeigneurs answered 13/6, 2019 at 17:14 Comment(3)
how do you removeEventListener with this?Springe
@Springe Can you not reference the arrow function to a variable and refer with the variable both in add and remove?Gottuard
@KorayTugay yes you can, but I'm pointing out that that the way this answer is structured you cannot.Springe
U
14

Also, one more way is to use the EventListener Interface (from DOM2 !! Wondering why no one mentioned it, considering it is the neatest way and meant for just such a situation.)

I.e, instead of a passing a callback function, You pass an object which implements EventListener Interface. Simply put, it just means you should have a property in the object called "handleEvent" , which points to the event handler function. The main difference here is, inside the function, this will refer to the object passed to the addEventListener. That is, this.theTicketTable will be the object instance in the belowCode. To understand what I mean, look at the modified code carefully:

ticketTable.prototype.render = function(element) {
...
var self = this;

/*
 * Notice that Instead of a function, we pass an object. 
 * It has "handleEvent" property/key. You can add other
 * objects inside the object. The whole object will become
 * "this" when the function gets called. 
 */

cell1.addEventListener('click', {
                                 handleEvent:this.handleCellClick,                  
                                 theTicketTable:this
                                 }, false);
...
};

// note the "event" parameter added.
ticketTable.prototype.handleCellClick = function(event)
{ 

    /*
     * "this" does not always refer to the event target element. 
     * It is a bad practice to use 'this' to refer to event targets 
     * inside event handlers. Always use event.target or some property
     * from 'event' object passed as parameter by the DOM engine.
     */
    alert(event.target.innerHTML);

    // "this" now points to the object we passed to addEventListener. So:

    alert(this.theTicketTable.tickets.length);
}
Uttermost answered 27/8, 2009 at 2:44 Comment(4)
Kinda neat, but seems you can't removeEventListener() with this?Appendicectomy
@knutole, yes you can. Just save the object in a variable and pass the variable to addEventListener. You can look here for reference: https://mcmap.net/q/196198/-why-does-removeeventlistener-not-work-in-this-object-context .Immunoreaction
TypeScript does not appear to like this syntax, so it won't compile a call to addEventListener with an object as the callback. Dammit.Upolu
@DavidRTribble Did you try assigning the object to a variable and then passing the variable? Haven't personally tried TypeScript, But if it is exactly a syntax problem and not that the "function does not accept parameter" type of problem, then it might be a solutionUttermost
I
9

With ES6, you can use an arrow function as that will use lexical scoping[0] which allows you to avoid having to use bind or self = this:

var something = function(element) {
  this.name = 'Something Good';
  this.onclick1 = function(event) {
    console.log(this.name); // 'Something Good'
  };
  element.addEventListener('click', () => this.onclick1());
}

[0] https://medium.freecodecamp.org/learn-es6-the-dope-way-part-ii-arrow-functions-and-the-this-keyword-381ac7a32881

Improbable answered 16/4, 2019 at 18:48 Comment(3)
how do you removeEventListener with this?Springe
@Springe You don't.Guyon
my point exactly, except I wasn't exactSpringe
E
8

According to https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener ,

my_element.addEventListener('click', (e) => {
  console.log(this.className)           // WARNING: `this` is not `my_element`
  console.log(e.currentTarget === this) // logs `false`
})

so if you use the arrow functions you can go safe beacause they do not have their own this context.

Exanimate answered 27/8, 2021 at 14:45 Comment(1)
e.currentTarget is really what many of us want :)Heard
S
5

I know this is an older post, but you can also simply assign the context to a variable self, throw your function in an anonymous function that invokes your function with .call(self) and passes in the context.

ticketTable.prototype.render = function(element) {
...
    var self = this;
    cell1.addEventListener('click', function(evt) { self.handleCellClick.call(self, evt) }, false);
...
};

This works better than the "accepted answer" because the context doesn't need to be assigned a variable for the entire class or global, rather it's neatly tucked away within the same method that listens for the event.

Shaggy answered 15/2, 2013 at 4:27 Comment(5)
With ES5 you get the same using just: cell1.addEventListener('click', this.handleCellClick.bind(this));. Leave the last argument false if you want compaibility with FF <=5.Immunoreaction
Problem with this is that if you have other functions called from the handler, you need to pass self down the line.Winola
This is the solution I used. I would have preferred to use the .bind() method but had to use .call() instead because our app has to support IE8 for a while yet.Upolu
Previously I used self, until I found self is the same as window, now I use me.Odorous
How do you use removeEventListener this way?Necaise
H
2

What about

...
    cell1.addEventListener("click", this.handleCellClick.bind(this));
...

ticketTable.prototype.handleCellClick = function(e)
    {
        alert(e.currentTarget.innerHTML);
        alert(this.tickets.length);
    }

e.currentTarget points to the target which is bound to the "click event" (to the element that raised the event) while

bind(this) preserves the outerscope value of this inside the click event function.

If you want to get an exact target clicked, use e.target instead.

Headstock answered 18/9, 2017 at 2:29 Comment(0)
P
1

Heavily influenced by kamathln and gagarine's answer I thought I might tackle this.

I was thinking you could probably gain a bit more freedom if you put handeCellClick in a callback list and use an object using the EventListener interface on the event to trigger the callback list methods with the correct this.

function ticketTable(ticks)
    {
        // tickets is an array
        this.tickets = ticks;
        // the callback array of methods to be run when
        // event is triggered
        this._callbacks = {handleCellClick:[this._handleCellClick]};
        // assigned eventListenerInterface to one of this
        // objects properties
        this.handleCellClick = new eventListenerInterface(this,'handleCellClick');
    } 

//set when eventListenerInterface is instantiated
function eventListenerInterface(parent, callback_type) 
    {
        this.parent = parent;
        this.callback_type = callback_type;
    }

//run when event is triggered
eventListenerInterface.prototype.handleEvent(evt)
    {
        for ( var i = 0; i < this.parent._callbacks[this.callback_type].length; i++ ) {
            //run the callback method here, with this.parent as
            //this and evt as the first argument to the method
            this.parent._callbacks[this.callback_type][i].call(this.parent, evt);
        }
    }

ticketTable.prototype.render = function(element)
    {
       /* your code*/ 
        {
            /* your code*/

            //the way the event is attached looks the same
            cell1.addEventListener("click", this.handleCellClick, false);

            /* your code*/     
        }
        /* your code*/  
    }

//handleCellClick renamed to _handleCellClick
//and added evt attribute
ticketTable.prototype._handleCellClick = function(evt)
    {
        // this shouldn't work
        alert(this.innerHTML);
        // this however might work
        alert(evt.target.innerHTML);

        // this should work
        alert(this.tickets.length);
    }
Plank answered 24/4, 2015 at 23:26 Comment(0)
D
0

The MDN explanation gives what to me is a neater solution further down.

In this example you store the result of the bind() call, which you can then use to unregister the handler later.

const Something = function(element) {
  // |this| is a newly created object
  this.name = 'Something Good';
  this.onclick1 = function(event) {
    console.log(this.name); // undefined, as |this| is the element
  };

  this.onclick2 = function(event) {
    console.log(this.name); // 'Something Good', as |this| is bound to newly created object
  };

  // bind causes a fixed `this` context to be assigned to onclick2
  this.onclick2 = this.onclick2.bind(this);

  element.addEventListener('click', this.onclick1, false);
  element.addEventListener('click', this.onclick2, false); // Trick
}
const s = new Something(document.body);

In the posters example you would want to bind the handler function in the constructor:

function ticketTable(ticks)
{
    // tickets is an array
    this.tickets = ticks;

    this.handleCellClick = this.handleCellClick.bind(this); // Note, this means that our handleCellClick is specific to our instance, we aren't directly referencing the prototype any more.
} 

ticketTable.prototype.render = function(element)
    {
        var tbl = document.createElement("table");
        for ( var i = 0; i < this.tickets.length; i++ )
        {
            // create row and cells
            var row = document.createElement("tr");
            var cell1 = document.createElement("td");
            var cell2 = document.createElement("td");

            // add text to the cells
            cell1.appendChild(document.createTextNode(i));
            cell2.appendChild(document.createTextNode(this.tickets[i]));

            // handle clicks to the first cell.
            // FYI, this only works in FF, need a little more code for IE
            this.handleCellClick = this.handleCellClick.bind(this); // Note, this means that our handleCellClick is specific to our instance, we aren't directly referencing the prototype any more.
            cell1.addEventListener("click", this.handleCellClick, false);

            // We could now unregister ourselves at some point in the future with:
            cell1.removeEventListener("click", this.handleCellClick);

            // add cells to row
            row.appendChild(cell1);
            row.appendChild(cell2);


            // add row to table
            tbl.appendChild(row);            
        }

        // Add table to the page
        element.appendChild(tbl);
    }

    ticketTable.prototype.handleCellClick = function()
    {
        // PROBLEM!!!  in the context of this function, 
        // when used to handle an event, 
        // "this" is the element that triggered the event.

        // this works fine
        alert(this.innerHTML);

        // this does not.  I can't seem to figure out the syntax to access the array in the object.
        alert(this.tickets.length);

    }
Deconsecrate answered 16/4, 2022 at 7:38 Comment(0)
B
0

I saw some questions about deleting an event handler created with an arrow function. You could put that arrow function in a variabele and use that vatiable with creation/deletion of the event. That also has another advantage that with that event signature the event only can be created once. So lets say you did call twice the creation of addEventListener with the same signature, then only one event Will be created.

Butterandeggs answered 29/7, 2023 at 22:12 Comment(0)
S
0

Adding and removing event handlers

There are a number of good answers here some attempt to deal with the possibility to remove the event handler. For me this does the trick:

class SomeClass {
    construct() {
      this.elementOkButton = document.getElementById('ok-button');
      this.init();
    };
    init() {
      this.elementOkButton.addEventListener(
        'click',
        ( this.okHandler = event => { this.doOkClick(event) } )
      );
    };
    doOkClick(event) {
      // do something

      // remove handler
      this.elementOkButton.removeEventListener('click', this.okHandler);
    };
}
const someObject = new SomeClass();

Shiller answered 11/1 at 10:37 Comment(0)
R
0

Aside from the many answers already given: you can avoid having to use this alltogether by using class free object oriented programming.

So let's provide an example factory for creating ticket tables.

const tickets = ticketTableFactory({
  tickts: [`t1`, `t2`, `t3`], 
  tableId: `table1`,
}).render(document.querySelector(`#ticketTables`));

const moreTickets = ticketTableFactory({
  tickts: [`more 1`, `more 2`], 
  tableId: `table2`,
  caption: `ticket table 2`,
}).render();

console.log(tickets.whoAmI);

function ticketTableFactory({tickts, tableId, caption} = {}) {
  const instance = {
    id: tableId,
    caption: caption ?? `#${tableId}`,
    get whoAmI() { 
      return `Who I am? I am a ticketTable instance with id "${
       tableId}". Click my cells!`; 
    },
    tickets: tickts,
    render,
  };

  return Object.freeze(instance);

  function render(element) {
    const tickets = instance.tickets;
    // create the table for instance
    const tbl = document.createElement("table");
    tbl.id = instance.id;
    tbl.insertAdjacentHTML(`beforeend`, `<caption>${
      instance.caption}</caption>`);
    tickets.forEach((ticket, i) =>
      tbl.insertAdjacentHTML(
        `beforeend`,
        `<tr><td>#${i}</td><td>${tickets[i]}</td></tr>`) );
    // append to [element] 
    // (when element is not given, append to body)
    (element ?? document.body).append(tbl);
    // add specific listener
    tbl.addEventListener(`click`, handleInstanceClickFactory(instance));
    return instance; // enables chaining
  }
}

function handleInstanceClickFactory(instance) {
  return function(evt) {
    const instanceTable = evt.target.closest(`#${instance.id}`);
    if (instanceTable) {
      console.clear();
      const tableHtml = `table innerHTML: ${instanceTable.innerHTML}`;
      console.log(`The instance with table id "${
        instance.id}" has ${instance.tickets.length} tickets`);
      console.log(tableHtml);
      setTimeout(() => { 
        console.clear();       
        console.log(`after click ${instance.whoAmI}`);  }, 4000);
    }
  }
}
table {
  min-width: 200px;
  margin-bottom: 2rem;
}
td { cursor: pointer; }
caption {
  color: green;
  background-color: #c0c0c0;
  border-bottom: 1px solid #999;
}
<div id="ticketTables"></div>
Roofing answered 14/1 at 13:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.