Accessing a method on a Backbone.js view inside of loop - javascript

I'm working on an app that has an ItemListView that contains a number of ItemView elements. In my ItemListView, I'm using the jQuery .each() method to loop through the collection of items and render them as list elements.
I've got all the pieces in place except for the actual attaching of the li elements to the containing ul. The sticking point is getting access to the ItemListView.appendItem method from inside of my .each() loop. I've tried using this.appendItem and self.appendItem, but inside the loop this is the item and self is the window object.
Here's what I have right now:
ItemListView = Backbone.View.extend({
el: '#item-rows',
initialize: function () {
this.collection = new Items();
this.render();
},
render: function () {
$.each(this.collection.models, function (i, item) {
var itemview = new ItemView( { model: item });
this.appendItem(itemview); // this refers to the item, so appendItem is undefined
});
},
appendItem: function (itemView) {
$(this.el).append(itemView.render().el);
}
});
var itemlistview = new ItemListView;
I'm pretty sure that the context issue is the only problem, as I've examined the other pieces of the this by outputting them to the console and they look fine.
What am I missing?

A more Backbone-y way to do this would be to use the collection's each, provided by underscore.js:
render: function() {
this.collection.each( function( item, index ) {
var itemView = new ItemView( { model:item } );
this.appendItem( itemView );
}, this );
return this;
}
Notes:
notice the second param to each which take a reference to bind the function to
the each takes the element first and the index second
render should generally return this for chaining purposes (as mentioned in the docs), I don't think your appendItem function will work as you expect without this part

Yeah, it's a pretty simple fix. You just gotta refer to the this in the outer context.
render: function () {
var somereftothis = this;
$.each(this.collection.models, function (i, item) {
var itemview = new ItemView( { model: item });
somereftothis.appendItem(itemview); // this refers to the item, so appendItem is undefined
});
},

Related

Backbone: Re-render existing model in new DOM element

I am using a collection view to render my array of model views. I have added a method that removes a single model view from the existing collection view, and attempts to re-render it in a new el: element.
I use collection.get(this.model) to save the model to a variable, I add that variable to my new collection which is the model of a new collection view associated with a new DOM element, and I re-use the same collection view render method. When I console.log() the new collection, I see the model that I picked from the old collection, but it's not rendering on the page.
<script>
(function($){
//---------SINGLE ENTRY MODEL----------
var Entry = Backbone.Model.extend({
defaults: function(){
return{
word: '',
definition: ''
}
}
});
//------------ENTRY MODEL COLLECTION------------
var EntryList = Backbone.Collection.extend({
model: Entry
});
//-----INSTANCIATE COLLECTION----
var dictionary = new EntryList();
var saved = new EntryList();
//-----SINGLE ENTRY VIEW------
var EntryView = Backbone.View.extend({
model: new Entry(),
tagName:'div',
events:{
'click .edit': 'edit',
'click .delete': 'delete',
'keypress .definition': 'updateOnEnter',
'click .save': 'save'
},
delete: function(ev){
ev.preventDefault;
dictionary.remove(this.model);
},
edit: function(ev){
ev.preventDefault;
this.$('.definition').attr('contenteditable', true).focus();
},
//method that adds existing model to new collection
save: function(ev){
ev.preventDefault;
var savedEntry = dictionary.get(this.model);
dictionary.remove(this.model);
saved.add(savedEntry);
console.log(savedEntry.toJSON());
},
close: function(){
var definition = this.$('.definition').text();
this.model.set('definition', definition);
this.$('.definition').attr('contenteditable', false).blur();
},
updateOnEnter: function(ev){
if(ev.which == 13){
this.close();
}
},
initialize: function(){
this.template = _.template($("#dictionary_template").html());
},
render: function(){
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
//--------------DICTIONARY VIEW------------
var DictionaryView = Backbone.View.extend({
model: dictionary,
el: $('#entries'),
initialize: function(){
this.model.on('add', this.render, this);
this.model.on('remove', this.render, this);
},
render: function(){
var self = this;
self.$el.html('');
_.each(this.model.toArray(), function(entry, i){
self.$el.append((new EntryView({model: entry})).render().$el);
});
return this;
}
});
//---------SAVED ENTRY VIEW-----------
var SavedView = Backbone.View.extend({
model: saved,
el: $('#saved'),
initialize: function(){
this.model.on('save', this.savedRender, this);
},
//method that renders new collection view with different el:
savedRender: function(){
var self = this;
self.$el.html('');
_.each(this.model.toArray(), function(saved, i){
self.$el.append((new EntryView({model: savedEntry})).render().$el);
});
return this;
}
});
//-------BINDING DATA ENTRY TO NEW MODEL VIEW-------
$(document).ready(function(){
$('#new-entry').submit(function(ev){
var entry = new Entry({word: $('#word').val(), definition: $('#definition').val() });
dictionary.add(entry);
dictionary.comparator = 'word';
console.log(dictionary.toJSON());
$('.form-group').children('input').val('');
return false;
});
var appView = new DictionaryView();
});
//--------------ROUTER----------------
var Router = Backbone.Router.extend({
routes:{
'':'home'
}
});
var router = new Router();
router.on('route:home', function(){
console.log('router home');
});
Backbone.history.start();
})(jQuery);
</script>
There are a number of problems here.
First, you do not have an instance of SavedView. The var SavedView = Backbone.View.extend(...); statement is just defining the SavedView class. In order to have a living instance of this class, you must initialize one with the new operator. You will need a line like the following somewhere in your code (a good place would be at the end of the jQuery ready handler):
var saved_view = new SavedView();
Next, we will investigate the save method of the EntryView class. The var savedEntry = dictionary.get(this.model); statement is completely unnecessary because we know that dictionary.get(this.model) will return this.model - which we obviously already have an instance of. So we can remove the clutter from this method and be left with the following:
ev.preventDefault;
saved.add(this.model);
dictionary.remove(this.model);
However, we are still not at our destination. If we turn our attention to the SavedView class definition, we see that it is binding its render method to the 'save' event on its collection, the saved object. Its not the 'save' event we should be binding to, but rather the 'add' event - as that is what will be triggered when we add models to saved:
this.model.on('add', this.savedRender, this);
If we test our code now we should get scolded with a reference error on savedEntry within SavedView.savedRender. It looks like this is a typo and what was intended was `saved'. (You will notice below that in addition to correcting the typo, I have also removed a set of parentheses from this expression that served no function save for making the code less readable):
self.$el.append(new EntryView({ model: saved }).render().$el);
EDIT:
In response to your follow-up question about the saved variable inside the SavedView.savedRender method:
The saved object in this case is a single Entry model. The reason for your confusion is that we are re-using the variable name "saved". Within the _.each callback we have defined the first parameter to be called "saved"; this "saved" is local to the callback and is not related to the EntryList collection defined previously. Within our callback, saved is an element of the saved collection (yikes!) - which is a lesson in why variable naming is important.
As I proceeded to change the name of "saved" in the savedRender method, I noticed a few other refactorings that were screaming to be made. I have listed my refactorings below:
A purpose of using Backbone (http://backbonejs.org/) is to give us access to convenient helpers for objects (models) and arrays (collections). Backbone collections have an each method we can make use of instead of passing our collection to Underscore's (http://underscorejs.org/) each.
As stated above, saved is a terrible name for our each callback parameter because it conflicts conceptually with the name of the collection. Because saved is a collection of Entry models, "entry" is a much more suitable name.
Backbone allows us to pass the context to our each callback that will be our this within that callback. This allows us to skip the step of caching our this in the self variable.
My refactored savedRender becomes:
savedRender: function () {
this.$el.empty();
this.model.each(function (entry) {
this.$el.append(new EntryView({ model: entry }).render().$el);
}, this);
return this;
}

Backbone view collection.each() bug?

This one should be simple, but it's not and it's giving me headache.
I copy/pasted a fairly simple "Hello Backbone" code, and simplified it some more, to get this:
(function($){
var Item = Backbone.Model.extend({
defaults: {
part1: 'hello',
part2: 'world'
},
initialize: function() {
//console.log("initialize item");
}
});
var List = Backbone.Collection.extend({
model: Item,
initialize: function() {
//console.log("initialize list");
}
});
var ListView = Backbone.View.extend({
//el: $('body'), // el attaches to existing element
initialize: function(){
_.bindAll(this, 'render'); // every function that uses 'this' as the current object should be in here
this.collection = new List();
this.render();
},
render: function(){
//console.log("listview render");
},
});
var listView = new ListView();
var item1 = new Item();
listView.collection.add(item1);
console.dir("Emptying collection...");
listView.collection.each(function (item) {
console.dir("Removing model cid:" + item.cid);
listView.collection.remove(item);
})
var item2 = new Item();
listView.collection.add(item2);
var item3 = new Item();
listView.collection.add(item3);
console.dir("Emptying collection...");
listView.collection.each(function (item) {
console.dir("Removing model cid:" + item.cid);
listView.collection.remove(item);
})
console.log(listView.collection.length);
})(jQuery);
First part is actually some backbone boilerplate stuff, and the last one should:
add a model to collection
clear collection
add two models to collection
clear collection
Finally, listView.collection.length should be 0, but it's not.
There is one model left.
What I see from console log, the listView.collection.each(function (item) { ... }) is not doing what it should be doing, because only one item is removed in the second collection iteration, instead of two items.
So, am I doing something wrong or there is a bug here somewhere?
UPDATE: here's the jsfiddle
You're altering a list of elements (listView.collection.remove(item)) while iterating over it, which usually leads to Bad Thingsā„¢ Here's a simplified test case : http://jsfiddle.net/nikoshr/Xx9uw/
Either clone your list of models before removing them:
_.each(_.clone(listView.collection.models), function (item) {
console.log("Removing model cid:" + item.cid);
listView.collection.remove(item);
});
http://jsfiddle.net/nikoshr/Xx9uw/2/
Or use collection.reset :
listView.collection.reset();
http://jsfiddle.net/nikoshr/Xx9uw/3/
The problem is that you're never creating a collection.
var list = new List();
var listView = new ListView({collection: list});
The moment I saw #nikoshr's answer I knew I was a sucker. Again. I'm repeating the same mistake throughout different languages, C/C++ (usually yields ye-olde-segfault), Java, C#, etc... and now JS/BB.
Damn.
In addition to nikoshr's answer, I found this collection-based solution that could avoid classic iteration (in C# one can use linq extensions for that purpose):
this.collection
.forEach(function (item) {
item.destroy()
});
Conveniently, Underscore gives you an option to filter out just the models you really need:
this.collection
.where({ YourAttribute: YourValue, YourOtherAttribute: YourOtherValue })
.forEach(function (item) {
item.destroy()
});

Backbone Not Firing Events

So here is an example of my app in jsfiddle: http://jsfiddle.net/GWXpn/1/
The problem is click event isn't being fired at all. I am not getting any JS errors in the console.
First, I wanted to display an unordered list with couple if items, each item should be clickable. This is what I did:
var FooModel = Backbone.Model.extend({});
var ListView = Backbone.View.extend({
tagName: 'ul', // name of (orphan) root tag in this.el
initialize: function() {
_.bindAll(this, 'render'); // every function that uses 'this' as the current object should be in here
},
render: function() {
for (var i = 0; i < 5; i++) {
var view = new SingleView({
model: new FooModel()
});
$(this.el).append(view.render().el);
}
return this; // for chainable calls, like .render().el
}
});
var SingleView = Backbone.View.extend({
tagName: 'li', // name of (orphan) root tag in this.el
initialize: function() {
_.bindAll(this, 'render', 'click'); // every function that uses 'this' as the current object should be in here
},
events: {
"click": "click"
},
click: function(ev) {
console.log("aaa");
alert(333);
},
render: function() {
$(this.el).append("aaa");
return this; // for chainable calls, like .render().el
}
});
I wanted to divide my app in to multiple modules (header, body, footer) so I created an abstract model and extended my modules from it:
var AbstractModule = Backbone.Model.extend({
getContent: function () {
return "TODO";
},
render: function () {
return $('<div></div>').append(this.getContent());
}
});
var HeaderModule = AbstractModule.extend({
id: "header-module",
});
var BodyModule = AbstractModule.extend({
id: "body-module",
getContent: function () {
var listView = new ListView();
return $("<div/>").append($(listView.render().el).clone()).html();
}
});
var ModuleCollection = Backbone.Collection.extend({
model: AbstractModule,
});
Then I just created my main view and rendered all its subviews:
var AppView = Backbone.View.extend({
el: $('#hello'),
initialize: function (modules) {
this.moduleCollection = new ModuleCollection();
for (var i = 0; i < modules.length; i++) {
this.moduleCollection.add(new modules[i]);
}
},
render: function () {
var self = this;
_(this.moduleCollection.models).each(function (module) { // in case collection is not empty
$(self.el).append(module.render());
}, this);
}
});
var appView = new AppView([HeaderModule, BodyModule]);
appView.render();
Any ideas why?
You have two bugs in one line:
return $("<div/>").append($(listView.render().el).clone()).html();
First of all, clone doesn't copy the events unless you explicitly ask for them:
Normally, any event handlers bound to the original element are not copied to the clone. The optional withDataAndEvents parameter allows us to change this behavior, and to instead make copies of all of the event handlers as well, bound to the new copy of the element.
[...]
As of jQuery 1.5, withDataAndEvents can be optionally enhanced with deepWithDataAndEvents to copy the events and data for all children of the cloned element.
You're cloning the <ul> here so you'll want to set both of those flags to true.
Also, html returns a string and strings don't have events so you're doubling down on your event killing.
I don't understand why you're cloning anything at all, you should just return the el and be done with it:
return listView.render().el;
If you insist on cloning, then you'd want something like this:
return $(listView.render().el).clone(true, true);
but that's just pointless busy work.
BTW, 'title' and 'Title' are different model attributes so you'll want to say:
console.log(this.model.get("title") + " clicked");
instead of
console.log(this.model.get("Title") + " clicked");
Also, Backbone collections have a lot of Underscore methods mixed in so don't mess with a collection's models directly, where you're currently saying:
_(this.moduleCollection.models).each(...)
just say:
this.moduleCollection.each(...)
And as Loamhoof mentions, 0.3.3 is ancient history, please upgrade to newer versions of Backbone, Underscore, and jQuery. You should also read the change logs so that you can use newer features (such as this.$el instead of $(this.el), fewer _.bindAll calls, listenTo, ...).
Partially Corrected Demo (including updated libraries): http://jsfiddle.net/ambiguous/e4Pba/
I also ripped out the alert call, that's a hateful debugging technique that can cause a huge mess if you get into accidental infinite loops and such, console.log is much friendlier.

Referencing object from within another object

I am creating a view in backbone that accepts a collection I want to then render that view then use the collection to append another view to the orginal but I don't know how to reference the original view in the success function of the collection. When I try the following code I get undefined.
new GenreView().render(new PopVideosCollection());
define (['jquery','underscore','backbone'],function($,_,Backbone) {
GenreView = Backbone.View.extend({
tagName:"div",
className:"sect",
template: _.template($("#genreView").html()),
render: function (collection)
{
this.$el.html(this.template);
collection.fetch ({success:function (video)
{
console.log(video.toJSON());
console.log(GenreView.el);
},
});
},
});
return GenreView;
});
You need to get a reference to the instance of GenreView from inside the callback. Something like this should get you there:
var context = this;
collection.fetch ({success:function (video){
console.log(video.toJSON());
console.log(context.el);
}
});
However, you should re-think your approach a little. It would be better to call fetch on your collection, and have the view subscribe the reset event of your collection. Starting with your example code, that would look something like:
var GenreView = Backbone.View.extend({
initialize: function() {
this.listenTo(this.model, "reset", this.appendSubView);
},
render: function() {
this.model.fetch();
},
appendSubView : function(video){
console.log(video.toJSON());
console.log(this.el);
}
});

Render jQuery object through Backbone View

I'm rather new to Backbone.js development, and have run into a bit of a roadblock while attempting to render a subview.
Currently, I have in place several views to render a custom dropdown-button, as well as other elements. I've taken this approach based on DocumentCloud's code
Here's what I have so far:
app.ui.SelectMenu = Backbone.View.extend({
className: 'btn-group group-item',
options: {
id: null,
standalone: false
},
events: {
"click .dropdown-menu a": "setLabel"
},
constructor: function (options) {
Backbone.View.call(this, options);
this.items = [];
this.content = JST['common-select_button'];
this.itemsContainer = $('.dropdown-menu', $(this.content.render()));
// Add any items that we may have added to the object params
if (options.items) {
this.addItems(options.items);
}
},
render: function () {
this.$el.html(this.content.render({
label: this.options.label,
items: this.itemsContainer
}));
this._label = this.$('.menu-label');
return this;
},
setLabel: function (label) {
$(this._label).text(label || this.options.label);
},
addItems: function (items) {
this.items = this.items.concat(items);
var elements = _(items).map(_.bind(function (item) {
var attrs = item.attrs || {};
_.extend(attrs, { 'class': 'menu_item' + (attrs['class'] || '') });
var el = this.make('li', attrs, item.title);
return el;
}, this));
$(this.itemsContainer).append(elements);
}
});
So far I have successfully rendered my button, as well as the appropriate label, but I cannot seem to populate the .dropdown-menu when calling the addItems function.
I'm assuming that when render hits, the items variable cannot be populated due to the fact that I am passing a jQuery object and not a string, yet whenever I use items: this.itemsContainer.html(), that simply pastes the html surrounded by quotes... I could simply replace the quotes but that just feels like a hack to me.
Any help would be much appreciated. Thanks!
jQuery's append doesn't take an array:
.append( content [, content] )
content: DOM element, HTML string, or jQuery object to insert at the end of each element in the set of matched elements.
content: One or more additional DOM elements, arrays of elements, HTML strings, or jQuery objects to insert at the end of each element in the set of matched elements.
If you want to append multiple elements in one call, you have to supply them as separate arguments:
$(x).append(e1, e2, e3, ...);
so you'd have to use apply to convert your array to separate arguments:
var $i = $(this.itemsContainer);
$i.append.apply($i, elements);
That sort of chicanery really isn't necessary though, you can add them one by one as you create them:
addItems: function (items) {
this.items = this.items.concat(items);
_(items).each(function (item) {
var attrs = item.attrs || {};
_.extend(attrs, { 'class': 'menu_item' + (attrs['class'] || '') });
this.itemsContainer.append(this.make('li', attrs, item.title));
}, this);
}
Also note that _.each can take a context argument so you don't need a separate _.bind call. And I'm pretty sure that this.itemsContainer is already a jQuery object so you don't need to wrap it $() again.
You might have problems with your render as well:
render: function () {
this.$el.html(this.content.render({
label: this.options.label,
items: this.itemsContainer
}));
this._label = this.$('.menu-label');
return this;
}
I suspect that items: this.itemsContainer is going to end stringifying this.itemsContainer, you might have better luck with something like this:
this.$el.html(this.content.render({ label: this.options.label });
this.$el.find('some selector').append(this.itemsContainer);
where 'some selector' would, of course, depend on the HTML structure; you'll have to adjust the template for this as well.
Your Github link is broken so I don't know what code you're adapting. I do know that your use of constructor is non-standard. Why not use the standard initialize?
constructor / initialize new View([options])
[...] If the view defines an initialize function, it will be called when the view is first created.
You should probably do it this way:
app.ui.SelectMenu = Backbone.View.extend({
// No 'constructor' in here or anywhere...
initialize: function (options) {
this.items = [];
this.content = JST['common-select_button'];
this.itemsContainer = $('.dropdown-menu', $(this.content.render()));
// Add any items that we may have added to the object params
if (options.items) {
this.addItems(options.items);
}
},
//...
});

Categories

Resources