Based on the issue in this question (ajaxStop was firing twice), I wrote the following ajaxStop event.
var ajaxCount = 0;
$(document).ajaxStop(function () {
ajaxCount += 1;
console.debug('calling ajaxStop, iteration ' + ajaxCount);
if (ajaxCount == 2) {
$('.fieldLoading').hide();
$('.fieldValue').show();
}
});
9 times out of 10 it works exactly as expected. The debug console shows "calling ajaxStop, iteration 1" as soon as the page loads. Then, after everything else fires, it shows "calling ajaxStop, iteration 2". This is what I expect. However, about 5 or 10 percent of the time it only displays iteration 1, after everything has fired (which means no data is shown).
I suggest adding an ajaxSend() handler to count the number of ajax requests and converting the ajaxStop() to an ajaxComplete(). Rather than performing:
if (ajaxCount == 2)
You can then do:
if (ajaxStopCount == ajaxStartCount)
Additionally, you could modify your counters to count ACTIVE requests (decrement the counter on ajaxComplete, increment it on ajaxSend (your loading dialog might disappear between requests, but will re-appear as soon as another request begins; I wouldn't image much of a delay between hiding/showing, but that depends on your code organization).
Add another handler for errors, and you should be set.
I ended up using a queue like so:
var ajaxQueue = $({});
$.ajaxQueue = function (ajaxOpts) {
// Hold the original complete function.
var oldComplete = ajaxOpts.complete;
// Queue our ajax request.
ajaxQueue.queue(function (next) {
// Create a complete callback to fire the next event in the queue.
ajaxOpts.complete = function () {
// Fire the original complete if it was there.
if (oldComplete) {
oldComplete.apply(this, arguments);
}
// Run the next query in the queue.
next();
};
// Run the query.
$.ajax(ajaxOpts);
});
};
Now I just call all my ajax events as ajaxQueue() instead of ajax(). Then, I have a $(document).ajaxStop() that I use to finish everything up.
Related
I'm using leaflet-ajax to load geoJSON on demand. I want to find the maximum theProperty value so I can use that to scale the feature's fill colors before I add them to the map.
Here's my general approach:
function maxBinPropertyValue(theProperty) {
var theGeoJson = null;
var maxPropertyValue = 0;
var propertyValue = null;
var theGeoJson = new L.GeoJSON.AJAX(binsFileName());
theGeoJson.on('data:loaded', function() {
console.log('The data is loaded');
theGeoJson.eachLayer(function(layer) {
console.log('Looping through the layers');
propertyValue = feature.properties[theProperty];
if (propertyValue > maxPropertyValue) {
maxPropertyValue = propertyValue;
console.log('Max so far: ' + maxPropertyValue);
};
});
});
theGeoJson = null;
console.log('The final maximum value: ' + maxPropertyValue);
return maxPropertyValue;
};
I'm trying to wait for the data:loaded event, then loop through all the features to find the maximum value of theProperty, which is returned to the calling routine.
Except it doesn't work. The first console.log says 'The data is loaded'. The second and third console.logs are never reached, and the fourth and final one reports a value of 0 for maxPropertyValue.
How can I examine all the features in a featureset before styling them, in a way guaranteed to not have asynchronous problems?
PS: I'm pretty sure I can't use onEachFeature: instead of the above approach, because I need to examine every feature's property to determine the maximum value in the set before I can style any of the features.
As for your issue about inspecting your data and retrieving the maximum value, you are indeed facing the classic asynchronous concept of JavaScript.
See How do I return the response from an asynchronous call?
Asynchronism is a problem if not dealt with properly, but an advantage if correctly handled.
To put the concept shortly, you do not manage asynchronism in a "standard" sequential way, but you should rather consider parts of code (callbacks) that are executed at a later time based on events.
Whenever you provide a function as an argument, it is certainly a callback that will be executed at a later time, but very probably much later than the next instructions.
So in your case, your 2nd and 3rd console.log are within a callback, and will be executed once your data is loaded, which will happen much later than your 4th console.log.
As for your next step (styling and adding to map), you actually do not need to perform an extra AJAX call, since you already have all data available in theGeoJson variable. You simply need to refactor / restyle it properly.
It is a good approach to break your problem in small steps indeed.
Good luck!
PS: that being said, ES7 provides async and await functionalities that will emulate a sequential execution for asynchronous functions. But to be able to use those, you need latest browser versions or transpilation, which is probably more work to learn and configure as of today for a beginner than understanding how to work with async JS.
I also had this problem and had to wrap my head around this, so giving an explicit example for solution here;
// make a request with your "url"
var geojsonLayer = new L.GeoJSON.AJAX("url");
// define your functions to interact with data
function thingToDoBeforeLoadingStarts () {
// do stuff
}
function thingToDoForEachFileDownloaded () {
// do stuff
}
function thingToDoAfterAllDownloadEnds () {
// do stuff
}
// attach listeners
geojsonlayer.on("data:loading",thingToDoBeforeLoadingStarts);
geojsonLayer.on("data:progress",thingToDoForEachFileDownloaded)
geojsonLayer.on("data:loaded",thingToDoAfterAllDownloadEnds);
I'm writing a test for my angular app.
I'm using protractor with jasmine.
The process in the page is as follows:
1) click a button|
2) handler creates a div with content : bar|
3) element(by.id('foo')).getText().then(function(data) { var some-var-i-declared-earlier = data });|
4) click a button which removes this div from DOM|
5) expect assertation for this element value.|
the problem will this promise is resolved the element is not present and thus I get null.
if i do expect (which resolves the promise) before I hit the button which removes the div I can see the value I need
the problem is that the flow must remove the div before i do expect.
how can I force the promise to resolve to get it's value?
of course any other solution is very welcome.
thanks in advance!
1) click a button| 2) handler creates a div with content : bar| 3) element(by.id('foo')).getText().then(function(data) { var some-var-i-declared-earlier = data });| 4) click a button which removes this div from DOM| 5) expect assertation for this element value.|
element(by...).click(); // step 1
var text = element(by.id('foo')).getText(); // step 3
element(by...).click(); // step 4
expect(text).toEqual('the-right-value'); // step 5
The reason why your original isn't working when you just assign the text data to some-var-i-declared-earlier is because this value is assigned in a promise. This means that by the time you reach the assert statement, the assignment hasn't been done yet (read https://github.com/angular/protractor/blob/master/docs/control-flow.md). In fact, step 1 hasn't even been executed by the time your assertion runs.
Alternatively you can do this (which helps you understand what's going on, but is less concise compared to the first answer):
element(by...).click(); // step 1
var some-var-i-declared-earlier = null;
var text = element(by.id('foo')).getText().then(function(data) {
some-var-i-declared-earlier = data
}); // step 3
element(by...).click().then(function() { // step 4
// the fact that this is run inside the 'then' makes sure it executes at the end of step 4
expect(some-var-i-declared-earlier).toEqual('the-right-value'); // step 5
});
I have a web application that submits an ajax request (to start an IR LED sending data) onmousedown, and stops it onmouseup. Generally works.
However if one is clicking fast I get the stop command going to the LED control before the start command. I added code to track order and the actual event submission (open on XMLHttpRequest) is in the proper order (and they are async). Then I instrumented the onstatechange to log each state and a time stamp when that state change occurs, and what I see is occasionally while the started (0) and server connection initialized (1) states occur in the proper order, the "received" (2) and subsequent states occur out of order.
My first question is whether these events are SUPPOSED to occur in the order submitted?
And if so, any idea what could cause them not to?
And if processing order is not assured, is there any way to force it (short of building my own queue and allowing only one ajax call to be outstanding, which is a paint)?
To demonstrate the issue (with client on Windows 8.1 and Chrome 39.0.2171.71m as well as IE 11.0.14, server on a raspberry pi running php 5.4.4-14+deb7u11 and Apache/2.2.22), here's a log captured from a series of events. It's small and a bit obscure, so I tried to color code each click (green, blue, orange in sequence), and the state code is in the 3rd column. Notice up (highlighted in yellow) precedes the down in the 3rd click once it gets to state 2.
Next is the code that submits it. I believe a lot of it to be irrelevant to the problem but am including it just in case. Note "RemoteFunc" is called both on mouseup and mousedown as well as (for separate objects) plain click, but each with a different func, then a POST is done. Each object either uses mouseup/mousedown or onclick, never both.
The processing time for the post can easily be greater then the duration of a mouse click, so it is not unexpected for a new "down" to come before the first "down" or "up" completes, but the issue is that I want them to process in order. And indeed they seem to be submitting in order and starting (going to state 1) in order, but are received out of order, so I assume ajax is creating multiple connections, and the response to those connections is getting out of order.
Which makes me suspect that order is not guaranteed. But I haven't found anything to confirm that (other than absence of a guarantee to be in order).
Any easy solution other than to manually build a queue and single stream the requests?
function RemoteFunc(str,func)
{
var xmlhttp=new XMLHttpRequest();
if(event.button!=0)
{
AddMessageEntry(str + " dismissed - not left click",true);
return true; //Only respond to left mouse down so context menus do not fire click and cause a competing event
}
if(taskrunning!="")
{
AddMessageEntry(str + " ignored, " + taskrunning + " already running",false);
return true;
}
if(func=="remoteup.cgi") { updown=" mouse-up"; }
else if (func=="remotedown.cgi") { updown=" mouse-down"; }
else { updown=""; }
AddMessageEntry(str + updown + " Started",true);
xmlhttp.onreadystatechange=function()
{
// if (xmlhttp.readyState==4)
// {
if(func=="remoteup.cgi") { updown=" mouse-up"; } // because this can run in overlapping functions we need to recalculate this here
else if (func="remotedown.cgi") { updown=" mouse-down"; }
else { updown=""; }
AddMessageEntry(str + updown + " " + states[xmlhttp.readyState] + " " + xmlhttp.responseText,false);
if(xmlhttp.readyState==4) {taskrunning="";}
// }
}
if(event.type.toString()=="click") // We only need to track running tasks invoked with a click -- mousedown/up are async since they run continually (from down) until stopped (from up)
{
taskrunning=str;
}
xmlhttp.open("POST","cgi-bin/" + func + "?Button="+str,true);
xmlhttp.send();
}
I'm working with along with an online tutorial and trying to understand the code below. What I don't get is why this works more than two times. When the loop has been executed two times i == len and the condition i < len isn't true anymore. So how come it's possible to toggle the different clases more than two times?
My guess is that when the condition is false i gets set to 0 again, did I understand that correctly? Hope someone can help me, I didn't find an explanation of this particular problem anywhere online.
HTML
<button>Normal</button>
<button>Changed</button>
CSS
.normal {background-color: white;color:black;}
.changed {background-color: black;color:white;}
JavaScript
(function() {
var buttons = document.getElementsByTagName("button");
for (var i = 0, len = buttons.length; i < len; i +=1)
buttons[i].onclick = function() {
var className = this.innerHTML.toLowerCase();
document.body.className = className;
}}
}());
The for loop gets excecuted only once and iterates through all buttons.
In the for loops body, you define an onclick function for each button.
So, before you can click anywhere the loop already has finished, and added an onclick function to each single button, which will be called everytime, you click on that button.
With button[i].onclick = function() {...} you add an event handler function to the buttons click event.
You should read more about event handlers in general.
I have a form with 2 text inputs and 2 span controls. Normally, when textbox A is changed an event is fired to change span A, and when textbox B is changed, an event is fired to change span B.
However, in one particualar case I would like a change either textbox A or textbox B to update both span A and B. I tried wiring the events up to the corresponding controls in this case, but it didn't work because there is much state that is set up in the event building code (not to mention each event calls 'this', which would make the logic use the wrong control if it were fired from a different one than it was intended).
To make things easy, it would be best to pass a string (representing the other text input id) to the event handler at the time it is created, and then calling the change() event manually on the second control. However, this puts things in an infinite loop of recursion. I thought of a way to get around the recursion, but it reqires a global variable.
Is there a better way than this, preferably one that doesn't require a global variable?
ml_inEvent = false;
$ctlToVal.bind('keyup change', {feedbackCtl: ml_feedback, controlsToMonitor: ary, validationGroup: this.validationGroup, controlToFire: ctlToFire}, function(event) {
// Other processing happens here...
if (event.data.controlToFire != '') {
var $controlToFire = $('#' + event.data.controlToFire);
if ($controlToFire.length) {
// Use a global variable to ensure this event is not fired again
// as a result of calling the other one
if (!ml_inEvent) {
ml_inEvent = true;
$controlToFire.change();
ml_inEvent = false;
}
}
}
});
You can use the extraParameters argument on .trigger() to break out, for example:
$ctlToVal.bind('keyup change', {feedbackCtl: ml_feedback, controlsToMonitor: ary, validationGroup: this.validationGroup, controlToFire: ctlToFire}, function(event, fire) {
// Other processing happens here...
if(fire !== false) $('#' + event.data.controlToFire).trigger('change', false);
});
You can give it a try here. What this does is the event handler callback not only receives the event object but also any other arguments you pass in, in this case we're just using a simple false and a !=== check this in important so undefined (the parameter not passed at all) still changes both controls. So for instance $("#control").change() would change both controls, but still not loop...you can test that here.