Backbone.js: Can't add the same model to a set twice - javascript

I have just started with backbone.js. And I'm having a problem in fetching the data from the server. Here's the response I'm getting from server.
[{
"list_name":"list1",
"list_id":"4",
"created":"2011-07-07 21:21:16",
"user_id":"123456"
},
{
"list_name":"list2",
"list_id":"3",
"created":"2011-07-07 21:19:51",
"user_key":"678901"
}]
Here's my javascript code...
// Router
App.Routers.AppRouter = Backbone.Router.extend({
routes: {
'': 'index'
},
initialize: function() {
},
index: function() {
var listCollection = new App.Collections.ListCollection();
listCollection.fetch({
success: function() {
new App.Views.ListItemView({collection: listCollection});
},
error: function() {
alert("controller: error loading lists");
}
});
}
});
// Models
var List = Backbone.Model.extend({
defaults: {
name: '',
id: ''
}
});
App.Collections.ListStore = Backbone.Collection.extend({
model: List,
url: '/lists'
});
// Initiate Application
var App = {
Collections: {},
Routers: {},
Views: {},
init: function() {
var objAppRouter = new App.Routers.AppRouter();
Backbone.history.start();
}
};
I get the error "Can't add the same model to a set twice" on this line in Backbone.js
if (already) throw new Error(["Can't add the same model to a set twice", already.id]);
I checked out the Backbone.js annotated and found out that the first model gets added to the collection but the second one gives this error. Why is this happening? Should I change something in the server side response?

Your List has id in its defaults property, which is making each instance have the same ID by default, and Backbone is using that to detect dupes. If your data uses list_id as the ID, you need to tell that to Backbone by putting idAttribute: 'list_id' inside your List class definition.
As an aside, I prefer to NOT duplicate type information in object attributes (and Backbone.js agrees on this point). Having consistent attribute names is what backbone expects and is easier to work with. So instead of having list_id and list_name, just use id, and name on all classes.

Use this fix to add models with same id.
When adding, use: collection.add(model,{unique: false})
var __hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
Backbone.Collection = (function(_super) {
__extends(Collection, _super);
function Collection() {
return Collection.__super__.constructor.apply(this, arguments);
}
Collection.prototype.add = function(models, options) {
var i, args, length, model, existing;
var at = options && options.at;
models = _.isArray(models) ? models.slice() : [models];
// Begin by turning bare objects into model references, and preventing
// invalid models from being added.
for (i = 0, length = models.length; i < length; i++) {
if (models[i] = this._prepareModel(models[i], options)) continue;
throw new Error("Can't add an invalid model to a collection");
}
for (i = models.length - 1; i >= 0; i--) {
model = models[i];
existing = model.id != null && this._byId[model.id];
// If a duplicate is found, splice it out and optionally merge it into
// the existing model.
if (options && options.unique) {
if (existing || this._byCid[model.cid]) {
if (options && options.merge && existing) {
existing.set(model, options);
}
models.splice(i, 1);
continue;
}
}
// Listen to added models' events, and index models for lookup by
// `id` and by `cid`.
model.on('all', this._onModelEvent, this);
this._byCid[model.cid] = model;
if (model.id != null) this._byId[model.id] = model;
}
// Update `length` and splice in new models.
this.length += models.length;
args = [at != null ? at : this.models.length, 0];
Array.prototype.push.apply(args, models);
Array.prototype.splice.apply(this.models, args);
// Sort the collection if appropriate.
if (this.comparator && at == null) this.sort({silent: true});
if (options && options.silent) return this;
// Trigger `add` events.
while (model = models.shift()) {
model.trigger('add', model, this, options);
}
return this;
};
return Collection;
})(Backbone.Collection);

Backbone prevent us to insert the same model into one collection...
You can see it in backbone.js line 676 to line 700
if you really want to insert the same models into collection,just remove the code there
if(existing = this.get(model)){//here
...
}

Related

Knockoutjs Function Expected Exception on updating model

After much research and trail and error, I haven't come up with a solution yet. Please help! The SearchCustomer method in the code has comments on the scenarios that work and don't work.
Situation
I use knockoutjs with the mapping plugin. I take a view model which contains a Workorder from the server and it contains some properties about it along with a Customer model underneath it and a Contact model underneath Customer.
On the workorder screen the user can search for a customer which pops up a modal search window. They select that customer and the customer's id and customer model comes back to the workorder. I update the workorder's customerID no problem, but when I try to update the customer data (including contact) I get the Function Expected error.
Code
function WorkorderViewModel(data) {
var self = this;
data = data || {};
mapping = {
'Workorder': {
create: function (options) {
return new Workorder(options.data, self);
}
}
}
ko.mapping.fromJS(data, mapping, self);
self.ViewCustomer = function () {
self.Workorder.Customer.View();
}
self.SearchCustomer = function () {
self.Workorder.Customer.Search(function (customerID, customer) {
self.Workorder.CustomerID(customerID); //Works
self.Workorder.Customer(customer) //Function Expected, I feel this should work! Please help!
self.Workorder.Customer = new Customer(customer, self.Workorder); //No Error doesn't update screen
self.Workorder.Customer.Contact.FirstName(customer.Contact.FirstName); //Works, updates screen, but I don't want to do this for every property.
self.Workorder.SaveAll(); //Works, reload page and customer data is correct. Not looking to reload webpage everytime though.
})
}
}
function Workorder(data, parent) {
var self = this;
data = data || {};
mapping = {
'Customer': {
create: function (options) {
return new Customer(options.data, self);
}
}
}
ko.mapping.fromJS(data, mapping, self);
}
function Customer(data, parent) {
var self = this;
data = data || {};
mapping = {
'Contact': {
create: function (options) {
return new Contact(options.data, self);
}
}
}
ko.mapping.fromJS(data, mapping, self);
}
function Contact(data, parent) {
var self = this;
data = data || {};
mapping = {};
ko.mapping.fromJS(data, mapping, self);
self.AddedOn = ko.observable(moment(data.AddedOn).year() == 1 ? '' : moment(data.AddedOn).format('MM/DD/YYYY'));
self.FullName = ko.computed(function () {
var fullName = '';
if (self.FirstName() != null && self.FirstName() != '') {
fullName = self.FirstName();
}
if (self.MiddleName() != null && self.MiddleName() != '') {
fullName += ' ' + self.MiddleName();
}
if (self.LastName() != null && self.LastName() != '') {
fullName += ' ' + self.LastName();
}
return fullName;
})
}
Thanks Everyone!
Since self.Workorder.Customer is originally populated using ko.mapping, when you want to repopulate it, you should use ko.mapping again, like:
ko.mapping.fromJS(customer, self.Workorder.Customer)
Try changing:
self.Workorder.Customer(customer);
to:
self.Workorder.Customer = customer;
My guess is that the Customer property of the Workorder is not an observable.

Nested Backbone Model doesn't have method 'get' until being moved from one Collection to another

The Setup
I am new to Backbone, and am using it with Backgrid to display a large amount of data. The data represents two lists of Ingredients: one with existing and one with updated values. There are no primary keys for this data in the DB so the goal is to be able to match the old and new Ingredients manually by name and then generate a DB update from the matched data. To do this I have three collections: ingredientsOld (database), ingredientsNew (update.xml), and ingredients. The ingredientsOld and ingredientsNew collections are just collections of the basic Ingredient model. The ingredients collection, however, is a collection of IngredientComp models which contain an integer status, an 'old' Ingredient, and a 'new' Ingredient.
var Ingredient = Backbone.Model.extend({});
var Ingredients = Backbone.Collection.extend({ model: Ingredient });
var IngredientComp = Backbone.Model.extend({
constructor: function(attributes, options) {
Backbone.Model.apply( this, arguments );
if (attributes.o instanceof Ingredient) {
this.o = attributes.o;
console.log("Adding existing ingredient: "+this.o.cid);
} else {
this.o = new Ingredient(attributes.o);
console.log("Adding new ingredient: "+this.o.get("name"));
}
if (attributes.n instanceof Ingredient) {
this.n = attributes.n;
} else {
this.n = new Ingredient(attributes.n);
}
}
});
var IngredientComps = Backbone.Collection.extend({
model: IngredientComp,
comparator: function(comp){
return -comp.get("status");
}
});
var ingredientsOld = new Ingredients();
var ingredientsNew = new Ingredients();
var ingredients = new IngredientComps();
The data is being generated by PHP and outputted to JSON like so:
ingredientsOld.add([
{"name":"Milk, whole, 3.25%","guid":"3BDA78C1-69C1-4582-83F8-5A9D00E58B45","item_id":16554,"age":"old","cals":"37","fat_cals":"18","protein":"2","carbs":"3","fiber":"0","sugar":"3","fat":"2","sat_fat":"1","trans_fat":"0","chol":"6","sod":"24","weight":"2.00","quantity":"1 each","parents":{"CC09EB05-4827-416E-995A-EBD62F0D0B4A":"Baileys Irish Cream Shake"}}, ...
ingredients.add([
{"status":3,"o":{"name":"Sliced Frozen Strawberries","guid":"A063D161-A876-4036-ADB0-C5C35BD9E5D5","item_id":16538,"age":"old","cals":"77","fat_cals":"0","protein":"1","carbs":"19","fiber":"1","sugar":"19","fat":"0","sat_fat":"0","trans_fat":"0","chol":"0","sod":"0","weight":"69.60","quantity":"1 each","parents":{"BC262BEE-CED5-4AB3-A207-D1A04E5BF5C7":"Lemonade"}},"n":{"name":"Frozen Strawberries","guid":"5090A352-74B4-42DB-8206-3FD7A7CF9D56","item_id":"","age":"new","cals":"77","fat_cals":"0","protein":"1","carbs":"19","fiber":"1","sugar":"19","fat":"0","sat_fat":"0","trans_fat":"0","chol":"0","sod":"0","weight":"","quantity":"69.60 Gram","parents":{"237D1B3D-7871-4C05-A788-38C0AAC04A71":"Malt, Strawberry"}}}, ...
When I display values from the IngredientComp model (from the render function of a custom Backgrid Cell), I initially have to output them like this:
render: function() {
col = this.column.get("name");
var v1 = this.model.get("o")[col];
var v2 = this.model.get("n")[col];
this.$el.html( v1 + "\n<br />\n<b>" + v2 + "</b>" );
return this;
}
The Problem
It is only after moving the IngredientComp models from one collection to another that the this.model.get("o").get(col); function works. Here is the function that moves the Ingredients from one collection to another:
function matchItems(oldId, newId) {
var oldItem = ingredientsOld.remove(oldId);
var newItem = ingredientsNew.remove(newId);
ingredients.add({'status': 1, 'o': oldItem, 'n': newItem});
}
I have updated the render function to try both methods of retrieving the value, but it is a bit slower and certainly not the proper way of handling the problem:
render: function() {
col = this.column.get("name");
// Investigate why the Ingredient model's get() method isn't available initially
var v1 = this.model.get("o")[col];
// The above line returns 'undefined' if the Ingredient model has moved from one
// collection to another, so we have to do this:
if (typeof v1 === "undefined"){ v1 = this.model.get("o").get(col)};
var v2 = this.model.get("n")[col];
if (typeof v2 === "undefined"){ v2 = this.model.get("n").get(col)};
this.$el.html( v1 + "\n<br />\n<b>" + v2 + "</b>" );
return this;
}
Can anyone shed some light on what might be causing this problem? I have done a bit of research on Backbone-relational.js, but it seems like a lot of overkill for what I am trying to accomplish.
I would first recommend using initialize instead of constructor, because the constructor function overrides and delays the creation of the model.
The main issue thought is that model.get('o') returns something different in this if statement. by doing this.o it is not setting the attribute on the model, but instead setting it on the model object. Therefore when the model is actually created model.get('o') is a regular object and not a backbone model.
if (attributes.o instanceof Ingredient) {
this.o = attributes.o;
console.log("Adding existing ingredient: "+this.o.cid);
} else {
this.o = new Ingredient(attributes.o);
console.log("Adding new ingredient: "+this.o.get("name"));
}
Changing the if statement to the following should solve the issue.
if (attributes.o instanceof Ingredient) {
this.o = attributes.o;
console.log("Adding existing ingredient: "+this.o.cid);
} else {
this.set('0', new Ingredient(attributes.o));
console.log("Adding new ingredient: "+this.o.get("name"));
}

Javascript prototypíng and ''this'' of instantiated object

here's a tricky one. I create a class...
App = function(o) {
var _app = this;
this.events = {
listeners : {
list : new Array(),
add : function(event, fn) {
if (! this.list[event]) this.list[event] = new Array();
if (!(fn in this.list[event]) && fn instanceof Function) this.list[event].push(fn);
if (_app.debug.get()) _app.events.dispatch('log.append','EVENTS:ADD:'+event);
},
remove : function(event, fn) {
if (! this.list[event]) return;
for (var i=0, l=this.list[event].length; i<l; i++) {
if (this.list[event][i] === fn) {
if (_app.debug.get()) _app.events.dispatch('log.append','EVENTS:REMOVE:'+event);
this.list[event].slice(i,1);
break;
}
}
}
},
dispatch : function(event, params) {
if (! this.listeners.list[event]) return;
for (var i=0, l=this.listeners.list[event].length; i<l; i++) {
if (_app.debug.get()) _app.events.dispatch('log.append','EVENTS:DISPATCH:'+event);
this.listeners.list[event][i].call(window, params);
}
}
};
};
and prototype more functionality later. Here's one;
App.prototype.connection = {
source : { 'default' : null },
types : new Array(),
pool : new Array(),
count : function() { return this.pool.length },
active : {
pool : new Array(),
count : function() { return this.pool.length },
add : function(o) { this.pool.push(o) },
remove : function(o) { this.pool.splice(this.pool.indexOf(o), 1); }
},
create : function(o) {
if (! o || ! o.exe) o.exe = this.source.default;
if (! o || ! o.type) o.type = 'xhr';
var c = new this.types[o.type];
App.events.dispatch('connection.created',c);
this.pool.push(c);
return c;
},
remove : function(o) {
App.events.dispatch('connection.removed',o);
this.pool.splice(this.pool.indexOf(o), 1);
},
abort : function(o) {
var i = this.pool.indexOf(o);
if (i===-1) return;
this.pool[i].abort();
}
};
then instantiate this into an object.
app = new App();
The problem is, I have a line called App.events.dispatch('connection.removed',o) which doesn't work. App needs to be the instantiation 'app' which ideally would be 'this', but this refers to App.prototype.connection. How do you get at the root in this case?
Thanks - Andrew
You cannot use the object literal approach to define the connection on the prototype, otherwise there's no way to access the App instance.
Note that when you are referencing App, you are referencing the constructor function, not the App instance. Also, this inside create for instance would not be working because this will point to the connection object, not the App instance either.
There are a few options:
function Connection(eventBus) {
this.eventBus = eventBus;
}
Connection.prototype = {
someFunction: function () {
this.eventBus.dispatch(...);
}
};
function App() {
// this.events = ...
//the instance could also be injected, but you would need to implement
//a setEventBus on the connection object, or simply do conn.eventBus = this;
this.connection = new Connection(this);
}
var App = new App();
Also, please note that all mutable values (e.g. objects) defined on the prototype will be shared across all instances. That's probably not what you want.
Also note that:
listeners : {
list : new Array()
Should be:
listeners : {
list : {}
An array is meant to have numeric indexes only, while a plain object is a better structure to use as a map.

Backbone collection validation

Mates
I have the following:
App.Collections.Bookings = Backbone.Collection.extend({
url: 'bookings/',
model: App.Models.Booking,
howManyArriving: function() {
var bg = _.countBy( this.models, function(model) {
return model.get('indate') == moment().format('YYYY-MM-DD') ? 'even' : 'odd';
});
var lv = _.filter( this.models, function(model){
return model.get('indate') == moment().format('YYYY-MM-DD');
});
var r = {
count: bg,
models: lv
}
return r;
},
availableBtwn: function(bed,indate,outdate) {
var gf = _.filter(this.models, function(model){
return (
model.get('outdate') > outdate &&
model.get('indate') <= indate &&
model.get('id_bed') == bed
);
});
return gf;
},
getBooking: function(bed, date){
var gf = _.filter(this.models, function(model){
return (
model.get('outdate') > date &&
model.get('indate') <= date &&
model.get('id_bed') == bed
);
});
return gf;
},
getFullName: function(id){
var b = this.get(id);
return b.get('nombre') + ' ' + b.get('apellido');
}
});
I need to check when I populate the collection and when I add a single model if there's already an existing model with determined propperties equal to the model/s that i'm attempting to create.
I've tried something like this:
App.Collections.Bookings.prototype.add = function(bookings) {
_.each( bookings, function(book){
var isDupe = this.any(function(_book) {
return _book.get('id') === book.id;
});
if (isDupe) {
//Up to you either return false or throw an exception or silently ignore
return false;
}else{
Backbone.Collection.prototype.add.call(this, book);
}
//console.log('Cargo el guest: ' + guest.get('id'));
}, this);
}
The thing is, it works, but when I populate the collection, it's not populated by App.Models.Booking, but with response's JSON.
Any idea?
Thanks a lot!
So, basically when you populate a collection, 3 flags are describing the behavior your method should have: add, remove, merge . We'll start by the default behavior of the set and add methods:
// Default options for `Collection#set`.
var setOptions = {add: true, remove: true, merge: true};
var addOptions = {add: true, merge: false, remove: false};
The add method in fact proxies the set method, as does the fetch method if you don't use the reset flag (which would cause to delete any model in your collection and create new ones each time you fetch them) which would call the reset method instead.
Now, how to use the flags. Well, it's the options specified in the doc. So basically, the default behavior for the add method is equivalent to this:
myCollection.add(myModels, {add: true, merge: false, remove: false});
Now, for the meaning of those flags:
- add: will add the news models (=the ones their id is not among the existing ones...) to the collection
- remove: will remove the old models (=the ones their id is not among the fetched models) of the collection
- merge: will update the attributes of the ones among the old and the fetched
What you should know about the merge flag: IT'S A REAL PAIN IN THE ASS. Really, I hate it. Why ? Because it uses an internal function that "prepares the models" :
if (!(model = this._prepareModel(models[i], options))) continue;
It means that it will create fake, volatile models. What's the big deal? Well, it means that it will execute the initialize function of those volatile models, possibly creating a chain reaction and unwanted behavior in your app.
So, what if you want this behavior but can't have volatile models created because it breaks your app? Well, you can set the merge flag to false and override the parse method to do it, something like:
parse: function(models) {
for(var i=0; i<models.length; i++) {
var model;
if(model = this.get(models[i].id)) {
model.set(models[i]);
}
}
return models;
}

Backbone.js: this.model is undefined

I've spent the past two weeks trying to learn Backbone.js and then also modularizing the app with Require.js. But seems there are something that I'm not getting in the whole process of initialization and fetching.
I have two routes, one shows an entire collection while the other shows just an individual model. And I want to be able to start the app with any of both routes.
If I start loading the collections url and later on a single model url, everything works as expected. If I start the app with the url route to a single model I got the error: TypeError: this.model is undefined this.$el.html(tmpl(this.model.toJSON()));on the view.
If I set defaults for the model, it renders the view but doesn't fetch it with the real data. I've tried also to handle the success event in the fetch function of the model without any luck.
router.js
define(['jquery','underscore','backbone','models/offer','collections/offers','views/header','views/event','views/offer/list',
], function($, _, Backbone, OfferModel, OffersCollection, HeaderView, EventView, OfferListView){
var AppRouter = Backbone.Router.extend({
routes: {
'event/:id' : 'showEvent',
'*path': 'showOffers'
},
initialize : function() {
this.offersCollection = new OffersCollection();
this.offersCollection.fetch();
var headerView = new HeaderView();
$('#header').html(headerView.render().el);
},
showEvent : function(id) {
if (this.offersCollection) {
this.offerModel = this.offersCollection.get(id);
} else {
this.offerModel = new OfferModel({id: id});
this.offerModel.fetch();
}
var eventView = new EventView({model: this.offerModel});
$('#main').html(eventView.render().el);
},
showOffers : function(path) {
if (path === 'betting' || path === 'score') {
var offerListView = new OfferListView({collection: this.offersCollection, mainTemplate: path});
$('#main').html(offerListView.render().el) ;
}
},
});
var initialize = function(){
window.router = new AppRouter;
Backbone.history.start();
};
return {
initialize: initialize
};
});
views/event.js
define(['jquery','underscore','backbone','text!templates/event/main.html',
], function($, _, Backbone, eventMainTemplate){
var EventView = Backbone.View.extend({
initalize : function(options) {
this.model = options.model;
this.model.on("change", this.render);
},
render : function() {
var tmpl = _.template(eventMainTemplate);
this.$el.html(tmpl(this.model.toJSON()));
return this;
}
});
return EventView;
});
You are creating and fetching the OffersCollection in initialize method of the router, so the else block in showEvent will never be hit since this.offersCollection is always truthy.
After the comments, I think you need to do this:
showEvent : function(id) {
var that = this;
var show = function(){
var eventView = new EventView({model: that.offerModel});
$('#main').html(eventView.render().el);
};
// offersCollection is always defined, so check if it has the model
if (this.offersCollection && this.offersCollection.get(id)) {
this.offerModel = this.offersCollection.get(id);
show();
} else {
this.offerModel = new OfferModel({id: id});
this.offerModel.fetch().done(function(){
// model is fetched, show now to avoid your render problems.
show();
});
// alternatively, set the defaults in the model,
// so you don't need to wait for the fetch to complete.
}
}

Categories