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.
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
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
Backbone.js newbie here.
General question: What is the best practice to track the number of models in a collection in order to display it on the UI? My use cases can involve changes on the server side so each time the collection is sync'd I need to be able to update the UI to the correct number from storage.
I'm using Backbone.js v1.0.0 and Underscore v1.4.4 from the amdjs project and Require.js v2.1.6.
Specific example: Simple shopping cart showing "number of items in the cart" that continually updates while the user is adding/removing items. In this example I'm almost there but (1) my code is always one below the real number of models and (2) I feel that there is a much better way to do this!
Here's my newbie code.
First, I have a collection of items that the user can add to their cart with a button. (NOTE: all AMD defines and returns are removed in code examples for brevity.)
var PackagesView = Backbone.View.extend({
el: $("#page"),
events: {
"click .addToCart": "addToCart"
},
initialize: function(id) {
this.collection = new PackagesCollection([],{id: id.id});
this.collection.fetch({
reset: true
});
this.collection.on("reset", this.render, this);
},
render: function(){
//other rendering stuff here
..............
//loop through models in collection and render each one
_.each(this.collection.models, function(item){
that.renderPackages(item);
});
}
renderPackages: function(item){
var packageView = new PackageView({
model: item
});
this.$el.append(packageView.render().el);
},
Next I have the view for each individual item in the cart PackageView which is called by the PackagesView code above. I have a "add to cart" button for each Package that has a "click" event tied to it.
var PackageView = Backbone.View.extend({
tagName:"div",
template:$(packageTemplate).html(),
events: {
"click .addToCart": "addToCart"
},
render:function () {
var tmpl = _.template(this.template);
this.$el.html(tmpl(this.model.toJSON()));
return this;
},
addToCart:function(){
cartView = new CartView();
cartView.collection.create(new CartItemModel(this.model));
}
Finally, I have a CartView that has a collection of all the items in the cart. I tried adding a listenTo method to react to changes to the collection, but it didn't stay in sync with the server either.
var CartView = Backbone.View.extend({
el: $("#page"),
initialize:function(){
this.collection = new CartCollection();
this.collection.fetch({
reset: true
});
this.listenTo(this.collection, 'add', this.updateCartBanner);
this.collection.on("reset", this.render, this);
},
render: function(){
$('#cartCount').html(this.collection.length);
},
updateCartBanner: function(){
//things did not work here. Just putting this here to show something I tried.
}
End result of specific example: The .create works correctly, PUT request sent, server adds the data to the database, "reset" event is called. However, the render() function in CartView does not show the right # of models in the collection. The first time I click a "add to cart" button the $('#cartCount') element does not get populated. Then anytime after that it does get populated but I'm minus 1 from the actual count on the server. I believe this is because I have a .create and a .fetch and the .fetch is happening before the .create finishes so I'm always 1 behind the server.
End result, I'm not structuring this the right way. Any hints in the right direction would be helpful!
You can try like this:
collection.on("add remove reset sync", renderCallback)
where renderCallback is function which refresh your UI.
Found an answer to my question, but could definitely be a better method.
If I change my code so instead of a separate view for each model in the collection as I have above, I have one view that iterates over all the models and draws then it will work. I still need to call a .create followed by a .fetch with some unexpected behavior, but the end result is correct.
Note that in this code I've completely done away with the previous PackageView and everything is drawn by PackagesView now.
var PackagesView = Backbone.View.extend({
el: $("#page"),
events: {
"click .addToCart": "addToCart"
},
initialize: function(id) {
this.collection = new PackagesCollection([],{id: id.id});
this.collection.fetch({
reset: true
});
this.collection.on("reset", this.render, this);
},
render: function(){
var that = this;
var tmpl = _.template($(packageTemplate).html());
//loop through models in collection and render each one
_.each(this.collection.models, function(item){
$(that.el).append(tmpl(item.toJSON()));
});
},
addToCart:function(e){
var id= $(e.currentTarget).data("id");
var item = this.collection.get(id);
var cartCollection = new CartCollection();
var cartItem = new CartItemModel();
cartCollection.create(new CartItemModel(item), {
wait: true,
success: function() {
console.log("in success create");
console.log(cartCollection.length);
},
error:function() {
console.log("in error create");
console.log(cartCollection.length);
}
});
cartCollection.fetch({
wait: true,
success: function() {
console.log("in success fetch");
console.log(cartCollection.length);
$('#cartCount').html(cartCollection.length);
},
error:function() {
console.log("in error fetch");
console.log(cartCollection.length);
}
});
Result: The $('#cartCount') in the .fetch callback injects the correction number of models. Unexpectedly, along with the correct .html() value the Chrome console.log return is (server side had zero models in the database to start with):
in error create PackagesView.js:88
0 PackagesView.js:89
in success fetch PackagesView.js:97
1
And I'm getting a 200 response from the create, so it should be "success" for both callbacks. I would have thought that the Backbone callback syntax for create and fetch were the same. Oh well, it seems to work.
Any feedback on this method is appreciated! Probably a better way to do this.
Incidentally this goes against the general advice here, although I do have a "very simple list" so perhaps its OK in the long run.
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.
I'm trying to add the functionality to my app so I can update my database and then update the DOM. The database gets updated fine, but the DOM doesn't. Here is part of my view:
App.Views.Table = Backbone.View.extend({
tagName: 'span',
initialize: function(options) {
this.model.on('change', this.render, this);
this.model.on('update', this.remove, this);
this.template = this.options.template;
this.url = this.options.url;
},
events: {
'click .verify': 'verify',
'click .spam': 'spam',
'click .duplicate': 'duplicate'
},
verify: function(e) {
id = e.currentTarget.id;
table = new App.Models.Table({ id: id });
table.urlRoot = this.url;
table.fetch();
table.toJSON();
table.set('verified', 1);
table.save();
},
spam: function(e) {
...
},
duplicate: function(e) {
...
},
remove: function() {
this.$el.remove();
console.log('hello');
},
retrieveTemplate: function(model) {
return _.template($('#' + this.template).html(), model);
},
render: function() {
//console.log(this);
this.$el.html(this.retrieveTemplate(this.model.toJSON()));
return this;
}
});
As I understand it, this.model.on('update', this.remove, this); should call my remove function when save completes. But the callback isn't firing because I'm not getting the console.log and my DOM isn't being updated. What am I doing wrong? I followed a tutorial, but everything works fine in the tutorial.
There is no update event. I think you mean sync
http://backbonejs.org/#Events-catalog
"sync" (model, resp, options) — when a model has been successfully synced with the server.
Also a helpful hit for debugging that I found was using the all event to see what events are getting triggered.
Edit:
After some debugging, the goal of the verify() function was to save the verified attribute to the model. To do that we needed to change verify() to
this.model.set('verified', 1);
this.model.save();
instead of creating a new App.Model.Table and setting it to the table variable. Doing table .save() was saving the new table model, instead of the old model, this.model. That's why the event handlers attached to this.model were net getting triggered.
There are no "create" or "update" events in Backbone.js. That's why your remove() callback isn't firing.
See http://backbonejs.org/#Events-catalog for a catalog of the possible events in Backbone.
UPDATE:
After looking at your code more closely, the answer is clear. They are different models:
initialize: function(options) {
this.model.on('change', this.render, this);
this.model.on('sync', this.remove, this);
and
table = new App.Models.Table({ id: id });
...
table.save();
The events that occur on your table object are not going to trigger event handlers that are bound to a totally different model (this.model).
Why create another model (table) when you already had a model? (this.model) ?
* UPDATE *
I don't really understand what you're trying to do, but perhaps try this:
table = new App.Models.Table({ id: id });
table.on('sync', this.remove, this);