0

I'm now developing on a jQuery plugin, and I want to make some pre-process operations before $.ajax sends:

// The signature is the same with $.ajax
$.myAjax = function(url, options) {


    var data = options.data;
    var promises = [];

    for(var name in data) {
        if(data.hasOwnProerty(name)) {
            var val = data[name];
            if(val instanceof File) {

                // I want to do some async pre-process here.
                var dfd = $.Deferred();

                var reader = new FileReader();
                reader.onload = function(e) {
                    data.name = e.target.result;
                    dfd.resolve();
                }
                reader.readAsText(val);

                promises.push(dfd.promise());
            }
        }
    }

    var deferred = $.Deferred();

    $.when.apply($, promises).done(function() {
        // In fact, I want to return, or wrap cascading this jqXHR
        //   in the outer function `$.myAjax`.
        var jqXHR = $.ajax(url, options).done(function(...) {
            // ??? If I want deferred to be a jqXHR like object, 
            // how to wrap the arguments here?
            deferred.resolve(/* Help to fill */); 
        }).fail(function(...) {
            deferred.reject(/* Help to fill */);
        });
    });

    // ** ATTENTION **
    // Here, I want to return a jqXHR compatible promise.
    // That is what I ask here.
    return deferred.promise();

}

And I want to return a Deferred object in myAjax, or more precisely speaking, a jqXHR object.

So that I can call, completely the same interface with a standard $.ajax method:

$.fn.myAjax({...}).done(function(data, textStatus, jqXHR) {
    // ...
}).fail(function(jqXHR, textStatus, errorThrown) {
    // ...
}) 
// .always ... etc.
Alfred Huang
  • 17,654
  • 32
  • 118
  • 189
  • 1
    Just avoid the [deferred antipattern](http://stackoverflow.com/q/23803743/1048572), use `then` instead, and you're done. Of course you cannot return a jqXHR object, only a promise that resolves in the same way. – Bergi Sep 28 '15 at 12:38
  • 1
    Btw, you've got the classical [closure in a loop problem](http://stackoverflow.com/q/750486/1048572). Best abstract a single promise out into a `readFile` function. – Bergi Sep 28 '15 at 12:40
  • possible duplicate of [Preserving arguments when returning data from deferred.then()](http://stackoverflow.com/q/30423899/1048572) – Bergi Sep 28 '15 at 12:42

2 Answers2

4

If I correctly understand what you're trying to do, it can't be done. The problem is that your code returns from $.myAjax() before you've even created the jqXHR object so there's no way that the jqXHR object can be the actual return object from the $.myAjax() function call. You can make it accessible from the returned promise, but the returned promise is going to be a promise you create before the ajax call has even been started.

FYI, you have some promise anti-patterns in your code too, as you return $.ajax() from the $.when() handler rather than use another deferred you created. Returning a promise from within a .then() handler automatically chains that promise to the original.


Here's a cleaned up version of what you posted as your solution. Summary of changes:

  1. Encapsulated the file reading into a local function to avoid declaring a function in a loop and to allow all core logic flow to just use promises rather than a mix of promises and callbacks (e.g. encapsulate the callbacks).
  2. Added error handling to the file reader
  3. Switched the deferred to the callback model (which std promises use)
  4. Removed deferred anti-pattern and instead just return the ajax promise whcih will give you the resolve or rejected arguments from the ajax call as desired
  5. Switched to .then() which has a more standard behavior and will not need to be changed when jQuery makes their promises standards compliant

The code:

// The signature is the same with $.ajax
$.myAjax = function(url, options) {

    function readFile(data, name) {
        var file = data[name];
        if (file instanceof File) {
            return $.Deferred(function(dfd) {
                var reader = new FileReader();
                reader.onload = function(e) {
                    dfd.resolve(e.target.result);
                    data[name] = e.target.result;
                };
                reader.onerror = reader.onabort = dfd.reject;
                reader.readAsText(file);

            }).promise();
        }
    }

    var data = options.data;
    var promises = [];

    for(var name in data) {
        if(data.hasOwnProerty(name)) {
            promises.push(readFile(data, name));
        }
    }

    // trigger when all file fields was loaded.
    // so the data were all constructed.
    return $.when.apply($, promises).then(function() {
        return $.ajax(url, options);
    });
}
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Thank you, but I'm not in fact using timeout, the timeout is just to illustrate the async operation. I will use `$.when` to collect a set of async operations on the data fields. – Alfred Huang Sep 28 '15 at 03:10
  • 1
    @fish_ball - please modify your question to show the ACTUAL problem you're trying to solve. You will find the community is far, far more useful at offering you solutions you never thought of if you explain the real problem you're trying to solve rather than just ask about issues with the solution you're trying. What you are doing is known as the [XY problem](http://meta.stackexchange.com/questions/66377/what-is-the-xy-problem) where you describe issues with your solution rather than the actual problem which severely limits the types of solutions we could help with. – jfriend00 Sep 28 '15 at 03:14
  • @fish_ball - I reread your question a couple times and I honestly have no idea what problem you're trying to solve. The usual way to combine multiple async operations one after the other is to chain them `return $.ajax(...).then(otherAsync)`, but I can't tell if that's what you're really trying to do or not. The `$.ajax()` function already returns a promise so it's unlikely you need to create your own promise in order to interact with it or combine it with some other async operation. – jfriend00 Sep 28 '15 at 03:18
  • I want to make some pre-process on some given `File` or `Blob` object, but because the read operation have to be async, so I must use `$.when` to collect when the read operations are all done, then start the ajax, But the outer wrapper cannot get the `$.ajax` returning promise object. That's my problem. – Alfred Huang Sep 28 '15 at 03:34
  • @fish_ball - OK, much better. I have an idea, but I have to try something. – jfriend00 Sep 28 '15 at 03:35
  • @fish_ball - You can't return the jqXHR from `$.myAjax()`. You can make it accessible, but the jqXHR object hasn't even yet been created when `$.myAjax()` returns. It doesn't exist. What you're returning from `$.myAjax()` is the deferred you created. You could make the jqXHR object accessible from the final promise result, but it won't be the return value from the function call. I could also help you improve your code quite a bit, but I'm not sure what direction you want to go. – jfriend00 Sep 28 '15 at 03:56
  • @fish_ball - here's a scheme that makes the jqXHR available from the final result: http://jsfiddle.net/jfriend00/kuazoshz/. Not sure that's what you want, but since the object isn't created until later, it's the only way to get access to it from outside. – jfriend00 Sep 28 '15 at 04:18
  • Thank you! And finally I use `deferred.resolveWith(callbackContext, [data, textStatus, jqXHR]);` inside the `$.ajax().done`, so that the returning promise takes the same arguments with the jqXHR do. – Alfred Huang Sep 28 '15 at 04:36
  • Well I've found the solution now, and works completely as I expected, thank you for your help. I posted the answer below, you can have a look. – Alfred Huang Sep 28 '15 at 08:09
  • 1
    @fish_ball - I added a cleaned up version of your code to the end of my answer. – jfriend00 Sep 28 '15 at 13:32
  • Thank you for teaching! Your good advise is so enlightening! I've just finished my plugin: https://github.com/fish-ball/jquery.formdata.js, so you made me find many points to impove that, thank you! – Alfred Huang Sep 28 '15 at 16:53
-1

My own way to the solution finally:

Finally, I tried out making a deferred which resolveWith or rejectWith a same argument list of the jqXHR.done() and jqXHR.fail() signature.

The signature reference: http://api.jquery.com/jQuery.ajax/#jqXHR

jqXHR.done(function( data, textStatus, jqXHR ) {});

jqXHR.fail(function( jqXHR, textStatus, errorThrown ) {});

jqXHR.always(function( data|jqXHR, textStatus, jqXHR|errorThrown ) {});

So the overall solution is:

// The signature is the same with $.ajax
$.myAjax = function(url, options) {

    var data = options.data;
    var promises = [];

    for(var name in data) {
        if(data.hasOwnProerty(name)) {
            var val = data[name];
            if(val instanceof File) {
                (function(name, val) {
                    // Deferred for a single field loaded.
                    var dfd = $.Deferred();
                    var reader = new FileReader();
                    reader.onload = function(e) {
                        data[name] = e.target.result;
                        dfd.resolve();
                    }
                    reader.readAsText(val);
                    promises.push(dfd.promise());
                })(name, val);
            }
        }
    }

    // Overall deferred to cascading jqXHR from ajax.
    // with returning the same argument list.
    var deferred = $.Deferred();

    // resolveWith or rejectWith requires a context.
    // Thought from the jQuery ajax source code.
    var callbackContext = options.context || options;

    // trigger when all file fields was loaded.
    // so the data were all constructed.
    $.when.apply($, promises).done(function() {

        // ********** FINAL SOLUTION **********
        $.ajax(url, options).done(
          function(data, textStatus, jqXHR) {
            deferred.resolveWith(context, 
                [data, textStatus, jqXHR]); 
        }).fail(
          function(jqXHR, textStatus, errorThrown) {
            deferred.rejectWith(context, 
                [jqXHR, textStatus, errorThrown]);
        });
    });

    // So that the resulting promise is well constructed.
    return deferred.promise();

}

So, now we can use the $.myAjax function the same way with $.ajax:

var dfd = $.myAjax({
    url: '...',
    // ...
}).done(function(data, textStatus, jqXHR) {
    // triggered when the inner ajax done ...
}).fail(function(jqXHR, textStatus, errorThrown) {
    // triggered when the inner ajax fail ...
});
Alfred Huang
  • 17,654
  • 32
  • 118
  • 189
  • So, this solution is not good for it matches an anti-pattern, @jfriend00 's answer had pointed out the reason. This answer should only be here to compare with the right one. – Alfred Huang Sep 28 '15 at 17:07