I'm new to backbone and i'm trying to re-render the contents of a view. I've put up the code in jsfiddle ...
http://jsfiddle.net/rzYLU/11/
So when user clicks re-render how can i remove the contents in the dom and show only the new items ?
The safest thing to do is to keep track of your QuestionView instances inside your AppView. Then you can call remove on each QuestionView before adding the new ones; the remove method is a:
Convenience function for removing the view from the DOM. Equivalent to calling $(view.el).remove();
Views should provide their own remove implementation to unbind from non-DOM events and thus prevent zombies. The default simply removes the view's el from the DOM but if you do it right from the beginning, things will keep working nicely when your code inevitably changes.
First adjust your QuestionView to have a remove method to remove the event you've bound to the model:
var QuestionView = Backbone.View.extend({
//...
remove: function() {
this.model.off('change', this.render);
this.$el.remove();
}
});
Then you'll need a couple adjustments to AppView to keep track of your QuestionViews:
var AppView = Backbone.View.extend({
//...
initialize: function() {
//...
this.sub_views = [ ];
//...
},
//...
addOne: function(question) {
var view = new QuestionView({
model: question
});
this.sub_views.push(view); // <----------------------- Add this
this.$("#question-list").append(view.render().el);
},
addAll: function() {
for(var i = 0; i < this.sub_views.length; ++i) // <--- And these three lines
this.sub_views[i].remove();
this.sub_views = [ ];
Questions.each(this.addOne);
}
Demo: http://jsfiddle.net/ambiguous/FF9eG/
I've also updated your code to use on and off instead of bind and unbind to match the new style. Newer versions of Backbone also have a cached version of $(this.el) in this.$el so I've updated the code to use that as well.
Related
I want to perform an action, clearing parent element, after a collection has fetched his models but prior to the models rendering.
I've stumbled upon before and after render methods yet they are model specific, which will cause my parent element to clear before every model rendering.
I'm able of course to perform the action pre-fetching yet I want it to occur when fetch is done and before models are rendered.
I tried using reset and change events listening on the collection yet both resulted unwanted end result.
Reset event seamed to go in that direction yet the passed argument was the entire collection and not a single model from the collection, therefore using the add event callback wasn't possible due to difference in argument type (collection and not a model as required)
Any ideas how to invoke a callback when fetch a collection fetch is successful yet models are yet to be rendered?
The model contains the returned attributes while collection contains url for fetching and parse method to return argument wrapped object.
Below is the code I use to render the collection view, which is basically rendering each model's view within the collection.
Collection View
---------------
var FoosView = Backbone.View.extend({
el: '#plans',
events: {
//'click tr': 'rowClick'
},
initialize: function() {
this.listenTo(this.collection, 'add', this.renderNew);
_.bindAll(this, "render");
this.render();
},
renderNew: function(FooModel) {
var item = new FooView({model: FooModel});
this.$el.prepend(item.render().$el);
}
...
});
The model view
--------
var FooView = Backbone.View.extend({
tagName: 'li',
initialize: function(options) {
this.options = options || {};
this.tpl = _.template(fooTpl);
},
render: function() {
var data = this.model.toJSON();
this.$el.html(this.tpl(data));
return this;
}
});
Thanks in advance.
OK, I think I understand your question and here is a proposed solution. You are now listening to the reset event on your collection and calling this.renderAll. this.renderAll will take the list of models from the collection and render them to the page, but only AFTER the list element has been emptied. Hope this helps :).
var FoosView = Backbone.View.extend({
el: '#plans',
collection: yourCollection, // A reference to the collection.
initialize: function() {
this.listenTo(this.collection, 'add', this.renderNew);
this.listenTo(this.collection, 'reset', this.renderAll);
},
renderAll: function() {
// Empty your list.
this.$el.empty();
var _views = []; // Create a list for you subviews
// Create your subviews with the models in this.collection.
this.collection.each(function(model) {
_views.push(new FooView({model: model});
});
// Now create a document fragment so as to not reflow the page for every subview.
var container = document.createDocumentFragment();
// Append your subviews to the container.
_.each(_views, function(subview) {
container.appendChild(subview.render().el);
});
// Append the container to your list.
this.$el.append(container);
},
// renderNew will only run on the collections 'add' event.
renderNew: function(FooModel) {
var item = new FooView({model: FooModel});
this.$el.prepend(item.render().$el);
}
});
I am forced to assume a few things about you html, but I think the above code should be enough to get you up and running. Let me know if it works.
I'm not totally sure about what you are asking but have you tried:
MyCollection.fetch({
success: function(models,response) {
//do stuff here
}
});
Also you may be interested taking a look at http://backbonejs.org/#Model-parse
Hope it helps!
Edit: there is no direct link between fetching and rendering my bet is that you binded rendering to model change.
USE===============>>>> http://backbonejs.org/#Model-parse
I am facing a problem while trying to click submit after re-render.
This is my view:
ShareHolderInfoView = Backbone.View.extend( {
template : 'shareholderinfo',
initialize: function() {
this.model = new ShareHolderInfoModel();
},
render : function() {
$.get("shareholderinfo.html", function(template) {
var html = $(template);
that.$el.html(html);
});
//context.loadViews.call(this);
return this;
},
events:{
"change input":"inputChanged",
"change select":"selectionChanged",
"click input[type=submit]":"showModel"
},
inputChanged:function(event){
var field = $(event.currentTarget);
var data ={};
data[field.attr('id')] = field.val();
this.model.set(data);
},
showModel:function(){
console.log(this.model.attributes);
alert(JSON.stringify(this.model.toJSON()));
}
});
This is my Router
var shareholderInfo, accountOwnerInfo;
App.Router = Backbone.Router.extend({
routes:{
'share':'share',
'joint':'joint'
},
share:function(){
$("#subSection").empty();
if(!shareholderInfo){
shareholderInfo = new ShareHolderInfoView();
$("#subSection").append(shareholderInfo.render().el);
} else{
$("#subSection").append(shareholderInfo.$el);
}
},
joint:function(random){
$("#subSection").empty();
if(!accountOwnerInfo){
accountOwnerInfo = new AccountOwnerInfoView();
$("#subSection").append(accountOwnerInfo.render().el);
} else{
$("#subSection").append(accountOwnerInfo.$el);
}
}
});
This is my HTML a div with id='subSection'.
if I check in console, I can able to see the events bound to that view.
Object {change input: "inputChanged", change select: "selectionChanged", click input[type=submit]: "showModel"}
But its not calling that showModel function afer i click submit. Please help.
Your fundamental problem is that you're improperly reusing views.
From the fine manual:
.empty()
Description: Remove all child nodes of the set of matched elements from the DOM.
[...]
To avoid memory leaks, jQuery removes other constructs such as data and event handlers from the child elements before removing the elements themselves.
So when you say:
$("#subSection").empty();
you're not just clearing out the contents of #subSection, you're also removing all event handlers attached to anything inside #subSection. In particular, you'll remove any event handlers bound to accountOwnerInfo.el or shareholderInfo.el (depending on which one is already inside #subSection).
Reusing views is usually more trouble than it is worth, your views should be lightweight enough that you can destroy and recreate them as needed. The proper way to destroy a view is to call remove on it. You could rewrite your router to look more like this:
App.Router = Backbone.Router.extend({
routes: {
'share':'share',
'joint':'joint'
},
share: function() {
this._setView(ShareHolderInfoView);
},
joint: function(random){
this._setView(AccountOwnerInfoView);
},
_setView: function(view) {
if(this.currentView)
this.currentView.remove();
this.currentView = new view();
$('#subSection').append(this.currentView.render().el);
}
});
If your views need any extra cleanup then you can override remove on them to clean up the extras and then chain to Backbone.View.prototype.remove.call(this) to call the default remove.
If for some reason you need to keep your views around, you could call delegateEvents on them:
delegateEvents delegateEvents([events])
Uses jQuery's on function to provide declarative callbacks for DOM events within a view. If an events hash is not passed directly, uses this.events as the source.
and you'd say things like:
$("#subSection").append(shareholderInfo.$el);
shareholderInfo.delegateEvents();
instead of just:
$("#subSection").append(shareholderInfo.$el);
I'd strongly recommend that you treat your views and cheap ephemeral objects: destroy them to remove them from the page, create new ones when they need to go on the page.
I'm working on a backbone.js project which is mainly to learn backbone framework itself.
However I'm stuck at this problem which i can't figure out but might have an idea about the problem...
I've got an Create View looking like this...
define(['backbone', 'underscore', 'jade!templates/addAccount', 'models/accountmodel', 'common/serializeObject'],
function(Backbone, underscore, template, AccountModel, SerializeObject){
return Backbone.View.extend({
//Templates
template: template,
//Constructor
initialize: function(){
this.accCollection = this.options.accCollection;
},
//Events
events: {
'submit .add-account-form': 'saveAccount'
},
//Event functions
saveAccount: function(ev){
ev.preventDefault();
//Using common/serializeObject function to get a JSON data object from form
var myObj = $(ev.currentTarget).serializeObject();
console.log("Saving!");
this.accCollection.create(new AccountModel(myObj), {
success: function(){
myObj = null;
this.close();
Backbone.history.navigate('accounts', {trigger:true});
},
error: function(){
//show 500?
}
});
},
//Display functions
render: function(){
$('.currentPage').html("<h3>Accounts <span class='glyphicon glyphicon-chevron-right'> </span> New Account</h3>");
//Render it in jade template
this.$el.html(this.template());
return this;
}
});
});
The problem is that for every single time I visit the create page and go to another and visit it again. It remebers it, it seems. And when i finally create a new account I get that many times I've visited total number of accounts...
So console.log("Saving!"); in saveAccount function is called x times visited page...
Do I have to close/delete current view when leaving it or what is this?
EDIT
Here's a part of the route where i init my view..
"account/new" : function(){
var accCollection = new AccountCollection();
this.nav(new CreateAccountView({el:'.content', accCollection:accCollection}));
console.log("new account");
},
/Regards
You have zombie views. Every time you do this:
new CreateAccountView({el:'.content', accCollection:accCollection})
you're attaching an event listener to .content but nothing seems to be detaching it. The usual approach is to call remove on a view to remove it from the DOM and tell it to clean up after itself. The default remove does things you don't want it to:
remove: function() {
this.$el.remove();
this.stopListening();
return this;
}
You don't want that this.$el.remove() call since your view is not responsible for creating its own el, you probably want:
remove: function() {
this.$el.empty(); // Remove the content we added.
this.undelegateEvents(); // Unbind your event handler.
this.stopListening();
return this;
}
Then your router can keep track of the currently open view and remove it before throwing up another one with things like this:
if(this.currentView)
this.currentView.remove();
this.currentView = new CreateAccountView({ ... });
this.nav(this.currentView);
While I'm here, your code will break as soon as you upgrade your Backbone. As of version 1.1:
Backbone Views no longer automatically attach options passed to the constructor as this.options, but you can do it yourself if you prefer.
So your initialize:
initialize: function(){
this.accCollection = this.options.accCollection;
},
won't work in 1.1+. However, some options are automatically copied:
constructor / initialize new View([options])
There are several special options that, if passed, will be attached directly to the view: model, collection, el, id, className, tagName, attributes and events.
so you could toss out your initialize, refer to this.collection instead of this.accCollection inside the view, and instantiate the view using:
new CreateAccountView({el: '.content', collection: accCollection})
In a backbone model, is it possible to trigger an event in the initialize function, for a nested view? I based my current code off this example: https://stackoverflow.com/a/8523075/2345124 and have updated it for backbone 1.0.0. Here is my initialize function, for a Model:
var Edit = Backbone.Model.extend({
initialize: function() {
this.trigger('marquee:add');
this.on('change', function(){
this.trigger('marquee:add');
});
}
...
}
I'm trying to call a method renderMarquee when the model is initialized:
var EditRow = Backbone.View.extend({
initialize: function() {
this.listenTo(this.model, "change", this.render); // works
this.listenTo(this.model, "marquee:add", this.renderMarquee); // only called when changed, but not when initially created
...
}
renderMarquee IS called when the model is changed, but not when it is initialized. 'change' events work as expected (this.render is called). Any thoughts?
Thanks!
I am currently facing a similar problem. I needed to trigger the change event in the initialize method of my model.
I looked into the backbone code which revealed why this is not happening:
var Model = Backbone.Model = function(attributes, options) {
...
this.set(attrs, options);
this.changed = {};
this.initialize.apply(this, arguments);
};
the set is executed before the initialize and this.change is emptied setting the model state to "nothing has changed".
In order to overwrite behavior this I added the following code to my initialize method.
initialize: function(attributes, options) {
...
this.changed = attributes;
this.trigger('change');
for (attr_name in attributes) {
this.trigger('change:' + attr_name);
}
},
I trigger all change events manually, this is important for me since inheriting models may bind to change or change:attrxy. But this is not enough, because if I just trigger the events the changedAttributes() method would return false therefore I also set this.changed to the current attributes.
This doesn't make a lot of sense because you are initializing the model somewhere prior to doing the view.listenTo call. Unfortunately, you don't really have a choice in that matter.
You are probably going to want to move the event handling to a Backbone.Collection which already has built in events you can listen on for adding/removing.
So I have a View that looks like this.
//base class
var SelectListView = Backbone.View.extend({
initialize: function() {
_.bindAll(this, 'addOne', 'addAll');
this.collection.bind('reset', this.addAll);
},
addAll: function() {
this.collection.each(this.addOne);
},
events: {
"change": "changedSelected"
},
changedSelected: function() {
this.selected = $(this.el);
this.setSelectedId($(this.el).val());
}
});
//my extended view
var PricingSelectListView = SelectListView.extend({
addOne: function(item) {
$(this.el).append(new PricingView({ model: item }).render().el);
}
});
I have instantiated the view like this...
var products = new ProductPricings();
var pricingsView = new PricingSelectListView({
el: $("#sel-product"),
collection: products
});
Somewhere else (another views custom method)I have updated the pricing view's collection
pricingsView.collection = new ProductPricings(filtered);
This does not seen to do anything.
pricingsView.render();
So now the collection has fewer items but the new view is never rendered or refreshed in the DOM.
How to I do I 1.) refresh the rendering in the DOM? 2.) Make it automatically refresh the DOM? Do I have to somehow tell it to render when ever the collection changes?
You bound addOne() to a reset event. When you just replace the pricingsView.collection instance then that event is not triggered and addOne() is not executed.
Try instead:
pricingsView.collection.reset(filtered);
This might work since you bind to collection's reset event already:
pricingsView.collection.reset(filtered);
http://backbonejs.org/#Collection-reset
You still have tweak your rendering logic to remove old markup from the view when reset happens.