Category Archives: jQuery

Defensive AJAX and AJAX retries in jQuery

A lot of code that I have seen over the years always assumes success, particularly with AJAX calls. This creates code that is fragile, and entirely dependant on the result of external (to the client) code.

There are a few ways we can attempt to protect against this. The first, is to always depend on the result of the call to make any changes to the DOM or UI. For example, let’s say we have a quantity field in a shopping basket on an e-commerce site. When the user clicks a plus next to the field, we make an AJAX call that increments a value in a basket stored server-side and updates the UI to match. It is very common, and tempting, to increment the value in the UI immediately, and then make the AJAX call. There are a few obvious problems here. If the AJAX call fails, now we have a UI that is inaccurate. We can decrement the value in the UI to make up for it, but now we are starting to confuse the user with the numbers jumping up and down, and if they have clicked the button six times in quick succession (very common use of this kind of control) our chance of error for accurate UI representation of data increases dramatically.

A much safer way to do this is to return the current stored quantity value from the server with each AJAX call, and only update the UI when the AJAX call completes. This results in a less “snappy” feeling UI, and it will be necessary to display some kind of visual cue that something is going on the background, but the user learns quickly how the site works and this is a much more robust process.

Without having any kind of genuine statistics to call upon, I would suggest that nine out of ten AJAX calls that fail are due to an issue that is temporary and would be resolved by a retry. Anything due to network issues, a lost packet somewhere, a brief server glitch, load balancing problems and so on can cause a timeout or 404 without the target resource actually being missing or consistently failing. Often when a website fails to load, I click refresh and there it is. An AJAX call is just the same. (The same applies to database connections, or anything dependant on a network resource.)

Therefore, rather than display an error on an AJAX timeout (“we could not connect to the server” or “there was a problem, please try later”) or worse, doing nothing at all, there are some things we can try to resolve the issue ourselves without bothering the user about it until we’re certain that it is broken.

Let’s look at a typical jQuery AJAX call.

$.ajax({
	url : 'ajaxurl.json',
	type : 'get',
	data : 	{name : 'value'},
	dataType : 'json',
	timeout : 20000,
	success : function(json) {
		//do something
	}
});

The immediate problem with this is it completely assumes (and depends upon) success. There is not even a basic error handler. Something like this is better:

$.ajax({
	url : 'ajaxurl.json',
	type : 'get',
	data : 	{name : 'value'},
	dataType : 'json',
	timeout : 20000,
	success : function(json) {
		//do something
	},
	error : function() {
		alert('Oops! There was a problem, sorry.');
	}
});

The error callback function is actually passed three arguments.

  • The XMLHTTPRequest object in use
  • A textual equivalent of the status
  • The actual exception thrown

These allow us to react in a more sophisticated manner:

$.ajax({
	url : 'ajaxurl.json',
	type : 'get',
	data : 	{name : 'value'},
	dataType : 'json',
	timeout : 20000,
	success : function(json) {
		//do something
	},
	error : function(xhr, textStatus, errorThrown ) {
		if (xhr.status == 500) {
			alert('Oops! There seems to be a server problem, please try again later.');
		} else {
			alert('Oops! There was a problem, sorry.');
		}
	}
});

Since the error function lives inside the AJAX object itself, and is called in that context, the this keyword very usefully points to the jQuery AJAX instance itself. Using this, and the arguments we are being passed we can very easily set the UI to retry the AJAX call on our behalf. Let’s attach some extra properties to the AJAX object:

$.ajax({
	url : 'ajaxurl.json',
	type : 'get',
	data : 	{name : 'value'},
	dataType : 'json',
	timeout : 20000,
	tryCount : 0,
	retryLimit : 3,
	success : function(json) {
		//do something
	},
	error : function(xhr, textStatus, errorThrown ) {
		if (xhr.status == 500) {
			alert('Oops! There seems to be a server problem, please try again later.');
		} else {
			alert('Oops! There was a problem, sorry.');
		}
	}
});

We have added tryCount and retryLimit. These are going to store how many attempts we have made, and how many attempts we will make respectively. Making use of these:

$.ajax({
	url : 'ajaxurl.json',
	type : 'get',
	data : 	{name : 'value'},
	dataType : 'json',
	timeout : 20000,
	tryCount : 0,
	retryLimit : 3,
	success : function(json) {
		//do something
	},
	error : function(xhr, textStatus, errorThrown ) {
		if (textStatus == 'timeout') {
			this.tryCount++;
			if (this.tryCount <= this.retryLimit) {
				//try again
				$.ajax(this);
				return;
			}
			alert('We have tried ' + this.retryLimit + ' times and it is still not working. We give in. Sorry.');
			return;
		}
		if (xhr.status == 500) {
			alert('Oops! There seems to be a server problem, please try again later.');
		} else {
			alert('Oops! There was a problem, sorry.');
		}
	}
});

So, now we are trying three times before giving in. Where I have actually used this, some modal dialogue boxes are used instead of window.alert() and I save a copy of the AJAX object when we reach our retry limit. At that point, although I tell the user we have given in, I still provide them with a button to try again themselves.

I am convinced that implementing techniques like this will rid us of many unnecessary bad user experiences, and many support calls. Keep the user informed (in simple language!), and assume things will fail. I hope this is helpful.

Centre div in window no matter what – Version 2

In a previous post I showed you how to center a div on screen regardless of if it was inside an iframe or not. This is the second version, which should work regardless of if the div is in the top level window, an iframe, or a regular frame.

/******************************************************************
	Name: center
	Description: Center a div on page, no matter what!
	Author: AK (www.zeroedandnoughted.com)
	Date: 22nd April 2009
	Version: 0.2
	Dependencies: jQuery
	Notes:
	******************************************************************/
	(function($) {
	    $.fn.center = function() {
	        return this.each(function() {
				var $this = $(this);
				var frameXOffset = 0, frameYOffset = 0, windowHeight = 0, windowWidth = 0;
				//Are we in a frame?
				if (top.document != window.document) {
					var frm = $('iframe',top.document.body);
					if (frm.length == 0) {
						//regular frame
						frm =  $('frame',top.document.body);
					}
					var i=frm.length;
					while (i--) {
						if (frm[i].contentDocument) {
							doc = frm[i].contentDocument;
						} else {
							doc = frm[i].contentWindow.document;
						}
						if (doc === document) {
							//located our frame!
							frameXOffset = $(frm[i]).offset().left;
							frameYOffset = $(frm[i]).offset().top;
							break;
						}
					};
					if (jQuery.browser.msie) {
						windowWidth = top.window.document.documentElement.clientWidth;
						windowHeight = top.window.document.documentElement.clientHeight;
					} else {
						windowWidth = top.window.innerWidth;
						windowHeight = top.window.innerHeight;
					}
				} else {
					//we are not in a frame
					if (jQuery.browser.msie) {
						windowWidth = window.document.documentElement.clientWidth;
						windowHeight = window.document.documentElement.clientHeight;
					} else {
						windowWidth = window.innerWidth;
						windowHeight = window.innerHeight;
					}
				}
				var elHeight = $this.height();
				var newTop = ((windowHeight/2) - (elHeight/2)) - frameYOffset + $(parent.document.documentElement).scrollTop();
				if ((newTop + elHeight) &amp;amp;amp;amp;gt; $(document).height()) {
					newTop = $(document).height() - elHeight;
				}
				$this.css ({
					left: ((windowWidth/2) - ($this.width()/2)) - frameXOffset + $(parent.document.documentElement).scrollLeft(),
					top: newTop
				});
			});
		};
	})(jQuery);

Centre div in window, in iFrame, from iFrame, without an iFrame ID

Update: This code has been replaced.

Centring a div is not so hard. Centring a div on a window regardless of the windows scroll position is a little harder. Centring a div in the viewport in an iFrame that is full height (i.e. does not scroll) in a window that does scroll when that window is scrolled and you do not know the ID of the iFrame is a bit of a pain in the arse. So I record it here, in case it helps anyone. This function accepts a jQuery object as its only argument.

function centerIt($el) {
	var frm = $('iframe',top.document.body);
	var iframeXOffset = 0, iframeYOffset = 0, windowHeight = 0, windowWidth = 0;
	var i=frm.length;
	while (i--) {
		if (frm[i].contentDocument) {
			doc = frm[i].contentDocument;
		} else {
			doc = frm[i].contentWindow.document;
		}
		if (doc === document) {
			//located our iframe!
			iframeXOffset = $(frm[i]).offset().left;
			iframeYOffset = $(frm[i]).offset().top;
			break;
		}
	};
	if (jQuery.browser.msie) {
		windowWidth = top.window.document.documentElement.clientWidth;
		windowHeight = top.window.document.documentElement.clientHeight;
	} else {
		windowWidth = top.window.innerWidth;
		windowHeight = top.window.innerHeight;
	}
	var elHeight = $el.height();
	var newTop = ((windowHeight/2) - (elHeight/2)) - iframeYOffset + $(parent.document.documentElement).scrollTop();
	if ((newTop + elHeight) > $(document).height()) {
		newTop = $(document).height() - elHeight;
	}
	$el.css ({
		left: ((windowWidth/2) - ($el.width()/2)) - iframeXOffset + $(parent.document.documentElement).scrollLeft(),
		top: newTop
	});
}

Of course, this makes much more sense as a jQuery chainable plug-in:

/******************************************************************
Name: center
Description: Center a div on page, even in an iframe
Author: AK (www.zeroedandnoughted.com)
Date: 8th Jan 2009
Version: 0.1
Dependencies: jQuery
Notes:
******************************************************************/
(function($) {
    $.fn.center = function() {
        return this.each(function() {
			var $this = $(this)
			var frm = $('iframe',top.document.body);
			var iframeXOffset = 0, iframeYOffset = 0, windowHeight = 0, windowWidth = 0;
			var i=frm.length;
			while (i--) {
				if (frm[i].contentDocument) {
					doc = frm[i].contentDocument;
				} else {
					doc = frm[i].contentWindow.document;
				}
				if (doc === document) {
					//located our iframe!
					iframeXOffset = $(frm[i]).offset().left;
					iframeYOffset = $(frm[i]).offset().top;
					break;
				}
			};
			if (jQuery.browser.msie) {
				windowWidth = top.window.document.documentElement.clientWidth;
				windowHeight = top.window.document.documentElement.clientHeight;
			} else {
				windowWidth = top.window.innerWidth;
				windowHeight = top.window.innerHeight;
			}
			var elHeight = $this.height();
			var newTop = ((windowHeight/2) - (elHeight/2)) - iframeYOffset + $(parent.document.documentElement).scrollTop();
			if ((newTop + elHeight) > $(document).height()) {
				newTop = $(document).height() - elHeight;
			}
			$this.css ({
				left: ((windowWidth/2) - ($this.width()/2)) - iframeXOffset + $(parent.document.documentElement).scrollLeft(),
				top: newTop
			});
		});
	};
})(jQuery);

Yes, I’m British. But yes, I spelt it “center”. I hate inconsistency worse than bastardised spelling. Comments welcomed, as always.

Update: It was possible, if you resized the frame, to get the centred box to appear off-screen. This has been fixed.

Resize iFrame to height of content, from iFrame, without an ID

There must be a more graceful way to reference the current iframe without knowing its ID?

function resizeIFrame() {
	var iframe = $('iframe',parent.document.body);
	var doc;
	var iHeightPadding;
	for (var i=0, j=iframe.length; i<j; i++) {
		if (iframe[i].contentDocument) {
			doc = iframe[i].contentDocument;
			iHeightPadding = 35;
		} else {
			doc = iframe[i].contentWindow.document;
			iHeightPadding = 0;
		}
		if (doc === document) {
			//located our iframe!
			$(iframe[i]).height(doc.body.scrollHeight + iHeightPadding);
			break;
		}
	};
}

assertArgumentType – test expected data types against actual data types

My colleague Andrew Beeching gets credit for this one. An amazingly useful piece of code to check that parameters you are receiving match your expectations. It also allows for null values to be passed, where you might be checking arguments that may or may not exist. It uses jQuery to check for parameters that might be functions and this functionality could be fallible as demonstrated in this article from DHTML Kitchen.

This is especially useful for TDD.

function assertArgumentType(expected, actual, allowNull){
        if (expected !== undefined) {
            actual = actual || null;
        }
        if (expected === Function) {
            if (!$.isFunction(actual)) {
                throw "InvalidArgNotAFunctionException";
            }
            // Match OR object is allowed to be null
        }
        else
            if ((allowNull && actual === null) || (actual.constructor && actual.constructor === expected)) {
                return;
            }
            else {
                switch (expected) {
                    case String:
                        throw ("InvalidArgNotAStringException");
                        break;
                    case Number:
                        throw ("InvalidArgNaNException");
                        break;
                    case Boolean:
                        throw ("InvalidArgNotABooleanException");
                        break;
                    case Array:
                        throw ("InvalidArgNotAnArrayException");
                        break;
                    case Object:
                        throw ("InvalidArgNotAnObjectException");
                        break;
                    case undefined:
                        throw ("InvalidArgUndefinedException");
                        break;
                    case null:
                        throw ("InvalidArgNullException");
                        break;
                }
            }
    }

jquery.metadata.js – in reverse!

Stand back, kids!

This requires jQuery, jquery.metadata.js and jquery.json.js. You will also need to modify jquery.json.js so it surrounds JSON names and values in single quotes rather than doubles, and escapes single quotes, but this is trivial.

To use:

jQuery(selector).setadata({‘name’:'value’});

It will also chain your original selection.

/******************************************************************
Name: setadata
Description: Add metadata into elements - or metadata in reverse
Author: AK
Date: 24th June 2008
Version: 0.2
Dependencies: jQuery, jquery.metadata.js, jquery.json.js
Notes:
******************************************************************/
(function($) {
    $.fn.setadata = function(jsonData) {
        return this.each(function() {
			data = '{}';
			var m = /({.*})/.exec(this.className);
            if (m) {
				data = m[1];
				this.className = $.trim(this.className.replace(data, ''));
			}
			if (data.indexOf('{') < 0) data = "{" + data + "}";
            data = eval("(" + data + ")");
			for (var property in jsonData) {
				data[property] = jsonData[property];
			}
			this.className += ' ' + $.toJSON(data);
		});
    };
})(jQuery);

jQuery plugin to emulate “shake” on login failure in OSX login box

Ridiculous, but still:

jQuery.fn.shake = function(intShakes /*Amount of shakes*/, intDistance /*Shake distance*/, intDuration /*Time duration*/) {
  this.each(function() {
    $(this).css({position:'relative'});
    for (var x=1; x<=intShakes; x++) {
      $(this).animate({left:(intDistance*-1)}, (((intDuration/intShakes)/4)))
      .animate({left:intDistance}, ((intDuration/intShakes)/2))
      .animate({left:0}, (((intDuration/intShakes)/4)));
    }
  });
  return this;
};
//example
$(function() {
  $('#btn').click(function() {
    $(this).shake(3, 6, 180);
  });
});