backbone.js - how and when to show a spinner - javascript

Is there any sort of hooks in backbone where I can easily say "whenever any of the collections is fetching data, show the spinner, hide it when they're done"?
I have a feeling it will be more complicated than that and require overwriting specific functions. When should I show the spinner? On fetch() or refresh() or something else?

You can use jQuery ajaxStart and ajaxStop. Those will globally run when an ajax request is made, so fetch and save will cause those to run. Add your code to show the spinner in the start and hide it in the end.

in Backbone.js 1.0.0 you can use the request and sync events http://backbonejs.org/#Events-catalog
This goes in the view.
initialize: function(){
this.items = new APP.Collections.itemCollection();
this.items.bind('request', this.ajaxStart, this);
this.items.bind('sync', this.ajaxComplete, this);
}
ajaxStart: function(arg1,arg2,arg3){
//start spinner
$('#item-loading').fadeIn({duration:100});
},
ajaxComplete: function(){
$('#item-loading').fadeOut({duration:100});
}
This can be applied per collection or per model here's some CSS for the spinner http://abandon.ie/notebook/simple-loading-spinner-for-backbonejs

Backbone doesn't trigger any event when Collection::fetch() starts (see source code), so you will have to override the fetch method. Maybe something like this:
var oldCollectionFetch = Backbone.Collection.prototype.fetch;
Backbone.Collection.prototype.fetch = function(options) {
this.trigger("fetch:started");
oldCollectionFetch.call(this, options);
}
This will override the fetch method to give you an event when the fetch starts. However, this only triggers the event on the specific collection instance so if you have a bunch of different collections you'll have to listen for that event on each collection.

The way i have done this without overriding backbone is:
In view
var myView = Backbone.View.extend({
initialize; function(){
this.$el.addClass('loading');
collection.fetch(success:function(){
this.$el.removeClass('loading')
})
}
})
The other route would be to remove the loading class when the models are added, usually you have:
var myView = Backbone.View.extend({
initialize; function(){
_.bindAll(this, 'addAll')
collection.bind('reset', this.addAll)
this.$el.addClass('loading');
collection.fetch();
},
addAll: function(){
this.$el.removeClass('loading');
collection.each(this.addOne);
}
})
These would be almost identical in most cases, and as the loader is really for the users experience removing it just prior to displaying the content makes sense.

And a little update. Since Dec. 13, 2012 have been added a "request" event to Backbone.sync, which triggers whenever a request begins to be made to the server. As well since Jan. 30, 2012 have been added a "sync" event, which triggers whenever a model's state has been successfully synced with the server (create, save, destroy).
So, you don't need to override or extand the native Backbone's methodes. For listening 'start/finish fetching' event you can add listener to your model/collection like this for example:
var view = new Backbone.View.extend({
initialize: function() {
this.listenTo(this.model, 'request', this.yourCallback); //start fetching
this.listenTo(this.model, 'sync', this.yourCallback); //finish fetching
}
});

You can create a method called sync on any of your models, and backbone.js will call that in order to sync. Or you can simply replace the method Backbone.sync. This will allow you to make the change in only one place in your source code.

I have used NProgress in my backbone and it is the best functioning loader/spinner out there.
var view = Backbone.View.extend({
initialize: function () {
this.items = new APP.Collections.itemCollection();
this.items.on('reset', this.myAddFunction, this);
NProgress.start();
collection.fetch({
reset:true,
success: function () {
NProgress.done(true);
}
});
}
});

Use Backbone sync method,
It will call every time backbone sync method, not only fetch, save, update and delete also
/* over riding of sync application every request come hear except direct ajax */
Backbone._sync = Backbone.sync;
Backbone.sync = function(method, model, options) {
// Clone the all options
var params = _.clone(options);
params.success = function(model) {
// Write code to hide the loading symbol
//$("#loading").hide();
if (options.success)
options.success(model);
};
params.failure = function(model) {
// Write code to hide the loading symbol
//$("#loading").hide();
if (options.failure)
options.failure(model);
};
params.error = function(xhr, errText) {
// Write code to hide the loading symbol
//$("#loading").hide();
if (options.error)
options.error(xhr, errText);
};
// Write code to show the loading symbol
//$("#loading").show();
Backbone._sync(method, model, params);
};

Related

Best method for rendering collection in Backbone

I'm using Backbone and I have the following model and collection
App.Models.Person = Backbone.Model.extend({
});
App.Collections.People = Backbone.Collection.extend({
model: App.Models.Person,
url: 'api/people',
});
However what I'm struggling on is the best way to render this collection. Here's how I've done it so far which works but doesn't seem to be the most elegant solution
App.Views.PeopleView = Backbone.View.extend({
el: $(".people"),
initialize: function () {
this.collection = new App.Collections.People();
//This seems like a bad way to do it?
var that = this;
this.collection.fetch({success: function(){
that.render();
}});
},
render: function () {
var that = this;
_.each(this.collection.models, function (item) {
that.renderPerson(item);
}, this);
},
I'm fairly new to Backbone but have to assign this to a different variable to I use it inside of the success function just seems like a bad way of doing things? Any help on best practices would be appreciated.
Backbone allows you to register for events that you can react to. When the collection is synchronized with the server, it will always fire the sync event. You can choose to listen for that event and call any given method. For instance ...
initialize: function () {
this.collection = new App.Collections.People();
this.listenTo(this.collection, "sync", this.render);
// Fetch the initial state of the collection
this.collection.fetch();
}
... will set up your collection so that it would always call this.render() whenever sync occurs.
The docs on Backbone Events are succinct but pretty good. Keep in mind a few things:
The method you use to register event listeners (i.e. listenTo or on) changes how you provide the context of the called function. listenTo, for instance, will use the current context automatically; on will not. This piece of the docs explains it pretty well.
If you need to remove a view, you will need to disconnect event listeners. The easiest way to do that is to use listenTo to connect them in the first place; then when destroying the view you can just call view.stopListening().
For rendering, there are a lots of suggestions for how to do it. Generally having a view to render each individual model is one way. You can also use Backbone.Collection#each to iterate over the models and control the scope of the iterating function. For instance:
render: function() {
this.collection.each(function(model) {
var view = new App.Collections.PersonView({ model: model });
view.render();
this.$el.append(view.$el);
}, this);
}
Note the second argument to .each specifies the scope of the iterator. (Again, have a look at the docs on controlling scope. If you'd rather have a framework help out with the rendering, check out Marionette's CollectionView and ItemView.
If your view is supposed to just render the collection you can send the collection to temnplate and iterate through in template, otherwise you can create another subView for that purpose or send the individual models of the collection to another subview and append to the container, hope it was helpful.

backbone.js: Fire event when view is rendered

I'm developing a webapplication with Resthub, so there is a backbone.js stack at the front-side. I need to call a method, everytime a new view (also all sorts of subviews) is rendered, to add some Twitter-Bootstrap specific stuff (help-popovers, kind of quick help, which get their options from a global json file, so the help-texts are easier to maintain).
As far as I know there isn't a backbone-built-in event which is fired every time a view is rendered.
So my question is: What is the easiest way to extend all views, so that they fire an event when the render method is (implicitly or explicitly) called. I want to extend all my views cause I don't want to trigger this event manually in all views I have, because it's error-prone and all developers has to remember that they've to trigger that event.
If you want to do something(fire an event or anything else) for all cases when the render method is called, the most straight forward way might be to update the render method in your copy of Backbone's source code (assuming you want the behavior across the project).
By default the render method just returns 'this'
render: function() {
return this;
},
If there is something you always want to do before render, you can add it within the render method
render: function() {
//add your extra code/call
return this;
},
Alternatively you can also override the prototype of Backbone.View function and update/create your own version(s) something like
_.extend(Backbone.View.prototype, Backbone.Events, {
render: function() {
console.log('This is a test');
return this;
}
});
var testView = Backbone.View.extend({
});
var testview = new testView();
testview.render(); //displays This is a test
//any view rendered will now have the console log
Taking this a step further, you can add your own version of render, calling it say 'myrender' and/or add your own event(s) say 'myevent' which can then be called before/after you call render/myrender
_.extend(Backbone.View.prototype, Backbone.Events, {
render: function() {
//console.log('This is a test');
this.mynewevent();
return this;
},
myrender: function() {
console.log('Pre-render work');
this.render();
},
mynewevent: function() {
console.log('New Event work');
}
});
var testView = Backbone.View.extend({
});
var testview = new testView();
//testview.render();
testview.myrender();
Underscore's extend is being used here and since Backbone has a dependency on Underscore, if you are using Backbone, Underscore should be available for you as well.

Backbone JS: can one view trigger updates in other views?

In my simple project I have 2 views - a line item view (Brand) and App. I have attached function that allows selecting multiple items:
var BrandView = Backbone.View.extend({
...some code...
toggle_select: function() {
this.model.selected = !this.model.selected;
if(this.model.selected) $(this.el).addClass('selected');
else $(this.el).removeClass('selected');
return this;
}
});
var AppView = Backbone.View.extend({
...some code...
delete_selected: function() {
_.each(Brands.selected(), function(model){
model.delete_selected();
});
return false;
},
});
Thing is, I want to know how many items are selected. In this setup selecting is NOT affecting the model and thus not firing any events. And from MVC concept I understand that views should not be directly talking to other views. So how can AppView know that something is being selected in BrandViews?
And more specifically, I AppView to know how many items were selected, so if more than 1 is selected, I show a menu for multiple selection.
You might want to have a read of this discussion of Backbone pub/sub events:
http://lostechies.com/derickbailey/2011/07/19/references-routing-and-the-event-aggregator-coordinating-views-in-backbone-js/
I like to add it in as a global event mechanism:
Backbone.pubSub = _.extend({}, Backbone.Events);
Then in one view you can trigger an event:
Backbone.pubSub.trigger('my-event', payload);
And in another you can listen:
Backbone.pubSub.on('my-event', this.onMyEvent, this);
I use what Addy Osmani calls the mediator pattern http://addyosmani.com/largescalejavascript/#mediatorpattern. The whole article is well worth a read.
Basically it is an event manager that allows you to subscribe to and publish events. So your AppView would subscript to an event, i.e. 'selected'. Then the BrandView would publish the 'selected' event.
The reason I like this is it allows you to send events between views, without the views being directly bound together.
For Example
var mediator = new Mediator(); //LOOK AT THE LINK FOR IMPLEMENTATION
var BrandView = Backbone.View.extend({
toggle_select: function() {
...
mediator.publish('selected', any, data, you, want);
return this;
}
});
var AppView = Backbone.View.extend({
initialize: function() {
mediator.subscribe('selected', this.delete_selected)
},
delete_selected: function(any, data, you, want) {
... do something ...
},
});
This way your app view doesn't care if it is a BrandView or FooView that publishes the 'selected' event, only that the event occured. As a result, I find it a maintainable way to manage events between parts of you application, not just views.
If you read further about the 'Facade', you can create a nice permissions structure. This would allow you to say only an 'AppView' can subscribe to my 'selected' event. I find this helpful as it makes it very clear where the events are being used.
Ignoring the problems with this that you already mention in your post, you can bind and trigger events to/from the global Backbone.Event object, which will allow anything to talk to anything else. Definitely not the best solution, and if you have views chatting with one another then you should consider refactoring that. But there ya go! Hope this helps.
Here is my case with a similar need: Backbone listenTo seemed like a solution to redirect to login page for timed out or not authenticated requests.
I added event handler to my router and made it listen to the global event such as:
Backbone.Router.extend({
onNotAuthenticated:function(errMsg){
var redirectView = new LoginView();
redirectView.displayMessage(errMsg);
this.loadView(redirectView);
},
initialize:function(){
this.listenTo(Backbone,'auth:not-authenticated',this.onNotAuthenticated);
},
.....
});
and in my jquery ajax error handler:
$(document).ajaxError(
function(event, jqxhr, settings, thrownError){
.......
if(httpErrorHeaderValue==="some-value"){
Backbone.trigger("auth:not-authenticated",errMsg);
}
});
You can use Backbone object as the event bus.
This approach is somewhat cleaner but still relies on Global Backbone object though
var view1 = Backbone.View.extend({
_onEvent : function(){
Backbone.trigger('customEvent');
}
});
var view2 = Backbone.View.extend({
initialize : function(){
Backbone.on('customEvent', this._onCustomEvent, this);
},
_onCustomEvent : function(){
// react to document edit.
}
});
Use the same model objects. AppView could be initialized with a collection, and BrandView initialized with one model from that collection. When attributes of a branch object change, any other code that has a reference to that model can read it.
So lets so you have some brands that you fetch via a collection:
var brands = new Brands([]);
brands.fetch();
Now you make an AppView, and an array of BrandView's for each model.
var appView = new AppView({brands: brands});
var brandViews = brands.map(function(brand) {
return new BrandView({brand: brand});
});
The appView and the brandViews now both have access to the same model objects, so when you change one:
brands.get(0).selected = true;
Then it changes when accessed by the views that reference it as well.
console.log(appView.brands.get(0).selected); // true
console.log(brandViews[0].brand.selected) // true
Same as John has suggested above, the Mediator Pattern works really good in this scenario, as Addy Osmani summing this issue up again in Backbone fundamentals.
Wound up using the Backbone.Mediator plugin which is simple and great, and makes my AMD View modules working together seamlessly =)

How should I attach my data to my Backbone Views?

I have my views correctly displaying fake information and so I am now trying to apply asynchronus data loading to retrieve actual data. The problem is that I am uncertain about how I should go about this. Should I create AJAX calls myself? Should I use the Socket API? Should I use the built in REST api (and how to do so asynchronously)? The server side handler is still unimplemented so as far as how the server serves up the data, that is completely flexible.
i doubt your own ajax calls is what is needed here...
i can't tell about sockets however i know it is possible and a solid idea depending on your app.
i have been using the default REST functionality and it works well for me,
a small example as how I would do it,
to make it less complex i will just act as if it is from page load, instead of using routers and all.
var myView = Backbone.View.extend({
initialize: function() {
_.bindAll(this, 'render');
var v = this;
this.model.bind("change", function(e) {
this.render();
});
},
render: function() {
this.el.empty();
this.el.text(this.model.get('name'));
}
});
var myModel = Backbone.Model.extend({
url: "/api/myModel", // change to your server code...
defaults: {
name: "john"
}
});
$(function(){
var m = new myModel({}); // dummy model
var v = new myView({ model: m, el: $('#myDiv')});
v.render();
m.fetch(); // takes the url of the model or collection and fetches it from the server side ...
});
if you want to test what the fetch would do, you can for try this code from your console, (or add it to the jquery document load function:
m.set({ name: 'peter' });
this changes the model's property 'name' and you will immediately see the view update itself, because it listens to the change event of the model.
more info on these events can be found here: http://documentcloud.github.com/backbone/#Events

Late JSON Response with Backbone.js

I am working on my first project using backbone.js. It should be a frontend to a Play! App with a JSON interface. Here's a part of my JS code
var api = 'http://localhost:9001/api'
// Models
var Node = Backbone.Model.extend();
// Collections
var Nodes = Backbone.Collection.extend({
model: Nodes,
url: api + '/nodes',
});
// Views NODE
var NodeView = Backbone.View.extend({
el: $("#son_node_elements"),
render: function(){
var source = $("#son_node_item_templ").html();
var template = Handlebars.compile(source);
$(this.el).append(template(this.model.toJSON()));
return this;
}
});
var NodeListView = Backbone.View.extend({
initialize: function(){
_.bindAll(this, "render");
this.collection = new Nodes();
this.collection.bind("change",this.render);
this.collection.fetch();
this.render();
},
render: function(){
_(this.collection.models).each(function(item){
var nodeView = new NodeView({model: item});
$(this.el).append(nodeView.render().el);
}, this);
}
});
Nodes = new NodeListView({el:$('#son_nodes')});
My Problem is that when this.render() is called, this.collection.fetch() is still not done and this.collection does not contain anithing to render. All works fine when I set a breakpoint at this.render() (for example using firebug), so this.render() is not called immediately. I get exactly the same result when I access a local JSON file instead of the api of my app. Any suggestions how to handle this issue?
Fetch can also be called from outside view, so let the view listen for that instead:
this.collection.bind("reset",this.render);  
Reset event will be triggered on every call to this.collection.fetch(); 
And finally, skip this.render(); Don't call this yourself, since reset event handler do this for you.
You need to call "render" inside the "success" callback for your "fetch()":
this.collection.fetch({
success: function(col) {
col.render();
}
});
That defers the rendering until the collection fetching is complete. It's all asynchronous.
Caveat: I barely know anything about Backbone in particular, but something along these lines is definitely your problem. In other words, there may be other things to worry about that I haven't mentioned (because I don't know what they are :-).

Categories