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

2

14

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;
Byrnie answered 4/7, 2012 at 14:39 Comment(0)
B
3

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

Byrnie answered 20/8, 2012 at 20:31 Comment(0)
H
3

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".

Hendiadys answered 17/8, 2012 at 11:13 Comment(12)
1. Yes, I am sure about that: the change event is fired in IE8, but it doesn't bubble up. It's a known issue 2. the change handler is called in both cases, that's not the problem. However when clicking the label, the handler is called after the checked state has changed, whereas the checked state is about to change when the checkbox is clicked directly. So if target.checked === true, has it just been checked, or is it going to be unchecked? No way of telling. Running out of charsByrnie
3. Adding blur and focus causes IE to crash: note that, because of onchange not bubbling I'm using the onfocusin and onfocusout events. If the handler blurs, and focuses, it'll just keep on calling itself infinitly 4. And by handler I mean both: strictly speaking changeDelegator is just a function, but one I call in the handlers' context (changeDelegator.apply(this,[e]);), so I consider it to be an extention/continuation of the actual handler. I know the focusin is fired when the tab is revived, but it's fired on for each event that is treated by the delegator function. chars....Byrnie
What I mean by the last thing is simply: I click a checkbox (the handler is fired, but the checked state doesn't change!), If I click that checkbox a second time, thén the handler isn't called, but the checked state changes all the same. If I don't click it a second time, but switch tabs and back again, the focusin event appears not to be called for that checkbox I clicked once. Really weird. On the performance front: there is a major difference, especially when not running it on V8. attaching it to each element on the whole page would add up to 333 (just counted them all) elems!chars...Byrnie
It's not the memory I'm worried about, its the scale of the event loop. binding directly would complicate my ajax onreadystatechange callbacks (attaching new handlers), not to mention memory leaks in IE with listeners for non-existing elements. That said, Yes, my code might be challenging to some of my co-workers, so I've taken the time to document it well, and -thankfully- I'm not the only one here loving JS :). Well, those are my reasons why I maintain that individual binding is not an option hereByrnie
Re 1: True; I understood from your question that the change event isn't triggered on the select element unless you focus out, but reading again, I see that what you mean is that you would want to have your custom event dispatcher triggered.Hendiadys
Re 2 and 3: You could use the propertychange event for checkboxes. It correctly fires after the checked property changes, and you'll get the right state from the checkbox.Hendiadys
Re memory leaks: You can fix the memory leaks yourself, you just have to remember to cleanup the event listeners that you registered.Hendiadys
The propertychange event is not an option, because -like onchange- it doesn't bubble, delegetion is therefore not an option. There's more to preventing mem-leaks in IE then just cleaning up event listeners, I dare say I know a thing or two about them. Check my answer, that's just to prevent window.onload from leaking. Sorry...Byrnie
True, propertychange doesn't bubble, but it's a better option that change for checkboxes. Would it work if instead of adding change listeners on all elements, you just add them for one element on focusin, and remove them again on focusout? This way only one element at a time has a local change listener, you don't have to write a lot of custom fragile code, and you clean up event listeners as soon as you don't need them.Hendiadys
nope, that wouldn't work: when checkbox receives focus, 9/10 it's been clicked ==> value has changed already. and the propertychange event wouldn't fire unless I did so manually (defeating the point). The only thing that sort of works is add a second delegator (onclick), that deals with the checkboxes only. However: this only solves 1 of the 4 problems I have, the main thing I would like to fix is the select: if the user selects a certain value he must be redirected instantly.Byrnie
Nope, that's not true, propertychange always comes after focusin. I've set up a jsfiddle to try this out, and I noticed that for the select element the order of events is always focusin, focus, change, propchange (two times), while for the checkbox it depends on where you click: on the label, you get focusin, propchange, focus, and a change at a later time (focusout or click again); on the checkbox itself: focusin, focus, propchange.Hendiadys
now try your fiddle in pure JS, and click the label of the checkbox. Also: Delegate the events, don't bind them directly and then you have a similar setup to mine. Sorry, but I have to ask: are you aware that I'm delegating the event here?Byrnie
B
3

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

Byrnie answered 20/8, 2012 at 20:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.