views:

581

answers:

6

So I have yet another little JavaScript issue with my current project: JS events aren't firing in the order that I expect them to.

Let's say I've got a text field with an onBlur event that fires an AJAX call that does some calculating on the server (I'm using .NET 3.5's WebServices with JSON). Its callback method populates a page full of text fields with results from the calculation.

I've also got a button with an onClick event that initiates a different calculation, but it relies on having finished the text field's onBlur event(s) before the results of its onClick event will be valid.

I have a simple mutex in place which works fairly well, until the following situation: The user changes a value in one of the text fields but has not yet left it, so focus is still on the text field. While the text field has focus, they click a button that has an onClick event. More often than not, the browser (IE7 in this case) fires the onClick event of the button BEFORE the onBlur event of the text field!

This causes some havoc, and I have no clue how to enforce the order of the events being fired. My little mutex works as follows: when any of the events are fired, I set a flag that means "wait for the callback." Any subsequent events that fire check the flag before doing anything. In the callback a reset the flag to false.

So, any ideas? I guess I almost want to make all of the web service calls synchronous, but I don't know if there's an easy way to do that.

Other things I've tried or things you should know: - Disabling the buttons in the onBlur of the text fields -- their onClicks still fire before they get disabled - Keep in mind, this only happens when editing a text field and immediately clicking a button. If the user clicks on empty space to fire the onBlur of the text field before clicking the button, it all works perfectly.

EDIT:

After some comments and more testing here's what I'd like to be able to do:

In the onBlur of the text field, I want to disable the button so that its onClick event doesn't fire. However, when I code this up, the onBlur event fires, I can see the button getting disabled, but its onClick event is still firing.

Here's some sample code. Sometimes the onClick event happens, and sometimes it doesn't. If you up the setTimeout to 500ms or so, it seems to always prevent it. Maybe this is an option...

<html>
<head>
<script type="text/javascript">

var onClickFlag = false;

function whatHappensOnFocus(el) {
    log('whatHappensOnFocus(): TextField focused.');
    onClickFlag = false;
    log('whatHappensOnFocus(): Set onClickFlag = false');
}

function whatHappensOnBlur(el) {
    log('whatHappensOnBlur(): TextField blurred.');
    document.getElementById('theButton').disabled = true;
    log('whatHappensOnBlur(): Disabled the Button');
    setTimeout(function() { someTestFieldFunction(); log('whatHappensOnBlur(): OnBlur callback called: someTestFieldFunction()'); }, 50);
    log ('whatHappensOnBlur(): setTimeout for someTestFieldFunction() for 50ms');
}

function log(msg) {
    document.getElementById('log').value += ('[' + (new Date().getTime()).toString() + '] ' + msg + '\n');
}

function someTestFieldFunction() {
    log('someTestFieldFunction(): Inside someTestFieldFunction()');
    log('someTestFieldFunction(): Test: onClickFlag = ' + onClickFlag);
    //alert('you blurred the text field.');
    //alert('onClickFlag = ' + onClickFlag);
    setTimeout(enableButton, 50);
    log('someTestFieldFunction(): setTimeout for enableButton()');
}

function enableButton() {
    document.getElementById('theButton').disabled = false;
    log('enableButton(): Button re-enabled');
    //onClickFlag = false;
}

function whatHappensOnClick(el) {
    log('whatHappensOnClick(): the Button onClick fired');
    someFunction();
    log('whatHappensOnClick(): someFunction() called');
}

function whatHappensOnMouseDown(el) {
    log('whatHappensOnMouseDown(): inside whatHappensOnMouseDown()');
    onClickFlag = true;
    log("whatHappensOnMouseDown(): set onClickFlag = true");
}

function someFunction() {
    log('someFunction(): inside someFunction()');
    log('someFunction(): You have successfully clicked the button');
    //alert("you clicked me! this shouldn't happen!")
}

</script>
</head>
<body>
<form id="testform">
<input type="text" size="10" onfocus="whatHappensOnFocus(this);" onblur="whatHappensOnBlur(this);" />
<br />
<br />
<input id="theButton" type="button" onmousedown="whatHappensOnMouseDown(this);" onmouseout="onClickFlag = false;" value="calculate" onclick="whatHappensOnClick(this);" />
<br />
<br />
<textarea rows="15" cols="100" id="log" style="overflow: scroll"></textarea>
<input type="reset" value="clear" />
</form>
</body>
</html>

UPDATE:

I updated the code I posted to reflect some of the issue, so you can see how I tested this. When you have focus on a text field and then click on a button, the onMouseDown event of the button fires BEFORE the onBlur event of the text field. Because of this, I can set a flag so that I know they tried to click the button. Then in the onSuccess callback event of the AJAX call fired by the onBlur event of the text field, I can see if the button flag is set (so I know they tried to click it) and fire that event (disabling everything else so it can complete). There are only a few buttons on the form that need this behavior, so I think this solution will be sufficient for now.

A: 

The easiest way (and probably the WORST, and by FAR), would be to make the program wait a certain time before proceeding to what you wanted it to do in the first place, this way, you could control the order of the callbacks.

As I said, this is probably the worst way to do it, but it's the only one I can think of. Here what it should look like :

function onClickHandler(callback)
{
    setTimeout(callback, 50);
}

function onBlurHandler(callback)
{
    //...
}

This way, your callback function on onBlur would always happen first.

Gab Royer
Please don't be to harsh on me! I just tried to help!
Gab Royer
I actually thought of something like this as well. I don't think it's the worst solution, as long as it _works_.
Cory Larson
Your honesty should save you from down votes :)
Allen
I posted some code that kind of implements this.... See me comments in the edit as well.
Cory Larson
Wow, I'm kind of amazed you did something similar to what I suggested!
Gab Royer
+2  A: 

Are you using AddEventListener to set up the events? If so, what are you passing in for the third parameter?

This may be helpful:

http://www.quirksmode.org/js/events_order.html

Tim Sylvester
+1  A: 

What about using js to cause the textbox to lose focus after it has finished doing what it needs to?

David Archer
A: 

I doubt the order of events are the source of your problem. I just tried the following code in IE7, IE8 and Chrome and the order of events is always blur then click.

<script>
    function dbg(t) {
        document.getElementById("dbg").value += t + "\n";
    }
</script>
<input type="text" onblur="dbg('text.blur')" />
<input type="button" value="PressMe" onclick="dbg('button.click')" />
<textarea id="dbg" cols="50" rows="10">
</textarea>

If I follow you correctly, you are doing some calls that require other calls completed before. This doesn't seems a very good approach, and you will either encounter bugs or high complexity of code with this approach.

Please try to explain what you do (from functional point of view) and try to find alternative ways of doing it, instead of trying to work out a complex technical approach.

Aleris
Unfortunately, the project is very complex. The fields we see on screen are serialized into groups or are sent individually to a web service that tinkers with values, creates a huge (50kb+) XML file which is forwarded to another web service that talks to a COM application that performs calculations on the XML data. The XML data is returned to the intermediate web service which parses the result back into JSON and returns it to the client for the fields to be updated. Changing one value may change hundreds of others, so it's crucial we not forcing calculations before others are completed.
Cory Larson
It's an integration project -- we added an external calculation engine to an existing web application. That external app only handles one request at a time, and it's only API is XML.
Cory Larson
Then maybe a queue on server that receive the requests and do the work in the received order might be a better approach. Resolving this in UI looks hacky and is surely error prone.
Aleris
A: 

I assume your click event relies on the result of the blur event. In that case, I think the best thing to do is set the button to be disabled (or hide it), and then enable it when your onblur request is finished.

<input type="submit" disabled="disabled" />

And the pseudo-Javascript...

onblur = function() {
  //ajax complete
  mybutton.disabled = false;   
}
Josh Stodola
A: 

The problem with a fixed timeout of 50ms is that you're relying on time to solve the problem. What if the computer is slow for some reason, or you're running on a slow Netbook or an old computer or an old browser or mobile? Maybe you should use setInterval instead of setTimer and keep waiting until you verify that the disabling actually happened.

Nosredna