How can I remove an event listener no matter how the callback is defined

For years I ran into problems trying to remove an event listener in JavaScript. Often I would have to create an independent function as the handler. But that is just sloppy and, especially with the addition of arrow functions, just a pain.

I am not after a ONCE solution. This needs to work in all situations no matter HOW the callback is defined. And this needs to be raw JS so anyone can use it.

The following code works fine since the function clickHandler is a unique function and can be used by both addEventListener and removeEventListener:

This example has been updated to show what I have run into in the past

const btnTest = document.getElementById('test');
let rel = null;

function clickHandler() {
  console.info('Clicked on test');
}

function add() {
  if (rel === null) {
    rel = btnTest.addEventListener('click', clickHandler);
  }
}

function remove() {
    btnTest.removeEventListener('click', clickHandler);
}

[...document.querySelectorAll('[cmd]')].forEach(
  el => {
    const cmd = el.getAttribute('cmd');
    if (typeof window[cmd] === 'function') {
      el.addEventListener('click', window[cmd]);
    }
  }
);
<button cmd="add">Add</button>
<button cmd="remove">Remove</button>
<button id="test">Test</button>

You used to be able to do it with arguments.callee:

var el = document.querySelector('#myButton');

el.addEventListener('click', function () {
  console.log('clicked');
  el.removeEventListener('click', arguments.callee); //<-- will not work
});
<button id="myButton">Click</button>

But using an arrow function does not work:

var el = document.querySelector('#myButton');

el.addEventListener('click', () => {
  console.log('clicked');
  el.removeEventListener('click', arguments.callee); //<-- will not work
});
<button id="myButton">Click</button>

Is there a better way??

UPDATE

As stated by @Jonas Wilms this way will work:

 var el = document.querySelector('#myButton');

 el.addEventListener('click', function handler() {
   console.log('clicked');
   el.removeEventListener('click', handler); //<-- will work
 });
<button id="myButton">Click</button>

Unless you need to using binding:

var obj = {
  setup() {
    var el = document.querySelector('#myButton');

    el.addEventListener('click', (function handler() {
      console.log('clicked', Object.keys(this));
      el.removeEventListener('click', handler); //<-- will work
    }).bind(this));
  }
}

obj.setup();
<button id="myButton">Click</button>

The problem is that there are too many ways to provide an event handler to the addEventListener function and your code might break if the way you pass in the function changes in a refactor.

Answers:

Answer

You can NOT use an arrow function or any anonymous function directly and expect to be able to remove the listener.

To remove a listener requires you pass the EXACT SAME ARGUMENTS to removeEventListener as you passed to addEventListener but when you use an anonymous function or an arrow function you do not have access to that function so it's impossible for you to pass it into removeEventListener

works

const anonFunc = () => { console.log("hello"); }
someElem.addEventListener('click', anonFunc);    
someElem.removeEventListener('click', anonFunc);  // same arguments

does not work

someElem.addEventListener('click', () => { console.log("hello"); });    
someElem.removeEventListener('click', ???) // you don't have a reference 
                                           // to the anon function so you
                                           // can't pass the correct arguments
                                           // to remove the listener

your choices are

  • don't use anonymous or arrow functions
  • use a wrappers that will track the arguments for you

One example is @Intervalia closure. He tracks the function and other arguments you passed in and returns a function you can use the remove the listener.

One solution I often use which often fits my needs is a class that tracks all the listeners and remove them all. Instead of a closure it returns an id but it also allows just removing all listeners which I find useful when I build up something now and want to tear it down something later

function ListenerManager() {
  let listeners = {};
  let nextId = 1;

  // Returns an id for the listener. This is easier IMO than
  // the normal remove listener which requires the same arguments as addListener
  this.on = (elem, ...args) => {
    (elem.addEventListener || elem.on || elem.addListener).call(elem, ...args);
    const id = nextId++;
    listeners[id] = {
      elem: elem,
      args: args,
    };
    if (args.length < 2) {
      throw new Error('too few args');
    }
    return id;
  };

  this.remove = (id) => {
    const listener = listeners[id];
    if (listener) {
      delete listener[id];
      const elem = listener.elem;
      (elem.removeEventListener || elem.removeListener).call(elem, ...listener.args);
    }
  };

  this.removeAll = () => {
    const old = listeners;
    listeners = {};
    Object.keys(old).forEach((id) => {
      const listener = old[id];
      if (listener.args < 2) {
        throw new Error('too few args');
      }
      const elem = listener.elem;
      (elem.removeEventListener || elem.removeListener).call(elem, ...listener.args);
    });
  };
}

Usage would be something like

const lm = new ListenerManager();
lm.on(saveElem, 'click', handleSave);
lm.on(newElem, 'click', handleNew);
lm.on(plusElem, 'ciick', handlePlusOne);
const id = lm.on(rangeElem, 'input', handleRangeChange);

lm.remove(id);  // remove the input event on rangeElem

lm.removeAll();  // remove events on all elements managed by this ListenerManager

note the code above is ES6 and would have to be changed to support really old browsers but the ideas are the same.

Answer

Just use a named function expression:

 var el = document.querySelector('#myButton');

 el.addEventListener('click', function handler() {
   console.log('clicked');
   el.removeEventListener('click', handler); //<-- will work
 });

For sure that can be wrapped in a function:

  function once(selector, evt, callback) {
    var el = document.querySelector(selector);

    el.addEventListener(evt, function handler() {
      callback();
      el.removeEventListener(evt, handler); //<-- will work
   });
}

once("#myButton", "clicl", () => {
  // do stuff
 });
Answer

You can use the once option of EventTarget.addEventListener():

Note: supported by all browsers but IE.

var el = document.querySelector('#myButton');

el.addEventListener('click', () => {
  console.log('clicked');
}, { once: true });
<button id="myButton">Click</button>

Answer

There is an easy solution using closures.

By moving the code to both addEventListener and removeEventListener into a single function you can accomplish the task easily:

function ael(el, evt, cb, options) {
  console.log('Adding', evt, 'event listener for', el.outerHTML);
  el.addEventListener(evt, cb, options);
  return function() {
    console.log('Removing', evt, 'event listener for', el.outerHTML);
    el.removeEventListener(evt, cb, options);
  }
}

    const btnTest = document.getElementById('test');
    let rel = null;

    function add() {
      if (rel === null) {
        rel = ael(btnTest, 'click', () => {
          console.info('Clicked on test');
        });
      }
    }

    function remove() {
      if (typeof rel === 'function') {
        rel();
        rel = null;
      }
    }

    function removeAll() {
      rels.forEach(rel => rel());
    }

    const rels = [...document.querySelectorAll('[cmd]')].reduce(
      (rels, el) => {
        const cmd = el.getAttribute('cmd');
        if (typeof window[cmd] === 'function') {
          rels.push(ael(el, 'click', window[cmd]));
        }

        return rels;
      }, []
    );
  <button cmd="add">Add</button>
  <button cmd="remove">Remove</button>
  <button id="test">Test</button>
  <hr/>
  <button cmd="removeAll">Remove All</button>

The function ael above allows the element, the event type and the callback to all be saved in the closure scope of the function. When you call ael it calls addEventListener and then returns a function that will call removeEventListener. Later in your code you call that returned function and it will successfully remove the event listener without worrying about how the callback function was created.

Here is an es6 version:

const ael6 = (el, evt, cb, options) => (el.addEventListener(evt, cb, options), () => el.removeEventListener(evt, cb, options));

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.