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:
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.
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:
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
:
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:
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:
app.LoaderView = Backbone.View.extend({
initialize: function() {
this.model.on('fetch:start', this.loading, this);
}
});
and add the loading
method:
loading: function() {
this._loader = this._loader || $('#loader-progress');
this._loader.html('loading ...');
}
The loading should stop on 'fetch:stop'. First the spec:
it("listens to model's fetch:stop with stopLoading", function() {
expect(model.on).toHaveBeenCalledWith('fetch:stop', view.stopLoading, view);
});
Then add to the initialize
:
this.model.on('fetch:stop', this.stopLoading, this);
And a simple stopLoading
function:
stopLoading: function() {
if (this._loader) this._loader.empty();
}
A working example can be found on github.