JavaScript
Article

Custom Events and Ajax Friendly Page-ready Checks

By Bruno Skvorc

Not so long ago, I built a Chrome extension that lets users export lists from Trello. You can see the short series on how that was done here. That extension had some room left for improvements, though.

For example, checking whether the board has changed – something that’s less than simple to do considering all of Trello is ajaxy and the URL changes via undetectable pushstate. We also want it to support having multiple Trello tabs open, so just checking the URL wouldn’t do. It would also be good if we could somehow make sure slow page loads don’t affect the “page ready” state – in the original extension, due to Trello’s “over-ajaxness”, the page was “ready” before the content loaded – even the board’s area was loaded via ajax and as such, events weren’t easy to attach to the core DOM elements of Trello’s UI.

For that purpose, and to make my life developing extensions for Trello easier in the future, I decided to build a TrelloUI library which will take care of this particular issue for me. The TrelloUI library will be extended with other functionality in the future, but for now, let’s build our “ready checker”.

What we’re building

We’ll be building a helper library we can call from our Chrome extension (or Firefox addon, if you’re so inclined) that allows us to attach an event listener to the document object for the event trelloui-boardready. Once that event is fired, we’ll know the board has loaded and we’re free to attach events to board UI elements. Finally, we’ll improve it by adding more events for other use cases, so that we can broadcast any event we want in the future.

Antenna mast sign

We’ll be testing the library on a Chrome extension, by including it as a content script. You can test it on a fresh version of ChromeSkel_a, the skeleton Chrome extension that is usable out-of-the-box, or on the version of Trello Helper we’ve built in the previous series.

All you need is an editor and Chrome with developer mode activated (go to chrome:extensions and tick the “developer mode” box).

Building the Library

Let’s start building. Prepare your environment by activating Chrome’s dev mode and setting up a test project.

Content Scripts

Trello’s client library requires jQuery, so we’ll include it in our project. Download a recent copy (preferably version 2+) and include it as a content script. Create a file called trelloui.js and another called main.js, then include them too. Your content script block should look like this:

"content_scripts": [
        {
            "matches": ["https://trello.com/b/*"],
            "js": [
                "lib/jquery-2.1.1.min.js",
                "lib/TrelloUI/trelloui.js",
                "scripts/main.js"
            ],
            "run_at": "document_idle"
        }

You can choose the folder structure you want – I like to put “libraries” into “lib”, but it’s not that important.

Bootstrapping

In trelloui.js, we start by creating a new “class”.

var TrelloUI = function () {};

It’s just a function we’ll be extending with some method properties.

Checking for end state

First, let’s think about what the end state is – when is the trelloui-boardready event going to get fired? We need to have a way to check the board has loaded and become visible, and then let the document know it happened via the event. But we need to make sure the checking stops once the board appears, else we’ll have an interval checker running for ever. Add the following to trelloui.js:

TrelloUI.prototype._checkState = function () {
    return $('#board').hasClass('trelloui-boardready');
};

Simple – we add a function that checks if the board element has the given class. We can add this class after the event is fired; we’ll do that later. But checking for the class a single time won’t do us much good – we need to keep checking if we want to be sure the extension’s functionality survives page reloads and board changes. Let’s change the above into:

var TrelloUI = function () {
    setInterval(this._checkState.bind(this), 1000);
};

TrelloUI.prototype._checkState = function () {
    if (!$('#board').hasClass('trelloui-boardready')) {
        this._registerEvents();
    }
};

This makes use of the “constructor” where we, upon calling new TrelloUI in our main code, get TrelloUI to automatically set the interval for checking whether the body element contains the class we want every second. If it doesn’t, we call _registerEvents (a function we’re yet to write) to add the class and dispatch the event as soon as the board appears.

Note that we’re using this._checkState.bind(this) instead of this._checkState because this gets detached during setInterval.

Building a new Event

You can read up on more details about creating custom events in this post. In our example, we’ll just use the most rudimentary of settings. Change the constructor to this:

var TrelloUI = function () {
    var eventDefaults = {
        bubbles: true,
        cancelable: true
    };

    this.possibleEvents = {
        boardEvent: new Event('trelloui-boardready', eventDefaults)
    };

    setInterval(this._checkState.bind(this), 1000);
};

We used eventDefaults to set defaults for any other additional events we may want to define later on, so we don’t have to keep repeating ourselves. Bubbles means the event bubbles to parent elements from the element it’s triggered on. Cancelable means it can be stopped with event.stopPropagation, if the user so desires. These flags mean virtually nothing to us right now, but they’re good defaults. We then define an internal possibleEvents property which holds all the possible events our little experiment can dispatch.

Options and The Constructor

We mentioned we might want to implement other events later on, so let’s make sure it’s easily possible:

var TrelloUI = function (options) {
    this._defaultOptions = {
        dispatchBoardReady: false
    };
    this.options = jQuery.extend({}, this._defaultOptions, options);

    var eventDefaults = {
        bubbles: true,
        cancelable: true
    };

    this.possibleEvents = {
        boardEvent: new Event('trelloui-boardready', eventDefaults)
    };

    setInterval(this._checkState.bind(this), 1000);
};

Here we want TrelloUI to dispatch an event when the board is ready, but we’re taking into account our potential future desire to implement other events. But checking for all the events by default would be quite resource intensive. (Well, not really – actually, all but the weakest computers would succeed in processing them all, even if we were dealing with hundreds, but when I see web pages and extensions using up 2GB+ of RAM just for idling, I tend to shy away from taking resources for granted.)

For simple merging of settings and passed in options, we’re using jQuery’s extend.

This setup allows us to do the following to use TrelloUI:

var tui = new TrelloUI({
        dispatchBoardReady: true
    });

Here we tell TrelloUI to instantiate and to keep an eye out for a possibility when it can trigger the boardReady event. If we don’t give it this option, the default in the constructor will stop it from trying, conserving resources.

Event Firing

Rocket

Finally, let’s build that event firing functionality.

TrelloUI.prototype._registerEvents = function () {

    var current = this;

    if (this.options.dispatchBoardReady) {
        var boardInterval = setInterval(function () {
            var board = $('#board');
            if (board && !$(board).hasClass(current.possibleEvents.boardEvent.type)) {
                document.dispatchEvent(current.possibleEvents.boardEvent);
                $(board).addClass(current.possibleEvents.boardEvent.type);
                clearInterval(boardInterval);
            }
        }, 100);
    }
};

Let’s break it down. First, we alias this into a local variable so we can easily use it within the closure below. Then, an interval is defined for every 100 miliseconds which first grabs the board element if it exists. If it does, and if the body still doesn’t have the class we want it to have, we dispatch the event, add the class, and clear the interval. Otherwise, the interval repeats.

Finally, let’s improve _checkState so it ignores the check if the option is set to false:

TrelloUI.prototype._checkState = function () {
    if (this.options.dispatchBoardReady) {
        if (!$('#board').hasClass(this.possibleEvents.boardEvent.type)) {
            this._registerEvents();
        }
    }
};

Additional Events

If you now add the following into your main.js script, you should be able to load it into Chrome and see “Board is ready” in your console:

var tui = new TrelloUI({
        dispatchBoardReady: true
    }
);

document.addEventListener('trelloui-boardready', function() {
    console.log("Board is ready!");
});

But… this still isn’t enough for our extension from the previous series. There, we interact with lists. And lists load after the board. Obviously, we need a listsReady event.

First, we add a new Event, both to the options and the list of possible Events:

var TrelloUI = function (options) {
    this._defaultOptions = {
        dispatchBoardReady: false,
        dispatchListsReady: false
    };
    this.options = jQuery.extend({}, this._defaultOptions, options);

    var eventDefaults = {
        bubbles: true,
        cancelable: true
    };

    this.possibleEvents = {
        boardEvent: new Event('trelloui-boardready', eventDefaults),
        listsEvent: new Event('trelloui-listsready', eventDefaults)
    };

    setInterval(this._checkState.bind(this), 1000);
};

Then, we update _registerEvents by adding the following block:

if (this.options.dispatchListsReady) {
        var listsInterval = setInterval(function() {
            var lists = $('.list');
            if (lists.length > 0 && !$(lists[0]).hasClass(current.possibleEvents.listsEvent.type)) {
                document.dispatchEvent(current.possibleEvents.listsEvent);
                $(lists[0]).addClass(current.possibleEvents.listsEvent.type);
                clearInterval(listsInterval);
            }
        }, 100);
    }

If there are lists, and the first list doesn’t have the class that indicates readiness yet, dispatch the event and add the class to the first list.

Finally, let’s tweak the _checkState again by adding a new block:

if (this.options.dispatchListsReady) {
        var lists = $('lists');
        if (!lists.length || !$(lists[0]).hasClass(this.possibleEvents.listsEvent.type)) {
            this._registerEvents();
        }
    }

Implementation

Implementing these events now is as simple as declaring the following in the main script:

var tui = new TrelloUI({
        dispatchBoardReady: true,
        dispatchListsReady: true
    }
);

document.addEventListener('trelloui-boardready', function() {
    console.log("Board is ready!");
});
document.addEventListener('trelloui-listsready', function() {
    console.log("Lists are ready!");
});

Each time you change the board now, you should be notified of the board and lists being ready. Add your logic in place of the console.log statements and make some magic happen!

Conclusion

In this short tutorial, we built a simple library for interaction with Trello’s UI – a helper which fires various “ready” events that can help us detect when “ajaxy” parts of the have finished loading, so we can properly interact with them.

We can still do much to improve this “library” – removing the jQuery dependency, for example, or extracting code that is similar in _checkState and _registerEvents into something that can be shared between them. Right now, though, it’s perfectly fine for our needs – letting us know when the Trello UI is ready for tweaking! Care to help? Pull requests are welcome on the Github repo!

Free Guide:

7 Habits of Successful CTOs

"What makes a great CTO?" Engineering skills? Business savvy? An innate tendency to channel a mythical creature (ahem, unicorn)? All of the above? Discover the top traits of the most successful CTOs in this free guide.

Comments
JohnGinsberg

Nice article Bruno. I notice you changed the method name checkState to _checkState midway through the story. Perhaps you could explain your reasoning (for example, convention used for private methods)?

swader

Oh snap, good catch, thanks! Fixed!

The method was supposed to be private all along, I just missed the underscore in the first code block. The underscore has always been an indicator of "privacy" in methods in various languages, so I used it here as convention. The checkState method isn't meant to be used by anything outside this "library", so marking it as private like so was only logical. It's also a sort of precursor to a further upgrade to the whole matter of this extension - building React components to simplify Trello UI customization even further.

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in JavaScript, once a week, for free.