daslicht
daslicht

Reputation: 823

Remove EventListener defined in a class

I am trying to remove an eventListener but it looks like I miss something.

Why is the following code not working, it doesn't remove the event listener from the button.

I also tried binding this to pass the scope, but that didn't work either

class Test {

  eventHandler(e) {
    console.log(e.target.id)
    alert()

    // no effect
    e.target.removeEventListener("click", this.eventHandler)

    // no effect either
    document.getElementById(e.target.id).removeEventListener("click", this.eventHandler)
  }
  constructor() {
    let b = document.getElementById("b")
    b.addEventListener("click", this.eventHandler)

    //b.addEventListener("click", this.eventHandler.bind(this) )
  }
}

new Test()
<button id="b">
click me
</button>

Upvotes: 3

Views: 1059

Answers (2)

Teemu
Teemu

Reputation: 23386

Prototype methods as event handlers are a bit problematic, specifically when you need both, the this value bound to the instance, and the reference to the actual event handler function.

By default, the event queue calls the handler in the context of the element the event was bound to. It's easy to change the context, but that provides you to create a new function, which then is used as the event handler, and that function is not the method in the prototype anymore.

If you want to keep the compact class structure, one way is to define the event handler methods as own properties of the instance, they simply can't be inherited. The simplest way would be to define the methods as arrow functions in the constructor.

class Test {
  constructor() {
    this.eventHandler = e => {
      console.log(e.target.id);
      e.target.removeEventListener("click", this.eventHandler);
    };
    let b = document.getElementById("b");
    b.addEventListener("click", this.eventHandler);
  }
}

new Test();
<button id="b">Click me!</button>

The arrow function keeps the reference to the lexical environment it was defined in, and the event queue can't override the context. Now this in the handler function is correctly bound to the instance, and this.eventHandler refers to the function, which was attached to the event.

A slightly less memoryconsuming option would be to use bind when creating the own property, like this:

class Test {
  constructor() {
    this.eventHandler = this.eventHandler.bind(this);
    let b = document.getElementById("b");
    b.addEventListener("click", this.eventHandler);
  }
  eventHandler (e) {
    console.log(e.target.id);
    e.target.removeEventListener("click", this.eventHandler);
  }
}

Here bind creates a new function object, which then calls the method in the prototype, the actual code of the method is not duplicated. This is loosely similar if you wrote:

this.eventHandler = e => Test.prototype.eventHandler.call(this, e);

It's notable, that when defining an own property with the same name an underlying prototype property has, the prototype property is not overridden, it's only shadowed in the instance, and multiple instances of the class will still work as intended.

Another option is to create your own "event model", which creates a wrapper function (like in the very last code example above) for all events, and stores the reference to that function. The wrapper calls the actual handler with call, which can bind the wanted this value to the event handler. The stored function references are used to remove events. Building such a model is not extremely complex, but it provides a bit knowledge of how the this binding and native event model work.

Upvotes: 2

Peter Seliger
Peter Seliger

Reputation: 13356

The OP's code does not work for two reasons.

  • in one case the prototypal eventHandler misses the correct this context.
  • for a second case of running this.eventHandler.bind(this) one creates a new (handler) function with no saved reference to it. Thus with removeEventHandler one never refers to the correct event handler.

Possible solution ...

function handleTestClickEvent(evt) {

  console.log(evt.currentTarget);
  console.log(this);
  console.log(this.eventHandler);

  // remove the instance specific (`this` context) `eventHandler`.
  evt.currentTarget.removeEventListener('click', this.eventHandler);
}

class Test {
  constructor() {
    // create own eventHandler with bound `this` context.
    this.eventHandler = handleTestClickEvent.bind(this);

    document
      .querySelector('#b')
      .addEventListener('click', this.eventHandler);
  }
}
new Test();
<button id="b">click me</button>

Another possible approach was the usage of an arrow-function based, thus instance-specific, event-handler. Arrow-functions do not support an explicit this binding. They always refer to the context where they are implemented in.

class Test {
  constructor() {
    // arrow-function based, thus instance-specific event-handler.
    this.eventHandler = evt => {

      console.log(evt.currentTarget);
      console.log(this);

      evt.currentTarget.removeEventListener('click', this.eventHandler);
    }
    document
      .querySelector('#b')
      .addEventListener('click', this.eventHandler);
  }
}
new Test();
<button id="b">click me</button>

Nevertheless, both approaches show, that the prototypal implementation of a reference-specific event-handler is not the path one should follow.

For the scenario provided by the OP I would pick the 1st solution for it provides code reuse by the locally implemented handleTestClickEvent. It also comes with a smaller footprint regarding the instance specific this.eventHandler which for the former gets created from handleTestClickEvent.bind(this) whereas the 2nd solution provides a full handler implementation to every single instance.

Upvotes: 1

Related Questions