backbonejs model.toJSON & render - backbone.js

I have some model and I want to bind render method to it on change. I'm trying to pass the model.toJSON to the render, but it doesn't work. However it works if I pass model and apply toJSON inside render.
(the whole code is here: http://plnkr.co/edit/xoeY4hexnqgHnkxap5uj?p=preview)
window.onload=function(){
var defaultModel = Backbone.Model.extend({
defaults: {
greeting: 'Hello, Dude',
content: 'Coming soon...'
}
}),
defaultView = Backbone.View.extend({
tagName: 'section',
className: 'default',
initialize: function(option) {
this.template = $('#tmpl-default').html();
this.render();
var _this = this;
this.model.bind('change', _.bind(this.render, this, this.model.toJSON()));
$('[name="default-input"]').on('blur', function() {
console.log('got blurred....');
_this.model.set('content', this.value);
});
},
render: function(content) {
if (!content) {
console.log('%cno content', 'color: green');
content = this.model.toJSON();
}
this.$el.html(_.template(this.template)(content));
$('#content').html(this.$el);
return this;
}
}),
viewDefault = new defaultView({
model: new defaultModel()
});
};
the code above doesn't work. If I change
this.model.bind('change', _.bind(this.render, this, this.model.toJSON()));
to
this.model.bind('change', _.bind(this.render, this, this.model));
and
if (!content) {
content = this.model.toJSON();
}
to
if (!content) {
content = this.model.toJSON();
}else{
content = content.toJSON();
}
But why?!

A more appropriate way is to use the listenTo function on the view, such as:
this.listenTo(this.model, "change", this.render);

I think the reason it doesn't work as you expect is because when you do
this.model.bind('change', _.bind(this.render, this, this.model.toJSON()));
The argument this.model.toJSON() passed to render method will always be the initial state of the model at the point when _.bind was called.
When you do content = this.model.toJSON(); inside render method, you get the current state, including the expected changes that triggered render.
You can better structure your view like this:
defaultView = Backbone.View.extend({
tagName: 'section',
className: 'default',
initialize: function(option) {
this.render();
this.model.on('change', _.bind(this.render, this));
},
template: _.template($('#tmpl-default').html()),
events: {
'blur [name="default-input"]': 'eventHandler'
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
$('#content').html(this.$el);
return this;
},
eventHandler: function(event) {
var val = $(event.currentTarget).val();
this.model.set('content', val);
}
});
Also, look into listenTo than on like #Jayem suggested to aviod unexpected memory leak issues

Related

Backbone not initializing view. "undefined is not a function"

I'm having problem in backbone where it's not finding my view. Here's the code
Here's the views.
App.Views.SummaryTableView = Backbone.View.extend({
tagName: 'tbody',
initialize: function () {
this.childViews = [];
this.collection.on('add', this.addOne, this);
this.collection.on('change reset', this.render, this);
console.log(this.collection);
},
addOne: function (appSummary) {
console.log('should be receiving model');
console.log(appSummary);
var appSumTable = new App.Views.SummaryListView({ model: appSummary });
console.log(appSummary);
this.$el.append(appSumTable.render().el);
this.childViews.push(appSumTable);
},
render: function () {
console.log('rendiring collection: ' + this.collection);
this.collection.each(this.addOne, this);
console.log('sending the model');
return this;
},
close: function () {
this.remove();
this.unbind();
this.childViews = [];
}
});
App.Views.SummaryListView = Backbone.View.extend({
tagName: 'tr',
template: template('app-summary-table-template'),
initialize: function () {
console.log(this.model);
this.model.on('add', this.render, this);
},
render: function () {
console.log('rendering');
var mod = this.model.toJSON();
this.$el.html(this.template(mod));
return this;
},
close: function () {
this.remove();
this.unbind();
}
});
The SummaryTableView has the collection, and the view sends the model to the SummaryListView. The collection is working fine, and the model contains the data. But for some reasons, when I run the code, it keeps saying SummaryListView is undefined. It can't find the view. Am I doing something wrong? I get the error in this line :
var appSumTable = new App.Views.SummaryListView({ model: appSummary });
you are referring SummaryListView before it is declared , hence the error So
you must Declare SummaryListView before SummaryTableView
the order should be like this
App.Views.SummaryListView = Backbone.View.extend({
tagName: 'tr',
.
.
});
App.Views.SummaryTableView = Backbone.View.extend({
tagName: 'tbody',
.
.
addOne: function (appSummary) {
console.log('should be receiving model');
console.log(appSummary);
var appSumTable = new App.Views.SummaryListView({ model: appSummary});
.
.
});
Fiddle

Backbone.js auto render view after destroy

I have the 2 views for my model. One creates the ul view, the other add the li elements. The views work but they do not update after the destroy function is called. I've tried various ways to bind to the render function, but it is not being called after this.model.destroy(); What am I missing?
var NoteListView = Backbone.View.extend({
tagName:'ul',
className: 'note-group',
initialize:function () {
_.bindAll(this, "render"); // I've tried all these:
this.model.bind("change", this.render, this);
this.model.bind("destroy", this.render, this);
this.model.bind("reset", this.render, this);
this.model.bind('change', _.bind(this.render, this));
this.model.on('change',this.render,this);
},
render:function (eventName) {
_.each(this.model.models, function (note) {
$(this.el).append(new NoteListItemView({
model:note
}).render().el);
}, this);
return this;
}
});
var NoteListItemView = Backbone.View.extend({
tagName:"li",
className: 'note-item',
template:_.template($('#tpl-note-item').html()),
initizlize:function(){
this.model.on('change',this.render,this);
},
render:function (eventName) {
$(this.el).html(this.template(this.model.toJSON()));
return this;
},
events: {
"click .note-btn-delete": "deleteNote"
},
deleteNote: function(e){
this.model.destroy();
}
});
Edit: here's the Collection:
var NoteCollection = Backbone.Collection.extend({
model:Note,
url:"api.php/notes",
initialize: function (models){
this.filtered = new Backbone.Collection(models);
},
filterByCid: function(id) {
var filtered = _.filter(this.models, function (model) {
if (id==0) return true;
return model.get("contactid") == id;
});
}
});
In the router:
this.noteList = new NoteCollection();
this.noteList.fetch({data:{contactid:id},
success: function (collection){
if (collection.length>0){
$('#note-container').html(new NoteListView({model:collection}).render().el);
}
else {
$('#note-container').html('No notes');
}
}
})
I see 2 issues with enter code here your code.
First
You are not listening to the remove event on the NodeListItemView
Second
_.each(this.model.models,
supposed to be
_.each(this.collection.models,
If models is present on the model, then it is supposed to be in the options of the model
_.each(this.model.options.models,
Code
var NoteListView = Backbone.View.extend({
tagName: 'ul',
className: 'note-group',
initialize: function () {
this.listenTo.on(this.collection, 'reset', this.render);
},
render: function (eventName) {
_.each(this.collection.models, function (note) {
$(this.el).append(new NoteListItemView({
model: note
}).render().el);
}, this);
return this;
}
});
var NoteListItemView = Backbone.View.extend({
tagName: "li",
className: 'note-item',
template: _.template($('#tpl-note-item').html()),
initizlize: function () {
_.bindAll(this, "deleteNote", "removeView");
this.listenTo.on(this.model, 'change', this.render);
this.listenTo.on(this.model, 'remove', this.removeView);
},
render: function (eventName) {
$(this.el).html(this.template(this.model.toJSON()));
return this;
},
events: {
"click .note-btn-delete": "deleteNote"
},
deleteNote: function (e) {
this.model.destroy();
},
removeView : function() {
this.remove();
}
});
EDIT
Change this line
new NoteListView({model:collection}).render().el
to use collection instead
new NoteListView({collection :collection}).render().el
And I do not believe your fetch method takes in any data
this.noteList.fetch({data:{contactid:id})
this.noteList.fetch()
It only takes in success and error callbacks

Backbone Client Side Collection Filtering

Consider a backbone app that has a select list of categories, and a table of files underneath it. All files, regardless of the category they belong to are in the App.collection.files collection. When a user selects a new category, I'd like to filter the table to only show those matching files.
I'd like to do this completely on the client side. I do NOT want to fetch a new list of files from the server each time a new category is picked.
I plan to to a App.collection.files.where({category_id: xx}) within App.View.Files.render() to accomplish the filtering itself.
What is the best Backbone-y way to trigger/respond to an event to get this working?
One idea I am playing with is another collection, that contains the filtered files, but not sure if this is the best approach.
App.View.FilesPage = Backbone.View.extend({
template: App.utils.template('files-page'),
className: 'container-fluid',
initialize: function(){
this.render();
},
render: function(){
this.$el.html(this.template());
this.$el.find('.categories').append(
new App.View.Categories({collection:App.collection.categories}).el
);
this.$el.find('.files table').append(
new App.View.Files({collection:App.collection.files}).el
);
return this;
}
});
App.View.Categories = Backbone.View.extend({
tagName: 'select',
events: {
'change': 'onChange'
},
initialize: function(){
this.$el.attr('size', this.collection.length + 1);
this.render();
},
render: function(){
var option = function(value, text, isSelected){
return $('<option></option>', {'value':value, 'text':text});
};
this.$el.html(option(0, 'All Documents'));
this.$el.append(
this.collection.invoke(function(){
return option(this.get('category_id'), this.get('title'));
})
).val(0);
return this;
},
onChange: function(event){
}
});
App.View.Files = Backbone.View.extend({
tagName: 'tbody',
initialize: function(){
this.render();
},
render: function(){
this.$el.html(
this.collection.invoke(function(){
return new App.View.File({model:this}).el;
})
);
return this;
}
});
Here is one solution I came up with, that seems fine and dandy on the surface.
Code changes are called out within comments.
Thoughts on this solution, and completely different solutions are welcome.
App.View.FilesPage = Backbone.View.extend({
template: App.utils.template('files-page'),
className: 'container-fluid',
initialize: function(){
// CHANGE: Store application wide reference to 'Categories' model
App.model.categories = new App.Model.Categories();
this.render();
},
render: function(){
this.$el.html(this.template());
this.$el.find('.categories').append(
new App.View.Categories({
// CHANGE: Pass model into 'Categories' view
model: App.model.categories,
collection: App.collection.categories
}).el
);
this.$el.find('.files table').append(
new App.View.Files({collection:App.collection.files}).el
);
return this;
}
});
App.View.Categories = Backbone.View.extend({
tagName: 'select',
events: {
'change': 'onChange'
},
initialize: function(){
this.$el.attr('size', this.collection.length + 1);
this.render();
},
render: function(){
var option = function(value, text, isSelected){
return $('<option></option>', {'value':value, 'text':text});
};
this.$el.html(option(0, 'All Documents'));
this.$el.append(
this.collection.invoke(function(){
return option(this.get('category_id'), this.get('title'));
})
).val(0);
return this;
},
onChange: function(){
// CHANGE: Update attribute within 'Categories' model
this.model.set('category_id', this.$el.val());
}
});
App.View.Files = Backbone.View.extend({
tagName: 'tbody',
initialize: function(){
this.collection.on('reset', this.render, this);
// CHANGE: Listen for change on 'Categories' model, and re-render this view
App.model.categories.on('change:category_id', this.render, this);
this.render();
},
render: function(){
this.$el.html(
_.invoke(this.getFilteredFiles(), function(){
return new App.View.File({model:this}).el;
})
);
return this;
},
// CHANGE: Return a filtered set of models from collection, based on 'Categories' model attributes
getFilteredFiles: function(){
var categoryId = App.model.categories.get('category_id');
if (categoryId!=0){
return this.collection.where({'category_id': categoryId});
}
else{
return this.collection.models;
}
}
});

Render not being called in Backbone view

Here is my Backbone:
App.Models.Count = Backbone.Model.extend({
url: this.url,
initialize: function() {
this.fetch({
success: function(data, response) {
this.count = data.get('count');
console.log(this.count); // 9, correct answer
}
});
}
});
App.Views.Count = Backbone.View.extend({
tagName: 'span',
initialize: function(options) {
this.count = this.options.count;
console.log(options); // returns correctly
this.model.on('reset', this.render, this);
},
render: function() {
console.log('test'); // not called
this.$el.html(this.model.toJSON());
return this;
}
});
And in my route:
var mc = new (App.Models.Count.extend({'url' : 'main-contact-count'}))();
var mcv = new (App.Views.Count.extend({ model: mc }))();
console.log(mcv); // 9, correct answer
$('#contactCount').html(mcv);
As you can see, my render method is never called. Also, it seems that my view is being called before my model, based on what I see console.log'd in Firebug. Is that because of the async? Why isn't render being called?
You're using Backbone in a funky way. Here's the more standard way to do this:
App.Models.Count = Backbone.Model.extend({
urlRoot: "main-contact-count"
});
App.Views.Count = Backbone.View.extend({
tagName: 'span',
initialize: function(options) {
this.model.on('change', this.render, this);
},
render: function() {
console.log('test');
this.$el.html(this.model.toJSON());
return this;
}
});
And in the router:
var mc = new App.Models.Count();
var mcv = new App.Views.Count({model: mc});
mc.fetch();
$('#contactCount').html(mcv.el);
EDIT
It turns out you're listening to "reset" on a Backbone model. This will never happen. Try listening on "change" instead of reset:
this.model.on('change', this.render, this);

Accessing Properties of Parent Backbone View

I have a backbone view that calls to a sub-view:
lr.MapView = Backbone.View.extend({
el: $('#map'),
foo: "bar",
initialize: function() {
var that = this;
_.bindAll(this, "render", "addAllEvents", "addOneEvent");
this.collection = new lr.Events();
this.collection.fetch({
success: function(resp) {
that.render();
that.addAllEvents();
}
});
},
addAllEvents: function() {
this.collection.each(this.addOneEvent);
},
addOneEvent: function(e) {
var ev = new lr.EventView({
model: e
});
},
render: function() {
}
});
Here is the sub-view:
lr.EventView = Backbone.View.extend({
initialize: function() {
_.bindAll(this, "render");
console.log(lr.MapView.foo); // will console.log 'undefined'
},
render: function() {
}
});
I'd like to be able to access properties the parent view within the sub-view, but it isn't working with the above code. For example, how can I access the 'foo' variable within the sub-view?
lr.MapView is a "class", everything that Backbone.View.extend builds will be in lr.MapView.prototype, not in lr.MapView. Run this with the console open and you'll see whats going on:
var MapView = Backbone.View.extend({ foo: 'bar' });
console.log(MapView);
console.log(MapView.prototype);
console.log(MapView.prototype.foo);
Demo: http://jsfiddle.net/ambiguous/DnvR5/
If you're only going to have a single MapView then you can refer to lr.MapView.prototype.foo everywhere:
initialize: function() {
_.bindAll(this, "render");
console.log(lr.MapView.prototype.foo);
}
Note that everywhere includes within lr.MapView instances so your foo will act like a "class variable" from non-prototype based OO languages.
The right way to do this is to use an instance variable for foo and pass the parent view instance to the sub-view instances when they're created:
// In MapView
addOneEvent: function(e) {
var ev = new lr.EventView({
model: e,
parent: this
});
}
// In EventView
initialize: function(options) {
_.bindAll(this, "render");
this.parent = options.parent; // Or use this.options.parent everywhere.
console.log(this.parent.foo);
}
Or better, add an accessor method to MapView:
_foo: 'bar',
foo: function() { return this._foo }
and use that method in EventView:
initialize: function(options) {
// ...
console.log(this.parent.foo());
}
Proper encapsulation and interfaces are a good idea even in JavaScript.
Just a guess, but could you try something like this in MapView:
addOneEvent: function(e) {
var that = this,
ev = new lr.EventView({
model: e,
parentView = that
});
}
And then access it like this:
lr.EventView = Backbone.View.extend({
initialize: function() {
_.bindAll(this, "render");
console.log(this.parentView.foo);
},
render: function() {
}
});

Resources