2

I have a function which wraps a function around another one, then attaches it to a element.

function addCustomEvent(element, eventName, handler, useCapture) {
    var wrappedHandler = function () {
        // Do something here.
        handler.call();
    };

    element.addEventListener(eventName, wrappedHandler, useCapture);
}

This works great and I also want to implement this function:

removeCustomEvent(element, eventName, handler, useCapture)

So I want to do something like this.

var clickHandler= function () { /* ... */ };

addCustomEvent(someElement, "click", clickHandler, false);
removeCustomEvent(someElement, "click", clickHandler, false);

There is a problem with this because I don't have a reference to the wrappedHandler in removeCustomEvent.

The only way I can think of now is to keep track of handlers and their corresponding wrappedHandlers in a dictionary so that I can find wrappedHandler from handler within the function, and remove it.

But I'm not fond of this approach because browser must have information about what handlers are attached, so creating a new dictionary seems redundant and waste of memory.

Is there a better, and much cleaner way?

CookieEater
  • 2,237
  • 3
  • 29
  • 54
  • 2
    You could set the wrapper as a property of the wrapped handler (functions are first class objects, so you can extend them). However, that would only work if you don't register the same handler more than once. Otherwise, you would have to store an array of wrappers. – Frédéric Hamidi Jun 20 '14 at 14:16
  • This might be relevant: http://stackoverflow.com/questions/2623118/inspect-attached-event-handlers-for-any-dom-element It looks like their is no good way to get the currently attached event handler(s) for a given element, so I think you will need to store them yourself. – Matt Burland Jun 20 '14 at 14:21
  • @FrédéricHamidi Thanks, that gives me some ideas, actually. I could try something and see if I can come up with something. – CookieEater Jun 20 '14 at 14:32
  • @CookieMonster: You mean you didn't even try anything before posting here? So basically, with my answer, I've done your work for you?? At least show some appreciation for my efforst, even if it is only a comment explaining why you don't think my answer is relevant – Elias Van Ootegem Jun 20 '14 at 15:01
  • You need to calm down. I saw your answer just now. Just because you put some effort into your answer doesn't mean you automatically get upvotes or positive feedback. I give upvotes to answers that are useful or helpful. (And yes, your answer is very helpful and I gave you an upvote.) – CookieEater Jun 26 '14 at 09:20

1 Answers1

2

Personally, I'd simply wrap the addCustomEvent and removeCustomEvent to a single module, and keep an object that tracks the bound handlers. You consider this "a waste of resources", but really, the impact of this approach would be negligible.
The upsides are: you have the beginning of a module that can easily be expanded on, to handle more complex event handlers (like simulating a tab event for mobile devices using the touchstart and touchend events).

An alternative approach would be to unbind the event handler internally, depending on the event object itself.
Then, you'll have to re-write your removeCustomEvent function to trigger a special event, that lets the bound handler know that you want to remove the event listener.

//in the wrappedHandler:
var wrappedHandler = function(e)
{
    e = e || window.event;
    if (e.synthetic === true)
    {
        e.preventDefault();
        e.stopPropagation();
        element.removeEventListener(eventName, wrappedHandler, useCapture);//<-- use closure vars
        return e;//or return false.
    }
    //do normal event
    handler.apply(this, [e]);//pass event object, and call handler in the same context!
};

var removeCustomEvent = function(event, node, capture)
{
    var e, eClass,
        doc = node.ownerDocument || (node.nodeType === (document.DOCUMENT_NODE || 9) ? node : document);
    if (node.dispatchEvent)
    {
        if (event === 'click' || event.indexOf('mouse') >= 0)
            eClass = 'MouseEvents';
        else
            eClass = 'HTMLEvents';
        e = doc.createEvent(eClass);
        e.initEvent(event, !(event === 'change'), true);
        //THIS IS THE TRICK:
        e.synthetic = true;
        node.dispatchEvent(e, true);
        return true;
    }
    if (node.fireEvent)
    {
        e = doc.createEventObject();
        e.synthetic = true;
        node.fireEvent('on' + event, e);
        return true;
    }
    event = 'on' + event;
    return node[event]();
};

here's a version of this code that is actually documented

I've set a synthetic property on the event object that will be passed to the event handler. the handler checks for this property, and if it's set to true, it will unbind the listener and return. This doesn't require you to keep DOM references and handlers in an object, but this is, I think you'll agree, quite a lot of work, too.
It also feels quite hacky, if you don't mind my saying so...

Compared to:

var binderModule = (function()
{
    var module = {},
        eventMap = {},
        addEvent = function (elem, eventName, handler, capture)
        {
            var i, wrappedHandler;
            if (!eventMap.hasOwnProperty(eventName))
                eventMap[eventName] = [];
            for (i=0;i<eventMap[eventName].length;++i)
            {//look for elem reference
                if (eventMap[eventName][i].node === elem)
                    break;
            }
            if (i>= eventMap[eventName].length)
            {
                i = eventMap[eventName].length;//set i to key
                eventMap[eventName].push({
                    node: elem,
                    handlers: []//keep handlers here, in array for multiple handlers
                 });
             }
             wrappedHandler = function(e)
             {
                 //stuff
                  return handler.apply(this, [e || window.event]);//pass arguments!
             };
             eventMap[eventNAme][i].handlers.push(wrappedHandler);
             return elem.addEventListener(eventName, wrappedHandler, capture);
        },
        removeEvent(elem, eventName, capture)
        {
            var i, temp;
            if (!eventMap.hasOwnProperty(eventName))
                return;//no handlers bound, end here
            for (i=0;i<eventMap[eventName].length;++i)
                if (eventMap[eventName][i].node === elem)
                    break;
             if (i < eventMap[eventName].length)
             {//found element, remove listeners!
                 //get handlers
                 temp = eventMap[eventName][i].handlers;
                 //remove element + handlers from eventMap:
                 eventMap[evetnName][i] = undefined;
                 for (i=0;i<temp.length;++i)
                     elem.removeEventListener(eventName, temp[i], capture);
             }
        };
    module.addCustomEvent = addEvent;
    module.removeCustomEvent = removeEvent;
    //or, perhaps better:
    Object.defineProperty(module, 'addCustomEvent', {value: addEvent});//read-only
    Object.defineProperty(module, 'removeCustomEvent', {value: removeEvent});
    return module;
}());

Note that this is the basic setup to keep track of event handlers that are bound to particular DOM nodes, and how to mangage them. This code is not finished and is not tested. It probably contains typo's, syntax errors and some consistency issues. But this should be more than enough to get you started.

Community
  • 1
  • 1
Elias Van Ootegem
  • 74,482
  • 9
  • 111
  • 149