BackboneJS: View renders fine, but refreshes with undefined collection - backbone.js

I am messing around with Backbone some weeks now and made some simple applications based on tutorials. Now I started from scratch again and tried to use the nice features Backbone offers as I am supposed to.
My view gets in the way though. When the page loads, it renders fine and creates its nested views by iterating the collection.
When I call render() again to refresh the whole list of just a single entry, all of the views attributes seem to be undefined.
The model of a single entry:
Entry = Backbone.Model.extend({
});
A list of entries: (json.html is placeholder for dataside)
EntryCollection = Backbone.Collection.extend({
model: Entry,
url: 'json.html'
});
var entries = new EntryCollection();
View for a single entry, which fills the Underscore template and should re-render itself, when the model changes.
EntryView = Backbone.View.extend({
template: _.template($('#entry-template').html()),
initialize: function(){
this.model.on('change', this.render);
},
render: function(){
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
View for the whole list of entries which renders a EntryView for each item in the collection and should re-render itself, if a new item is added. The button is there for testing purposes.
EntryListView = Backbone.View.extend({
tagName: 'div',
collection: entries,
events: {
'click button': 'addEntry'
},
initialize: function(){
this.collection.on('add',this.render);
},
render: function(){
this.$el.append('<button>New</button>'); //to test what happens when a new item is added
var els = [];
this.collection.each(function(item){
els.push(new EntryView({model:item}).render().el);
});
this.$el.append(els);
$('#entries').html(this.el);
return this;
},
addEntry: function(){
entries.add(new Entry({
title: "New entry",
text: "This entry was inserted after the view was rendered"
}));
}
});
Now, if I fetch the collection from the server, the views render fine:
entries.fetch({
success: function(model,response){
new EntryListView().render();
}
});
As soon as I click the button to add an item to the collection, the event handler on EntryListView catches the 'add' event and calls render(). But if I set a breakpoint in the render function, I can see that all attributes seem to be "undefined". There's no el, there's no collection...
Where am I going wrong?
Thanks for your assistance,
Robert

As is, EntryListView.render is not bound to a specific context, which means that the scope (this) is set by the caller : when you click on your button, this is set to your collection.
You have multiple options to solve your problem:
specify the correct context as third argument when applying on
initialize: function(){
this.collection.on('add', this.render, this);
},
bind your render function to your view with _.bindAll:
initialize: function(){
_.bindAll(this, 'render');
this.collection.on('add', this.render);
},
use listenTo to give your function the correct context when called
initialize: function(){
this.listenTo(this.collection, 'add', this.render);
},
You usually would do 2 or/and 3, _.bindAll giving you a guaranteed context, listenTo having added benefits when you destroy your views
initialize: function(){
_.bindAll(this, 'render');
this.listenTo(this.collection, 'add', this.render);
},
And if I may:
don't create your main view in a fetch callback, keep it referenced somewhere so you can manipulate it at a later time
don't declare collections/models on the prototype of your views, pass them as arguments
don't hardwire your DOM elements in your views, pass them as arguments
Something like
var EntryListView = Backbone.View.extend({
events: {
'click button': 'addEntry'
},
initialize: function(){
_.bindAll(this, 'render');
this.listenTo(this.collection, 'reset', this.render);
this.listenTo(this.collection, 'add', this.render);
},
render: function(){
var els = [];
this.collection.each(function(item){
els.push(new EntryView({model:item}).render().el);
});
this.$el.empty();
this.$el.append(els);
this.$el.append('<button>New</button>');
return this;
},
addEntry: function(){
entries.add(new Entry({
title: "New entry",
text: "This entry was inserted after the view was rendered"
}));
}
});
var view = new EntryListView({
collection: entries,
el: '#entries'
});
view.render();
entries.fetch({reset: true});
And a demo http://jsbin.com/opodib/1/edit

Related

A proper example of backbone views: Change attributes, CRUD, without Zombie Views

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

Marionette ItemView how to re-render model on change

I'm using Handlebars template engine.
so, I have Model:
Backbone.Model.extend({
urlRoot: Config.urls.getClient,
defaults: {
contract:"",
contractDate:"",
companyTitle:"",
contacts:[],
tariff: new Tariff(),
tariffs: [],
remain:0,
licenses:0,
edo:""
},
initialize:function(){
this.fetch();
}
});
then Marionette ItemView:
Marionette.ItemView.extend({
template : templates.client,
initialize: function () {
this.model.on('change', this.render, this);
},
onRender: function () {
console.log(this.model.toJSON());
}
});
and then I call everything as:
new View({
model : new Model({id:id})
})
and, it's immediately render a view for me and this is cool.
But after the model fetched data it's trigger "change", so I see in console serialised model twice, and I see for first time empty model and then filled one.
But, the view is NOT updated.
How I can fix it?
P.S. I understand, that I can call a render method on fetch done callback. But I also need it for further actions, when user will change model.
In the View, You can use following code
modelEvents: {
'change': 'render'
}
instead of
initialize: function () {
this.model.on('change', this.render, this);
},
onRender: function () {
console.log(this.model.toJSON());
}
Actually, Backbone and Marionette are smart enough to do it.
Problem was in template and data as I found it another question. So, I re-checked everything and got result.

How to remove an item from a collection and collection view by not re-rendering the whole list? Is it possible?

Hi I'm a bit new to backbone.js
Is there a way to remove an item (and just that item) from a collection and does not have to refresh the list. Instead only remove that item from the view and collection and no refresh/re-rendering happens the view collection. Or is it even possible?
Here's a snippet of my code:
var DateModel = Backbone.Model.extend({
defaults: function() {
return {
index: '',
valueFrom: '',
valueTo: '',
status: '',
showText: '',
text: ''
}
},
});
var DateList = Backbone.Collection.extend({
model: DateModel
});
var dateList = new DateList();
var DateView = Backbone.View.extend({
model: new DateModel(),
...
});
var DateListView = Backbone.View.extend({
model: dateList,
el: $('#date-list-container'),
initialize: function() {
this.listenTo(this.model, 'add', this.render);
this.listenTo(this.model, 'remove', this.render);
this.listenTo(this.model, 'reset', this.render);
},
render: function() {
// code for rendering here...
_.each(this.model.models, function(date){...});
}
});
You can likely do this by changing your event listener from this.render to a custom listener function that receives the model that was removed as an argument and, instead of re-rendering the entire view, simply removes the HTML element representing that model from the view. That way, you don't always have to re-render the entire view, which can be costly, but only alter the part that has changed.
For example, this line:
this.listenTo(this.model, 'remove', this.render);
would be replaced with a different event handler function that received the arguments for the remove event:
remove (model, collection, options) — when a model is removed from a collection.
So you might have an event listener function such as this:
modelRemoved: function(model, collection, options) {
// code to find the HTML element for 'model' and remove it from the DOM
}
And then use it as the event handler for remove events:
this.listenTo(this.model, 'remove', this.modelRemoved);

How do you create Backbone views with an 'el' that was dynamically created?

I have two views -- an OverlayView and a StoryView. The StoryView needs to get appended to the OverlayView (both are created dynamically). I create the #overlay div dynamically in the OverlayView, but when I set the 'el' of the StoryView to #overlay, this.$el of StoryView is an empty array. The #overlay div definitely exists in the DOM by the time the StoryView is created.
How do I get the StoryView to recognize the dyanmically created #overlay as its 'el'? Am I correct in assuming the 'el' should be the 'parent' container to which the view is appended? Should the 'el' of the OverlayView actually be '#overlay'?
OverlayView:
OverlayView = Backbone.View.extend({
el: $('body'),
events: {
'click #film': 'hideOverlay'
},
initialize: function() {
this.render();
},
render: function() {
this.$el.append('<div id="overlay"></div>');
return this;
}
});
StoryView:
var StoryView = Backbone.View.extend({
el: $('#overlay'),
events: {
'click .close': 'closeStory'
},
initialize: function() {
this.render();
},
render: function() {
console.log(this.$el); // Returns empty array []
return this;
}
});
Your problem is that your StoryView el should simply be a selector — not a jQuery object. That will cause Backbone to try to retrieve the object (which does not yet exist) at the time you specify el. Just change el: $('#overlay') to el: '#overlay'. You might also do the same for $('body') in your first view, since it's not necessary. Just always use selectors instead of jQuery objects and you'll be fine.
Working example here.

Backbone.Collection.Create not triggering "add" in view

Hopefully this is an easy question. I'm trying to learn backbone and i'm stuck on a really simple thing. the render on the view never gets called when I update the collection by using the create method. I thought this should happen without explicitly calling render. I'm not loading anything dynamic, it's all in the dom before this script fires. The click event works just fine and I can add new models to the collection, but the render in the view never fires.
$(function(){
window.QuizMe = {};
// create a model for our quizzes
QuizMe.Quiz = Backbone.Model.extend({
// override post for now
"sync": function (){return true},
});
QuizMe._QuizCollection = Backbone.Collection.extend({
model: QuizMe.Quiz,
});
QuizMe.QuizCollection = new QuizMe._QuizCollection
QuizMe.QuizView = Backbone.View.extend({
el:$('#QuizMeApp'),
template: _.template($('#quizList').html()),
events: {
"click #addQuiz" : "addQuizDialog",
},
initialize: function() {
// is this right?
_.bindAll(this,"render","addQuizDialog")
this.model.bind('add', this.render, this);
},
addQuizDialog: function(event){
console.log('addQuizDialog called')
QuizMe.QuizCollection.create({display:"this is a display2",description:"this is a succinct description"});
},
render: function() {
console.log("render called")
},
});
QuizMe.App = new QuizMe.QuizView({model:QuizMe.Quiz})
});
Your problem is that you're binding to the model:
this.model.bind('add', this.render, this);
but you're adding to a collection:
QuizMe.QuizCollection.create({
display: "this is a display2",
description: "this is a succinct description"
});
A view will usually have an associated collection or model but not both. If you want your QuizView to list the known quizzes then:
You should probably call it QuizListView or something similar.
Create a new QuizView that displays a single quiz; this view would have a model.
Rework your QuizListView to work with a collection.
You should end up with something like this:
QuizMe.QuizListView = Backbone.View.extend({
// ...
initialize: function() {
// You don't need to bind event handlers anymore, newer
// Backbones use the right context by themselves.
_.bindAll(this, 'render');
this.collection.bind('add', this.render);
},
addQuizDialog: function(event) {
this.collection.create({
display: "this is a display2",
description: "this is a succinct description"
});
},
render: function() {
console.log("render called")
// And some stuff in here to add QuizView instances to this.$el
return this; // Your render() should always do this.
}
});
QuizMe.App = new QuizMe.QuizView({ collection: QuizMe.QuizCollection });
And watch that trailing comma after render, older IEs get upset about that and cause difficult to trace bugs.
I'd give you a quick demo but http://jsfiddle.net/ is down at the moment. When it comes back, you can start with http://jsfiddle.net/ambiguous/RRXnK/ to play around, that fiddle has all the appropriate Backbone stuff (jQuery, Backbone, and Underscore) already set up.

Resources