Recently I’ve found myself repeating a pattern in my Javascripts that works, but I’m not exactly sure why. I’ve tried to rationalize how the underlying system might work, but it’s kind of half-assed and I’m not convinced that it’s accurate in the slightest.
The code looks like this:
<style>
.hidden { display: none; }
.show { display: block !important; }
</style>
<script>
var $input = $('input[type=text].hidden');
$input.addClass('show');
$input.focus();
</script>
That is, all I want to do is display a previously hidden input element and then set focus on it. Seem straightforward, except that this code as its written does not set the focus properly, at least in webkit browsers; focus()
has no effect on an input that is not visible on the screen, and I’m guessing that after the addClass()
the browser is still in the midst of actually adding the class and/or reflowing the layout. It’s Javascript’s asynchronicity run amuck.
My hacky workaround is to wrap the focus call around a setTimeout()
:
$input.addClass('show');
window.setTimeout(function() { $input.focus(); }, 0);
Like most people, I started by setting a “fudge factor” of 100ms or 200ms, in hopes that the browser would be done with whatever it has to do. Of course, it’s wrong to have arbitrary delays in your UI (which wouldn’t even guarantee correctness on slow browsers), so I started shaving ms’s off, until to my surprise 0ms works just as well.
My theory is that setTimeout()
here acts as a blocking function, effectively waiting until the addClass()
is completely done before executing its callback. This kind of makes sense if you remember that plain ol’ Javascript is single-threaded, which means that timing functions like setTimeout()
and setInterval()
don’t promise to run at the exact delayed time, only at first able opportunity after the delayed period.
It just seems wrong that manipulating the DOM is considered an asynchronous event, especially since the DOM API doesn’t have the same callback hooks that other, more obvious async resource APIs have. Even if this were the case, setTimeout()
is totally the wrong function to use, but I don’t know of a better one.
Has anybody else come across this? What are some more elegant solutions smarter people have found?
Don’t forget that 0ms is actually not 0. It’s 4ms or 20ms depending on the browser (if I remember correctly). It’s more than enough to reflow/repaint.
I’m inclined to believe that the wait time you’re describing is the interpreter/JIT compiler running through the rest of the function before running the actual setTimeout() call, rather than it delaying the setTimeout() randomly.
It’s not random: http://www.w3.org/TR/html5/timers.html#timers
For setTimeout():
“If the currently running task is a task that was created by the setTimeout() method, and timeout is less than 4, then increase timeout to 4.”
and for setInterval():
“If timeout is less than 10, then increase timeout to 10.”
Great find! This explains why setTimeout(func, 0) seems to work. I guess the next experiment would be to see whether something that would take >4 ms (or even, say 100 ms) would still be blocked by the setTimeout(); I’d guess no.
Instead of adding another class to your DOM, try removing the hidden class:
$input.removeClass(‘hidden’).focus();
Well, this is more of a toy problem, and I’ve run across other times where there’s less of a workaround (e.g., adding a style directly, hooking up an event handler).
Also, you might consider chaining the code:
$input.addClass(‘show’).focus();
Ideally, focus will only be called once it gets the object(s) returned by addClass — automatically removing any async behaviour.
I don’t think chaining helps; like you say, the problem is that I have to make the focus call happen after addClass has fully executed, and in JS that’s communicated via a callback.
I remember reading that some modern browsers try to reduce the number of reflows/repaints by delaying each (in an attempt to batch changes to the DOM). I suggest you refer to:
http://www.phpied.com/rendering-repaint-reflowrelayout-restyle/
under section “browsers are smart”
Here is how it works:
http://1.bp.blogspot.com/_9Oa_nKSHrrE/S_g5i2evTPI/AAAAAAAAAP4/gKANQuj1Xqo/s1600/scripted-browser.jpg
Your addClass, triggers a reflow, then a repaint and after all that your timeout event occurs, JavaScript is one threaded.
You should watch this: http://video.yahoo.com/video/play?vid=111582
I’m not certain about this.. Just taking a stab in the dark, but would jQuery’s (assuming that’s what your $ is an instance of) show() help here?
Ie. $input.show().focus()?
Maybe jQ does some reflow-waiting magic in that function..