6

How do I remove the click listener I bound to window in the constructor below? I need it to listen on window, and I need access to the button instance inside it.

class MyEl extends HTMLButtonElement {
  constructor() {
    super();
    this.clickCount = 0;
    window.addEventListener('click', this.clickHandler.bind(this));
  }
  
  clickHandler(e) {
    if (e.target === this) {
      this.textContent = `clicked ${++this.clickCount} times`;
      window.removeEventListener('click', this.clickHandler);
    }
  }
  
  disconnectedCallback() {
      window.removeEventListener('click', this.clickHandler);
  }
}

customElements.define('my-el', MyEl, { extends: 'button' });
<button is="my-el" type="button">Click me</button>
connexo
  • 53,704
  • 14
  • 91
  • 128
  • Do you have control of the JavaScript code? – guest271314 Feb 28 '19 at 08:20
  • 1
    Does this answer your question? [Removing event listener which was added with bind](https://stackoverflow.com/questions/11565471/removing-event-listener-which-was-added-with-bind) – caramba Apr 20 '20 at 06:35

3 Answers3

15

It's not possible with your current implementation - every call of .bind creates a new separate function, and you can only call removeEventListener to remove a listener if the passed function is the same (===) as the one passed to addEventListener (just like .includes for arrays, or .has for Sets):

const fn = () => 'foo';
console.log(fn.bind(window) === fn.bind(window));

As a workaround, you could assign the bound function to a property of the instance:

class MyEl extends HTMLButtonElement {
  constructor() {
    super();
    this.clickCount = 0;
    this.boundListener = this.clickHandler.bind(this);
    window.addEventListener('click', this.boundListener);
  }
  
  clickHandler(e) {
    this.textContent = `clicked ${++this.clickCount} times`;
    window.removeEventListener('click', this.boundListener);
  }
}

customElements.define('my-el', MyEl, { extends: 'button' });
<button is="my-el" type="button">Click me</button>
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • So this basically means you cannot use functions defined on the prototype as event listeners/callback for outside calling? – connexo Aug 12 '21 at 10:54
  • Somewhat, the handler function passed to `addEventListener` can only be a method on the prototype if the method doesn't reference `this`. If you need `this`, you need to bind or use a class field, or pass a handler function that isn't on the prototype, like one defined in the constructor where `this` is set properly. – CertainPerformance Aug 13 '21 at 13:28
4

Create a wrapper func for your clickHandler like so.

class MyEl extends HTMLButtonElement {
  constructor() {
    super();
    this.clickCount = 0;
    this.wrapper = e => this.clickHandler.apply(this, e);
    window.addEventListener('click', this.wrapper);
  }
  
  clickHandler(e) {
    this.textContent = `clicked ${++this.clickCount} times`;
    
    window.removeEventListener('click', this.wrapper);
  }
}

customElements.define('my-el', MyEl, { extends: 'button' });
<button is="my-el" type="button">Click me</button>
kemicofa ghost
  • 16,349
  • 8
  • 82
  • 131
  • This is the one that worked for me, but it should be noted that `apply` takes an array of arguments and `call` takes them individually, so I ended up using the latter to pass on the event. – Carlos Garcia Nov 20 '19 at 20:42
  • You dont even need to use apply in that case because the arrow function preserves the scope. Just `e => this.clickHandler(e)` – br4nnigan Jul 01 '21 at 11:24
4

Another pattern is to keep your Listener inside the constructor.

To remove an Event Listener (no matter what pattern) you can add a 'remove' function the moment you create an Event Listener.

Since the remove function is called within the listen scope, it uses the same name and function

pseudo code:

  listen(name , func){
    window.addEventListener(name, func);
    return () => window.removeEventListener( name , func );
  }

  let remove = listen( 'click' , () => alert('BOO!') );

  //cleanup:
  remove();

Run Code Snippet below to see it being used with multiple buttons

Events bubbling UP & shadowDOM

to save you an hour once you do more with events...

Note that WebComponents (ie CustomElements with shadowDOM) need CustomEvents with the composed:true property if you want them to bubble up past its shadowDOM boundary

    new CustomEvent("check", {
      bubbles: true,
      //cancelable: false,
      composed: true       // required to break out of shadowDOM
    });

Removing added Event Listeners

Note: this example does not run on Safari, as Apple refuses to implement extending elements : extends HTMLButtonElement

class MyEl extends HTMLButtonElement {
  constructor() {
    let ME = super();// super() retuns this scope; ME makes code easier to read
    let count = 0;// you do not have to stick everything on the Element
    ME.mute = ME.listen('click' , event => {
      //this function is in constructor scope, so has access to ALL its contents
      if(event.target === ME) //because ALL click events will fire!
        ME.textContent = `clicked ${ME.id} ${++count} times`;
      //if you only want to allow N clicks per button you call ME.mute() here
    });
  }

  listen(name , func){
    window.addEventListener( name , func );
    console.log('added' , name , this.id );
    return () => { // return a Function!
      console.log( 'removeEventListener' , name , 'from' , this.id);
      this.style.opacity=.5;
      window.removeEventListener( name , func );
    }
  }
  eol(){ // End of Life
    this.parentNode.removeChild(this);
  }
  disconnectedCallback() {
      console.log('disconnectedCallback');
      this.mute();
  }
}

customElements.define('my-el', MyEl, { extends: 'button' });
button{
  width:12em;
}
<button id="One" is="my-el" type="button">Click me</button>
<button onclick="One.mute()">Mute</button> 
<button onclick="One.eol()">Delete</button> 
<br>
<button id="Two" is="my-el" type="button">Click me too</button>
<button onclick="Two.disconnectedCallback()">Mute</button> 
<button onclick="Two.eol()">Delete</button> 

Notes:

  • count is not available as this.count but is available to all functions defined IN constructor scope. So it is (kinda) private, only the click function can update it.

  • onclick=Two.disconnectedCallback() just as example that function does NOT remove the element.


Also see: https://pm.dartus.fr/blog/a-complete-guide-on-shadow-dom-and-event-propagation/

Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49
  • Either I don't get it or you missed the crucial `.bind(this)` part when adding the listener (which is the actual cause of my problem). – connexo Mar 01 '19 at 14:58
  • ``bind()`` is about setting scope, since the listener function in this example runs in ``constructor`` scope there is no need to use ``bind()``. I will update the first sentence to say 'another pattern' – Danny '365CSI' Engelman Mar 01 '19 at 15:11
  • .. and if your clickhandler is huge and you have thousands and thousands of buttons and memory is a concern you can ofcourse ``bind`` a class function. The same listen/mute approach for an EventListener applies ► ``ME.mute=Me.listen('click',ME.handler.bind(ME));`` – Danny '365CSI' Engelman Mar 01 '19 at 15:34
  • Unfortunately your understanding of `this` fails you here. The function runs in scope of the element/object the listener is attached to unless you use `.bind(this)`. – connexo Mar 01 '19 at 18:23
  • Sorry,I wasn't clear enough. Here are two different patterns **not** using ``bind()`` https://jsfiddle.net/dannye/a6nvk173/ – Danny '365CSI' Engelman Mar 02 '19 at 10:30