9

Let's say I have a $.Deferred and a jqXHR object. Is there a way to transfer all the handlers bound to the deferred (then, always, done, fail) over to the XHR object (which, as I understand it, is an extension of Deferred)?


Here's what I had in mind:

$.ajaxOne = function(options) {
    var xhr = null;
    return function() {
        if(xhr) xhr.abort();
        xhr = $.ajax(options).always(function() {
            xhr = null;
        });
    }
}

I wanted to create a function, similar to $.ajax, except that if you call it multiple times in rapid succession, it will abort the last request and only complete the most recent one. This is useful in many scenarios where you want to validate a user's input.

For example, you might want to check if a username is taken, but if they start typing in the username field again after you've started your ajax call, you don't care about the last result, only the most recent one.

Also, I don't think requests are guaranteed to return in the same order they went out (I suppose depending on your server setup), so you could have a syncing issue as well.

Anyway, the problem with the above code is that because it returns a function, you can execute your ajax call whenever you like, but you can't bind your completion handlers to it. So I have to somehow mix the deferred handlers in and rebind them to the XHR object.

mpen
  • 272,448
  • 266
  • 850
  • 1,236
  • 3
    I don't think that can be done... why don't you resolve the deferred using jqXHR callbacks.... `xhr.done(function(){ deferred.resolveWith.apply(deferred, arguments); }).fail(function(){ deferred.rejectWith.apply(deferred, arguments); })` – Arun P Johny Oct 31 '13 at 17:21
  • Out of curiosity, why do you have this setup? Why didn't you just use the jqXHR object, instead of making a new `$.Deferred`? Also, in your solution do you want your deferred to be resolved or not in the end? – gen_Eric Oct 31 '13 at 17:26
  • 2
    @RocketHazmat: In this particular scenario, I essentially want to build a 'ajax' object without immediately executing it. So I was thinking about returning a Deferred instead so that users could still bind their events to it, and then when they execute it, it would run the ajax call... I'll post a more concrete example if you want, I admit having not fully thought it through :-) – mpen Oct 31 '13 at 20:04
  • @Mark: Would you then want the user to be able to bind to the callback of the AJAX call or not? Would something like this work? `$d = new $.Deferred; /* your code... make sure to call d.resolve() somewhere */ return d.then(function(){ $.ajax({}); }).promise();`? I'm pretty sure you can do that. – gen_Eric Oct 31 '13 at 20:06
  • `"except that if you call it multiple times in rapid succession, it will abort the last request and only complete the most recent one"` This is called "debouncing". Check out this plugin: http://benalman.com/projects/jquery-throttle-debounce-plugin/ – gen_Eric Oct 31 '13 at 20:13
  • 1
    @RocketHazmat: I'm actually using it in combination with debouncing. I debounce for ~300ms, but if the AJAX takes a really long time to return, it can still fire off twice. I *could* delay executing the new AJAX call until the last one returns, but that would provide an inferior user experience...the calls could stack up and they would be waiting a long time for the latest results. – mpen Oct 31 '13 at 20:14
  • 1
    @RocketHazmat: I don't know how your `$.Deferred` example would work...you haven't bound anything to `$.ajax` -- only that object knows when the call has come back. I think Arun has the best solution so far. – mpen Oct 31 '13 at 20:18
  • @Mark: That example was before I knew what you were trying to do. It doesn't really make much sense now ;) – gen_Eric Oct 31 '13 at 20:19

3 Answers3

2

Let's say I have a $.Deferred and a jqXHR object. Is there a way to transfer all the handlers bound to the deferred (then, always, done, fail) over to the XHR object (which, as I understand it, is an extension of Deferred)?

More or less, yes, but not in the way you expected. Instead of "moving handlers", you just resolve the deferred (that has the handlers) with the XHR deferred. This will make the deferred adopt the state of the ajax promise - or not, since jQuery is not Promise A+-compatible. So you will need to put the triggers as handlers manually:

var deferred = $.Deferred(),
    xhr = $.ajax(…);
xhr.done(deferred.resolve).fail(deferred.reject).progress(deferred.notify);

However, a use like that is discouraged, just use xhr wherever you needed deferred - they're equal. Or use xhr.then() to create a brand new promise object that resolves exactly like xhr will.

Anyway, the problem with the above code is that because it returns a function, you can execute your ajax call whenever you like, but you can't bind your completion handlers to it. So I have to somehow mix the deferred handlers in and rebind them to the XHR object.

You still can return each xhr object from that returned function, and bind your handlers to that. In case it is aborted, its error handlers will be called.

$.ajaxOne = function(options) {
    var xhr = null;
    return function(name) {
        options.data = name;
        if (xhr) xhr.abort();
        return xhr = $.ajax(options).always(function() {
//      ^^^^^^
            xhr = null;
        });
    }
}
var checkUserAccount = $.ajaxOne({…});
$input.keyup(function(e) {
    checkUser(this.value).done(function(ajaxResult) {
        // do anything here with the ajaxResult from the latest call
        // if there was another keyup event, this callback never fires
    });
});

Also, I don't think requests are guaranteed to return in the same order they went out (I suppose depending on your server setup), so you could have a syncing issue as well.

Not if you call abort on each old one when the function is called again - that will hold the invariant that there is only at most one active ajax request at a time.

I wanted to create a function, similar to $.ajax, except that if you call it multiple times in rapid succession, it will abort the last request and only complete the most recent one.

Sounds pretty much like an event stream. You will want to have a look at Functional Reactive Programming!

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Returning `xhr` doesn't solve the problem. Promises can only be resolved once. The idea behind `ajaxOne` is that you create an instance of it, bind your completion handlers, and then execute it as many times as you like. [RxJS](https://github.com/Reactive-Extensions/RxJS) might be a better fit; not sure. Haven't tried it out yet. – mpen Aug 17 '14 at 21:56
  • Hm, returning multiple promises (one from each function call) would work - see my extended example code. Of course, [a row of promise is just a stream](http://stackoverflow.com/a/17414193/1048572) :-) Yes, if you want to attach a single handler that is fired multiple times then FRP is the solution (I've mentioned it in the last paragraph); you might want to have a look at Bacon.js as well. – Bergi Aug 17 '14 at 22:48
0

Came up with this:

function AjaxOne(options) {
    this.options = options;
    this._xhr = null;
    this._always = [];
    this._success = [];
    this._fail = [];
};

$.extend(AjaxOne.prototype, {
    always: function(cb) {
        this._always.push(cb);
        return this;
    },
    done: function(cb) {
        this._success.push(cb);
        return this;
    },
    fail: function(cb) {
        this._fail.push(cb);
        return this;
    },
    then: function(success, fail) {
        this._success.push(success);
        this._fail.push(fail);
        return this;
    },
    run: function(options) {
        if(this._xhr) {
            this._xhr.abort();
        }
        this._xhr = $.ajax($.extend({},options,this.options,{context:this}))
            .always(function() {
                this._xhr = null;
                for(var i=0; i<this._always.length;++i) this._always[i].apply(this,arguments);
            })
            .done(function() {
                for(var i=0; i<this._success.length;++i) this._success[i].apply(this,arguments);
            })
            .fail(function() {
                for(var i=0; i<this._fail.length;++i) this._fail[i].apply(this,arguments);
            });
    }
});

Seems to be working pretty well so far.... but it doesn't answer my original question.

So a comprehensive answer would be: you can't copy the callbacks from one deferred to another. I tried making copies of the deferred in various ways, $.extend({}, myDeferred) I couldn't get anything to work. I think you have to manually make copies of each method and fire off the appropriate callbacks, similar to what I've done.

You can propogate the callbacks off to the original deferred as Arun suggests in his comment (although I think his syntax is slightly off; according to the docs you don't need the .apply in there; the purpose of the 'with' methods is to allow you to set a context). In my case I wanted to be able to fire the methods more than once, so that doesn't work for my scenario.

mpen
  • 272,448
  • 266
  • 850
  • 1,236
  • No, please don't do this. Your [`then`](http://api.jquery.com/deferred.then/) method is broken, and to have your `AjaxOne` instances be identified as promises they should have a [`.promise()` method](http://api.jquery.com/deferred.promise/) – Bergi Aug 17 '14 at 21:15
  • @Bergi Promises don't have a `.promise` method, deferreds do. How's my `then` broken? – mpen Aug 17 '14 at 21:50
  • 1
    All objects that can be cast to jQuery promises (including deferreds, promises, or even [jquery collections](http://api.jquery.com/promise/)) do have a `promise` method. `then` is broken because it is expected to return a new promise for the return values of the handlers, not the original promise (admittedly, jQuery's `then` was broken until v1.8 as well, see http://stackoverflow.com/q/9583783/1048572, http://stackoverflow.com/q/12011925/1048572, http://stackoverflow.com/q/5436327/1048572) – Bergi Aug 17 '14 at 22:56
0

Fiddle here.

I have implemented without using deferred objects.

The keyup Event -

$("#uID").keyup(function () {
    console.log("KeyUp")
    var textElement = this;
    clearTimeout(textElement.timer);
    if(textElement.xhrReq && textElement.xhrReq.abort){
        textElement.xhrReq.abort();
    }
    textElement.timer = setTimeout(function(){
        console.log("Invoking validation : " + textElement.value);
        validateUID(textElement);
    }, 1000);
});

In validateUID(), I have assigned the entire XHR object to the input element's property -

textElement.xhrReq = $.ajax({
...
});

One drawback - and I admit it's there - we need to keep the entire XHR object in the element.

Avi
  • 56
  • 2
  • I don't know whether or not this answers the question but it could be written to run more efficiently in particular by keeping various data and the setTimeout callback function in a closure. **[updated fiddle](http://jsfiddle.net/kq0t87k4/6/)** – Roamer-1888 Aug 17 '14 at 22:43