1

I'm relatively new to js so please forgive me if my wording isn't quite right. I've also created a jsfiddle to demonstrate the issue.

Overview

In the app I'm working on, I have a function with a jquery ajax call, like this:

function scenario1(ajaxCfg) {
    return $.ajax(ajaxCfg)
}

I want to change this function, but without in any way changing the inputs or outputs (as this function is called hundreds of times in my application).

The change is to make a different ajax call, THEN make the call specified. I currently have it written like this:

function callDependency() { //example dependency
    return $.ajax(depUri)
}

function scenario2(ajaxCfg) {
    return callDependency().then(() => $.ajax(ajaxCfg))
}

Desired Result

I want these two returned objects to be identical:

let result1 = scenario1(exampleCall)
let result2 = scenario2(exampleCall)

More specifically, I want result2 to return the same type of object as result1.

Actual Result

result1 is (obviously) the result of the ajax call, which is a jqXHR object that implements the promise interface and resolves to the same value as result2, which is a standard promise.

Since result2 is not a jqXHR object, result2.error() is undefined, while result1.error() is defined.

I did attempt to mock up these methods (simply adding a .error function to the return result, for example), but unfortunately even when doing this, result1.done().error is defined while result2.done().error is undefined.

Wrapping (or unwrapping) it up

In a nutshell, I want to return the jqXHR result of the .then() lambda function in scenario2 as the result of the scenario2 function. In pseudocode, I want:

function scenario2(ajaxCfg) {
    return callDependency().then(() => $.ajax(ajaxCfg)).unwrapThen()
} //return jqXHR

Daniel
  • 1,695
  • 15
  • 33
  • 1
    ^^^ the senario1 does not have a return before the ajax call – Taplar Mar 25 '20 at 17:57
  • And to be even more pedantic: `$.ajax()` returns a _"jqXHR object"_ which is not a `Promise` but a [`Deferred object`](https://api.jquery.com/category/deferred-object/) that implements the `Promise` interface. – Andreas Mar 25 '20 at 18:03
  • @Andreas oops! Fixed that. The jsfiddle was correct, I just copied over incorrectly. But to your other point: that's it, I want the jgXHR object. How do I get that as a return value to the function? – Daniel Mar 25 '20 at 18:08
  • An `jqXHR` object doesn't have `.resolve()` and `.reject()` methods therefore it is not a Deferred. It is effectively a (jQuery) Promise. – Roamer-1888 Mar 27 '20 at 00:29
  • Can you please specify what is your primary goal? What do you want to achieve here? – Jasdeep Singh Mar 31 '20 at 14:48
  • I'm trying to replace a method that makes a single ajax call and returns a jqXHR object with a new method that chains together two ajax calls and still returns a jqXHR object (since that is what all callers expect). – Daniel Mar 31 '20 at 15:50

3 Answers3

1

What about something like this? The approach is a little different, but in the end you can chain .done() etc. to the scenario2() function:

const exampleCall = { url: 'https://code.jquery.com/jquery-1.12.4.min.js'};
const depUri = { url: 'https://code.jquery.com/jquery-1.12.4.min.js'};

function callDependency() { //example dependency
    return $.ajax(depUri).done(() => console.log('returned callDependancy'))
}

let obj = { //creating an object with the scenario2 as a method so that I can bind it with defer.promise()
    scenario2: function(ajaxCfg) {
        return $.ajax(ajaxCfg).done(() => console.log('returned senario2')) // Purposely NOT calling the exampleCall() function yet
    }
}

defer = $.Deferred(); // Using some JQuery magic to be able to return a jqXHR
defer.promise(obj); // Set the object as a promise
defer.resolve(callDependency()); // Invoking the callDependency() by default on promise resolve

obj.done(() => {
    obj.scenario2() // Resolving so the callDependency() function can be called
}).scenario2(exampleCall).done(() => { // Here you can invoke scenario2 and FINALLY chain whatever you want after everything has been called
    console.log('Here I can chain whatever I want with .done\(\) or .fail\(\) etc.')
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

What I think is cool about this way of doing it is that you can just keep adding methods to the object that you created, and then all your secondary functions that are built on top of callDependency() can be in one place. Not only that, but you can reuse those same methods on top of other AJAX calls.

Read more about this here.

I hope this helps!

Ludolfyn
  • 1,806
  • 14
  • 20
  • Got this to run as desired in this fiddle: http://jsfiddle.net/dipique/hdf84ab3/. Very nicely done. I tried using a deferred before, but wasn't able to find the magic configuration to make it work. I particularly like that the ugliness/complexity of this method should be pretty easy to encapsulate. Thanks! – Daniel Apr 03 '20 at 14:29
  • 1
    Awesome! So cool that it's working @Daniel ! =D I'm actually going to see if I can get the other solution that I posted—about adding a method the `prototype` object—to work in your JSFiddle. Will keep you posted in the comments if I get it working. And thanks for the upvote! – Ludolfyn Apr 03 '20 at 14:53
0

I feel like your life would be made a lot easier if you used async/await syntax. Just remember though that async functions return a promise. So you could instead write:

async function scenario2(ajaxCfg) {
    let jqXhrResult;
    try {
        await callDependency();
        jqXhrResult = {
            jqXhr: $.ajax(ajaxCfg)
        };
    } catch() {
        // Error handling goes here
    }

    return jqXhrResult;
}
ZakDaniels99
  • 505
  • 4
  • 9
  • Unfortunately, the result of the async function would not be the jqXHR object, so it would not support the same methods (such as `.done().error()`). – Daniel Apr 01 '20 at 13:22
  • I've updated my answer to cater for error handling as well. You would move your logic in your error callback into your catch block and your logic for your done callback after awaiting your scenario 2 function. There's a really good explanation on how jquery jqXHR/promise objects are resolved here: https://stackoverflow.com/questions/45286834/how-to-use-jquerys-post-method-with-async-await-and-typescript – ZakDaniels99 Apr 01 '20 at 15:49
  • I have added your suggestion to the jsfiddle; I hope that makes it clear why that isn't a solution. Let me know if I'm missing something. http://jsfiddle.net/dipique/8gvxfck5/latest/ – Daniel Apr 01 '20 at 21:05
  • I've updated my answer again, and tested it here: http://jsfiddle.net/gre2ab5q/. As a quick note, I'd really discourage doing things this way even though it works. The best path of action would be to refactor your front-end to be compliant with asyc/await syntax, but I understand you might be forced not to since you mentioned this function is used all over your app. – ZakDaniels99 Apr 02 '20 at 09:36
  • Your jsfiddle "cheats" in a subtle way; you've changed the caller to run from within an async function, `async function runLogic()`. As you can imagine, I can't modify every caller in the application to run from within an async function. But yes, I agree that this is not a good pattern for building a new application. – Daniel Apr 03 '20 at 13:58
  • This isn't "cheating", its a reinforcement of the entire point I'm trying to make. Asynchronous code should be run from an asynchronous context. Think about how difficult it was to implement this small change. By taping on solutions to just get things to work, you're gonna make your code harder to maintain. Wrapping jqXhr callbacks in an object isn't how these callback functions are meant to be used, this is just a hack to get things working, and it's going to bite you a few months down the line when this function needs to change for whatever reason. – ZakDaniels99 Apr 04 '20 at 07:32
  • sorry if I offended you. What I meant is, it doesn't answer the question or solve the problem. As for the value of implementing this solution, my purpose is because the existing dependency is running synchronously, and this solution allows me to make that dependency run asynchronously without breaking compatibility. This means that areas of the application can be converted to being fully async, using standard promises, one at a time rather than requiring a massive conversion. – Daniel Apr 06 '20 at 02:12
0

I actually thought of a way easier way to do this.

You can do it by adding a method to the function constructor's prototype object. That way any created function can inherit that method and you can still use the .done() syntax. It's referred to as prototypal inheritance:

const exampleCall = { url: 'https://code.jquery.com/jquery-1.12.4.min.js'};
const depUri = { url: 'https://code.jquery.com/jquery-1.12.4.min.js'};

function callDependency() {
    return $.ajax(depUri).done(() => console.log('returned callDependancy'))
}

Function.prototype.scenario2 = function(ajaxCfg, ...args) {
    return this(...args).then(() => $.ajax(ajaxCfg))
}

callDependency.scenario2(exampleCall).done(data => {
    console.log(data)
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
Ludolfyn
  • 1,806
  • 14
  • 20
  • I wasn't able to get this working in my jsfiddle. If you're able to let me know and I'll check it out. I think you're passing one of the test cases simply by virtue of using a different version of jquery, but that version causes the "scenario1" case to fail as well. – Daniel Apr 03 '20 at 14:08