Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaScript: How to simulate change event in internet explorer (delegation)

UPDATE: (recap, fiddle and bounty)

This question hasn't been getting too much attention, so I'm going to spend some rep on it. I know I tend to be overly verbose in both my answers and questions. That's why I went ahead and set up this fiddle, which is, in my view, a decent representation of the kind of code I'm currently having to use to come close to a bubbling change event. A couple of issues I'm trying to resolve:

  1. The pseudo-change event doesn't fire on a select element, unless it looses focus. In some cases the client should be redirected upon selecting a new value. How do I achieve this?
  2. The handler is called both when the labels are clicked, as well as the checkboxes themselves. In itself that's what you'd expect, but due to the event bubbling it's (AFAIK) impossible to determine which element was clicked. IE's event object doesn't have a realTarget property.
  3. When changing the checked-state of a checkbox in IE by clicking the label, all is well (though it requires some nasty workarounds), but when clicking the checkbox directly, the handler is called, but the checked state remains unchanged, until I click a second time. Then the value changes, but the handler isn't called.
  4. When I switch to a different tab, and back again, the handler is called multiple times. Three times if the checked state actually changed, twice if I clicked the checbox directly only once.

Any information that could help me resolve one or more of the issues above would be greatly appreciated. Please, I didn't forget to add a jQuery tag, I like pure JS, so I'm looking for a pure JS answer.


I've got a web page with well over 250 select elements on it, and 20~30 checkboxes. I also have to track the users' actions, and take appropriate actions. It is therefore quite natural for me to delegate the change event, rather then adding hundreds of listeners, IMHO.

Of course, IE -company policy: IE8 has to be supported- doesn't fire the onchange event when I need it. So I'm trying to fake an onchange event. What I have thus far is working reasonably well apart from 1 thing that really bugs me.
I'm using onfocusin and onfocusout to register the events. In some cases, when the user selects a new value from the a select element, the script should respond immediately. However, as long as the select hasn't lost focus, this won't happen.

Here's what I came up with so far:

i = document.getElementById('content');
if (!i.addEventListener)
{
    i.attachEvent('onfocusin',(function(self)
    {
        return function(e)
        {
            e = e || window.event;
            var target = e.target || e.srcElement;
            switch (target.tagName.toLowerCase())
            {
                case 'input':
                    if (target.getAttribute('type') !== 'checkbox')
                    {
                        return true;
                    }
                    return changeDelegator.apply(self,[e]);//(as is
                case 'select':
                    self.attachEvent('onfocusout',(function(self,current)
                    {
                        return function(e)
                        {
                            e = e || window.event;
                            var target = e.target || e.srcElement;
                            if (target !== current)
                            {
                                return;
                            }
                            self.detachEvent('onfocusout',arguments.callee);//(remove closure
                            return changeDelegator.apply(self,[e]);
                        }
                    })(self,target));
                default: return;
            }
        }
    })(i));//(fake change event, buggy
}
else
{//(to put things into perspective: all major browsers:
    i.addEventListener('change',changeDelegator,false);
}

I've tried attaching another event listener inside the onfocusin handler, bound to the onclick event. It fired the onfocusout event of whatever select has focus ATM. The only problem is, 99.9% of the users will click on a select, so the focusin event fires an onclick, too.

To get round that, I created closure, and passed the current select-in-focus and it's original value to it as arguments. But some users do use their keyboard, and these users often tab to the next select box without changing the value. Each time binding a new onclick listener... I do believe there HAS to be an easier way than to check all e.type's and treat them separately.
Just as an example: the code with an extra onclick listener: all code is the same as the first snippet, so I'm only pasting the case 'select': block

                case 'select':
                    self.attachEvent('onfocusout',(function(self,current)
                    {
                        return function(e)
                        {
                            e = e || window.event;//(IE...
                            var target = e.target || e.srcElement;
                            if (!(target === current))
                            {
                                return;
                            }
                            self.detachEvent('onfocusout',arguments.callee);//(remove closure
                            return changeDelegator.apply(self,[e]);
                        };
                    })(self,target));
                    self.attachEvent('onclick',(function(self,current,oldVal)
                    {
                        return function(e)
                        {
                            e = e || window.event;
                            var target = e.target || e.srcElement;
                            if (target.tagName.toLowerCase() === 'option')
                            {
                                while(target.tagName.toLowerCase() !== 'select')
                                {
                                    target = target.parentNode;
                                }
                            }
                            if (target !== current)
                            {//focus lost, onfocusout triggered anyway:
                                self.detachEvent('onclick',arguments.callee);
                                return;
                            }
                            var val = target.options[target.selectedIndex].innerHTML;//works best in all browsers
                            if (oldVal !== target.options[target.selectedIndex].innerHTML)
                            {
                                self.detachEvent('onclick',arguments.callee);
                                return target.fireEvent('onfocusout');
                            }
                        };
                    })(self,target,target.options[target.selectedIndex].innerHTML));
                default: return;
like image 928
Elias Van Ootegem Avatar asked Jul 04 '12 14:07

Elias Van Ootegem


People also ask

How does Javascript event delegation work?

Event Delegation is basically a pattern to handle events efficiently. Instead of adding an event listener to each and every similar element, we can add an event listener to a parent element and call an event on a particular target using the . target property of the event object.

How do you delegate in Javascript?

The delegate() method attaches one or more event handlers for specified elements that are children of selected elements, and specifies a function to run when the events occur. Event handlers attached using the delegate() method will work for both current and FUTURE elements (like a new element created by a script).

What is event bubbling and delegation in Javascript?

Event Bubbling Part of the event propagation model wherein listeners are fired from the target of the event, up. Event Delegation The process of using event propagation to handle events at a higher level in the DOM than the element on which the event originated.

What are the benefits of event delegation in Javascript?

Benefits: Simplifies initialization and saves memory: no need to add many handlers. Less code: when adding or removing elements, no need to add/remove handlers. DOM modifications: we can mass add/remove elements with innerHTML and the like.


2 Answers

While I agree that it would be better having only one event listener on the whole form instead of many listeners, one for each element, you have to evaluate the costs and benefits of your decision. The benefit of one listener is a reduced memory footprint. The downside is that you have to do such complex code tricks to get around one browser's incorrect implementation of events, increasing the execution time, misusing event types, registering and unregistering event listeners repeatedly, maybe causing confusion to some users.

Coming back to the benefit, the memory footprint isn't so big if you just attach the same function as a listener for all the elements. And memory is something that current computers don't lack.

Even if this doesn't answer your question, I'd advise you to stop trying to make this work, and instead attach listeners on each form element.

To address your points a bit:

  1. Are you sure about that? I've tried the basic example from Microsoft's documentation, and both IE7 and IE8 fire the onchange listener after I click on an option from the dropdown. It even fires when changing the selection with the up/down keys.

  2. While it's true that you can't easily connect an event on a label to the affected checkbox, why would you want to do that? The change event will be triggered on the checkbox anyway, and you should only care about that one. However, if you must get to the checkbox from an event on a label, then you could do that manually. If the event targets a label, then you know that the label is somehow related to an input. Depending on how you use labels, you could either select the input element nested inside the label, or get the element with the ID found in the for attribute of the label.

  3. One way of fixing the checkboxes return the previous value on change bug is to do a this.blur() on focus events on checkboxes.

  4. When you say "handler", you mean the onfocusin event handler, or the changeDelegator? It is normal to see a focusin event fire when reactivating a tab. I don't know for sure why the event is fired more than once, so I'm just guessing: one might be the focus that the active input receives; the second might be the focus that the document itself receives; I have no idea why a third call happens. I don't understand what do you mean by "if the checked state actually changed, [...] if I clicked the checkbox directly only once".

like image 92
Sergiu Dumitriu Avatar answered Sep 30 '22 02:09

Sergiu Dumitriu


Well, I had another crack at it, and I've come up with a fairly decent approach (at work it did the trick - I've tried to replicate the code I wrote but after a few beers it might contain some errors but the spirit remains the same)

window.attachEvent('onload',function ieLoad()
{
    var mainDiv = document.getElementById('main');//main div, from here events will be delegated
    var checks = mainDiv.getElementsByTagName('input');//node list of all inputs
    var checkStates = {};
    for (var i=0;i<checks.length;i++)
    {
        if (checks[i].type === 'checkbox')
        {//get their checked states on load, this object serves as a reference
            checkStates[checks[i].id] = checks[i].checked;
        }
    }
    mainDiv.attachEvent('onfocusin',(function(initState)
    {//initState holds a reference to the checkStates object
        return function(e)
        {
            e = e || window.event;
            var target = e.target || e.srcElement;
            //id of checkboxes used as key, so no checking for tagName or type required
            if (!initState.hasOwnProperty(target.id) || target.checked === initState[target.id])
            {//don't call method if checkstate is unchanged, either. I'll explain in a minute
                return e;
            }
            initState[target.id] = target.checked;//set new checked-state
            changeDelegator.apply(target,[e]);//delegate
        };
    })(checkStates));
    window.detachEvent('onload',ieLoad);//avoid mem-leak with onload handler!
});

I've found out that the focusin events fire twice in some cases for radio's and checkboxes. Using an object that holds the actual checked states of all checkboxes is less expensive than individual handlers, and it allows me to only delegate the event after the value of the element has changed.

The changeDelegator function is only called when needed, but the anon function that I posted here still gets called Waaaay more than I wanted it, but this approach still outperforms the individual handlers-take.

I left out the selects, but I got them working, too (similar take, in the full version of my code the closure has 2 objects, and I made it, so I can flag an id, fire the blur event when needed, and the client is redirected).
At the end of the run, even though I've learned some new tricks, the main thing I take away from this exercise is an even more profound hatred of that ghastly, gritty golem of a thing called IE... But if anybody else might ever want to delegate change events in IE, know that it is (almost) possible

like image 26
Elias Van Ootegem Avatar answered Sep 29 '22 02:09

Elias Van Ootegem