Skip to content

Commit

Permalink
add "global" Ajax events as well as an extra "ajaxBeforeSend" event
Browse files Browse the repository at this point in the history
"Global" events aren't really global because Zepto doesn't support them
yet. Instead they are simply fired on `document`.

Ajax lifecycle is now:
  1. ajaxStart (global) – only fired if there are no active requests
  2. beforeSend callback (cancellable)
  3. ajaxBeforeSend (global, cancellable)
  4. ajaxSend (global)
  5. success/error callback
  6. ajaxSuccess/ajaxError (global)
  7. complete callback
  8. ajaxComplete (global)
  9. ajaxStop (global) – only fired if this is the last active request

Also added:
  - $.active (0) – number of active requests
  - $.ajaxSettings.global (true) – whether global events will fire

If Ajax "context" element is given, "global" events are fired on this
element instead of `document` and they bubble.
  • Loading branch information
mislav committed Oct 30, 2011
1 parent ef686f3 commit c22ca59
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 22 deletions.
78 changes: 66 additions & 12 deletions src/ajax.js
Expand Up @@ -5,9 +5,62 @@
(function($){
var jsonpID = 0,
isObject = $.isObject,
document = window.document,
key,
name;

// trigger a custom event and return false if it was cancelled
function triggerAndReturn(context, eventName, data) {
var event = $.Event(eventName);
$(context).trigger(event, data);
return !event.defaultPrevented;
}

// trigger an Ajax "global" event
function triggerGlobal(settings, context, eventName, data) {
if (settings.global) return triggerAndReturn(context || document, eventName, data);
}

// Number of active Ajax requests
$.active = 0;

function ajaxStart(settings) {
if (settings.global && $.active++ === 0) triggerGlobal(settings, null, 'ajaxStart');
}
function ajaxStop(settings) {
if (settings.global && !(--$.active)) triggerGlobal(settings, null, 'ajaxStop');
}

// triggers an extra global event "ajaxBeforeSend" that's like "ajaxSend" but cancelable
function ajaxBeforeSend(xhr, settings) {
var context = settings.context;
if (settings.beforeSend.call(context, xhr, settings) === false ||
triggerGlobal(settings, context, 'ajaxBeforeSend', [xhr, settings]) === false)
return false;

triggerGlobal(settings, context, 'ajaxSend', [xhr, settings]);
}
function ajaxSuccess(data, xhr, settings) {
var context = settings.context, status = 'success';
settings.success.call(context, data, status, xhr);
triggerGlobal(settings, context, 'ajaxSuccess', [xhr, settings, data]);
ajaxComplete(status, xhr, settings);
}
// type: "timeout", "error", "abort", "parsererror"
function ajaxError(error, type, xhr, settings) {
var context = settings.context;
settings.error.call(context, xhr, type, error);
triggerGlobal(settings, context, 'ajaxError', [xhr, settings, error]);
ajaxComplete(type, xhr, settings);
}
// status: "success", "notmodified", "error", "timeout", "abort", "parsererror"
function ajaxComplete(status, xhr, settings) {
var context = settings.context;
settings.complete.call(context, xhr, status);
triggerGlobal(settings, context, 'ajaxComplete', [xhr, settings]);
ajaxStop(settings);
}

// Empty function, used as default callback
function empty() {}

Expand Down Expand Up @@ -39,26 +92,26 @@
$.ajaxJSONP = function(options){
var callbackName = 'jsonp' + (++jsonpID),
script = document.createElement('script'),
context = options.context,
abort = function(){
$(script).remove();
if (callbackName in window) window[callbackName] = empty;
ajaxComplete(xhr, options, 'abort');
},
xhr = { abort: abort }, abortTimeout;

window[callbackName] = function(data){
clearTimeout(abortTimeout);
$(script).remove();
delete window[callbackName];
options.success.call(context, data);
ajaxSuccess(data, xhr, options);
};

script.src = options.url.replace(/=\?/, '=' + callbackName);
$('head').append(script);

if (options.timeout > 0) abortTimeout = setTimeout(function(){
xhr.abort();
options.error.call(context, xhr, 'timeout');
ajaxComplete(xhr, options, 'timeout');
}, options.timeout);

return xhr;
Expand All @@ -81,6 +134,8 @@
complete: empty,
// The context for the callbacks
context: null,
// Whether to trigger "global" Ajax events
global: true,
// Transport
xhr: function () {
return new window.XMLHttpRequest();
Expand Down Expand Up @@ -149,6 +204,8 @@
var settings = $.extend({}, options);
for (key in $.ajaxSettings) if (!settings[key]) settings[key] = $.ajaxSettings[key];

ajaxStart(settings);

if (/=\?/.test(settings.url)) return $.ajaxJSONP(settings);

if (!settings.url) settings.url = window.location.toString();
Expand All @@ -166,8 +223,7 @@
}

var mime = settings.accepts[settings.dataType],
xhr = $.ajaxSettings.xhr(), abortTimeout,
context = settings.context;
xhr = $.ajaxSettings.xhr(), abortTimeout;

settings.headers = $.extend({'X-Requested-With': 'XMLHttpRequest'}, settings.headers || {});
if (mime) settings.headers['Accept'] = mime;
Expand All @@ -182,13 +238,11 @@
catch (e) { error = e; }
}
else result = xhr.responseText;
if (error) settings.error.call(context, xhr, 'parsererror', error);
else settings.success.call(context, result, 'success', xhr);
if (error) ajaxError(error, 'parsererror', xhr, settings);
else ajaxSuccess(result, xhr, settings);
} else {
error = true;
settings.error.call(context, xhr, 'error');
ajaxError(null, 'error', xhr, settings);
}
settings.complete.call(context, xhr, error ? 'error' : 'success');
}
};

Expand All @@ -197,15 +251,15 @@
if (settings.contentType) settings.headers['Content-Type'] = settings.contentType;
for (name in settings.headers) xhr.setRequestHeader(name, settings.headers[name]);

if (settings.beforeSend.call(context, xhr, settings) === false) {
if (ajaxBeforeSend(xhr, settings) === false) {
xhr.abort();
return false;
}

if (settings.timeout > 0) abortTimeout = setTimeout(function(){
xhr.onreadystatechange = empty;
xhr.abort();
settings.error.call(context, xhr, 'timeout');
ajaxError(null, 'timeout', xhr, settings);
}, settings.timeout);

xhr.send(settings.data);
Expand Down
53 changes: 43 additions & 10 deletions test/ajax.html
Expand Up @@ -9,6 +9,7 @@
<script src="evidence_runner.js"></script>
<script src="../src/polyfill.js"></script>
<script src="../src/zepto.js"></script>
<script src="../src/event.js"></script>
<script src="../src/ajax.js"></script>
</head>
<body>
Expand All @@ -25,6 +26,10 @@ <h1>Zepto Ajax unit tests</h1>

Evidence('ZeptoAjaxTest', {

tearDown: function() {
$(document).off();
},

testAjaxBase: function(t){
var xhr = $.ajax({ url: 'fixtures/ajax_load_simple.html' });
this.assertEqual('function', typeof xhr['getResponseHeader']);
Expand Down Expand Up @@ -66,17 +71,30 @@ <h1>Zepto Ajax unit tests</h1>
});
},

// FIXME: this test sometimes passes even if purposely broken. Something stinks
testNumberOfActiveRequests: function(t) {
var maxActive = 0, ajaxStarted = 0, ajaxEnded = 0, requestsCompleted = 0;
t.assertIdentical(0, $.active);
$(document)
.on('ajaxStart', function() { ajaxStarted++ })
.on('ajaxEnd', function() { ajaxEnded++ })
.on('ajaxSend', function() {
if ($.active > maxActive) maxActive = $.active;
})
.on('ajaxComplete', function() {
if (++requestsCompleted == 3) setTimeout(function() {
t.resume(function() {
t.assertEqual(3, maxActive);
t.assertIdentical(0, $.active);
});
}, 10)
});

// TODO: It's time to integrate a real webserver (webrick) to test POSTS
//
//testAjaxPostWithData: function(t) {
// t.pause();
// $.post('fixtures/ajax_post_json_echo.php', { sample: 'data', letters: ['a', 'b', 'c'] }, function(response) {
// t.resume(function() {
// this.assertEqual(response, '{"sample":"data","letters":["a","b","c"]}');
// });
// });
//},
$.ajax({url: 'fixtures/ajax_load_simple.html'});
$.ajax({url: 'fixtures/ajax_load_simple.html'});
$.ajax({url: 'fixtures/ajax_load_simple.html'});
t.pause();
},

testAjaxPostWithAcceptType: function(t) {
t.pause();
Expand Down Expand Up @@ -196,6 +214,7 @@ <h1>Zepto Ajax unit tests</h1>

tearDown: function() {
$.ajaxSettings.xhr = OriginalXHR;
$(document).off();
},

testTypeDefaultsToGET: function(t) {
Expand Down Expand Up @@ -370,6 +389,20 @@ <h1>Zepto Ajax unit tests</h1>
t.assert(xhr.aborted);
},

testGlobalBeforeSendAbort: function(t) {
var xhr;
$(document).on('ajaxBeforeSend', function(e, x) { xhr = x; return false });
t.assertFalse($.ajax());
t.assert(xhr.aborted);
},

testGlobalAjaxSendCantAbort: function(t) {
var xhr;
$(document).on('ajaxSend', function(e, x) { xhr = x; return false });
t.assert($.ajax());
t.assert(!xhr.aborted);
},

testCompleteCallback: function(t) {
var status, xhr;
$.ajax({ complete: function(x, s) { status = s, xhr = x } });
Expand Down

0 comments on commit c22ca59

Please sign in to comment.