Event Handler Namespace in Vanilla JavaScript
Asked Answered
D

4

46

I'm familiar with namespaces in jQuery's event handlers. I can add an event handler in a specific namespace:

$('#id').on('click.namespace', _handlerFunction);

And then I can remove all event handlers in that namespace:

$('#id').off('.namespace');

The advantage here is that I can remove only the events in this namespace, not any user-added/additional events that should be maintained.

Does anyone have any tips on how I can not use jQuery, but achieve a similar result?

Domenic answered 16/2, 2014 at 22:10 Comment(2)
danml.com/js/events.js has stuff like that: you can select with a regexp. also search bower and microjs for "event" for more stuff like that...Synchro
Related: Namespaced Custom Events TriggerPalladium
P
11

I think you are looking for addEventListener and removeEventListener. You can also define custom events and fire them using dispatchEvent.

However, to remove an event listener, you will need to retain a reference to the event function to remove just the function you want to remove instead of clearing the whole event.

Parapsychology answered 16/2, 2014 at 22:25 Comment(4)
Makes sense. I was so focused on placing the event handler with a namespace, I didn't check the docs on removeEventListener. Thank you.Domenic
How does this help with namespacing an event? can you show an example how one could namespace a mouseup event on the window and then remove just that one (assuming there were others non-namespaced as well)?Palladium
But what if I don't have a reference to the function where I want to removeEventListener? Namespacing was quite convenient in jQuery.Komsa
@Palladium see my answer below.Putrescine
P
44

For anyone still looking for this, I ended up making a helper singleton which keeps track of the function references for me.

class EventHandlerClass {
  constructor() {
    this.functionMap = {};
  }

  addEventListener(event, func) {
    this.functionMap[event] = func;
    document.addEventListener(event.split('.')[0], this.functionMap[event]);
  }

  removeEventListener(event) {
    document.removeEventListener(event.split('.')[0], this.functionMap[event]);
    delete this.functionMap[event];
  }
}

export const EventHandler = new EventHandlerClass();

Then just import EventHandler and use like:

EventHandler.addEventListener('keydown.doop', () => console.log("Doop"));
EventHandler.addEventListener('keydown.wap', () => console.log("Wap"));
EventHandler.removeEventListener('keydown.doop');
// keydown.wap is still bound
Putrescine answered 8/6, 2017 at 3:31 Comment(8)
Beauty is in its simplicity +1Ramification
very nice! saving the reference of the callback function is a nice idea.Sullins
Yeah. I like this one. Cleanest of allGreenfield
@MaxCore yes, developer.mozilla.org/en-US/docs/Web/API/Event/EventTolle
Renamed to remove the conflict with the existing Event APIPutrescine
If you add an event with the same signature without removing the prior, you would lose reference to the previous one, there would be two events on the item, but one bound reference. You need better control on the functionMap.Appenzell
True, though I'd probably suggest adding error handling to alert you if you're trying to bind a new function over an existing event so that you can pick a more specific namespace for your event rather than trying to gracefully handle binding multiple functions to the same event, as removing an event listener from an event which has two functions bound to it is likely to cause bugs if you are not expecting it to unbind both functions.Putrescine
Downvoted. This does not show how to add an event listener to a dom element. This is a global solution for the document.Constituency
P
16

In this solution I've extended the DOM to have on and off methods with the ability to use events namespacing:

var events = {
  on(event, cb, opts){
    if( !this.namespaces ) // save the namespaces on the DOM element itself
      this.namespaces = {};

    this.namespaces[event] = cb;
    var options = opts || false;
    
    this.addEventListener(event.split('.')[0], cb, options);
    return this;
  },
  off(event) {
    this.removeEventListener(event.split('.')[0], this.namespaces[event]);
    delete this.namespaces[event];
    return this;
  }
}

// Extend the DOM with these above custom methods
window.on = Element.prototype.on = events.on;
window.off = Element.prototype.off = events.off;


window
  .on('mousedown.foo', ()=> console.log("namespaced event will be removed after 3s"))
  .on('mousedown.bar', ()=> console.log("event will NOT be removed"))
  .on('mousedown.baz', ()=> console.log("event will fire once"), {once: true});

// after 3 seconds remove the event with `foo` namespace
setTimeout(function(){
    window.off('mousedown.foo')
}, 3000)
Click anywhere 
Palladium answered 8/6, 2017 at 9:36 Comment(4)
I upvoted this, but there are caveats associated with storing js object references in the DOM, as it can lead to memory leaks if you don't know what you are doing. This could happen if you remove one of the elements from the DOM, while it still stores additional references to an event handler, for example. Just make sure you call your element's 'off' method if you intend to remove that element and all should be well. I think most(?) modern(?) garbage collectors are wise to this issue, but there are certainly some (especially older) browsers out there that aren't.Queridas
Related: addEventListener-memory-leaksPalladium
Downvoted this, you didn't provide a way to addEventListener to a dom element. Looks like you added a global event listener to the window. This is not what the OP asked for.Constituency
@Constituency - Yes I did. Read the code :) See demo of same code with DOM element (button)Palladium
P
11

I think you are looking for addEventListener and removeEventListener. You can also define custom events and fire them using dispatchEvent.

However, to remove an event listener, you will need to retain a reference to the event function to remove just the function you want to remove instead of clearing the whole event.

Parapsychology answered 16/2, 2014 at 22:25 Comment(4)
Makes sense. I was so focused on placing the event handler with a namespace, I didn't check the docs on removeEventListener. Thank you.Domenic
How does this help with namespacing an event? can you show an example how one could namespace a mouseup event on the window and then remove just that one (assuming there were others non-namespaced as well)?Palladium
But what if I don't have a reference to the function where I want to removeEventListener? Namespacing was quite convenient in jQuery.Komsa
@Palladium see my answer below.Putrescine
K
2

Approaches to create event group(namespace):

  1. Pass AbortController.signal to the 'signal' property in the 'options' parameter of the addEventListener API.
  2. Atrach all events of the group to separate HTMLElement.
  3. Cache event type, listener and useCapture for each subscription.

Remove all events within a group:

  1. Call AbortController.abort(): all events, that are configured with this AbortController instance 'signal' will be removed.
  2. Remove the HTMLElement: This will destroy all event listeners, associated with the HTMLElement.
  3. Use removeEventListener by passing the type, listener and useCapture configuration of the event.

Note: The 2nd approach requires events to be dispatched for every group EventTarget(HTMLElement). The other approaches allow single EventTarget to be used for event subscription and dispatch.

Kakaaba answered 23/10, 2022 at 11:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.