6
\$\begingroup\$

I'm a JavaScript rookie, but I wrote a JavaScript function as part of a Chrome extension that will activate the tab to the left of the currently active tab. The function is pretty ugly, but I couldn't figure out another way to write it.

The logic is pretty simple, but I ran into problems setting variables throughout the various scopes. How would I move the functionality of the Chrome API calls into individual functions that return values? Can callback functions return a value?

// Activates the tab to the left of the currently active tab
tabLeft = function() {
    var parent = this;
    var activeTabIndex;
    var targetTabIndex;

    // Get the index of the active tab
    chrome.tabs.query({'active':true, 'currentWindow': true}, function(tabs) {
        parent.activeTabIndex = tabs[0].index;
    });

    // Get the count of the number of tabs in the current window
    chrome.tabs.query({'currentWindow': true}, function(tabs) {
        var tabCount = tabs.length;
        parent.targetTabIndex = (parent.activeTabIndex == 0) ? (tabCount - 1) : (parent.activeTabIndex - 1);

        // Get the tabId of the of the tab we want to activate
        chrome.tabs.query({'index': parent.targetTabIndex}, function(tab) {
            var tabId = tab[0].id;

            // Activate the target tab
            chrome.tabs.update(tabId, {'active': true}, function() {});
        }); 
    });
};
\$\endgroup\$
1
2
\$\begingroup\$

Some of the Chrome APIs are implemented using callbacks. This means that these methods are potentially async. Code doesn't run in the order they're written, you have been warned.

The closest you'll get to sequentially written async code is by using Promises. In simple terms, these are objects that hold some state, and when a certain state is attained (resolve or reject), the attached handlers get executed.

The beauty lies in then which when returned a promise, attaches the next chained then to that promise instead. This makes the next then appear to wait for the previous async operation. However, when returned a non-promise value, it uses that value as the resolved value for the next chained then. The ability make the code appear to wait, the ability to alter the resolved values as well as carry values through makes Promises powerful.

I think Chrome should have support for Promises already. Here's an example where a we wrap the chrome.tabs.query in a promise, and when it succeeds, it resolves the promise.

function promiseQuery(options){
  return new Promise(function(resolve,reject){
    chrome.tabs.query(options, resolve);
  });
}

function promiseUpdate(tabId,options){
  return new Promise(function(resolve,reject){
    chrome.tabs.update(tabId,options,resolve);
  });
}

function tabLeft(){

  promiseQuery({'active':true, 'currentWindow': true})
  .then(function(tabs){
    // This function executes when the promiseQuery resolves

    // Get the current index
    var activeIndex = tabs[0].index;

    // We fire another query. But instead of returning a promise, we can chain a then
    // to alter the resolved value.
    return promiseQuery({'currentWindow': true})
      .then(function(tabs){
        return{
          length : tabs.length,
          activeIndex : activeIndex
        }
      });
  })
  .then(function(data){
    // This executes with data being the altered resolved value

    var targetIndex = ((data.activeIndex === 0) ? data.length : data.activeIndex) - 1;
    return promiseQuery({'index': targetIndex});
  })
  .then(function(tabs){
    // same here
    var tabId = tab[0].id;

    // As long as you return a promise
    return promiseUpdate(tabId, {'active': true});
  })
  .then(function(){
    // Done
  });
}

For now, it still looks like a jumble. However, you can now pull out functions and just hand references to then. There's no more problem with persisting values in an outer scope since you can just pass them through the then chain.

function promiseQuery(options){
  return new Promise(function(resolve,reject){
    chrome.tabs.query(options, resolve);
  });
}

function promiseUpdate(tabId,options){
  return new Promise(function(resolve,reject){
    chrome.tabs.update(tabId,options,resolve);
  });
}

function initialCall(){
  return promiseQuery({'active':true, 'currentWindow': true})
}

function activeAndLengthQueries(){
  var activeIndex = tabs[0].index;
  return promiseQuery({'currentWindow': true})
    .then(function(tabs){
      return{
        length : tabs.length,
        activeIndex : activeIndex
      }
    });
}

function targetTabQuery(){
  var targetIndex = ((data.activeIndex === 0) ? data.length : data.activeIndex) - 1;
  return promiseQuery({'index': targetIndex});
}

function tabUpdateFunction(){
  var tabId = tab[0].id;
  return promiseUpdate(tabId, {'active': true});
}

function doneFunction(){

}

function tabLeft(){
  initialCall()
  .then(activeAndLengthQueries)
  .then(targetTabQuery)
  .then(tabUpdateFunction)
  .then(doneFunction);
}

Breaking down an operation into promises has no fixed rule. However, I write down the code in the sequence I want (just a flat list of things to do). Then run though them again. This time, if you see an async operation, that ends a then (notice how the previous example ends with return somePromiseGeneratingFunction). Do that operation and return it's promise. The sequence will continue in the next then.

\$\endgroup\$
3
  • \$\begingroup\$ Promises were indeed the way to go. I converted my code over to an implementation that works using jQuery. I like how you broke down the code, and will use the function for query and update similarly to how you did. Thanks for all the work. \$\endgroup\$
    – GregB
    Nov 17 '14 at 21:04
  • \$\begingroup\$ @GregB Updated the answer :) \$\endgroup\$
    – Joseph
    Nov 18 '14 at 2:34
  • 1
    \$\begingroup\$ @JosephtheDreamer Please encourage users to post separate follow-questions for iterative reviews. \$\endgroup\$ Nov 18 '14 at 4:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.