How to removeEventListener that is addEventListener with anonymous function?
Asked Answered
I

6

93
function doSomethingWith(param)
{
    document.body.addEventListener(
        'scroll',
        function()
        {
            document.write(param);
        },
        false
    ); // An event that I want to remove later
}
setTimeout(
    function()
    {
        document.body.removeEventListener('scroll', HANDLER ,false);
            // What HANDLER should I specify to remove the anonymous handler above?
    },
    3000
);
doSomethingWith('Test. ');
Invalid answered 14/4, 2011 at 7:51 Comment(1)
Possible duplicate of JavaScript: remove event listenerCathar
D
118

You can't. You have to use a named function or store the reference somehow.

var handler;

function doSomethingWith(param) {
    handler = function(){
        document.write(param);
    };  
    document.body.addEventListener('scroll', handler,false);
}
setTimeout(function() {
     document.body.removeEventListener('scroll', handler ,false);
}, 3000);

The best would be to do this in a structured way, so that you can identify different handlers and remove them. In the example above, you obviously could only remove the last handler.

Update:

You could create your own handler handler (:)) :

var Handler = (function(){
    var i = 1,
        listeners = {};

    return {
        addListener: function(element, event, handler, capture) {
            element.addEventListener(event, handler, capture);
            listeners[i] = {element: element, 
                             event: event, 
                             handler: handler, 
                             capture: capture};
            return i++;
        },
        removeListener: function(id) {
            if(id in listeners) {
                var h = listeners[id];
                h.element.removeEventListener(h.event, h.handler, h.capture);
                delete listeners[id];
            }
        }
    };
}());

Then you can use it with:

function doSomethingWith(param) {
    return Handler.addListener(document.body, 'scroll', function() {
        document.write(param);
    }, false);
}

var handler = doSomethingWith('Test. ');

setTimeout(function() {
     Handler.removeListener(handler);
}, 3000);

DEMO

Delorasdelorenzo answered 14/4, 2011 at 7:54 Comment(7)
Could you explain what the structured way is? My English skill is not good enough to understand that... Thanks.Invalid
@Japboy: You're welcome :) I just noticed a slight mistake and fixed it.Delorasdelorenzo
Why would you wrap the addListener and removeListener?Sahara
@useless: Do you mean the self-invoking function? To keep i and listeners "private".Delorasdelorenzo
Can you update the functions so that it doesn't leak memory? Or at least state that it does?Libriform
@Bergi: Done. Let me know if I missed something.Delorasdelorenzo
Nice answer +1 but the code is old school with room for improvements, see my answer (which provides both JS and TypeScript solutions)Hebel
B
11

You can't, you need a reference to the function:

function doSomethingWith(param) {
   var fn = function(){ document.write(param); };
   document.body.addEventListener('scroll', fn, false);
   setTimeout(function(){ document.body.removeEventListener('scroll', fn, false); }, 3000);
}
doSomethingWith('Test. ');
Benavides answered 14/4, 2011 at 7:59 Comment(2)
how do u pass event object?Abbyabbye
@Abbyabbye var fn = function(event){ document.write(param); };Fellow
H
7

You could also do this like that:

const ownAddEventListener = (scope, type, handler, capture) => {
  scope.addEventListener(type, handler, capture);
  return () => {
    scope.removeEventListener(type, handler, capture);    
  }
}

Then you can remove the event listener like this:

// Add event listener
const disposer = ownAddEventListener(document.body, 'scroll', () => { 
  // do something
}, false);

// Remove event listener
disposer();
Hermaphroditus answered 13/7, 2017 at 16:17 Comment(2)
Is it possible to determine which events have been bound on the object like this?Taiga
You could just add some properties to the function, like the type, scope etc. const disposerFn = () => { scope.removeEventListener(type, handler, capture); } disposerFn.type = type; return disposerFn;Hermaphroditus
H
4

This significantly improves upon the top @FelixKling answer. Improvements are:

(1) Aligned with MDN docs which refer to type, listener, useCapture

(2) Removes old school IIFE and adopts a class based approach

(3) Added addEventListenerById method which supports using your own custom ID (and avoids the need to retrieve ID from return value)

(4) removeEventListener method returns ID of removed listener or null

(5) Added length method to return the number of active listeners

(6) Added SO code snippet (to prove it works)

Code follows:

class Listeners {

  #listeners = {} // # in a JS class signifies private 
  #idx = 1

  // add event listener, returns integer ID of new listener
  addEventListener(element, type, listener, useCapture = false) {
    this.#privateAddEventListener(element, this.#idx, type, listener, useCapture)
    return this.#idx++
  }
  
  // add event listener with custom ID (avoids need to retrieve return ID since you are providing it yourself)
  addEventListenerById(element, id, type, listener, useCapture = false) {
    this.#privateAddEventListener(element, id, type, listener, useCapture)
    return id
  }

  #privateAddEventListener(element, id, type, listener, useCapture) {
    if (this.#listeners[id]) throw Error(`A listener with id ${id} already exists`)
    element.addEventListener(type, listener, useCapture)
    this.#listeners[id] = {element, type, listener, useCapture}
  }

  // remove event listener with given ID, returns ID of removed listener or null (if listener with given ID does not exist)
  removeEventListener(id) {
    const listen = this.#listeners[id]
    if (listen) {
      listen.element.removeEventListener(listen.type, listen.listener, listen.useCapture)
      delete this.#listeners[id]
    }
    return !!listen ? id : null
  }

  // returns number of events listeners
  length() {
    return Object.keys(this.#listeners).length
  }
}

// For demo purposes only, a button to which the click listeners will be attached
const testBtn = document.querySelector('.test-button')

// Usage
const listeners = new Listeners() // in a modular environment ... export const listeners = new Listeners()

const listener1 = listeners.addEventListener(testBtn, 'click', e => console.log('hello from click listener #1', e.offsetX, e.offsetY))
const listener2 = listeners.addEventListener(testBtn, 'click', e => console.log('hello from click listener #2', e.offsetX, e.offsetY))
listeners.addEventListenerById(testBtn, 'listener-id', 'click', e => console.log('hello from click listener #listener-id', e.offsetX, e.offsetY))

// Click function for the 3 remove listener buttons (not for the testBtn to which the listeners are attached)
const onRemoveClick = (id) => {
  const removed = listeners.removeEventListener(id)
  if (removed == null) {
    console.log(`cannot remove listener #${id} (does not exist)`)
  } else {
    console.log(`ID of removed listener #${removed}`)
  }
  const listenersCount = listeners.length()
  console.log(`there are ${listenersCount} listeners still listening`)
}
.test-button {
  width: 35%;
  height: 40px;
  float: left;
}

.remove-listener-button {
  width: 15%;
  height: 40px;
  float: left;
}
<button class="test-button">click to prove multiple listeners are listening to my click</button>

<button class="remove-listener-button" onclick="onRemoveClick(1)">remove listener #1</button>
<button class="remove-listener-button" onclick="onRemoveClick(2)">remove listener #2</button>
<button class="remove-listener-button" onclick="onRemoveClick('listener-id')">remove #listener-id</button>

FOR TYPESCRIPT LOVERS

Here's the same in TypeScript with some enhancements to the above JS version:

(1) removeEventListener method returns details of the removed listener (including ID) as opposed to only the ID of the removed listener

(2) Added ids method which returns the ids of all active listeners

(3) Added 3 methods addEventListeners addEventListenersByIds removeEventListeners which allow you to add / remove multiple listeners in one call (see usage example below)

(4) Added removeAllEventListeners and destroy methods for cleanup (essentially these 2 are the same but the latter does not return a value)

Here's the TypeScript code (just copy and paste into a new .ts file):

interface IInternalListener {
  element: HTMLElement
  id: string
  type: string
  listener: EventListenerOrEventListenerObject
  useCapture: boolean
}

export interface IListener extends Omit<IInternalListener, 'id'> {
  id: string | number
}

class Listeners {

  #listeners: { [key: string]: IInternalListener } = {}
  #idx = 1 // # in a JS class signifies private

  // add event listener, returns integer ID of new listener
  addEventListener(element: HTMLElement, type: string, listener: EventListenerOrEventListenerObject, useCapture = false): number {
    this.#privateAddEventListener(element, this.#idx.toString(), type, listener, useCapture)
    return this.#idx++
  }

  addEventListeners(element: HTMLElement, types: string[], listener: EventListenerOrEventListenerObject, useCapture = false): number[] {
    const returnIds: number[] = []
    types.forEach((type: string) => {
      const returnId: number = this.addEventListener(element, type, listener, useCapture)
      returnIds.push(returnId)
    })
    return returnIds
  }

  // add event listener with custom ID (avoids need to retrieve return ID since you are providing it yourself)
  addEventListenerById(element: HTMLElement, id: string | number, type: string, listener: EventListenerOrEventListenerObject, useCapture = false): string | number { // eslint-disable-line max-len
    return this.#privateAddEventListener(element, id.toString(), type, listener, useCapture)
  }

  addEventListenersByIds(element: HTMLElement, ids: Array<string | number>, types: string[], listener: EventListenerOrEventListenerObject, useCapture = false): Array<string | number> { // eslint-disable-line max-len
    const returnIds: Array<string | number> = []
    if (ids.length !== types.length) throw Error(`Cannot add ${types.length} event listeners using ${ids.length} ids - ids and types must be of equal length`)
    types.forEach((type: string, idx: number) => {
      const id: string | number = ids[idx]
      const returnId: string | number = this.addEventListenerById(element, id, type, listener, useCapture)
      returnIds.push(returnId)
    })
    return returnIds
  }

  // remove event listener with given ID, returns removed listener or null (if listener with given ID does not exist)
  removeEventListener(id: string | number): IListener | null {
    const strId: string = id.toString()
    const internalListener: IInternalListener = this.#listeners[strId]
    if (internalListener) {
      internalListener.element.removeEventListener(internalListener.type, internalListener.listener, internalListener.useCapture)
      const listener: IListener = this.#privateGetListener(internalListener)
      delete this.#listeners[strId]
      return listener
    }
    return null
  }

  // noinspection JSUnusedGlobalSymbols
  removeEventListeners(ids: Array<string | number>): Array<IListener | null> {
    const returnListeners: Array<IListener | null> = []
    ids.forEach((id: string | number) => {
      const returnListener: IListener | null = this.removeEventListener(id)
      returnListeners.push(returnListener)
    })
    return returnListeners
  }

  // removes all event listeners and resets idx
  removeAllEventListeners(): Array<IListener | null> {
    const ids: Array<string | number> = this.ids()
    const returnListeners: Array<IListener | null> = this.removeEventListeners(ids)
    this.#idx = 1
    return returnListeners
  }

  // same as removeAllEventListeners but no return value
  destroy(): void {
    this.removeAllEventListeners()
  }

  // returns ids of events listeners
  ids(): Array<string | number> {
    const ids: string[] = Object.keys(this.#listeners)
    return ids.map(id => this.#privateExternalId(id))
  }

  // returns number of events listeners
  length(): number {
    return Object.keys(this.#listeners).length
  }

  #privateAddEventListener(element: HTMLElement, id: string, type: string, listener: EventListenerOrEventListenerObject, useCapture: boolean): string | number {
    if (this.#listeners[id]) throw Error(`A listener with id ${id} already exists`)
    element.addEventListener(type, listener, useCapture)
    this.#listeners[id] = { id, element, type, listener, useCapture }
    return this.#privateExternalId(id)
  }

  #privateGetListener(listener: IInternalListener): IListener {
    return {
      ...listener,
      id: this.#privateExternalId(listener.id)
    }
  }

  #privateExternalId(id: string): string | number {
    const idIsInteger = /^\d+$/.test(id)
    if (idIsInteger) return parseInt(id, 10)
    return id
  }
}

export const listeners: Listeners = new Listeners()

And TypeScript usage:

import { listeners, IListener } from './your-path/listeners'

// Add and remove listener
const listenerId: number = listeners.addEventListener(mainVideo, 'timeupdate', (evt: Event) => console.log('hi', evt))
listeners.removeEventListener(listenerId)

// Add and remove listener by custom ID
listeners.addEventListenerById(mainVideo, 'custom-id', 'timeupdate', (evt: Event) => console.log('hello', evt))
listeners.removeEventListener('custom-id')

// Log id of all active listeners
console.log(listeners.ids())

// Log active listeners count
console.log(listeners.length())

// Get details of removed listener
listeners.addEventListenerById(mainVideo, 'fred', 'timeupdate', (evt: Event) => console.log('bye', evt))
const removedListener: IListener | null = listeners.removeEventListener('fred')
console.log('removed listener was', removedListener)

// clean up
const removedListeners: Array<IListener | null> = listeners.removeAllEventListeners()
console.log('removed listeners were', removedListeners)

// simple quick clean up
listeners.destroy()

Advanced TypeScript usage for adding / removing multiple listeners in one call:

// Add multiple event listeners
const listenerIds: number[] = listeners.addEventListeners(this.video, ['timeupdate', 'seeking', 'pause', 'play', 'playing'], (evt: Event) => {
  const target = evt.target as HTMLVideoElement
  this.currentTime = target.currentTime
})
console.log(listenerIds)

// Add multiple event listeners with custom IDs
listeners.addEventListenersByIds(this.video, ['id-one', 'id-two', 'id-three'], ['timeupdate', 'seeking', 'pause'], (evt: Event) => {
  console.log(evt)
})

// Remove multiple event listeners
const removedListeners: Array<IListener | null> = listeners.removeEventListeners([...listenerIds, 'id-two'])
console.log(removedListeners)
Hebel answered 21/9, 2021 at 17:23 Comment(0)
D
3

if you dont have to support IE, you can use the once option

[Element].addEventListener('click', () => {...}, {
  capture: false,
  once: true
});
Deontology answered 15/8, 2019 at 13:9 Comment(0)
P
0

For completeness sake, since many people seem to look for this post:

You can directly use an abort controller to abort the event handler on an anonymous function, instead of going over removeEventListener()

In your example:

const controller = new AbortController();
const signal = controller.signal;

function doSomethingWith(param) {
    document.body.addEventListener(
        'scroll',
        function()
        {
            document.write(param);
        },
        {signal}
    ); // An event that I want to remove later }

setTimeout(
    function()
    {
        controller.abort();
    },
    3000 );
doSomethingWith('Test. ');
Peirsen answered 1/3 at 13:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.