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.
Related
I think I am missing something very trivial here. I have created a Backbone View as follows(without extending Backbone.View):
var PlayersView = new Backbone.View({
initialize: function() {
this.render();
},
render: function() {
console.log("hello World");
}
});
But it doesn't log anything when I run this code. It doesn't work when I explicitly do: PlayersView.render(); as well.
But the following code works :
var PlayersView = Backbone.View.extend({
initialize: function() {
this.render();
},
render: function() {
console.log("hello World");
}
});
var playersview = new PlayersView();
The View constructor does not accept properties to add to the constructed object. It accepts a few special options like 'model', 'tagName', and so on. But the initialize(...) and render(...) properties in your first code snippet are effectively ignored.
The proper way to provide initialize, render, is to use Backbone.View.extend({...}).
From http://backbonejs.org/#View-extend
extend
Backbone.View.extend(properties, [classProperties]) Get started with
views by creating a custom view class. You'll want to override the
render function, specify your declarative events, and perhaps the
tagName, className, or id of the View's root element.
In other words, your first view's render method was not overridden by your custom render/initialize function.
When using extend you actually let Backbone understand you wish to use your own methods instead of the "default" ones.
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.
I need to run a layout script as soon as my views are inserted into the DOM. So...
$(".widgets").append(widgets.render().el)
$(".widgets .dashboard").isotope # <-- This needs to be called whenever new widgets are inserted
The problem is I have to insert new widgets a few different views and re-call this script a few different places, which is not DRY. I am wondering how I can define the isotope in the View class.
Would it be a good idea to define an event listener to watch for append into the ".widgets" and to run the script? Is there a built in way of building views that are smart about when they are added to the DOM?
(For that matter, it would be also useful to define a callback for when a View is removed from the DOM.)
How about calling the isotope each time the view is rendered? You'll need to be careful to call render() only after the widget is injected, but this ought to take care of your problem:
//in Backbone.view.extend({
initialize: function() {
// fix context for `this`
_.bindAll(this);
},
render: function() {
// .. do rendering..
this.isotope();
return this;
}
// }) // end .extend
use:
var self = this;
this.$el.on('DOMNodeInserted', function(evt){
self.isotope();
$(evt.target ).stopPropagation();
})
I have a backbone app with a view structure that looks like the following - note that I've removed implementations, models, collections, etc. for brevity:
NewsListView = Backbone.View.extend({
el: $('li#newspane'),
// This is what I would like to be able to do
// events: { 'filtered': 'reset' }
initialize: function() {
_.bindAll(this);
},
render: function() {
},
reset: function(){
}
});
FilterView = Backbone.View.extend({
el: $('li.filter'),
initialize: function() {
},
render: function() {
},
toggleFilter: function() {
}
});
AllView = Backbone.View.extend({
initialize: function() {
this.newsListView = new NewsListView();
this.filterView = new FilterView();
}
});
Essentially, whenever the FilterView's toggleFilter() function is called, I would like to fire off an event called filtered or something like that that is then caught by the NewsListView, which then calls its reset() function. Without passing a reference of a NewsListView object to my FilterView, I'm not sure how to send it an event. Any ideas?
You're on the right track. It sounds like what you need is a global event dispatcher. There a decent article and example here: http://www.michikono.com/2012/01/11/adding-a-centralized-event-dispatcher-on-backbone-js/
You might be able to do this using the already available functionality of jquery events and the backbone events property.
For example, instead of doing this from inside your subview:
this.trigger("yourevent", this);
do this instead:
this.$el.trigger("yourevent", this);
Then in any view that is a parent, grandparent, etc of your child view, listen for the event on that view's $el by defining a property on that view's events object:
events:{
"yourevent":"yourhandler"
}
and define the handler on that view as well:
yourhandler:function(subview) {
}
So this way, a view doesn't need to know about what descendant views exist, only the type of event it is interested in. If the view originating the event is destroyed, nothing needs to change on the ancestor view. If the ancestor view is destroyed, Backbone will detach the handlers automatically.
Caveat: I haven't actually tried this out yet, so there may be a gotcha in there somewhere.
You should check out the Backbone.Courier plugin as bubbling events is a perfect use case:
https://github.com/dgbeck/backbone.courier
The easiest way I've found to trigger and listen to events is to just use the Backbone object itself. It already has the events functions mixed in to it, so you can just trigger eg:
Backbone.trigger('view:eventname',{extra_thing:whatever, thing2:whatever2});
then, in any other backbone view in your app, you can listen for this event eg:
Backbone.on('view:eventname', function(passed_obj) {
console.log(passed_obj.extra_thing);
});
I'm not exactly sure what the advantage is in not using the Backbone object as your event handler, and instead creating a separate object to do it, but for quick-and-dirty work, the above works fine. HTH!
NOTE: one disadvantage to this is that every listener will "hear" every single event triggered in this way. Not sure what the big O is on that, but work being careful to not overload your views with lots of this stuff. Again: this is quick and dirty! :)
This problem can be solved using small backbone.js hack. Simply modify Backbone.Events.trigger for passing events to the this.parent
if this.parent != null
So, I came up a with a solution - create an object that extends Backbone.Events, and pass it as a parameter to multiple views. This almost feels like message passing between actors, or something. Anyway - I'm posting this as an answer in case anybody else needs a quick solution, but I'm not going to accept the answer. This feels hacky. I'd still like to see a better solution.
NewsListView = Backbone.View.extend({
el: $('li#newspane'),
// Too bad this doesn't work, it'd be really convenient
// events: { 'filtered': 'reset' }
initialize: function() {
_.bindAll(this);
// but at least this does
this.options.eventProxy.bind('filtered', this.reset);
},
render: function() {},
reset: function() {}
});
FilterView = Backbone.View.extend({
el: $('li.filter'),
initialize: function() {},
render: function() {},
toggleFilter: function() {
this.options.eventProxy.trigger('filtered');
}
});
AllView = Backbone.View.extend({
initialize: function() {
var eventProxy = {};
_.extend(eventProxy, Backbone.Events);
this.newsListView = new NewsListView({eventProxy: eventProxy});
this.filterView = new FilterView({eventProxy: eventProxy});
}
});
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);
};