1

I have two functions getStudentById(studentId) and getBookTitleById(bookId) where the data is retrieved via ajax calls (these functions are to be left alone)

My goal, using Deferreds:

  1. get the Student object,
  2. then get the Book Title based on the Student object's favoriteBookId property,
  3. then call the method showMessage(student, bookTitle) resulting in "John likes Book 222"

This example is simplified. In essence the goal is to make a certain number of ajax calls in succession, where the next call depends on the result of the previous, and at the end, offer access to ALL the resolves

code:

var getStudentById = function(studentId){
  return $.Deferred(function(def){
      def.resolve({name:"John",favoriteBookId:222});  //assume ajax gets Student obj
    }).promise();  
}

var getBookTitleById = function(bookId){
  return $.Deferred(function(def){
      def.resolve("Book " + bookId);  //assume ajax call gets title
    }).promise();   
}

var showMessage = function(student, bookTitle){
   alert(student.name + " likes " + bookTitle)
} 

I've seen many solutions that follow the following pattern

deferred_a.then(b).then(c).then(y).then(z).done(r)

which would translate to:

getStudentById(5)    //resolves with student object
.then(function(student){
    return getBookTitleById(student.favoriteBookId)}  
)   
.done(function(bookTitle){
    alert(bookTitle);  //obviously  this works

    // how can I get reference to the student object here? so i can say:
    // showMessage (student, bookTitle)
});

but i need to call the method showMessage(student, bookTitle), where the parameters are the result of each ajax call in the chain

I cannot find an (elegant) example where ALL the results of sequential Deferred calls are accessible at the end of the chain, as it the done function gets the result of the LAST then

My best solution is to wrap the second Deferred in a factory-type method, but this cant be the best solution, right? Am I missing something from the Deferred usage that should simplify this?

getStudentById(5)    
.then(function(student){
        return $.Deferred(function(def){
            getBookTitleById(student.favoriteBookId)  
            .done(function(bookTitle){def.resolve({student:student, bookTitle:bookTitle})})
        }).promise();   
    })  
.done(function(studentAndBookTitle){
    alert(studentAndBookTitle.student.name + " likes " + studentAndBookTitle.bookTitle);
});
genob
  • 23
  • 5

2 Answers2

0

Another way to chain together promises is to have one then handler return another promise. The advantage of this is that you can make use of closures in order to reference variables from the resolution of the first promise within the resolution of the second promise.

So your example would look like:

getStudentById(5)
.then(function(student) {
    return getBookTitleById(student.favoriteBookId)
    .then(function (bookTitle) {
        doSomethingWithStudentAndBookTitle(student, bookTitle)
    });
});

function doSomethingWithStudentAndBookTitle(student, bookTitle) {
    alert(student.name + " likes " + bookTitle);
}

EDIT: This solution is somewhat similar to your last snippet; but

  1. getBookTitleById already returns a promise, you don't need to wrap it in a deferred and then get the promise from it.

  2. You're already making use of closure to wrap those two properties into a single object; there's no reason (at least none that I can see) that you need to wrap those properties up and then do something with them in a different spot, rather than just doing something with them right there.

EDIT 2: Created a function to separate data consumption from data providing.

Retsam
  • 30,909
  • 11
  • 68
  • 90
  • Excellent! I neglected closures completely for my params (Student object), yet, used it in my unnecessary deferred wrapper. Thank you! much more concise and straightforward. – genob May 28 '14 at 00:25
0

Genob, your "best solution" is totally viable and acceptable. It just needs tidying :

getStudentById(5).then(function(student) {
    return getBookTitleById(student.favoriteBookId).then(function(bookTitle) {
        return {
            student: student,
            bookTitle: bookTitle
        });
}).done(function(superObj) {
    alert(superObj.student.name + " likes " + superObj.bookTitle);
});

You might also consider a mild variant of the same approach, which is possible because student is already an object, allowing it to be extended rather than creating a super-object.

getStudentById(5).then(function(student) {
    return getBookTitleById(student.favoriteBookId).then(function(bookTitle) {
        return $.extend(student, {
            favoriteBookTitle: bookTitle
        });
    });
}).done(function(extendedStudent) {
    alert(extendedStudent.name + " likes " + extendedStudent.favoriteBookTitle);
});

This approach (both variants) may be summarised as "passing data down the promise chain in an accumulator object" or more simply a "promised data accumulator" pattern (my term - you won't find it anywhere else).

However achieved, it is good to keep the "consumer" method (the terminal .done()) free from the confines of nesting/closures.

Roamer-1888
  • 19,138
  • 5
  • 33
  • 44
  • Thank you! while your proprietary "promised data accumulator" solution is indeed interesting, in my case, I do prefer your first (non-extending) super-object snippet. It is a bit more verbose than Retsam's, however i do like the clarity of the final "showMessage/alert" call in the "terminal" done, vs within the nested then() function. Thanks. – genob May 28 '14 at 00:32
  • Yes, deinitely definitely don't nest the consumer method. For sure, that would work but it's bad practice. – Roamer-1888 May 28 '14 at 00:34
  • Reasons: (a) Expectation/readability - when you or someone else comes to read the code in the future, it's easier to read code in which data delivery is separated from data consumption; and (b) Splitability/reusability - the data delivery part can be readily consigned to a function which returns a promise, allowing the consumer part to be chained to the function call. – Roamer-1888 May 28 '14 at 00:40
  • Honestly, I think arbitrarily adding properties to an object is a bad practice; you really shouldn't mutate your data more than necessary, certainly not just for convenience of not restructuring your code. (By the way, your `$.extends` is functionally identical to the more readable `student.favoriteBookTitle = bookTitle`) – Retsam May 28 '14 at 15:51
  • If you want to separate the consumption from the providing; don't jump through unnecessary hoops of mutating data or creating unnecessary wrappers, just create a function and pass the data to the function. I've edited my answer to show this. – Retsam May 28 '14 at 15:56