Backbone: Update not calling callback function - backbone.js

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);

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

Backbone.js - delete views

This is my second day trying to use backbone and im completely lost. I am following this tutorial - http://net.tutsplus.com/tutorials/javascript-ajax/build-a-contacts-manager-using-backbone-js-part-3/
What I have done is loaded a contacts list and rendered it to the screen, but if you look at my render1 function - this takes a form input and appends it to my template. The problem is that I can't delete these items after they are created - the others can be deleted. help please?
var ContactView = Backbone.View.extend({
tagName: "contacts",
className: "contact-container",
template: $("#contactTemplate").html(),
initialize: function(){
this.model.on('change', this.render, this);
this.model.on('add', this.render1, this);
this.model.on('destroy', this.remove, this);
},
events: {
'click .deleteUser': 'delete'
},
test: function () {
alert("here");
},
delete: function () {
this.model.destroy();
},
render: function () {
console.log(this);
var tmpl = _.template(this.template);
$(this.el).html(tmpl(this.model.toJSON()));
temp = tmpl(this.model.toJSON());
console.log(temp);
return this;
},
render1: function () {
console.log(this);
var tmpl = _.template(this.template);
temp = tmpl(this.model.toJSON());
temp='<contacts class="contact-container">'+temp+'</contacts>';
console.log(temp);
$("#contacts").append(temp);
$(this.el).html(tmpl(this.model.toJSON()));
return this;
}
});
var AddPerson = Backbone.View.extend({
el: $("#addPerson"),
// el: $("form/"),
events: {
'click': 'submit',
'submit': 'submit'
},
submit: function(e) {
// alert("here");
e.preventDefault();
this.collection = new Directory();
// var data = (contacts[0]);
var contact = new Contact(contacts[0]);
var contactView = new ContactView({model: contact});
this.collection.add(contact);
this.collection.add(contactView);
}
});
seasick, there are quite a few issues in this code.
var contact = new Contact(contacts[0]);
var contactView = new ContactView({model: contact});
this.collection.add(contact);
this.collection.add(contactView);
Contact is a Backbone.Model but ContactView is a Backbone.View. Yet, you are adding both to the this.collection (which I assume is a Backbone.Collection of Contact?). See the problem here? In Backbone, there is no such concept of a 'collection of views'. You just get one concept: views, that are tied to a model.
So, here, you create a Contact and you add it to the Collection. That is all! It takes care of the Model part. The rendering part needs to be handled with events and renders.
When you add a model to a collection (this.collection.add(contact)), the collection will trigger a 'add' event, that you can hook to with a .on to create a new ContactView and append it to the DOM somewhere.
So when you write this...
this.model.on('add', this.render1, this);
You are actually saying 'When the Contact model triggers an add event, run render1', which isn't what you want, what you probably want is a collection.on('add', ...). The model will never trigger an add event (well, you could make it trigger one, but it wouldn't be an expected behavior!), the add/remove events are at the collection level.
In other words, you are missing some binding on the collection in your AddPerson view to 'react' to adding a new Contact to the collection. The code of the function bound to the add event should probably look a bit like:
onAdd: function(newContact){
var newContactView = new ContactView({model: newContact});
$("#contacts").append(newContactView.render().el);
}
There are other issues in your code, but I guess an outline of the steps to take would be like:
Remove the binding to add in ContactView: ContactView is only concerned with one contact, not how to manage multiple contacts. This is probably why you are having issues with only some (the first one?) of the contacts 'working'
Move that logic to the AddContact view which seems to be more concerned with the Collection of contacts. Use the collection 'add' event to create new ContactView and append them to the DOM
Hope this helps!

How to track the number of models in a collection and stay in sync with the server changes Backbone.js using AMD

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.

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.

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