Create a Deferred Wrapper around jQuery Ajax
In the applications I help build, jQuery is a large part of our custom framework that makes it all possible. In our framework, we always had a wrapper around jQuery's $.ajax so we could have our own success and failure logic, and to abstract away some of the lower level code from developers that didn't really need to know about the implementation.
Now, when jQuery 1.5 came out with the changes to ajax that included the Deferred syntax, it soon became apparent that our current implementation of the ajax wrapper was no longer suitable. With deferred methods available from anywhere, we could no longer control the data that became the arguments of those methods.
Today, I'll go over how I solved this problem, by creating a new wrapper around jQuery's $.ajax with the new $.Deferred. I'll keep this generic enough for you to be able to use for your own purposes.
First, let's start with our application's ajax module: Application.ajax. I want to be able to pass it an object of options just like jQuery's ajax:
Application.ajax = function(options) { };
Next, our ajax wrapper needs to be able to call the deferred methods just like jQuery's ajax, so let's create a Deferred object and return a promise.
Application.ajax = function (options) { var deferred = $.Deferred(function (d) { // implementation going here }); var promise = deferred.promise(); return promise; };
Even though we are returning a Deferred, we don't have those cool .success and .error methods. We need those on our ajax too, and all they really do is just map to the .done and .fail methods of the Deferred's promise. A little syntactic sugar is all we need; for simplicities sake we'll add these methods directly to the promise:
Application.ajax = function (options) { var promise = $.Deferred(function (d) { // implementation going here }); var promise = deferred.promise(); promise.success = deferred.done; promise.error = deferred.fail; promise.complete = deferred.done; return promise; };
Cool, things are starting to come together. Basically we are telling our deferred that when the success method is called, execute the same function that the Deferred uses for done. Same thing goes for error and complete.
Continuing on the implementation, let's get a unified settings object so we can get ready for a request:
Application.ajax = function (options) { var deferred = $.Deferred(function (d) { var defaults = { cache: false, type: 'post', traditional: true, dataType: 'json' }, settings = $.extend({}, defaults, options); d.done(settings.success); d.fail(settings.error); d.done(settings.complete); }); var promise = deferred.promise(); promise.success = deferred.done; promise.error = deferred.fail; promise.complete = deferred.done; return promise; };
What's going on here? First, I am creating a new settings object which is a merger between our predefined default options and the options argument, with the arguments passed in taking preference. Then we take our three ajax callbacks (success, error, and complete) and attach them using the native deferred methods, effectively attaching them to our Deferred when it resolves.
The code I am going to write next is a little application specific, but the process will remain the same: create new settings which get passed to the native $.ajax and override the success, error, and complete methods. Each of these methods needs to resolve the deferred in a certain manner:
Application.ajax = function (options) { var deferred = $.Deferred(function (d) { var defaults = { cache: false, type: 'post', traditional: true, dataType: 'json' }, settings = $.extend({}, defaults, options); d.done(settings.success); d.fail(settings.error); d.done(settings.complete); var jqXHRSettings = $.extend({}, settings, { success: function (response, textStatus, jqXHR) { if (settings.dataType === 'json') { if (response && response.success) { d.resolve(response.data); } else { d.reject(response.data); } } else { d.resolve(response); } }, error: function (jqXHR, textStatus, errorThrown) { console.log(jqXHR); d.reject(jqXHR); }, complete: d.resolve }); if (!Application.clientValidates()) { d.reject(); } else { $.ajax(jqXHRSettings); } }); var promise = deferred.promise(); promise.success = deferred.done; promise.error = deferred.fail; promise.complete = deferred.done; return promise; };
Our implementation is complete. For this application, I am creating my settings for my ajax request, and overriding the status callbacks to implement my own custom logic. I also do a little client-side validation, which will not even make the ajax request, and instead, reject the deferred and call the registered error callbacks. If the request goes through, then in the success method, I am checking if the request is JSON. If so, see if the response returned from the server was successful and resolve our Deferred, thereby executing all the .done callbacks. If there was a failure, call the .fail callbacks and so on.
We are now free to register as many success or error callbacks as we want, just like $.ajax, but now we also have the power to have our own logic and data passed to those methods as our applications see fit.