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:
- 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? - 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. - 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. - 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;
Solution 1:
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:
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.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 theinput
element nested inside thelabel
, or get the element with the ID found in thefor
attribute of thelabel
.One way of fixing the checkboxes return the previous value on change bug is to do a
this.blur()
onfocus
events on checkboxes.When you say "handler", you mean the
onfocusin
event handler, or thechangeDelegator
? It is normal to see afocusin
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".
Solution 2:
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