Writing a Trello Bookmarklet with Async

I love Trello and it seems like I use it for everything. Whether it's a project for work, a side-project on my own, a personal To Do list, or planning a family vacation - I probably have a Trello board for it.

Although the built-in Search for Trello is pretty solid, sometimes it just doesn't give me what I want. For example, since I have so many different boards, I often want to be able to see all of the cards that are in my "Doing" lists across all boards. To accomplish this, I decided to try to write a Trello bookmarklet in conjunction with the Trello client.js library.

I found the best place to start was the Trello-Bookmarklet repository. This bookmarklet creates Trello cards from other services (e.g., Github, Gmail, etc.) which is functionality I wasn't necessarily interested in. But what it did show me was a good example of the boilerplate code I needed to include to get started with interacting with the Trello API. This boilerplate code includes:

  1. Loading jQuery (needed for DOM manipulation for showing cards)
  2. Loading the Trello client.js library
  3. Authorizing Trello (this includes getting your token from https://trello.com/app-key, showing a dialog to paste it in, and storing it in local storage)

This boilerplate code isn't trivial and I didn't want to have to include it every time I wanted to write a bookmarklet. So I refactored this code a little to put it in a file I called trello-bookmarklet.core.js - I also had it load async.js because this library drastically simplifies writing async JavaScript.

For my bookmarklet, I need to use the Trello client.js to execute the following steps:

  1. Get the member ID of the current user
  2. Get all the open boards for the current user
  3. For each open board, find all the lists that have "Doing" in the name
  4. For each of these "Doing" lists, get all the cards that are either unassigned or assigned to the current user
  5. Display these cards on a dialog

The Trello library doesn't natively support promises. This is where an async library comes in handy. Without it, I'm find myself in the Pyramid of Doom with deeply nested callbacks.

My first step is to put the 5 steps above into the async waterfall structure:

async.waterfall([
    // Get ID for current user
    function (callback) {
        callback();
    },
    // Get all open boards
    function (callback) {
        callback(null, boards);
    },
    // Get "Doing" Lists from all open boards
    function (openBoards, callback) {
        callback(null, doingLists);
    },
    // Get all cards across all "Doing" lists
    function (doingLists, callback) {
        callback(null, doingCards);
    }, 
    // Display all "Doing" cards
    function (err, doingCards) {
    }
]);

The waterfall() method will sequentially execute these async functions in order, with each function passing its results to the next function in the sequence. The first argument is an error condition - if you're not passing anything to the next function, no arguments are necessary (null error condition assumed) - just execute the callback() method to signify function completion. If you are passing objects to the next method, then the first argument is null (no error) and the next arguments will be passed in to the next function.

In the first function, I just want to get my memberId and store it for later:

function (callback) {
    Trello.get('members/me', { fields: 'id' }, function (data) {
        memberId = data.id;
        callback(null);
    });
}

The Trello object is available in my context since "core" has loaded it. It enables me to pass a fields parameter so I constrain the response payload to include only the single field ("id") that I'm interested in.

In the second function, I want to get all my open boards and pass them to the next method:

Trello.get('members/me/boards', { filter: 'open' }, function (boards) {
    callback(null, boards);
});

The third function is where things get more interesting. For each open board, I need to find any list that has "Doing" in the name. This means I need to fire off an unknown number of async calls, yet keep their results coordinated - in fact, combine their results into a single, larger result. The concat() method is exactly what I need here. It will make all these async calls in parallel (I don't care about the order) and for each call I can store the results. The concat() method then has a final callback which has an array containing all of the previously stored results (i.e., all arrays are concatenated together into this larger single array).

async.concat(openBoards, function (board, callback) {
    Trello.get('boards/' + board.shortLink + '/lists', { filter: 'open' }, function (lists) {
        var doing = $.grep(lists, function (item) {
            return item.name.indexOf('Doing') !== -1;
        });
        var doingLists = doing.map(function (item) {
            return {
                name: item.name,
                board: board,
                id: item.id
            };
        });
        callback(null, doingLists);
    });
}, function (err, doingLists) {
    callback(null, doingLists);
});

The fourth function to get all the cards across these lists uses the same technique with the concat() method (code omitted here for brevity).

The fifth and final function, takes all these cards and displays the result:

function (err, doingCards) {
    var htmlResult = '';
    $.each(doingCards, function (index, card) {
        htmlResult +=
        '<h2><a target="_blank" href="' + card.shortUrl + '">' + card.name + '</a></h2>' +
        '<p>in <strong>' + card.list + '</strong> on <strong>' + card.board + '</strong></p><hr/>';
    });
    trelloBookmarkletCore.overlayPrompt(htmlResult, false);
});

Note the trelloBookmarketCore provides this overlayPrompt() method for convenience.

The final result will look something like this:

The full code for the trello-doing-cards-bookmarklet.js is here. The entire Github repository is located here.

To invoke the bookmarklet, the following code is necessary:

javascript:(function (a) { var b = a.createElement("script"); b.src = "https://rawgit.com/smichelotti/trello-bookmarklets/master/trello-doing-cards-bookmarklet.js"; a.getElementsByTagName("head")[0].appendChild(b);})(document);

Notice I'm serving the files from rawgit.com but I could certainly move them to my own CDN.

The main optimization that I could make is to use gulp/grunt to concatenate/uglify the JavaScript dependencies (i.e., async, Trello client.js, jQuery) in advance. Currently, there is certainly a little lag the first time it runs to get these files.

Hopefully this is helpful for anyone interested in writing a bookmarklet or interested in using the Trello API in general.

Tweet Post Share Update Email RSS