Backbone Views inside Collections - javascript

I am quite new to backbone and need use it to create a list item. Each list item has a model, and a view. Because its a list it seems like an ideal solution for collections, but I'm struggling to use them.
Here is the current version, which I would like to chaneg to use collections:
// The Model & view
var IntroModel = Backbone.Model.extend({});
var Introview = Backbone.View.extend({
template: _.template( $('#taglist-intro').text() ),
render: function() {
console.log( this.model.attributes );
this.$el.append( this.template( this.model.attributes ) );
}
});
// We will store views in here
// Ideally this would be a collection
views = [];
// Get the data for that collection
$.getJSON( url, function( json ) {
_.each( json, function( item ) {
// Create each model & view, store views in the views array
var model = new IntroModel( item );
var view = new Introview({
model : model
})
views.push( view );
})
})
// I can render a view like this
// But I'd rather it rendered the view when I add items to the collection
views[0].render()
So what i have works, but its not really doing it 'the backbone way'. Which seem a little pointless because:
It would be better to use a collection, not an array
It would be better that views render when items are added to the array
Its not Backbone really is it..
Grateful for any pointers, if you cant provide specific code examples I'd still be very grateful to links & resources covering this issue.
Cheers,
Richard

Your right that the current implementation is not the Backbone way. Most of what you are doing is handled directly by the collection object in backbone. In backbone collections are essentially just an array with additional methods attached to them. These methods are what gives collections their power. Backbone has a number of features including:
'url' property: using this property the collection will automatically populate itself when you run the fetch method (e.g. myCollection.fetch() ).
You can bind a function to the 'reset' event of the collection. This event triggers when when you populate the collection. By including a call to your collection's render event your collection can automatically render the related view when the collection changes. There are also other collection events (e.g. 'add' new model, etc) which you can also attach functions to.
I find the Backbone documentation to be the best place to start. However a simple example is always useful. The following code shows how a simple collection can be defined, and how you would create two views (one view which creates a list, and another view which renders the item within the list). Note the use of the url property in the collection. Backbone uses this to retrieve the content of the collection when you run the fetch() method (See OrgListView object). Also note how the view's render method is bound to the collections 'reset' event, this ensures that the render event is called after populating the collection (See OrgsListView's initialize method).
/**
* Model
*/
var Org = Backbone.Model.extend();
/**
* Collection
*/
var Orgs = Backbone.Collection.extend({
model: Org,
url: '/orgs.json'
});
/**
* View - Single Item in List
*/
var OrgItemView = Backbone.View.extend({
tagName: 'li',
initialize: function() {
_.bindAll(this, 'onClick', 'render');
this.model = this.options.model;
// Create base URI component for links on this page. (e.g. '/#orgs/ORG_NAME')
this.baseUri = this.options.pageRootUri + '/' + encodeURIComponent(this.model.get('name'));
// Create array for tracking subviews.
/*var subViews = new Array();*/
},
events: {
'click a.test': 'onClick'
},
onClick: function(event) {
// Prevent default event from firing.
event.preventDefault();
if (typeof this.listContactsView === 'undefined') {
// Create collection of contacts.
var contacts = new ContactsByOrg({ url: '/orgs.json/' + encodeURIComponent(this.model.get('name')) });
this.listContactsView = new ListContactsView({ collection: contacts, baseUri: this.baseUri });
this.$el.append(this.listContactsView.render().el);
}
else {
// Close View.
this.listContactsView.close();
// Destroy property this.listContactsView.
delete this.listContactsView;
}
},
onClose: function() {
// console.log('Closing OrgItemView');
},
render: function() {
// TODO: set proper value for href. Currently using a dummy placeholder
this.$el.html('<a class="test" href="' + this.baseUri + '">' + this.model.get('name') + '</a>');
return this;
}
});
/**
* View - List of organizations
*/
var OrgsListView = Backbone.View.extend({
className: 'orgs-list',
initialize: function() {
console.log('OrgsListView');
_.bindAll(this, 'render');
this.pageRootUri = this.options.pageRootUri;
this.collection = this.options.collection;
// Bind render function to collection reset event.
this.collection.on('reset', this.render);
// Populate collection with values from server.
this.collection.fetch();
},
onClose: function() {
this.collection.off('reset', this.render);
// console.log('Closing OrgsListView');
},
render: function() {
var self = this;
this.$el.html('<ul></ul>');
this.collection.each(function(org, index) {
var orgItemView = new OrgItemView({ model: org, pageRootUri: self.pageRootUri });
self.$('ul').append(orgItemView.render().el);
});
return this;
}
});

Related

Trigger "reset" backbone event on collection's empty result set

I have a Backbone collection which I am fetching and rendering the following way:
var View = Backbone.View.extend({
initialize : function(options){
var self = this;
this.template = _.template(Index);
this.collection = new WineCollection();
this.collection.url = ApiConfig.winetards.getWineList;
this.collection.on("reset", function(){self.render()});
return this;
}
At somepoint the wineList collection will be empty. How can I trigger "reset" and then call render on when the result set is empty?
You have two options - write the logic in View or the Collection. If other parts of your application will care when the collection is empty, do it in the Collection. Otherwise it probably belongs in the View.
Unless you edit the Collection's models Array directly, the Collection can only become empty after a reset or remove event. You can listen to these events and check for an empty Collection.
In the view
var View = Backbone.View.extend({
initialize : function(options){
this.listenTo(this.collection, 'remove reset', this.renderIfEmpty, this);
},
renderIfEmpty: function() {
if(this.collection.isEmpty()) {
this.render();
}
}
}
In the collection
var WineList = Backbone.Collection.extend({
initialize: function() {
this.on('remove reset', this.checkEmpty);
// If you want to trigger an event when an empty collection is created:
this.checkEmpty();
},
checkEmpty: function() {
if(this.isEmpty()) {
this.trigger('emptied', this)
}
}
})

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 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.

backbone.js cannot render a view

Below I have the code for one of my modules. This is kind of spagetti-ish code, but all I want to accomplish is having a model, a collection, and render a view (using underscore templates) connecting the data from the collection to the views. I'm failing miserably. The problem I'm getting is that trying to run the last call down there to testfeed.render() tells me that render is not a function, yet it is clearly defined. I'm able to fetch that data and seemingly add it to the collection from the api. What am I doing wrong here?
// Create a new module.
var Tagfeed = app.module();
// Default model.
Tagfeed.Model = Backbone.Model.extend({
defaults : {
name : '',
image : ''
},
initialize : function(){
console.log('tagfeed model is initialized');
this.on("change", function(){
console.log("An attribute has been changed");
});
}
});
var feedCollection = Backbone.Collection.extend({
model: Tagfeed.Model,
initialize : function () {
console.log('feedcollection is initialized');
},
fetch: function () {
var thisCollection = this;
Api_get('/api/test', function(data){
$.each(data.data, function(){
thisCollection.add(this);
});
return thisCollection;
})
}
});
var test = new Tagfeed.Model({name:'test'});
var newFeedCollection = new feedCollection();
newFeedCollection.fetch();
console.log(newFeedCollection.at(0));
var testfeed = Backbone.View.extend({
el: $('#main'),
collection : newFeedCollection,
render: function( event ){
var compiled_template = _.template( $("#tag-template").html() );
this.$el.html( compiled_template(this.model.toJSON()) );
return this; //recommended as this enables calls to be chained.
}
});
testfeed.render();
EDIT * updated code from #mu is short suggestions
// Create a new module.
var Tagfeed = app.module();
// Default model.
var tagModel = Backbone.Model.extend({
defaults : {
name : '',
image : '',
pins : 0,
repins : 0,
impressions : 0
},
initialize : function(){
console.log('tagfeed model is initialized');
this.on("change", function(){
console.log("An attribute has been changed");
});
}
});
var feedCollection = Backbone.Collection.extend({
model: tagModel,
initialize : function () {
console.log('feedcollection is initialized');
},
fetch: function () {
var thisCollection = this;
Api_get('/reporting/adlift/pin_details', function(data){
thisCollection.add(data.data);
return data.data;
})
}
});
var test = new tagModel({name:'test'});
var newFeedCollection = new feedCollection();
newFeedCollection.fetch();
console.log(newFeedCollection.at(0));
var TestFeed = Backbone.View.extend({
el: $('#main'),
render: function( event ){
console.log('here');
var compiled_template = _.template( $("#tag-template").html(), this.collection.toJSON());
this.el.html( compiled_template );
return this; //recommended as this enables calls to be chained.
},
initialize: function() {
console.log('initialize view');
this.collection.on('reset', this.render, this);
}
});
//Tagfeed.testfeed.prototype.render();
var testfeed = new TestFeed({ collection: newFeedCollection });
testfeed.render();
and now when i run testfeed.render() I don't see any error, nor do i see that console.log in the render function. thoughts?
Your problem is right here:
var testfeed = Backbone.View.extend({ /*...*/ });
testfeed.render();
That makes your testfeed a view "class", you have to create a new instance with new before you can render it:
var TestFeed = Backbone.View.extend({ /*...*/ });
var testfeed = new TestFeed();
testfeed.render();
You're also doing this inside the "class":
collection : newFeedCollection
That will attach newFeedCollection to each instance of that view and that might cause some surprising behavior. The usual way of getting a collection into a view is pass it to the constructor:
var TestFeed = Backbone.View.extend({ /* As usual but not collection in here... */ });
var testfeed = new TestFeed({ collection: newFeedCollection });
testfeed.render();
The view constructor will automatically set the view's this.collection to the collection you pass when building the view.
Another thing to consider is that this:
newFeedCollection.fetch();
is usually an AJAX call so you might not have anything in your collection when you try to render it. I would do two things to deal with this:
Your view's render should be able to deal with an empty collection. This mostly depends on your template being smart enough to be sensible when the collection is empty.
Bind render to the collection's "reset" event in the view's initialize:
initialize: function() {
this.collection.on('reset', this.render, this);
}
Another problem you'll have is that your view's render is trying to render this.model:
this.$el.html( compiled_template(this.model.toJSON()) );
when your view is based on a collection; you want to change that to:
this.$el.html(compiled_template({ tags: this.collection.toJSON() }));
You'll need the tags in there so that the template has a name to refer to when looking at the collection data.
Also, you should be able to replace this:
$.each(data.data, function(){
thisCollection.add(this);
});
with just this:
thisCollection.add(data.data);
There's no need to add them one by one, Collection#add is perfectly happy with an array of models.
And here's a demo with (hopefully) everything sorted out:
http://jsfiddle.net/ambiguous/WXddy/
I had to fake the fetch internals but everything else should be there.
testfeed is not an instance - it's a constructor function.
var instance = new testfeed();
instance.render();
would probably work (what with you defining el during View definition - making it a prototype property, IIRC).

Swap a view's model?

Basically I'm trying to figure out the best way to swap a model and react to that event.
class View extends Backbone.View
initialize: ()->
#do stuff
swapModel: (newModel)->
#model = newModel
view = new View({model:firstModel})
view.swapModel(newModel)
Is this all I have to do to swap out a view's model? Are there any other side effects I should plan for? What would be the best way to respond to this swap? Should I trigger a swap event in swapModel?
Thanks!
Don't swap models in a view. You'll run in to all kinds of problems related to DOM event, Model events in the view, etc. I've tried to do this a dozen times or more, and in every single case, I re-wrote my code so that I would create a new view instance for each model. The code was cleaner, simpler, easier to understand and easier to maintain and work with.
A very simple example of one way to do it. Why are you trying to swap models though?
MyView = Backbone.View.extend({
initialize: function() {
this.myTrigger = {};
_.extend(this.myTrigger, Backbone.Events);
this.myTrigger.on("modelChange", function(msg) {
alert("Triggered " + msg);
},this);
},
swapModel: function(model) {
// do something with model
// then trigger listeners
this.myTrigger.trigger("modelChange", "a model change event");
}
});
var myview = new MyView()
myview.swapModel()
You could use a collection that only allows one model. This way you don't touch the model and can call render as many times as you want. Something like this:
var SpecialCollection = Backbone.Collection.extend({
swap: function (model) {
//remove all models
this.reset();
//add one model
this.add(model);
}
});
var MyView = Backbone.View.extend({
initialize: function(){
this.listenTo(this.collection, 'add', this.render);
},
render: function() {
this.model = this.collection.first()
//do your normal rendering here
}
});
var c = new SpecialCollection();
var v = new MyView({collection: c});
c.swap({name: 'Sam'});
//view should render
c.swap({name: 'Dave'});
//view should render
You could lock down the Collection rules a bit further but I think it serves as a good example to get you going.

Categories

Resources