0

I'm very new to Promises, deferred and all that kind of thing and I'm trying to change my old habits (callbacks hell) to Promises, using jQuery (I know it doesn't respect Promise A+, but that's not the point).

What I've done now is a mix of both, and I'm trying to get rid of the callbacks. I'm also using TypeScript, but it shouldn't be related as far as I know. I am just giving fair warning that the code is not pure JS.

// TODO I provide the "done" and "fail" callback here, but I'd like to use .done and .fail instead, but I want them to be executed AFTER the automatic response handling.
WidgetContext.getContext(
    function(data){
        console.log(data)
    },
    function(error){
        console.log(error)
    }
);

// In WidgetContext class, TODO here I want to get rid of the callbacks as well.
public static getContext(done: any, fail: any): JQueryPromise<WidgetContext> {
    return Payline.WebService.WidgetWSProxy.ajax(
        'context1.json',
        {
            userId: 123456
        },
        done,
        fail
    );
}

// In WidgetWSProxy class, TODO here again, there should not be any callback.
public static ajax(method: string = '', data: any = {}, done?: any, fail?: any, options: JQueryAjaxSettings = WidgetWSProxy._defaultOptions): JQueryPromise<WidgetWSProxy>{
    var url = WidgetWSProxy.buildUrl(WidgetWSProxy._url, method);

    return WidgetWSProxy._ajax(url, data, done, fail, WidgetWSProxy._processAjaxResponseData, WidgetWSProxy._logId + ' ' +  url, options);
}

// In AbstractHttpProxy class, TODO the only callback should be the responseHandler.
protected static _ajax(url: string, data: any, done?: any, fail?: any, responseHandler?: any, logId: string = AbstractHttpProxy._logId, options: JQueryAjaxSettings = AbstractHttpProxy._defaultOptions): JQueryPromise<AbstractHttpProxy>{
    // On log la requête.
    log.group(logId);
    log.time(logId);

    // Si aucun gestionnaire de réponse n'est correctement fourni, utilisation de celui par défaut.
    if(!_.isFunction(responseHandler)){
        responseHandler = AbstractHttpProxy._defaultResponseHandler;
    }

    // On injecte les data dans les options, on fait ainsi afin de cacher la logique jQuery pour ce paramètre particulier qui sera souvent utilisé.
    options = _.merge(options, {data: data});

    log.info('Requête HTTP envoyée: ' + JSON.stringify({
            url: url,
            options: options
        }));

    // On effectue l'appel ajax et on retourne une Promise jQuery.
    return $.ajax(url, options)
        // Succès
        .done(function(data){
            if(_.isFunction(done)){
                responseHandler(url, data, true, function(data){
                    // TODO HERE I execute the "done" callback inside the done() function, but I should not. I just need to call the responseHandler and update the data so the next call to ".done()" would use the updated data, even though I define it when I call the "WidgetContext.getContext()" method.
                    done(data);
                });
            }else{
                logMissingCallback(getCallerName());
            }
        })
        // Erreur (connexion interrompue, 404, 500, peu importe)
        .fail(function(error){
            if(_.isFunction(fail)){
                responseHandler(url, error, false, function(error){
                    // TODO Same stuff here, with the fail().
                    fail(error);
                });
            }else{
                logMissingCallback(getCallerName());
            }
        })
        // Sera toujours exécuté, peu importe ce qu'il se passe. (succès/erreur)
        .always(function(){
            log.timeEnd(logId);
            log.groupEnd();
        }
    );
}

My aim is to hide some logic behind the use of a proxy (WidgetWSProxy), to automatically log all requests and also handle HTTP responses to format them as I want, then use the transformed response using .done deferred function.

It works here, but if I do something like this, it doesn't log the updated response in the .done call.

WidgetContext.getContext(
    function(data){
        console.log(data)
    },
    function(error){
        console.log(error)
    }
)
.done(function(data){
        console.log('init')
        console.log(data)
    });

It's hard to get rid of the callback hell way to think when you've used it for years... Thanks for the help!

bnieland
  • 6,047
  • 4
  • 40
  • 66
Vadorequest
  • 16,593
  • 24
  • 118
  • 215

1 Answers1

1

then use the transformed response using .done deferred function.

Even if you're using jQuery deferreds, you should accustom yourself to always and only use then, for it actually does let callbacks transform a value - and returns a chainable promise for the transformed value.

It's hard to get rid of the callback hell way to think when you've used it for years...

  • Remove all success/error callbacks from your code. All of them.
  • Every function that does something asynchronous, so that it would normally need a callback that is called once at the end, should return a promise instead.

That would include your responseHandlers as well for example - they need to return promises, instead of accepting a callback, if they are asynchronous. Your code would become

WidgetContext.getContext().then(function(data){
    console.log(data)
}, function(error){
    console.log(error)
});

public static getContext(): JQueryPromise<WidgetContext> {
    return Payline.WebService.WidgetWSProxy.ajax(
        'context1.json',
        {
            userId: 123456
        }
    );
}

public static ajax(method: string = '', data: any = {}, options: JQueryAjaxSettings = WidgetWSProxy._defaultOptions): JQueryPromise<WidgetWSProxy>{
    var url = WidgetWSProxy.buildUrl(WidgetWSProxy._url, method);

    return WidgetWSProxy._ajax(url, data, WidgetWSProxy._processAjaxResponseData, WidgetWSProxy._logId + ' ' +  url, options);
}

protected static _ajax(url: string, data: any, responseHandler?: any, logId: string = AbstractHttpProxy._logId, options: JQueryAjaxSettings = AbstractHttpProxy._defaultOptions): JQueryPromise<AbstractHttpProxy>{
    if(!_.isFunction(responseHandler))
        responseHandler = AbstractHttpProxy._defaultResponseHandler;
    options = _.merge(options, {data: data});

    log.group(logId);
    log.time(logId);
    log.info('Requête HTTP envoyée: ' + JSON.stringify({
        url: url,
        options: options
    }));

    return $.ajax(url, options).then(function(data){
        return responseHandler(url, data, true);
    }, function(error){
        return responseHandler(url, error, false);
    }).always(function(){
        log.timeEnd(logId);
        log.groupEnd();
    });
}
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • I tought `then` was equivalent to `done` and `fail` but that they makes more sense, semantically talking. Isn't that true? Why do you recommend not to use them? – Vadorequest Apr 21 '15 at 15:26
  • 1
    Because [they don't chain](http://stackoverflow.com/a/15694010/1048572). Usually, you will want to return a promise from your function, and `done`/`fail` don't return one for the transformed value. Admittedly, sometimes you can use them - at the end of the chain. And things like `.always` that do return the original promise can be useful as well. – Bergi Apr 21 '15 at 15:33
  • Okay! So `done` and `fail` are for the very final call then? I should only use them at the very end of the chain call? – Vadorequest Apr 21 '15 at 15:36
  • 1
    Yes, you can use them like `WidgetContext.getContext().done(console.log).fail(console.error);` for example. However, just always using `then` is easier and less error-prone – Bergi Apr 21 '15 at 15:38
  • (I'll test your solution and upvote/accept your answer ASAP, but not right now.) – Vadorequest Apr 21 '15 at 15:38
  • I tried your solution but when I try to call it with `WidgetContext.getContext().done(console.log)` I get the following exception: `Uncaught TypeError: Illegal invocation`, I tried with `then` too but same thing. What am I missing? – Vadorequest Apr 21 '15 at 16:19
  • 1
    You've probably missed a `return` somewhere if the `done`/`then` methods don't work. Alternatively, depending on your browser, you might need to use `console.log.bind(console)` (or use the function expression) when you pass the `log` method around as a callback. – Bergi Apr 21 '15 at 16:24
  • Exactly, thanks a lot, that was because of the `console.log.bind(console)` indeed. It works fine. Last question, if I want to type the returned value as an `AbstractResponse`, does changing the signature as `protected static _ajax(url: string, ... : JQueryPromise` would do the trick or am I doing wrong? – Vadorequest Apr 22 '15 at 08:22
  • I can only guess, as I don't know TypeScript, but that seems to be fine. – Bergi Apr 22 '15 at 10:57