In Backbone this.model is undefined, why? - javascript

I've looked everywhere for an answer but wasn't satisfied with what I've found.
The issue is, I'm doing a tutorial from Addy Osmani to make a 'Todo' app in Backbone, but when I look at the console, I get an error saying that this.model is undefined.
I even tried this SO answer Backbone model error displayed in console, but I still get the same error. Please tell me what is wrong.
By the way, what are this.model or this.collection? I've got an idea that they refer to Backbone.Model and Backbone.Collection but how do they work? I'm asking this because in another tutorial this.collection and this.model.models were also undefined, when I've clearly defined the Model and Collection.
Many Thanks
JS:
//Model
var Todo = Backbone.Model.extend({
defaults: {
title: 'Enter title here',
completed: true
},
validate: function(attrs) {
if (attrs.title === undefined) {
return 'Remember to enter a title';
}
},
initialize: function() {
console.log('This model has been initialized');
this.on('change:title', function() {
console.log('-Title values for this model have changed');
});
this.on('invalid', function(model, error) {
console.log(error);
});
}
});
//View
var TodoView = Backbone.View.extend({
el: '#todo',
tagName: 'li',
template: _.template($('#todoTemplate').html()),
events: {
'dbclick label': 'edit',
'click .edit': 'updateOnEnter',
'blur .edit': 'close'
},
initialize: function() {
_.bindAll(this, 'render');
this.render();
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
this.input = this.$('.edit');
console.log(this.model.toJSON());
return this;
},
edit: function() {
//do something...
},
close: function() {
//do something...
},
updateOnEnter: function() {
//do something...
}
});
var todoview = new TodoView();
console.log(todoview.el);
//Collection
var TodoList = Backbone.Collection.extend({
model: Todo
});

You need to instantiate a Model or Collection and pass it to your View. Otherwise, when the render method is called on your TodoView, this.model will be null.
For example, try rearranging the last few lines of your code like this:
//Collection
var TodoList = Backbone.Collection.extend({
model: Todo
});
var todos = new TodoList();
var todoview = new TodoView({model: todos});
From that point onward, you can modify todos (which is a Collection) and your view can listen to todos' events and re-render accordingly.

The answer in the other question is the answer to your question: you're not passing the model to the view when you instantiate the view.
var model = new Todo();
var todoview = new TodoView({model: model});
When you pass an object to a view's constructor, it looks for certain keys and attaches them directly to the view.
You can see which by looking at Backbone's source and searching for viewOptions.
That's how you get the this.model and this.collection automatically attached to the view's this.

You didn't say, but I assume the error you are getting is occurring in the render() method.
Your problem is that you define a new type of model (var Todo = Backbone.Model.extend({...) however you never instantiate it, nor do you pass the model to the todoview constructor.
So at the very least you need to do:
var todomodel = new Todo();
var todoview = new TodoView({
model: todomodel
});

Related

Display collection length in a view in Marionette.js

I'm learning Marionette.js and have a scenario, where my app has:
app.addRegions({
main: '#omen',
newItem: '#addnewitem',
counter: '#counter'
});
These regions. I have these Model/Collections:
var Item = Backbone.Model.extend(),
Items = Backbone.Collection.extend({
model: Item,
url: 'api/items'
}),
I have an Item view and Items view:
ItemView = Mn.ItemView.extend({
tagName: 'tr',
template: '#itemView',
events: {
'click #btnDeleteBook' : 'deleteItem'
},
deleteItem: function() {
app.trigger('item:delete', this.model);
}
}),
ItemsView = Mn.CollectionView.extend({
tagName: 'table',
childView: ItemView,
onShow: function(view) {
TweenMax.staggerFrom($(view).find('td'), 1, {
x: 100
}, 2);
}
}),
I have an initializer function, that listens for events above and does stuff through app.ItemController. It all works fine.
But now I want to add a region (counter region), that displays the total number of items in my collection. I need this to be a separate view ideally, because I will be displaying it in different places.
So I do this:
DisplayCounter = Mn.ItemView.extend({
template: _.template('Total: '+ app.Items.length),
}),
app.Items is an instance of Collection declared above. But even before instantiation of DisplayCounter, I get error:
Uncaught TypeError: Cannot read property 'length' of undefined.
Please help... :(
------------------------- E D I T ----------------------
I've achieved it, but it seems to be so complicated to do such a tiny thing.
Changed my collection like so:
Items = Backbone.Collection.extend({
model: Item,
url: 'api/items',
initialize: function() {
this.listenTo(this, 'add', function() {
app.trigger('collection:updated', this);
});
}
}),
and changed my DisplayCounter like this:
DisplayCounter = Mn.ItemView.extend({
template: _.template('Total: <%=length%>'),
templateHelpers: function() {
return {
length: this.lengthVariable || 0
}
},
initialize: function() {
app.on('collection:updated', function(params){
this.lengthVariable = params.length;
this.render();
}.bind(this));
}
}),
I can't believe there's no easier way to do this.. :/
The code that sets up DisplayCounter is being run before the code that puts an instance of Items into app.Items.
Even if you avoided this problem by assigning app.Items first, you'd still have a problem - the template property is only set once so you'd only ever see the length of app.Items at the time that you define DisplayCounter.
Rather than hard-coding the value directly into the template string, you should supply a value at render time. Mn.View.serializeData allows you to customise the data that is passed into the template function at render time:
DisplayCounter = Mn.ItemView.extend({
template: _.template('Total:: <%= itemCount %>),
serializeData: function() {
return { itemCount: app.Items.length }
}
}),
app.Items is not being defined.
In Marionette you can define which collection or model are your views going to use.
ItemsView = Mn.CollectionView.extend({
tagName: 'table',
childView: ItemView,
collection: myItems // An instance of your collection
onShow: function(view) {
TweenMax.staggerFrom($(view).find('td'), 1, {
x: 100
}, 2);
}
}),
So marionette is going to render one itemView per element in your collection. Then inside of your collection view this.collection is going to refer to the collection instance. So this.collection.length will have what you need.
And in your ItemView you can get the corresponding model by using this.model

Unable to get the Backbone View working for a simple example

I am trying to write a simple example using Backbone.js for study. Some how nothing gets printed in the browser. Need a little help here. The code is given below.
Html:
<div id="container">
<ul id="person-list">
</ul>
</div>
Models
var Person = Backbone.Model.extend({
defaults: {
id: 0,
name: ''
}
});
var PersonStore = Backbone.Collection.extend({
model: Person,
url: 'api/person', //currently not using
initialize: function () {
console.log("Store initialize");
}
});
Views
var PersonView = Backbone.View.extend({
tagName: 'li',
initialize: function () {
_.bindAll(this, "render");
},
render: function () {
$(this.el).append(this.model.name) //model.name shows undefined here
return this;
}
});
var PersonListView = Backbone.View.extend({
el: $('#person-list'),
tagName:'ul',
initialize: function () {
_.bindAll(this, "render");
this.render();
},
render: function () {
self = this;
this.collection.each(function (person) { //name property undefined here on person
var personView = new PersonView({ model: person });
$(self.el).append(personView.render().el);
});
}
});
Sample Run
var persons = new PersonStore([
new Person({id:1, name: "Person 1"}),
new Person({ id: 2, name: "Person 2" }),
]);
new PersonListView({ collection: persons });
The above setup prints nothing(blank) on screen. I have struggled now for some time and need a little help here as to why the two Person's name does not get displayed in the browser.
To make your code work you have to replace
this.$el.append(this.model.name)
with
this.$el.append(this.model.get('name'))
Always use method .get() to access model properties.
Also i highly recommend you use templates for rendering views. This approach let you write .render() implementation once and will be no need to change it if you need visual changes, you can make in template

Using views in backbone.js

I'm currently learning backbone.js and have a little problem. I dont' quite get how the view works.
I have created a model, a collection, and another model that again contains the collection:
Sensor = Backbone.Model.extend({
defaults: {
channel: '',
name: '',
temperature: 0,
tempMin: 0,
tempMax: 0
}
});
SensorList = Backbone.Collection.extend({
model: Sensor
});
Now I created a view, so I am able to render the sensor collection with handlebar.js template:
TemperatureView = Backbone.View.extend({
initialize: function() {
this.render();
},
render: function(eventName) {
var source = $('#sensor-list-template').html();
var template = Handlebars.compile(source);
var html = template(this.collection.toJSON());
this.$el.html(html);
} 
});
Now I want to load some data and render the information. But I don't know how to get the data into my view...I tried this:
$(document).ready(function() {
var temps = new TemperatureRequest();
temps.fetch({
success: function() {
console.log(temps);
var test = temps.get("sensors");
console.log(test);
var tempView = new TemperatureView({
collection: test
});
}
});
});
The data is fetched correctly. I have a collection of sensors. And now I want to pass them to the view so it is getting rendered....but I don't understand how this is done..pls help!
Since you are passing the collection to the view while creating it, you can access the same using this.collection inside your view anywhere.
var tempView = new TemperatureView({
collection: test
});
More over you have added the render function inside your initialize , it automatically calls the render function.Inside the render it fetches the collection and since your template needs only json object you are converting your collection it to json array objects.Templates takes care of appending the values to html.
If you want to add automatic view render to happen whenever the collection removes a model or adds a model into it you can add a listener and callback function to it
initialize : function(){
console.log("initializing view");
this.collection.on('add', this.render, this);
this.collection.on('reset', this.render, this);
this.render();
}
I just got it. Took me a while and I have definitly some reading to do.
There were several problems. First of all I have to overwrite the parse function, so the collection is stored correctly in my model:
TemperatureRequest = Backbone.Model.extend({
urlRoot: '/temperatures',
defaults: {
timestamp: '',
logfile: '',
sensorList: new SensorList()
},
parse: function(response) {
response.sensorList = new SensorList(response.sensors);
return response;
},
success: function(response) {
console.log('success');
}
});
In my view I know add the listen to events as suggested and also fetch the data within the initialize function to get rid of the success callback:
TemperatureView = Backbone.View.extend({
el: '#temperatures',
initialize: function() {
this.listenTo(this.model, 'reset', this.render);
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'add', this.render);
this.model.fetch();
},
render: function(eventName) {
var list = this.model.get('sensorList');
console.log(list.toJSON());
var source = $('#sensor-list-template').html();
var template = Handlebars.compile(source);
var html = template(list.toJSON());
this.$el.html(html);
this.renderTimestamp();
},
renderTimestamp: function() {
var tsText = $("<p></p>").addClass("text-right");
var timestamp = $("<div></div>").addClass("col-sm-4 col-sm-offset-8").append(tsText);
tsText.text(this.model.get('timestamp'));
$('#timestamp').append(timestamp);
}
});
now I can do this to render the data:
$(document).ready(function() {
var temps = new TemperatureRequest();
var tempsView = new TemperatureView({
model: temps
});
});
Instead of passing the collection to the view I pass the model to it and fetch the data inside of the initialize function.
What I still don't understand is when I have to use "this" and when I have to use _bindAll...

Uncaught TypeError: Object function (){return i.apply(this,arguments)} has no method 'on'

For some reasons, I keep on getting this error, (See attached screenshot). I've tried adding a _.bindAll(this); and even tried upgrading my code to have the latest version of backbonejs. Still no luck.
Can someone help me on this?
var app = app || {};
(function ($) {
'use strict';
app.EmployeeView = Backbone.View.extend({
el: '#container',
model: app.Employee,
events: {
'click #save' : 'saveEntry'
},
initialize: function(){
console.log('Inside Initialization!');
this.$empName = this.$('#txtEmpName');
this.$department = this.$('#txtDepartment');
this.$designation = this.$('#txtDesignation');
this.listenTo(app.employees, 'add', this.addEmployee);
app.employees.fetch();
console.log('End of Initialization!');
//this.render();
},
render: function () {
console.log('Inside Render!!');
console.log(this.model);
this.$el.html(this.template(this.model.toJSON()));
console.log('Inside End of Render!!');
return this;
},
newAttributes: function(){
return{
empName: this.$empName.val(),
department: this.$department.val(),
designation: this.$designation.val()
};
},
saveEntry: function(){
console.log('Inside SaveEntry!');
//console.log(this.newAttributes());
console.log('this.model');
console.log(app.Employee);
//app.employees.create(this.newAttributes());
app.Employee.set(this.newAttributes());
app.employees.add(app.Employee);
console.log('After SaveEntry!');
},
addEmployee: function (todo) {
var view = new app.EmployeeItemView({ model: app.Employee });
$('#empInfo').append(view.render().el);
}
})
})(jQuery);
Code for "collections/employees.js"
var app = app || {};
(function (){
console.log('Inside collection');
var Employees = Backbone.Collection.extend({
model: app.Employee,
localStorage: new Backbone.LocalStorage('employee-db')
});
app.employees = new Employees();
})();
Code for "model/employee.js"
var app = app || {};
(function(){
'use strict';
app.Employee = Backbone.Model.extend({
defaults: {
empName: '',
department: '',
designation: ''
}
});
})();
You're saying this in your view:
model: app.Employee
app.Employee looks like a model "class" rather than a model instance. Your view wants a model instance in its model property. Normally you'd say something like this:
var employee = new app.Employee(...);
var view = new app.EmployeeView({ model: employee });
this.model.toJSON() won't work since this.model is the app.Employee constructor. Actually I don't see any meaning in your EmployeeView.render method. If it is aggregate view why you have model on it? Otherwise what is the second view class EmployeeItemView? If you're following ToDo MVC example you can see that there is no model in AppView, that is why I conclude you need not model in your EmployeeView. And render method you provided seems to belong to EmployeeItemView.
Secondly, you call app.Employee.set which is also a call on a constructor not on an object. I think you meant
saveEntry: function(){
console.log('Inside SaveEntry!');
app.employees.create(this.newAttributes());
console.log('After SaveEntry!');
},
If you want to pass a model to app.EmployeeItemView you should use callback argument.
addEmployee: function (employee) {
var view = new app.EmployeeItemView({ model: employee });
$('#empInfo').append(view.render().el);
}

Removing model from collection

This is a first attempt to making a backbone.js application.
I have a contact which is my model and a list/collection of contacts.
The initial rendering and fetching of the list of contacts works fine.
Now I'm trying to remove a contact from the collection after a click event.
It seems like I'm doing something wrong in the delete function of the ContactsView.
When I console.log contacts at the end of that method, the this.contacts collection is not changed.
Any help is appreciated!
var Contact = Backbone.Model.extend({
});
var Contacts = Backbone.Collection.extend({
model: Contact,
url: '/backbone/crm/contact'
});
var ContactsView = Backbone.View.extend({
initialize: function() {
this.contacts = new Contacts();
this.contacts.bind("reset", this.render, this);
this.contacts.bind("change", this.render, this);
this.contacts.bind("remove", this.render, this);
this.contacts.fetch();
},
events: {
"click .delete": "delete"
},
render: function() {
$("#contacts tbody").replaceWith(
$("#contacts_tmpl").render({ 'contacts': this.contacts.toJSON() }));
},
delete: function(e) {
var id = $(e.currentTarget).parents("tr").attr("id");
var model = this.contacts.get(id);
this.contacts.remove(model);
}
});
var contactsView = new ContactsView({ el: $("#contacts")});
The model is effectively removed from the collection. But now I need to remove it server side to.
I've implemented this by doing:
var Contacts = Backbone.Collection.extend({
model: Contact,
url: '/backbone/crm/contact',
initialize: function() {
this.bind("remove", this.delete, this);
},
delete: function(model) {
model.destroy();
}
This works and sends the correct delete request, but I find this kind of awkward, to call a destroy method from the delete method.

Categories