Search code examples
jqueryvariablesdeferredchaining

jquery - Pass variables into a through a deferred chain and avoid nesting


I'm currently building a simple lightbox for my website using jquery.deferred patterns and while doing so I'm trying to maintain a flat chain of asynchronous operations and avoid the infamous nested callback pyramid of doom. Here is what I have so far:

$('#gallery a').click( function() {

    var id = $(this).attr('id');    // Id of the current selection
    var current = 0;                // Array index of the current item

    getTpl()                        // Get and load lightbox template
        .then(getData)              // Get JSON data of the selected item
        .then(getItem)              // Pass JSON data and get the item
        .then(loadItem)             // Use JSON data to create the HTML output
        .done(function() {
            // Wrap everything up, intialize the lightbox controls
            // (requires the JSON data) and load the lightbox
        });
});

The first two steps (getTpl, getData) are simple AJAX promises and work fine. I can also pass the JSON data further down the chain to the third step (getItem). That's where the problems start:

  1. I have to pass the variables 'id' and 'current' from the higher scope to the functions that are called during the chain (getItem and loadItem use these variables to construct URLs and create HTML output). How do I do that without breaking the asynchronous chain?
  2. Do I really have to return my JSON data (and other variables) from every function along the way to use it further down the chain? Isn't there an easier way to keep the data and necessary variables available throughout the entire chain?
  3. Can I throw random operations into the chain, e.g. a simple jquery fadeIn? That's what '.done' is for, because '.then' expects a new promise, right?

Thanks in advance!


Solution

  • If you do it like this:

    var id = $(this).attr('id');    // Id of the current selection
    var current = 0;                // Array index of the current item
    
    getTpl()                        // Get and load lightbox template
        .then(getData)              // Get JSON data of the selected item
        .then(getItem)              // Pass JSON data and get the item
        .then(loadItem)
    

    Then, you are leaving it to the promise system to set up the arguments for your next function in the chain. Since the promise system is going to use the return from the previous operation in the chain, if you want to pass arguments through to all of these, then all functions have to participate in passing them on. With this exact structure, there is no way around it.

    For more general approaches to sharing data with the chain, see:

    How to chain and share prior results with Promises

    A common scheme for passing an arbitrary number of variables through like this is to put them on an object and just have each function accept and object and return that object.

    var options = {
        id: $(this).attr('id'),     // Id of the current selection
        current: 0                  // Array index of the current item
    };
    
    getTpl(options)                 // Get and load lightbox template
        .then(getData)              // Get JSON data of the selected item
        .then(getItem)              // Pass JSON data and get the item
        .then(loadItem)
    

    Then, each function getTpl(), getData(), getItem() and loadItem() can expect their first argument to be this option object and they should all resolve with that options object. They are each free to add new properties onto the object that will be passed on through the chain.


    You do have some other options for how to structure it. If your functions are inline, then they can directly access part variables within scope:

    var id = $(this).attr('id');    // Id of the current selection
    var current = 0;                // Array index of the current item
    
    getTpl()                        // Get and load lightbox template
        .then(function() {
            // code here can directly access current and id variables
        }).then(...)
    

    Or, you can call your functions directly and pass them whatever variables they need:

    var id = $(this).attr('id');    // Id of the current selection
    var current = 0;                // Array index of the current item
    
    getTpl()                        // Get and load lightbox template
        .then(function() {
            return getData(id, current);
        }).then(function(data) {
            return getItem(data, id);
        }).then(function(item) {
            return loadItem(item, current);
        });