Better way to maintain this reference on event listener functions

# Mark Kennedy (3 years ago)

Sorry if I've missed any previous requests for this feature, but is there any goal to allow callbacks passed to event listeners to keep their this context without having to store a reference to the this-binded event listener first?

I frequently find myself doing this in my class methods:

this.el = document.createElement('div');
this.listener = this.onClick.bind(this); // store a reference just to bind the `this` to remove it later
this.el.addEventListener('click', this.listener)

only to ensure the correct listener mapping gets removed later on in a destroy-like function with this code:

this.el.removeEventListener('click', this.listener);

A few years ago I would've never thought of a good reason to add such sugar, but now that JS is moving forward with class declarations, it seems more warranted now.

# Andrea Giammarchi (3 years ago)

This is rather about DOM land, nothing to do with EcmaScript standard.

I'll quickly answer but please bear in mind this mailing list is about ES, not about WHATWG or W3C, thanks.

Since DOM Level 3 has been available (every browser compatible with addEventListener, polyfilled for IE8 too if necessary [1] )

// generic handler: any class, any object
function handleEvent(e) {
  this['on' + e.type](e);
};

// as class
function SomeClass(someNode) {
  someNode.addEventListener('click', this);
}
SomeClass.prototype.onclick = function () {
  alert('clicked');
};
SomeClass.prototype.handleEvent = handleEvent;

// as object
document.body.addEventListener('click', {
  name: 'runtime',
  onclick: function () {
    alert(this.name);
  },
  handleEvent: handleEvent
});


// as utility [2]
Handler.add('body', {
  name: 'utility',
  click: function () { alert('clicked ' + this.name); }
});

The advantage is that not only you don't need to store a reference and create a bound version of each method, you can also always remove the listener later on whenever is needed.

// class example
someNode.removeEventListener('click', this);

As long as you have a listener able to drop the handler [3]

Best

[1] WebReflection/ie8 [2] WebReflection/dom-handler [3] www.webreflection.co.uk/blog/2015/10/22/how-to-add-dom-events-listeners#-and-about-that-this-reference-

# Mark Kennedy (3 years ago)

Wow that's so ironic because I posted this same idea (literally copied and pasted) in WHATWG's "DOM land" and they told me this was an es-discuss issue. So which is it? Oh and thanks for the code sample but it uses the old prototypical method of replicating a class by creating a function which is now not the most efficient way (there's the class keyword). And I don't see how what you're doing isn't any different from a roundabout way of what I did. I already know about the handleEvent() stuff, I like that it's available and its polyfills. They are great, but my original question is to implement sugar so that you don't have to use polyfills or the handleEvent().

# Andrea Giammarchi (3 years ago)

Raising a concern about addEventListener and its context probably made me think it was rather a WHATWG concern.

Anyway, I don't know why you had to point out the class keyword ... I mean ....

class SomeClass {
  constructor(someNode) {
    someNode.addEventListener('click', this);
  },
  onclick(e) {
    alert(this.constructor.name); // SomeClass
  },
  handleEvent(e) {
    this['on' + e.type](e);
  }
}

There you go

Best

# Andrea Giammarchi (3 years ago)

uhm, I've used commas ... anyway, the sugar is desugaring to old methods, this is working example:

class SomeClass {
  constructor(someNode) {
    someNode.addEventListener('click', this);
  }
  onclick(e) {
    alert(this.constructor.name); // SomeClass
  }
  handleEvent(e) {
    this['on' + e.type](e);
  }
}

new SomeClass(document.documentElement);

The difference with your example is that you will always be able to remove the instance without needing to store upfront every bound listener.

To know where the addEventListener was set you always have the e.currentTarget so basically you have a weakmap between a node and an object where you can always retrieve the initial node that used the object through the event, keeping the node clean from "expando" links.

More sugar than this, I'm not sure what would be.

You could also have a simple naming convention where every method that starts with on will be set as listener using the current instance, and do the same, if necessary, on teardown/destroy.

What kind of sugar would you use otherwise?

The only proposal discussed so far is el.addEventListener('click', :: this.onClick), unfortunately that doesn't solve anything because AFAIK they don't see any advantage in having ::this.onClick === ::this.onClick which is what I've raised already as "that's what developers would expect" but apparently it's too costy or complicated or ... dunno.

¯_(ツ)_/¯

Best

# Mark Kennedy (3 years ago)

Yes but what happens when you have multiple event targets using the same type of event? You're going to require a lot of extra conditionals in onclick method. Again this approach is cumbersome and, imo, not the most efficient.

These may not be the best solutions but here are a few options I've thought of:

  1. When an arrow function is used as the second parameter to addEventListener, the language can evaluate and use its scoped context when the same function is used with a subsequent removeEventListener call, so essentially the following code would remove the listener appropriately when calling destroy.
class MyClass {
    constructor () {
        someNode.addEventListener('click', (e) => this.onClick(e))
    }

    onClick (e) {
        // do something here
    }

    destroy () {
         someNode.removeEventListener('click', (e) => this.onClick(e))
    }
}

  1. Another solution would be if we could maybe pass a method string as the second parameter and then a context as the fourth parameter to addEventListener and removeEventListener as follows:
class MyClass {
    constructor () {
        someNode.addEventListener('click', 'onClick', {}, this)
    }

    onClick (e) {
        // do something here
    }

    destroy () {
        someNode.removeEventListener('click', 'onClick', {}, this)
    }
}

# Andrea Giammarchi (3 years ago)

not sure I understand. What you are doing is exactly the same, there is no difference whatsoever with what I've shown to you.

Yes but what happens when you have multiple event targets using the same type of event? You're going to require a lot of extra conditionals in onclick method. Again this approach is cumbersome and, imo, not the most efficient.

conditionals what? the onclick is called only when the node where you attached the listener as instance gets invoked. It never triggers in any other cases. Or better, it is exactly the same as someNode.addEventListener('click', (e) => this.onClick(e)) ... really, PRECISELY the same.

Whenever your click gets triggered, the handleEvent would behave exactly the same. Not sure I've stressed the exactly part enough so nothing is cumbersome, maybe you don't understand or you've never used this approach before.

Otherwise, please show me a single example where adding a listener as callback, or bound callback, would be triggered differently from adding an instance with an inherited, or own, handleEvent.

Going on ...

  1. When an arrow function is used as the second parameter to addEventListener, the language can evaluate and use its scoped context ...

This is not going to happen. It's an exception to the arrow that would confuse even more about its context.

  1. Another solution would be if we could maybe pass a method string as

the second parameter and then an optional context as the fourth parameter ...

This is DOM land, since it's about addEventListener signature, and I would personally vote -1 to that variant.

You should really try to understand how handleEvent works, IMO. It's the sugar you're looking for already since it makes you set any listener you want without needing to store upfront the bound method: fast, clean, simple.

To remove a listener at any time, you don't need to store upfront a bound version of the method.

Best

# Andrea Giammarchi (3 years ago)

reading again I think I've got what you meant by different methods triggered through same click listener. Your example with this.listener = this.onClick; made me think you had one click.

In that case, having a state works pretty well or you could simply have a list of methods to be triggered when click happens.

class Click {
  constructor(el) {
    this._click = [this.sayHello, this.sayGoodbye];
    el.addEventListener('click', this);
  }
  sayHello() {
    alert('Hello from ' + this.constructor.name);
  }
  sayGoodbye() {
    alert('Goodbye from ' + this.constructor.name);
  }
  handleEvent(e) {
    this._click.forEach((method) => method.call(this, e));
  }
}

// test
new Click(document.documentElement);

At this point all you have to do is to push, pop, or splice listeners, instead of passing through the DOM each time. You can also copy or pass states around and you'll never need to directly bind each method.

You can always drop all at once simply doing el.removeEventListener('click', this);

If none of my ideas work for you, then you'll need to come up with some better proposal or discuss with WHATWG about a different addEventListener signature.

The only thing that could work on top of my head, thanks to recent changes to the third parameter, is eventually this:

el.addEventListener('click', this.onClick, {context: this});

// with a counterpart
el.removeEventListener('click', this.onClick, {context: this});

AFAIK latest changes to the API do not need you to store that third argument, only its content matters, so that this would be the easiest way to implement what you're after, yet this is DOM-land, so it's in WHATWG ml that this should be discussed.

Hope something helped.

Best

# Mark Kennedy (3 years ago)

Sorry maybe I'm not explaining this the right way. My original intent is to lessen the code that I would have to write to achieve multiple event handlers which do different things on multiple DOM elements when constructing a class and also REMOVING the event listeners manually. I am assuming this is a scenario where the DOM elements are outliving their event listeners (for single page web applications for instance) so they must be removed manually. Let's say I have a few buttons on a page and I want them all to do something different when they are clicked.

<button id="button1">button 1</button>
<button id="button2">button 2</button>
<button id="button3">button 3</button>

With your approach, I would have to do this, right?

class SomeClass {
  constructor() {
    let button1 = document.getElementById('button1');
    button1.addEventListener('click', this);
    let button2 = document.getElementById('button2');
    button2.addEventListener('click', this);
    let button3 = document.getElementById('button3');
    button3.addEventListener('mouseover', this);
  }

  onclick(e) {
   // if e.target is button1,
          // show modal
   // if e.target is button2,
          // navigate backwards
   // i f e.target is button 3,
          // do something else
  }

    onmouseover (e) {
      // if e.target is button 3
          // do something else different from the above
    }

  handleEvent(e) {
    this['on' + e.type](e);
}

It may just be me confused here, but the above code is much more overwhelming and much less intuitive than this.

class MyClass {
    constructor () {
        this.button1 = document.getElementById('button1');
        this.button1.addEventListener('click', this.showModal);
        this.button2 = document.getElementById('button2');
        this.button2.addEventListener('click', this.navigateBack);
        this.button3 = document.getElementById('button3');
        this.button3.addEventListener('mouseover', this.logSomething);
    }

    showModal (e) {
        // do something here
    }
    
    navigateBack () {
        // navigate back
    }
    
    logSomething () {
        // log something
    }

    destroy () {
        this.button1.removeEventListener('click', this.showModal);
        this.button2.removeEventListener('click', this.navigateBack);
        this.button3.removeEventListener('mouseover', this.logSomething);
    }
}

Hopefully this helps clarify a few things.

# Boris Zbarsky (3 years ago)

On 5/9/16 12:37 PM, Mark Kennedy wrote:

and also REMOVING the event listeners manually.

I think this is the key part. What is the precise use case for removing here? I think that affects how this would best be designed.

The obvious case is when you want to remove the listener at the point when it fires. This would be most easily addressed by providing a way to get at the listener from inside itself somehow. Are there other uses?

# Andrea Giammarchi (3 years ago)

Gotcha ... so, you've a JS class coupled with the DOM so, since you use IDs, all you need to eventually do is

onbutton1click(e) {
  // ...
}
onbutton2click(e) {
  // ...
}
onbutton3click(e) {
  // ...
}
handleEvent(e) {
  this['on' + e.curentTarget.id + 'click'](e);
}

I believe you would end up with similar code anyway because without handleEvent you would need three different methods anyway.

Once again, my examples were answering your first question that was: I'd like to set handlers without needing to store the bound reference each time.

Hence all my answers, but I guess it's a matter of personal taste.

Anyway, if it has to be native and different from handleEvent, the el.addEventListener('click', this.onClick, {context: this}); variant would be my best pick.

Best

# Andrea Giammarchi (3 years ago)

Oh well, if removing after first click is the use-case, el.addEventListener('click', this.onClick, {once: true}) is already in the standard pipeline (and polyfilled in dom4)

Best

# Mark Kennedy (3 years ago)

Haha, no problem, Andrea. I think I may have not explained the scenario as clearly in my original post so I apologize for that. But like Boris mentioned, removing the event listeners in a much easier way is also a part of my goal here.

I do like passing context in an argument and if using the third argument is a possibility, that would be nice. I would vote for:

el.addEventListener('click', this.onClick, {context: this});
el.removeEventListener('click', this.onClick, {context: this});

which would 1) decrease the amount of code necessary when adding multiple event listener methods, 2) add more flexibility since you dont have to rely on a handleEvent() delegation method, and 3)allow us to easily remove the listener manually without having to store it in some variable.

# Bob Myers (3 years ago)

I'm confused by this whole thread. There is nothing here that cannot be handled easily by a minimal amount of user glue code, to give just one example:

function listen(element, type, handler) {
  element.addEventListener(type, handler);
  return function() {
    element.removeEventListener(type, handler);
  };
}

var unlisten = listen(myElement, 'click', e => this.handler(e));

unlisten();

To remove after one call:

function listenOnce(element, type, handler) {
  function _handler(e) {
    handler(e);
    element.removeEventListener(type, _handler);
  }
  element.addEventListener(type, _handler);
}

If you want to more easily use the same handler on multiple elements:

function makeHandler(type, handler) {
  return {
    listen(elt)   { elt.addEventListener   (type, handler); },
    unlisten(elt) { elt.removeEventListener(type, handler); }
  };
}

var handler = makeHandler('click', e => this.handleIt(e));

handler.listen(elt);
handler.unlisten(elt);

There are other useful possibilities opened up by using the EventListener interface. None of this requires any change to addEventListener signature as far as I can tell.

And so on.

Bob

# Mark Kennedy (3 years ago)

Thanks, Bob. Unless I'm missing something, this code you wrote will not remove the event listener when unlisten is called. Check out this answer on stackoverflow.

function listen(element, type, handler) {
  element.addEventListener(type, handler);
  return function() {
    element.removeEventListener(type, handler);
  };
}

var unlisten = listen(myElement, 'click', e => this.handler(e));

unlisten();
# Bob Myers (3 years ago)

Yes, you are missing something. This code will absolutely remove the handler, because the handler being passed to both addEventListener and removeEventListener is identical (in the === sense).

# Mark Kennedy (3 years ago)

Oh yeah my apologies, handler is a variable that gets passed to removeEventListener when invoked.

# Mark Kennedy (3 years ago)

I do still feel your examples are still storing a reference to the intended handler function and its a bummer to have to write this sort of "glue" for every set of code that needs it. It would be nice if this was handled by the language automatically.

# Bob Myers (3 years ago)

Yes, they store the handler, precisely so you don't have to do it.

You don't need to rewrite such utilities for every case by any means.