Backbone render placement - backbone.js

Just wondering why this works:
window.NewListView = Backbone.View.extend({
template: _.template('Create New List'),
render: function(){
$(this.el).html(this.template());
return this;
}
});
window.List = new (Backbone.Router.extend({
routes: { "": "index" },
initialize: function(){
this.newListView = new NewListView();
},
start: function(){
Backbone.history.start();
},
index: function(){
$('.lists').append(this.newListView.render().el);
}
}));
$(function(){ List.start(); })
And this doesn't:
window.NewListView = Backbone.View.extend({
template: _.template('Create New List'),
render: function(){
$(this.el).html(this.template());
return this;
}
});
window.List = new (Backbone.Router.extend({
routes: { "": "index" },
initialize: function(){
this.newListView = new NewListView();
$('.lists').append(this.newListView.render().el);
},
start: function(){
Backbone.history.start();
},
index: function(){
}
}));
$(function(){ List.start(); })
The difference is just moving
$('.lists').append(this.newListView.render().el);
between initialize() and index() of the router.

Its because of the way, and when, you're creating an instance of your router.
When you do:
window.List = new (Backbone.Router.extend({...
You're creating an instance of the router before the DOM is loaded. Therefore, in your initialize function, your jQuery selector is not returning any nodes.
If you open up a console, you can see the order of operations logged to it on this jsFiddle:
http://jsfiddle.net/edwardmsmith/x64hw/
window.NewListView = Backbone.View.extend({
template: _.template('Create New List'),
render: function(){
$(this.el).html(this.template());
return this;
}
});
window.List = new (Backbone.Router.extend({
routes: { "": "index" },
initialize: function(){
this.newListView = new NewListView();
console.log("List Initialize");
$('.lists').append(this.newListView.render().el);
},
start: function(){
Backbone.history.start();
},
index: function(){
}
}));
$(function(){
console.log("Before List Start");
List.start();
console.log("After List Start");
})​
Results in:
List Initialize
Before List Start
After List Start
But, if you create your router instance after the DOM loads:
window.NewListView = Backbone.View.extend({
template: _.template('Create New List'),
render: function(){
$(this.el).html(this.template());
return this;
}
});
window.List = Backbone.Router.extend({
routes: { "": "index" },
initialize: function(){
this.newListView = new NewListView();
console.log("List Initialize");
$('.lists').append(this.newListView.render().el);
},
start: function(){
Backbone.history.start();
},
index: function(){
}
});
$(function(){
console.log("Before List Start");
list = new List();
list.start();
console.log("After List Start");
})​
The order is as you would expect, and it works:
Before List Start
List Initialize
After List Start
as shown in this jsFiddle:
http://jsfiddle.net/edwardmsmith/eDWfh/

Related

Backboe memory leak in subviews

I still strugling with memory leak in my app. I wannted to do it without huge changes in code.
var ItemsView = Backbone.View.extend({
id:'products', // If I change it to el: document.getElementById('products') and without passing views into items object, my views are properly rendered but with memory leak
events: { },
initialize: function() {
_.bindAll(this);
this.listenTo(this.collection, 'reset', this.reset);
this.listenTo(this.collection, 'add', this.addItem);
this.listenTo(this.collection, 'change', this.changeItem);
this.listenTo(this.collection, 'destroy', this.delItem);
this.items = [];
},
reset: function(){ console.log("reset");
this.el.innerHTML = null;
this.render();
},
render: function(){
for(var i=0; i < this.collection.length; i++){
this.renderItem(this.collection.models[i]);
}
},
renderItem: function( model ){
var itemView = new ItemView({ model: model });
itemView.render();
this.items.push(itemView);
jBone(this.el).append(itemView.el);
},
addItem: function(){ console.log("addItem");
this.renderItem();
},
changeItem: function(){ console.log("changeItem"); },
delItem: function(){ console.log("delItem"); },
remove: function() {
_.invoke(this.items, 'remove');
this.items = [];
Backbone.View.prototype.remove.call(this);
}
});
return ItemsView;
This is my Itemsview it is executed when user hit orderview, there is created ItemView for every model in collection:
var ItemView = Backbone.View.extend({
tagName: "li",
className: "productcc",
initialize: function () {
_.bindAll(this, 'addItem', 'removeItem', 'updateItem');
this.listenTo(this.model, 'remove', this.removeItem);
this.listenTo(this.model, 'change', this.updateItem);
},
events: {},
render: function () {
var model = this.model.toJSON();
this.el.innerHTML += '<div class="tabody"><h4 class="tablename">'+model.title+'<h4>'+model.status+'</div>';
return this;
},
addItem: function(){
this.collection.create({"table_no":"demo"});
},
changeItem: function(e){
e.preventDefault();
this.model.save({ table_no: 'demo' });
},
updateItem: function(newstuff){
console.log("updateItem");
console.log(this.el);
},
delItem: function(){
this.model.destroy({ silent: true });
},
removeItem: function(model){
console.log("removeItem");
console.log(model);
var self = this;
self.el.remove();
}
});
return ItemView;
MY ROUTER:
var AppRouter = Backbone.Router.extend({
routes: {
'' : 'home',
'home' : 'home',
'customer/:customer_id': 'showItems'
}
});
var initialize = function(options) {
window.app_router = new AppRouter;
window.socket = io.connect('www.example.com');
this.socketOrdersCollection = new SocketOrdersCollection();
this.ordersView = new OrdersView({ collection: this.socketOrdersCollection });
this.socketOrdersCollection.fetch({ reset: true });
app_router.on('route:home', function() { });
app_router.on('route:showItems', function(customer_id) {
if (this.itemsView) {
this.itemsView.remove();
}
this.socketItemsCollection = new SocketItemsCollection();
this.socketItemsCollection.fetch({ data: { id: customer_id}, reset: true });
this.itemsView = new ItemsView({
collection: this.socketItemsCollection,
model: { tableName: customer_id }
});
});
Backbone.history.start();
};
I have to remove also ItemsView after click to another order...
Thanks for any opinion.
Ok. Let me take a stab at what you're attempting here.
var ItemsView = Backbone.View.extend({
el: document.getElementById('products'),
events: { },
initialize: function() {
// everything you had before
this.items = [];
},
// etc.
renderItem: function( model ){
var itemView = new ItemView({ model: model });
itemView.render();
this.items.push(itemView);
jBone(this.el).append(itemView.el);
},
// etc.
// we're overloading the view's remove method, so we clean up our subviews
remove: function() {
_.invoke(this.items, 'remove');
this.items = [];
Backbone.View.prototype.remove.call(this);
}
});
return ItemsView;
And then in the router:
var initialize = function(options) {
// etc.
app_router.on('route:home', function() { });
app_router.on('route:showItems', function(customer_id) {
if (this.itemsView) {
this.itemsView.remove();
}
// everything else the same
});
Backbone.history.start();
};
So now, your ItemsView will clean up any child items it has, and when you change customers, you'll clean up any ItemsView you have open before generating a new one.
EDIT
I see what you're having a problem with now.
In your route handler, you're going to need to do something along these lines:
app_router.on('route:showItems', function(customer_id) {
// everything you already have
jBone(document.getElementById('container')).append(this.itemsView);
});

Cannot read property 'toJSON' of undefined + backbone

I am really new to backbone, and in a world of confusion behind it. Right now I am trying to render a view of a single person, preferably without iterating through the entire collection, using the collection.get() method.
Here is the user collection:
App.Collections.UserCollection = Backbone.Collection.extend({
url: "/users",
model: App.Models.User,
initialize: function(){
console.log('users collection');
},
});
The user model:
App.Models.User = Backbone.Model.extend({
rootURL: '/users',
initialize: function(){
console.log('User model being generated');
}
});
the Users View (all users):
App.Views.Users = Backbone.View.extend({
el: "body",
model: 'user',
initialize: function(){
console.log('User view rendering');
this.collection.fetch();
this.listenTo(this.collection, 'reset', this.addAll);
},
clearDiv: function(){
var container = $('#render-area');
$('body').removeClass('homepage');
$('#main-nav').fadeIn(100);
container.empty();
this.addUser();
},
addUser: function(){
var current_user_model = this.collection.get(currentUser);
var user = new App.Views.ViewUser({ model: current_user_model });
$('#render-area').append(user.el);
},
events: {
'click #view-profile' : 'clearDiv'
}
});
and the viewUser view (single user):
App.Views.ViewUser = Backbone.View.extend({
initialize: function(){
console.log("single user view");
this.template = HandlebarsTemplates['user_profile'];
this.render();
},
render: function(){
$('#render-area').html(this.template(this.model.toJSON()));
},
});
I thought that I was grabbing the single user model in the Users view, and passing it to the ViewUser view with these lines
var current_user_model = this.collection.get(currentUser);
var user = new App.Views.ViewUser({ model: current_user_model });
But it seems that I am not. Any help and/or explanation is appreciated, thanks!
did you check the this.collection.get is return an object?
Maybe it can not fetch the currentUser?
before render the view you should
a) check the model is a valid model before calling the view and render a "not found" view or
b) in the view check if the model is valid and render a "not found" view
c) or you can handle it in handlebars template
Example A (untested):
var current_user_model = this.collection.get(currentUser);
if (current_user_model) {
var user = new App.Views.ViewUser({ model: current_user_model });
} else {
new AppViews.ViewUserNotFound();
}
Example B :
App.Views.ViewUser = Backbone.View.extend({
initialize: function(){
console.log("single user view");
this.template = HandlebarsTemplates['user_profile'];
this.templateNotFound = HandlebarsTemplates['user_profile_not_found'];
this.render();
},
render: function(){
if(this.model) {
$('#render-area').html(this.template(this.model.toJSON()));
} else {
$('#render-area').html(this.templateNotFound());
}
},
});
var current_user_model = this.collection.get(currentUser);
var user = new App.Views.ViewUser({ model: current_user_model });
Example C:
App.Views.ViewUser = Backbone.View.extend({
initialize: function(){
console.log("single user view");
this.template = HandlebarsTemplates['user_profile'];
this.templateNotFound = HandlebarsTemplates['user_profile_not_found'];
this.render();
},
render: function(){
if(!this.model) {
this.model = new this.model(); // create model with defaults
}
$('#render-area').html(this.template(this.model.toJSON()));
},
});
var current_user_model = this.collection.get(currentUser);
var user = new App.Views.ViewUser({ model: current_user_model });
all examples untested..
hope this helps a little bit,
best Carsten
I would do that:
App.Views.Users = Backbone.View.extend({
el: "body",
model: 'user',
initialize: function(){
console.log('User view rendering');
this.listenTo(this.collection, 'reset', this.addAll);
},
clearDiv: function(){
var self = this;
this.collection.fetch({
success: function() {
var container = $('#render-area');
$('body').removeClass('homepage');
$('#main-nav').fadeIn(100);
container.empty();
self.addUser();
}
});
},
addUser: function(){
var current_user_model = this.collection.get(currentUser);
var user = new App.Views.ViewUser({ model: current_user_model });
$('#render-area').append(user.el);
},
events: {
'click #view-profile' : 'clearDiv'
}
});

Backbone appends to list view instead of replacing it

I am producing a single page website with Wordpress and Backbone.js, i have come into a problem with when i fetch new data. It simply adds DOM elements onto the container el rather than replacing them. The collection updates correctly as i can see it has the right amount of elements in the console.
var PostItem = Backbone.Model.extend();
var PostItems = Backbone.Collection.extend({
model: PostItem,
url: '/wp-admin/admin-ajax.php'
});
var postItems = new PostItems();
var PostView = Backbone.View.extend({ /* Model View */
tagName : 'article',
className : 'widget',
template : _.template( $('#widgetPost').html() ),
render: function(){
var attributes = this.model.toJSON();
this.$el.html( this.template( attributes ) );
return this;
}
});
var PostListView = Backbone.View.extend({ /* Collection View */
el : '#content',
initialize: function(){
this.collection.on('add', this.addOne, this);
this.collection.on('reset', this.addAll, this);
},
addOne: function(postItem){
var postView = new PostView({ model : postItem });
this.$el.append( postView.render().el );
},
addAll: function(){
this.collection.forEach(this.addOne, this);
},
render: function(){
this.addAll();
},
});
var postListView = new PostListView({
collection : postItems
});
$(function(){
$('a#posts').click(function(){
postItems.fetch({
data: {
action: 'do_ajax',
fn: 'get_the_posts'
}
});
return false;
});
$('a#pages').click(function(){
postItems.fetch({
data: {
action: 'do_ajax',
fn: 'get_the_pages'
}
});
return false;
});
});
You need to clear out your collectionView's $el! :)
addAll: function(){
this.$el.empty();
this.collection.forEach(this.addOne, this);
}
This should do the trick.

Backbone.js Collections View forEach doesn't work

I am trying to use collections to list my data coming from my api.
But the problem is, when I use forEach, the function that I called (addOne) doesn't run.
There is also something I suspect working wrong. Should my collection save the returning JSON under the models like that?
Object -> models -> 0 -> attributes -> ...
My View:
s.Views.Fs = Backbone.View.extend({
className: "",
template: _.template("<%= name %>"),
initialize: function() {
},
render: function() {
this.collection.forEach(this.addOne, this);
},
addOne: function(f) {
alert("a");
var fV = new s.Views.PF({model: f});
this.$el.append(fV.render().el);
}
});
My Collection:
s.Collections.FL = Backbone.Collection.extend({
url: "api/fs/",
model: s.Models.F,
});
My Model:
s.Models.F = Backbone.Model.extend( {
urlRoot: 'api/fs/',
defaults: {
...
},
...
parse: function(response) {
return response;
},
});
My Route (And App):
var sApp = new (Backbone.Router.extend({
f_a: function() {
this.fL= new s.Collections.FL();
this.fLV= new s.Views.Fs({collection: this.fL});
this.fL.fetch();
this.fLV.render();
},
});
Listening for events is made by this.collection.on('add', this.addOne, this); under collection's view. Here is tested code's summary (Thanks for the tip 'mu is too short'):
VIEW
s.Views.Fs = Backbone.View.extend({
className: "",
template: _.template("<%= name %>"),
initialize: function() {
this.collection.on('add', this.addOne, this);
this.collection.on('reset', this.render, this);
},
render: function() {
this.collection.forEach(this.addOne, this);
},
addOne: function(f) {
var fV = new s.Views.PF({model: f});
fV.render();
this.$el.append(fV.el);
}
});
COLLECTION
s.Collections.FL = Backbone.Collection.extend({
url: "api/fs/",
model: s.Models.F,
});
MODEL
s.Models.F = Backbone.Model.extend( {
urlRoot: 'api/fs/',
// No need to parse here.
});
ROUTER
var sApp = new (Backbone.Router.extend({
f_a: function() {
this.fL= new s.Collections.FL();
this.fLV= new s.Views.Fs({collection: this.fL});
this.fLV.render();
$("#content").html(this.fLV.el);
this.fL.fetch();
},
});

Backbone view doesn't render template passed to it

I need to be able to pass different template IDs to different routes.
(function() {
window.App = {
Models: {},
Collections: {},
Views: {},
Router: {}
};
var vent = _.extend({}, Backbone.Events);
_.templateSettings.interpolate = /\[\[(.+?)\]\]/g;
App.Router = Backbone.Router.extend({
routes: {
'' : 'index',
'send-message' : 'sendMessage',
'*other' : 'other'
},
index: function() {
t = new (App.Collections.Tables.extend({ url: 'main-contact'}))();
tables = App.Views.Tables({ collection: t, template: 'mainContactTemplate' });
$('#web-leads').html(tables.el);
},
sendMessage: function() {
// t = new (App.Collections.Tables.extend({ url: 'send-message'}))();
// tables = new App.Views.Tables.extend({ collection: t, template: template('sendMessageTemplate')});
// $('#web-leads').html(tables.el);
},
other: function() {
}
});
// Main Contact
App.Models.Table = Backbone.Model.extend({});
App.Collections.Tables = Backbone.Collection.extend({
model: App.Models.Table,
initialize: function(models, options) {
this.fetch({
success: function(data) {
//console.log(data.models);
}
});
if (options) {
this.url = this.url || options.url;
}
}
});
App.Views.Tables = Backbone.View.extend({
tagName: 'ul',
initialize: function() {
this.collection.on('reset', this.render, this);
},
render: function() {
return this.collection.each(this.addOne, this);
},
addOne: function(model) {
var t = new App.Views.Table({ model: model});
this.$el.append(t.render().el);
return this;
}
});
App.Views.Table = Backbone.View.extend({
tagName: 'li',
initialize: function(options) {
this.template = options.template;
console.log(this.options);
},
retrieveTemplate: function(model) {
return _.template($('#' + this.template).html(), model);
},
render: function() {
this.$el.html(this.retrieveTemplate(this.model.toJSON()));
return this;
}
});
new App.Router();
Backbone.history.start();
})();
But I get an error than n is undefined. I think I need to pass this.template into my retrieveTemplate function. But shouldn't it already be set? This code works, by the way, if I hard code in the name of the template ID in the retrieveTemplate function.
EDIT: the template isn't being passed from the call in the router. That's where this is breaking down.
EDIT: I took out the call to extend in the second line of the index route and now I get this._configure is not a function
WORKING VERSION:
(function() {
window.App = {
Models: {},
Collections: {},
Views: {},
Router: {}
};
var vent = _.extend({}, Backbone.Events);
_.templateSettings.interpolate = /\[\[(.+?)\]\]/g;
App.Router = Backbone.Router.extend({
routes: {
'' : 'index',
'send-message' : 'sendMessage',
'*other' : 'other'
},
index: function() {
var t = new (App.Collections.Tables.extend({ url: 'main-contact'}))();
var tables = new (App.Views.Tables.extend({ collection: t, options: {template: 'mainContactTemplate' }}))();
$('#web-leads').html(tables.render().el);
},
sendMessage: function() {
// t = new (App.Collections.Tables.extend({ url: 'send-message'}))();
// tables = new App.Views.Tables.extend({ collection: t, template: template('sendMessageTemplate')});
// $('#web-leads').html(tables.el);
},
other: function() {
}
});
// Main Contact
App.Models.Table = Backbone.Model.extend({});
App.Collections.Tables = Backbone.Collection.extend({
model: App.Models.Table,
initialize: function(models, options) {
this.fetch({
success: function(data) {
//console.log(data.models);
}
});
if (options) {
this.url = this.url || options.url;
}
}
});
App.Views.Tables = Backbone.View.extend({
tagName: 'ul',
initialize: function(options) {
this.collection.on('reset', this.render, this);
this.template = this.options.template;
},
render: function() {
this.collection.each(this.addOne, this);
return this;
},
addOne: function(model, options) {
//console.log(model);
var t = new App.Views.Table({ model: model, template: this.options.template});
this.$el.append(t.render().el);
return this;
}
});
App.Views.Table = Backbone.View.extend({
tagName: 'li',
initialize: function(options) {
//console.log(this.options);
this.template = this.options.template;
},
retrieveTemplate: function(model) {
return _.template($('#' + this.template).html(), model);
},
render: function() {
//console.log(this);
this.$el.html(this.retrieveTemplate(this.model.toJSON()));
return this;
}
});
new App.Router();
Backbone.history.start();
})();
Your router says this:
tables = App.Views.Tables({ collection: t, template: 'mainContactTemplate' });
So you're giving a template: '...' to App.Views.Tables. The initialize in App.Views.Tables looks like this:
initialize: function() {
this.collection.on('reset', this.render, this);
}
so it ignores the template option. If we look at App.Views.Table (singular!), we see this:
initialize: function(options) {
this.template = options.template;
console.log(this.options);
}
but App.Views.Table is instantiated without a template option:
var t = new App.Views.Table({ model: model});
You need to fix how you use App.Views.Table. Backbone will put a view's constructor options in this.options for you so you just need to say:
var t = new App.Views.Table({ model: model, template: this.options.template });
A couple other things to consider:
You have some accidental globals in your router's index method, you should have var t and var tables rather than just t and tables.
A view's render method conventionally returns this so that you can say $x.append(v.render().el) so you might want to adjust your render methods to match the convention.
You probably need to bind the context. Underscore can help you with that.
.bindAll or .bind should do it.
I typically just use _.bindAll during initialization as shown below.
...
initialize: function(options) {
_.bindAll(this); // apply appropriate context
this.template = options.template;
},
...
Hope this helped, best of luck.

Resources