1

I have an existing function that returns an AJAX promise. I want to update this function to display a confirmation alert before running the AJAX call, but other parts of the code use this function and is already expecting a promise.

Here's what my original function looks like:

function doTheDeed() {
  return $.ajax({
    url: '/api/delete/123',
    method: 'POST'
  }).done(function () {
    alert('The deed is done.');
  });
}

doTheDeed().done(function {} { /*Do something else*/ });

Now I want to confirm with the user before running the AJAX call. How do maintain the API agreement of returning a promise, while waiting for the users confirmation or cancel?

function doTheDeed() {
  bootbox.confirm('Are you sure?', function (result) {
    if (result) {
      // Too late to return a promise, promise should've been returned a long time ago
      return $.ajax({
        url: '/api/delete/123',
        method: 'POST'
      }).done(function () {
        alert('The deed is done.');
      });
    } else {
      //return what??
    }
  });
}

doTheDeed().done(function {} { /*Do something else*/ });

Do I need to conditionally return different promises based on the user's response?

TruMan1
  • 33,665
  • 59
  • 184
  • 335
  • as you are using jquery, it's [deferred object](https://api.jquery.com/category/deferred-object/) should be able to do what you want – Jaromanda X Dec 24 '15 at 13:44
  • Make a promise for the result of the `bootbox.confirm` call (which seems to be async as well, you can't just `return` from the callback). Then *chain* it together with the ajax call. – Bergi Dec 24 '15 at 13:51

2 Answers2

4

You could use jQuery's Deferreds ?

function doTheDeed() {
    var def = $.Deferred();

    bootbox.confirm('Are you sure?', def.resolve);

    return def.promise().then(function(result) {
        return result ? $.post('/api/delete/123') : "no go";
    });
}

doTheDeed().done(function (data) { 
    if (data == 'no go') {
        // user declined
    } else {
        // item deleted
    }
}).fail(function(err) {
    // something failed
});
adeneo
  • 312,895
  • 29
  • 395
  • 388
  • 1
    Please promisify only `bootbox.confirm` and then avoid the [deferred antipattern](http://stackoverflow.com/q/23803743/1048572)! – Bergi Dec 24 '15 at 13:49
  • @Bergi - wouldn't that require actually changing Bootbox, or using something that has a promisify method, like Bluebird – adeneo Dec 24 '15 at 14:02
  • No, it can be done easily in a separate method (modifying the `bootbox` object itself is possible but not necessary), or even right within `doTheDeed`. I'm more focused on that you use the deferred exclusively for the confirm, and nothing else. Then chain the ajax call using `then`. – Bergi Dec 24 '15 at 14:05
  • @Bergi - I think I'll just leave it the way it is, seems more jQuery'ish as that's how jQuery Deferreds usually are used, antipattern or not. Had Bootbox returned a jQuery deferred I could have done `bootbox.confirm('msg').done(...` but it doesn't, and I see no reason to change the plugin or wrap it in a new method, unless it's going to be used like this multiple places, in which case one could do something like this -> https://jsfiddle.net/adeneo/25qo7L4o/2/ – adeneo Dec 24 '15 at 14:24
  • If that is how jQuery deferreds are usually used, that's sad - because it *is* an antipattern and would be easy to avoid. You don't even need to wrap it in a new method, just use the deferred only for the confirm and then do `return def.promise().then(result => result ? $.ajax(…) : 'no go')` (ES6 for abbreviation) – Bergi Dec 24 '15 at 14:31
  • 1
    Yes, exactly. Looks much better than the current code in your answer, doesn't it? (And also handles the ajax fail case correctly) – Bergi Dec 24 '15 at 14:41
  • It does, and that's jQuery'ish enough for me as well, I'll edit – adeneo Dec 24 '15 at 14:42
  • 1
    Thx guys, this is very elegant! – TruMan1 Dec 24 '15 at 16:23
2

Building on Adeneo's answer, and adhering to the principle of promisifying at the lowest level, bootbox could be monkeypatched with a reusable promisification of bootbox.confirm(), without having to hack into bootbox itself.

If you do this then it will be appropriate :

  • to allow the challenge text and noGo message to be specified by the caller.
  • to send the noGo condition down the error path.
if(!bootbox.confirmAsync) {
    bootbox.confirmAsync = function (challenge, noGoMessage) {
        challenge = challenge || 'Are you sure?';
        noGoMessage = noGoMessage || 'User declined';
        return jQuery.Deferred(function (def) {
            bootbox.confirm(challenge, function (confirmed) {
                confirmed ? def.resolve() : def.reject(new Error(noGoMessage));
            });
        }).promise();
    }
}

doTheDeed() would then comprise a totally conventional then chain starting with bootbox.confirmAsync(...), for example :

function doTheDeed () {
    return bootbox.confirmAsync('Are you sure you want to delete?', 'User declined to delete')
    .then(function (data) {
        return $.post('/api/delete/123');
    })
    .then(null, function (err) {
        // if user declined, then 'User declined to delete' will come back as err.message
        console.log(err.message);
        return err; // jQuery v<3.0
        // throw err; // jQuery v>=3.0
    });
}
Roamer-1888
  • 19,138
  • 5
  • 33
  • 44
  • I would avoid `return err` in a jQuery "`catch`" handler. Even when you know what it does, you also know that it won't work any more when switching to a proper promise library. – Bergi Dec 24 '15 at 22:22
  • We are pretty well committed to jQuery here. Bootbox is dependent on it as is the means of achieving AJAX. If the OP was to move away from jQuery a number of things would need to change, not just `return err`. – Roamer-1888 Dec 25 '15 at 00:41
  • Yeah, `bootbox.confirm` and `$.post` might very well depend on jQuery, but that doesn't stop you from using a different promise library alongside of it. – Bergi Dec 25 '15 at 23:46
  • Bergi, I'm not sure I get your point. If doTheDeed's caller wants to assimilate the returned jQuery promise with say `Promise.resolve(doTheDeed())`, it's free to do so. Inside `doTheDeed()` as written, everything is guaranteed to be jQuery and `return err` is correct - it's nothing more than jQuery's equivalent of `throw err`. What am I missing? – Roamer-1888 Dec 26 '15 at 00:51
  • Ah I see, I meant in the case that you would rewrite `doTheDeed` or `confirmAsync` to A+ promises - especially in the latter case you might not realise that the catch code works differently now. That's why I would've avoided it in the first place. – Bergi Dec 26 '15 at 17:27
  • 1
    I have added a comment. – Roamer-1888 Dec 26 '15 at 18:32