I am learning backbone and I seems to be having problem with my render method below. The render is always returning undefined when I run it. Please can anyone help out with this.
var Todo = Backbone.Model.extend({
defaults : {
status: 'true' ,
description : 'Nothing'
},
toggleStatus: function() {
if (this.get('status') === "completed")
this.set({
'status': 'completed'
});
else
this.set({
'status': 'incompleted'
});
}
});
// Simple View
var simpleView = Backbone.View.extend({
events: {
'change input': 'toggleStatus'
},
initialize: function() {
this.model.on('change', this.render, this);
},
toggleStatus: function() {
this.model.toggleStatus();
},
template : _.template('<h3 class="<%= status %>">' +
'<% if(status === "complete") print("checked") %>/>' +
' <%= description %></h3>'),
render: function() {
console.log('render was called');
$(this.el).html(this.template(this.model.toJSON()));
// return this;
}
});
var todonew = new Todo();
var newView = new simpleView({
model: todonew
});
You should probably put brackets in the if statement of your template, that might cause trouble in some browsers. Change this
'<% if(status === "complete") print("checked") %>/>' +
to this
'<% if(status === "complete") {print("checked")} %>/>' +
Then there are at least three different ways to render the content to your page. You need to give the view a DOM element which it can append its content to. Otherwise, it renders it's content into an empty <div> that is floating around in memory. You are using jQuery, which makes it easy to select a DOM element. I am going to use the $('body').html() to replace the entire page html, modify your selector according to what you need.
Option 1: Uncomment return this in your render function, and modify this code at the end
var newView = new simpleView({
model: todonew
});
$('body').html(newView.render().el);
$(this.el).html(this.template(this.model.toJSON()));
to this
$('body').html(this.template(this.model.toJSON()));
Option 2: Send in the element you want to add the content to when you create the view
var newView = new simpleView({
model: todonew,
el: $('body') //or whatever selector you want
});
Option 3: change this line in your render function
$(this.el).html(this.template(this.model.toJSON()));
to this
$('body').html(this.template(this.model.toJSON()));
Again, this replaces the HTML for your whole page, use a different selector, or use .append() or another method instead of .html() depending on your needs.
you need to set your el, this is the problem of your script when return error on this. try this for example:
var newView = new simpleView({
model: todonew,
el: $("#somediv") //put here a div or the element where you want to put your template
});
I have a similar problem when I write:
$('#foo').append(bar.render().el);
I got this error:
Uncaught TypeError: Cannot read property 'el' of undefined
To correct this, I added return this; in my render:function(){}.
render: function() {
// something else
return this; // add this line
}
Reference: Backbone.js Docs
A good convention is to return this at the end of render to enable chained calls.
Related
Trying to make a reasonable teaching model of Backbone that shows proper ways to take advantage of backbone's features, with a grandparent, parent, and child views, models and collections...
I am trying to change a boolean attribute on a model, that can be instantiated across multiple parent views. How do I adjust the listers to accomplish this?
The current problem is that when you click on any non-last child view, it moves that child to the end AND re-instantiates it.
Plnkr
Click 'Add a representation'
Click 'Add a beat' (you can click this more than once)
Clicking any beat view other than the last one instantiates more views of the same beat
Child :
// our beat, which contains everything Backbone relating to the 'beat'
define("beat", ["jquery", "underscore", "backbone"], function($, _, Backbone) {
var beat = {};
//The model for our beat
beat.Model = Backbone.Model.extend({
defaults: {
selected: true
},
initialize: function(boolean){
if(boolean) {
this.selected = boolean;
}
}
});
//The collection of beats for our measure
beat.Collection = Backbone.Collection.extend({
model: beat.Model,
initialize: function(){
this.add([{selected: true}])
}
});
//A view for our representation
beat.View = Backbone.View.extend({
events: {
'click .beat' : 'toggleBeatModel'
},
initialize: function(options) {
if(options.model){
this.model=options.model;
this.container = options.container;
this.idAttr = options.idAttr;
}
this.model.on('change', this.render, this);
this.render();
},
render: function(){
// set the id on the empty div that currently exists
this.$el.attr('id', this.idAttr);
//This compiles the template
this.template = _.template($('#beat-template').html());
this.$el.html(this.template());
//This appends it to the DOM
$('#'+this.container).append(this.el);
return this;
},
toggleBeatModel: function() {
this.model.set('selected', !this.model.get('selected'));
this.trigger('beat:toggle');
}
});
return beat;
});
Parent :
// our representation, which contains everything Backbone relating to the 'representation'
define("representation", ["jquery", "underscore", "backbone", "beat"], function($, _, Backbone, Beat) {
var representation = {};
//The model for our representation
representation.Model = Backbone.Model.extend({
initialize: function(options) {
this.idAttr = options.idAttr;
this.type = options.type;
this.beatsCollection = options.beatsCollection;
//Not sure why we have to directly access the numOfBeats by .attributes, but w/e
}
});
//The collection for our representations
representation.Collection = Backbone.Collection.extend({
model: representation.Model,
initialize: function(){
}
});
//A view for our representation
representation.View = Backbone.View.extend({
events: {
'click .remove-representation' : 'removeRepresentation',
'click .toggle-representation' : 'toggleRepType',
'click .add-beat' : 'addBeat',
'click .remove-beat' : 'removeBeat'
},
initialize: function(options) {
if(options.model){this.model=options.model;}
// Dont use change per http://stackoverflow.com/questions/24811524/listen-to-a-collection-add-change-as-a-model-attribute-of-a-view#24811700
this.listenTo(this.model.beatsCollection, 'add remove reset', this.render);
this.listenTo(this.model, 'change', this.render);
},
render: function(){
// this.$el is a shortcut provided by Backbone to get the jQuery selector HTML object of this.el
// so this.$el === $(this.el)
// set the id on the empty div that currently exists
this.$el.attr('id', this.idAttr);
//This compiles the template
this.template = _.template($('#representation-template').html());
this.$el.html(this.template());
//This appends it to the DOM
$('#measure-rep-container').append(this.el);
_.each(this.model.beatsCollection.models, function(beat, index){
var beatView = new Beat.View({container:'beat-container-'+this.model.idAttr, model:beat, idAttr:this.model.idAttr+'-'+index });
}, this);
return this;
},
removeRepresentation: function() {
console.log("Removing " + this.idAttr);
this.model.destroy();
this.remove();
},
//remove: function() {
// this.$el.remove();
//},
toggleRepType: function() {
console.log('Toggling ' + this.idAttr + ' type from ' + this.model.get('type'));
this.model.set('type', (this.model.get('type') == 'line' ? 'circle' : 'line'));
console.log('Toggled ' + this.idAttr + ' type to ' + this.model.get('type'));
this.trigger('rep:toggle');
},
addBeat: function() {
this.trigger('rep:addbeat');
},
removeBeat: function() {
this.trigger('rep:removebeat');
}
});
return representation;
});
This answer should be working properly for all views, being able to create, or delete views without effecting non related views, and change attributes and have related views auto update. Again, this is to use as a teaching example to show how to properly set up a backbone app without the zombie views...
Problem
The reason you are seeing duplicate views created lies in the render() function for the Beat's view:
render: function(){
// set the id on the empty div that currently exists
this.$el.attr('id', this.idAttr);
//This compiles the template
this.template = _.template($('#beat-template').html());
this.$el.html(this.template());
//This appends it to the DOM
$('#'+this.container).append(this.el);
return this;
}
This function is called when:
when the model associated with the view changes
the beat view is first initialized
The first call is the one causing the problems. initialize() uses an event listener to watch for changes to the model to re-render it when necessary:
initialize: function(options) {
...
this.model.on('change', this.render, this); // case #1 above
this.render(); // case #2 above
...
},
Normally, this is fine, except that render() includes code to push the view into the DOM. That means that every time the model associated with the view changes state, the view not only re-renders, but is duplicated in the DOM.
This seems to cause a whole slew of problems in terms of event listeners being bound incorrectly. The reason, as far as I know, that this phenomenon isn't caused when there is just one beat present is because the representation itself also re-renders and removes the old zombie view. I don't entirely understand this behavior, but it definitely has something to do with the way the representation watches it's beatCollection.
Solution
The fix is quite simple: change where the view appends itself to the DOM. This line in render():
$('#'+this.container).append(this.el);
should be moved to initialize, like so:
initialize: function(options) {
if(options.model){
this.model=options.model;
this.container = options.container;
this.idAttr = options.idAttr;
}
this.model.on('change', this.render, this);
this.render();
$('#'+this.container).append(this.el); // add to the DOM after rendering/updating template
},
Plnkr demo with solution applied
Previous Title :- Search functionality in Backbone.js.
I am working on Backbone.js to achieve search functionality( when user types something in search textbox, the list should get filetered as per search criteria).
For this I have my controller as :-
var departments = backbone.Collection.extend({
model: departmentModel,
url: '/MyController/GetDepartments',
comparator: function (department) {
return department.get('name');
},
initialize: function () {
this.selected = [];
},
search: function (letters) {
if (letters == "") return this;
var pattern = new RegExp(letters, "gi");
return _(this.filter(function (data) { //without wrapping the filter with the underscore function, the filter does not return a collection
return pattern.test(data.get("Name"));
}));
}
});
return departments;
In my Backbone View I have my keyup event defined on search textbox
"keyup #searchDepartments": "searchDepartments",
Where my searchDepartments is -
searchDepartments: function (e) {
var letters = $(e.currentTarget).val();
var searchResult = this.collection.search(letters);
var collection = new departmentCollection(searchResult.toArray());
// debugger;
this.renderFileteredData(collection);
//$(e.currentTarget).val(letters);
}
Lastly, renderFilteredData is simple
renderFileteredData: function (departments) {
$(this.$el).html(this.template(departments.toJSON()));
return this;
},
Now the issue is- when ever I am typing any text the list gets filtered out but the search text goes off. Whats wrong?
EDIT:- As per suggestions, now I have created a different view as below for search text box:-
function ($, _, backbone) {
'use strict';
var searchDepartmentView = backbone.View.extend({
el: "#search-container",
tagName: 'div',
template: Handlebars.templates.DepartmentSearchView,
initialize: function () {
//this.render();
return this;
},
render: function () {
console.log('in search departmnet view render method');
// $(this.$el).html(this.template());// I am trying to render this template.. But its not working
this.$el.html('in search departmnet view render method');//Passing dummy value to it
return this;
}
});
return searchDepartmentView;
});
and in my main view :-
render: function () {
// _.bindAll(this, "search");
this.innerView = new departmentSearchView();
$(this.$el).html(this.template(this.collection.toJSON()));
this.$el.find("#search-container").html(this.innerView.render().$el); //Tried approach 1 as suggested
// $(this.$el).html(this.innerView.render().el);//Tried approach 2 as suggested
return this;
},
Now, I am not getting my search text in search departmnet view render method displayed in main view
Finally, I found a way to do it. Thank to the great answer by Kelvin Peel in how-to-handle-initializing-and-rendering-subviews-in-backbone-js
Thanks to YuruiRayZhang for his suggestions!
Code snippet from there, which describes a detailed process of nested views :-
var ParentView = Backbone.View.extend({
el: "#parent",
initialize: function() {
// Step 1, (init) I want to know anytime the name changes
this.model.bind("change:first_name", this.subRender, this);
this.model.bind("change:last_name", this.subRender, this);
// Step 2, render my own view
this.render();
// Step 3/4, create the children and assign elements
this.infoView = new InfoView({el: "#info", model: this.model});
this.phoneListView = new PhoneListView({el: "#phone_numbers", model: this.model});
},
render: function() {
// Render my template
this.$el.html(this.template());
// Render the name
this.subRender();
},
subRender: function() {
// Set our name block and only our name block
$("#name").html("Person: " + this.model.first_name + " " + this.model.last_name);
}
});
Also, an extended tutorial for the above mentioned process backbone-js-subview-rendering-trick/
I have a Backbone view (see below) that I believe to be doing the right thing
Index = Backbone.View.extend({
render: function() {
var activities = new Activities();
activities.fetch();
var tpl = Handlebars.compile($("#activities-template").html());
$(this.el).html(tpl({activities: activities.toJSON()}));
return this;
}
});
If execute each line in the render() function with Chrome JS console I get the expected result with the element I pass in getting populated with the template output. However, when I run this using the following
var i = new Index({el: $("body")})
i.render()
"i.$el" is completely empty--the HTML is not getting rendered like it does in console. Any ideas why?
fetch is an AJAX call so there's no guarantee that activities.toJSON() will give you any data when you do this:
activities.fetch();
var tpl = Handlebars.compile($("#activities-template").html());
$(this.el).html(tpl({activities: activities.toJSON()}));
Executing the code in the console probably gives the AJAX call time to return with something before you try to use activities.
You should do two things:
Fix your template to do something sensible (such as show a loading... message of some sort) if activities is empty.
Attach your view's render to the collection's "reset" event:
initialize: function() {
// Or, more commonly, create the collection outside the view
// and say `new View({ collection: ... })`
this.collection = new Activities();
this.collection.on('reset', this.render, this);
this.collection.fetch();
},
render: function() {
var tpl = Handlebars.compile($("#activities-template").html());
this.$el.html(tpl({activities: this.collection.toJSON()}));
return this;
}
I also switched to this.$el, there's no need to $(this.el) when Backbone already gives you this.$el.
I'm making a test for event change to a selectbox view in backbone which is 'should provide the correct path to router.navigate when changed'. this is the scenario, If I select a value in the dropdown, it should redirect to the correct url.
this is the test (campaign.test.js):
it('should provide the correct path to router.navigate when changed', function() {
var routeName = 'test_campaign';
var routerSpy = sinon.spy(Insights.router, 'navigate');
this.view.$el.bind('change', function() {
expect(routerSpy).toHaveBeenCalledWith('campaign/' + routeName, true);
});
//create the option element holding the test value
this.view.$el.append($('<option value="' + routeName +'" selected="selected" />'));
this.view.$el.trigger('change');
routerSpy.restore();
});
this is the module (campaign.js):
DropdownView: Backbone.View.extend({
tagName: 'select',
className: 'campaign-list',
events: {
'change' : 'navCampaign'
},
initialize: function() {
_.bindAll(this);
this.collection.on('reset', this.render);
this.collection.on('add', this.addCampaign);
//if no campaigns exist then add a dummy placeholder
if(this.collection.models.length === 0) {
this.$el.append('<option value="">No Campaigns</option>');
}
},
navCampaign: function() {
Insights.router.navigate();
},
addCampaign: function(campaign) {
//remove the No Campaigns placeholder when campaigns are added
var firstChild = this.$el.children(':first');
if(firstChild.attr('value') === '') {
firstChild.remove();
}
var view = new Insights.Campaign.DropdownItem({ model: campaign });
var item = view.render().el;
this.$el.append(item);
},
render: function() {
this.collection.each(this.addCampaign);
return this;
}
})
In my test I created a new OPTION element then set a value with attribute selected.
How can I pass this as the currentTarget of the change event and send it to trigger()?
Or is there a easier way to do this?
Im getting this test fail. Error: Expected Function to have been called with 'campaign/test_campaign', true.
Your test have to look like this:
it('should provide the correct path to router.navigate when changed', function() {
var routeName = 'test_campaign';
var routerSpy = sinon.spy(Insights.router, 'navigate');
var view = new DropdownView();
//create the option element holding the test value
this.view.$el.append($('<option value="' + routeName +'" selected="selected" />'));
this.view.$el.trigger('change');
expect(routerSpy).toHaveBeenCalledWith('campaign/' + routeName, true);
routerSpy.restore();
});
First of all you have to create an instance of your view in the test and you have to create the spy before you create the view. Both can also be done in the beforeEach block.
When you adding the expect code in an event handler it can happen that it will called before the navigate method on your router is called. Cause two handlers are added to the listener, one to call navigate on the router and one that test that it was called on the router. As you can't insure which one is called first the test will fail when the listener from the test is called first.
Maybe its better to test it with passing the collection with data to the view, not setting the DOM of the view element directly in the test. So you will also test that your initialize and addData method will work.
Here are the changes, that made this test passed. :)
I changed the value of variable routeName, in campaign.test.js
from
var routeName = 'test_campaign';
to
var routeName = this.view.$el.val();
Then implemented this test to campaign.js
navCampaign: function() {
var newRoute = 'campaign/' + this.$el.val();
Insights.router.navigate(newRoute, true);
}
there I got this green. :)
sorry, I would like to make this clear with the help of my team mate.
var routeName = this.view.$el.val(); is pointing to null
by making the value dynamic it will loose the test's effectiveness,
so I put back to var routeName = 'test_campaign'.
the point of this test is to specify what value it is expecting based on a predefined set of inputs.
I've been staring at this for a while and trying various tweaks, to no avail.
Why am I getting a "this.model is undefined" error at
$(function(){
window.Sentence = Backbone.Model.extend({
initialize: function() {
console.log(this.toJSON())
}
});
window.Text = Backbone.Collection.extend({
model : Sentence,
initialize: function(models, options){
this.url = options.url;
}
});
window.SentenceView = Backbone.View.extend({
initialize : function(){
_.bindAll(this, 'render');
this.template = _.template($('#sentence_template').html());
},
render : function(){
var rendered = this.template(this.model.toJSON());
$(this.el).html(rendered);
return this;
}
})
window.TextView = Backbone.View.extend({
el : $('#notebook') ,
initialize : function(){
_.bindAll(this, 'render');
},
render : function(){
this.collection.each(function(sentence){
if (sentence === undefined){
console.log('sentence was undefined');
};
var view = new SentenceView({model: sentence});
this.$('ol#sentences').append(view.render().el);
});
return this;
}
});
function Notebook(params){
this.text = new Text(
// models
{},
// params
{
url: params.url
}
);
this.start = function(){
this.text.fetch();
this.textView = new TextView({
collection: this.text
});
$('body').append(this.textView.render().el);
};
}
window.notebook = new Notebook(
{ 'url': 'js/mandarin.js' }
);
window.notebook.start();
})
There's an online version wher eyou can see the error in a console at:
http://lotsofwords.org/languages/chinese/notebook/
The whole repo is at:
https://github.com/amundo/notebook/
The offending line appears to be at:
https://github.com/amundo/notebook/blob/master/js/notebook.js#L31
I find this perplexing because as far as I can tell the iteration in TextView.render has the right _.each syntax, I just can't figure out why the Sentence models aren't showing up as they should.
var view = new SentenceView({model: sentence});
I'm pretty sure when you pass data to a backbone view constructor, the data is added to the Backbone.View.options property.
Change this line
var rendered = this.template(this.model.toJSON());
to this
var rendered = this.template(this.options.model.toJSON());
and see if it works
UPDATE:
From the doco:
When creating a new View, the options you pass are attached to the view as this.options, for future reference. There are several special options that, if passed, will be attached directly to the view: model, collection, el, id, className, and tagName
So, disregard the above advice - the model should by default be attached directly to the object
Things to check next when debugging:
confirm from within the render() method that this is actually the SentenceView object
confirm that you are not passing in an undefined sentence here:
var view = new SentenceView({model: sentence});
UPDATE2:
It looks like the collection is borked then:
this.textView = new TextView({
collection: this.text
});
To debug it further you'll need to examine it and work out what's going on. When I looked in firebug, the collection property didn't look right to me.
You could have a timing issue too. I thought the fetch was asynchronous, so you probably don't want to assign the collection to the TextView until you are sure it has completed.
Backbone surfaces underscore.js collection methods for you so you can do this. See if this works for you:
this.collection.each(function(sentence) {
// YOUR CODE HERE
});
I think the problem is on line 48 of notebook.js shown below:
render : function(){
_(this.collection).each(function(sentence){
var view = new SentenceView({model: sentence});
this.$('ol#sentences').append(view.render().el);
});
The problem is you are wrapping the collection and you don't have to. Change it to
this.collection.each(function(sentence){ ...
hope that fixes it
EDIT:
OK i'm going to take another crack at it now that you mentioned timing in one of your comments
take a look at where you are fetching and change it to this:
this.start = function(){
this.text.fetch({
success: _.bind( function() {
this.textView = new TextView({
collection: this.text
});
$('body').append(this.textView.render().el);
}, this)
);
};
I typed this manually so there may be mismatching parentheses. The key is that fetch is async.
Hope this fixes it
try using _.each
_.each(this.collection, function(sentence){
if (sentence === undefined){
console.log('sentence was undefined');
};
var view = new SentenceView({model: sentence});
this.$('ol#sentences').append(view.render().el);
},this);