Lugo Labs

Backbone Loader

One of the rules I follow when developing Backbone apps is: never call views directly, get them react to model/collection changes. In this spirit, let's build a simple functionality that adds a loader hint when fetching the model from the server.

(You can find the files used for this example on github).

I use Jasmine for writing the JavaScript specs, so I downloaded their stand-alone version into the project.

The Model

Backbone doesn't come with a specific event when starting to fetch a model, so we'll create one. Let's write the spec for it:

js
describe("app.LoaderModel", function() {
    describe("#fetch", function() {

        it("triggers fetch:start", function() {
            var loader =    new app.LoaderModel,
                triggered = 0;

            loader.on("fetch:start", function() {
                triggered = 1;
            });

            loader.fetch();

            expect(triggered).toEqual(1);
        });
    });
});

We need a fetch:start event to be fired when the fetch starts. I use a triggered variable to check this, but Jasmine spies can also be appropriate. In general, I use plain JavaScript code first, but libraries can come handy too.

In order to get our spec to pass, let's create a app.LoaderModel and override its fetch method.

js
var app = app || {};

app.LoaderModel = Backbone.Model.extend({
    fetch: function(options) {
        this.trigger('fetch:start');
        this.constructor.__super__.fetch.apply(this, [opions]);
    }
});

Overriding the model's fetch method allows us to trigger the event everytime fetch is called. After triggering the event, we call the Backbone's function.

We also need a fetch:stop event to be fired when the fetch is complete. Here's the spec:

js
it("triggers fetch:stop", function() {
    var loader =    new app.LoaderModel,
        triggered = 0,
        complete = false;

    loader.on("fetch:stop", function() {
        triggered = 1;
    });

    runs(function() {
        loader.fetch();
        setTimeout(function() {
            complete = true;
        }, app.syncTimeout + 10);
    });

    waitsFor(function() {
        return complete;
    }, 'Fetch', app.syncTimeout + 20);

    runs(function() {
        expect(triggered).toEqual(1);
    });
});

The spec uses the app.syncTimeout variable used to emulate the server wait for the response. We make sure that the check for the event trigger is done after the fetch is complete.

To keep it simple, we'll trigger the event using the success option passed to fetch:

js
fetch: function(options) {
    this.trigger('fetch:start');
    options = options ? _.clone(options) : {};
    var success = options.success;
    var model = this;
    options.success = function() {
        model.trigger('fetch:stop');
        if (success) success();
    };
    this.constructor.__super__.fetch.apply(this, [options]);
}

The View

The view needs to listen to the fetch:start event and call a loading function. Backbone uses the on function to tie up the events to their handlers, so let's check that this happens during the view's initialization:

js
describe("app.LoaderView", function() {
    describe("#initialize", function() {
        var model, view;

        beforeEach(function() {
            model = new app.LoaderModel;
            spyOn(model, 'on');
            view = new app.LoaderView({model: model});
        });

        afterEach(function() {
            model = null;
            view = null;
        });

        it("listens to model's fetch:start", function() {
            expect(model.on).toHaveBeenCalledWith('fetch:start', view.loading, view);
        });
    });
});

We use a spy function, provided by Jasmine, which takes over our function and tracks its calls, arguments, and returning values.

To get the spec to pass let's override the initialize method on our view:

js
app.LoaderView = Backbone.View.extend({
    initialize: function() {
        this.model.on('fetch:start', this.loading, this);
    }
});

and add the loading method:

js
loading: function() {
    this._loader = this._loader || $('#loader-progress');
    this._loader.html('loading ...');
}

The loading should stop on 'fetch:stop'. First the spec:

js
it("listens to model's fetch:stop with stopLoading", function() {
    expect(model.on).toHaveBeenCalledWith('fetch:stop', view.stopLoading, view);
});

Then add to the initialize:

js
this.model.on('fetch:stop', this.stopLoading, this);

And a simple stopLoading function:

js
stopLoading: function() {
    if (this._loader) this._loader.empty();
}

A working example can be found on github.