Today I updated backbone.js (0.9.2 => 0.9.9) and backbone-relational (0.5.0 => 0.7.0), and a piece of code stopped working and I can't figure our why. (I even tried to upgrade step by step (e.g. backbone-relational 0.5.0 => 0.6.0), but that didn't helped.)
My models (a book that contains several chapters):
window.Book = Backbone.RelationalModel.extend({
relations:[{
type: Backbone.HasMany,
key:"chapters",
relatedModel: "Chapter",
collectionType:"ChaptersCollection",
reverseRelation:{
key:"book"
}
}]
});
window.BookCollection = Backbone.Collection.extend({
model: Book,
url: "/books"
});
window.Chapter = Backbone.RelationalModel.extend();
window.ChaptersCollection = Backbone.Collection.extend({
model: Chapter,
url: "/chapters"
});
Now I want to add a chapter to an existing book, and do something during the "add" event. The problem is that chapter.get("book")is undefined, although the book is returned in the successcallback. But see yourself:
var book = ...;
book.get("chapters").on("add", function (chapter) {
console.log("add");
console.log(chapter.get("id"));
console.log(chapter.get("book_id"));
console.log(chapter.get("book"));
});
var chapter = book.get("chapters").create({title:"Foo"}, {
wait: true,
success:function () {
console.log("success")
console.log(chapter.get("id"));
console.log(chapter.get("book_id"));
console.log(chapter.get("book"));
}
});
Console output:
> add
> 83
> 3
> null <<<<<<< Expecting a book object here, why null?
> success
> 83
> 3
> Book{...}
Do you have any idea what I am doing wrong?
The issue seems to be that when the add event is fired for the child collection, the reverse relation hasn't yet been updated. I'm not sure what change in Backbone or Backbone.Relational between versions caused this change, but it's easy to fix.
Instead of listening to the add event on the related collection like you do here:
book.get("chapters").on("add", function (chapter) {
You should listen to the add:<key> event on the model itself as described in the Backbone.Relational documentation:
book.on("add:chapters", function (chapter) {
Check out this fiddle for a working example.
Related
I want to fire fetch method on Backbone Collection which would pass an Id parameter similar to what happens in Model.fetch(id)
E.g.
var someFoo= new Foo({id: '1234'});// Where Foo is a Backbone Model
someFoo.fetch();
My Backbone collection:-
var tasks = backbone.Collection.extend({
model: taskModel,
url: '/MyController/GetTasks',
initialize: function () {
return this;
}
});
In my View when I try to fetch data:-
var _dummyId = 10; //
// Tried approach 1 && It calls an api without any `id` parameter, so I get 500 (Internal Server Error).
this.collection.fetch(_dummyId);
// Tried approach 2 && which fires API call passing Id, but just after that
// I am getting error as below:- Uncaught TypeError: object is not a function
this.collection.fetch({
data: {
id: _dummyId
}
});
Found it very late : To cut short the above story I want something like Get /collection/id in backbone.
Thank you for your answers, finally I got the solution from Backbone.js collection options.
Apologies that I couldn't explain the question properly while for same requirement others have done brilliantly and smartly.
Solution : I can have something like :-
var Messages = Backbone.Collection.extend({
initialize: function(models, options) {
this.id = options.id;
},
url: function() {
return '/messages/' + this.id;
},
model: Message,
});
var collection = new Messages([], { id: 2 });
collection.fetch();
Thanks to nrabinowitz. Link to the Answer
As mentioned by Matt Ball, the question doesn't make sense: either you call fetch() on a Collection to retrieve all the Models from the Server, or you call fetch() on a Model with an ID to retrieve only this one.
Now, if for some reason you'd need to pass extra parameters to a Collection.fetch() (such as paging information), you could always add a 'data' key in your options object, and it may happen that one of this key be an id (+add option to add this fetched model rather than replace the collection with just one model)... but that would be a very round-about way of fetching a model. The expected way is to create a new Model with the id and fetch it:
this.collection = new taskCollection();
newTask = this.collection.add({id: 15002});
newTask.fetch();
In your code however, I don't see where the ID is coming from, so I am wondering what did you expect to be in the 'ID' parameter that you wanted the collection.fetch() to send?
I'm using backbone with tastypie adapter and jquery for interacting with DOM. I found something in backbone that I can't understand. I tried to simplify the code for this question.
Here is my app.js:
$(document).ready(function() {
Point = Backbone.Model.extend({
defaults:{
lat:0,
lng:0
},
urlRoot: '/api/v1/point' ,
initialize: function(attributes){
var that = this;
$('#b1').bind('click',function() {
that.set('lat',that.get('lat')+1);
});
$('#b2').bind('click',function() {
that.save();
});
$('#b3').bind('click',function() {
console.log(that.get('lat'));
});
}
});
point = new Point ();
//****
$('#b1').click();
$('#b2').click();
$('#b3').click(); // >> 1
$('#b1').click();
$('#b2').click();
$('#b3').click(); // >> 2
//****
});
And my html:
<script src="/static/js/jquery.js"></script>
<script src="/static/js/backbone.js"></script>
<script src="/static/js/backbone-tastypie.js"></script>
<script src="/static/js/app.js"></script>
<button id="b1">b1</button>
<button id="b2">b2</button>
<button id="b3">b3</button>
As you see, I'm calling clicking on buttons in the code and the console is logging '1' and then '2' as expected.
The problem occurs when I am removing code between stars (//*) and pressing these buttons 'b1', 'b2', 'b3', 'b1', 'b2', 'b3' manually in browser and got '1' and '1' in console.
I wait for response from server for 1 second and check for it in console but the behavior of app is still the same: '1', '2' with a code with stars and '1', '1', with the button pressing.
Do you know why?
I wait for response from server for 1 second and check for it in console but [...]
Changing the model manually (click the button using your mouse) instead of programmatically (click()) is the crucial part here.
I assume you do not return the final model from your server, as Backbone update's your model with that data.
See section 53 and
if (options.wait) {
if (attrs && !this._validate(attrs, options)) return false;
current = _.clone(this.attributes);
}
section 55
options.success = function(resp, status, xhr) {
done = true;
var serverAttrs = model.parse(resp);
if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
if (!model.set(serverAttrs, options)) return false;
if (success) success(model, resp, options);
};
of the Backbone.js documentation. Especially these lines:
var serverAttrs = model.parse(resp); (Parse model data from response)
if (!model.set(serverAttrs, options)) return false; (Update your model's attributes)
When your first code (the one where you click() the buttons) runs, it increases your models attribute and loggs it before the server returns. Thats
Note: using the wait option or a setTimeout() in your 2nd press of button three in your first code would equal the behaviour.
But what you want, is to return a valid model from your server (after persisting it).
Off topic tip: you should not bind your model and your view in the models initializer or constructor as it would couple them too tight and they wouldn't be interchangable. (For this question, it is certainly acceptable. ;)
*UPDATE: See final answer code in the last code block below.*
Currently I am having an issue displaying a collection in a collection view. The collection is a property of an existing model like so (pseudo code)
ApplicationVersion { Id: 1, VersionName: "", ApplicationCategories[] }
So essentially ApplicationVersion has a property called ApplicationCategories that is a javascript array. Currently when I render the collection view associated with ApplicationCategories nothing is rendered. If I debug in Chrome's javascript debugger it appears that the categories have not been populated yet (so I assume ApplicationVersion has not been fetched yet). Here is my code as it stands currently
ApplicationCategory Model, Collection, and Views
ApplicationModule.ApplicationCategory = Backbone.Model.extend({
urlRoot:"/applicationcategories"
});
ApplicationModule.ApplicationCategories = Recruit.Collection.extend({
url:"/applicationcategories",
model:ApplicationModule.ApplicationCategory,
initialize: function(){
/*
* By default backbone does not bind the collection change event to the comparator
* for performance reasons. I am choosing to not preoptimize though and do the
* binding. This may need to change later if performance becomes an issue.
* See https://github.com/documentcloud/backbone/issues/689
*
* Note also this is only nescessary for the default sort. By using the
* SortableCollectionMixin in other sorting methods, we do the binding
* there as well.
*/
this.on("change", this.sort);
},
comparator: function(applicationCategory) {
return applicationCategory.get("order");
},
byName: function() {
return this.sortedBy(function(applicationCategory) {
return applicationCategory.get("name");
});
}
});
_.extend(ApplicationModule.ApplicationCategories.prototype, SortableCollectionMixin);
ApplicationModule.ApplicationCategoryView = Recruit.ItemView.extend({
template:"application/applicationcategory-view-template"
});
ApplicationModule.ApplicationCategoriesView = Recruit.CollectionView.extend({
itemView:ApplicationModule.ApplicationCategoryView
});
ApplicationCategory template
<section id="<%=name%>">
<%=order%>
</section>
ApplicationVersion Model, Collection, and Views
ApplicationModule.ApplicationVersion = Backbone.Model.extend({
urlRoot:"/applicationversions"
});
ApplicationModule.ApplicationVersions = Recruit.Collection.extend({
url:"/applicationversions",
model:ApplicationModule.ApplicationVersion
});
ApplicationModule.ApplicationVersionLayout = Recruit.Layout.extend({
template:"application/applicationversion-view-template",
regions: {
applicationVersionHeader: "#applicationVersionHeader",
applicationVersionCategories: "#applicationVersionCategories",
applicationVersionFooter: "#applicationVersionFooter"
}
});
ApplicationModule.ApplicationVersionController = {
showApplicationVersion: function (applicationVersionId) {
ApplicationModule.applicationVersion = new ApplicationModule.ApplicationVersion({id : applicationVersionId});
var applicationVersionLayout = new Recruit.ApplicationModule.ApplicationVersionLayout({
model:ApplicationModule.applicationVersion
});
ApplicationModule.applicationVersion.fetch({success: function(){
var applicationVersionCategories = new Recruit.ApplicationModule.ApplicationCategoriesView({
collection: ApplicationModule.applicationVersion.application_categories
});
applicationVersionLayout.applicationVersionCategories.show(applicationVersionCategories);
}});
// Fake server responds to the request
ApplicationModule.server.respond();
Recruit.layout.main.show(applicationVersionLayout);
}
};
Here is my ApplicationVersion template
<section id="applicationVersionOuterSection">
<header id="applicationVersionHeader">
Your Application Header <%= id %>
</header>
<section id="applicationVersionCategories">
</section>
<footer id="applicationVersionFooter">
Your footer
</footer>
One thing to note I am currently using Sinon to mock my server response, but I don't think this is causing the issues as it is responding with the information as I expect looking through the javascript debugger (and like I said it is displaying ApplicationVersion id correctly). I can provide this code as well if it helps
It is currently displaying the application version id (id in the template), so I know it is fetching the data correctly for normal properties, it just is not rendering my ApplicationCategories javascript array property.
So ultimately I am binding to the success of the fetch for ApplicationVersion, then setting up the view for the ApplicationCategories. Since this isn't working like I expect I am wondering if there is a better way to create this collection view?
Thanks for any help
UPDATE: Working code example that Derek Bailey lead me too.
ApplicationModule.ApplicationVersionController = {
showApplicationVersion: function (applicationVersionId) {
ApplicationModule.applicationVersion = new ApplicationModule.ApplicationVersion({id : applicationVersionId});
var applicationVersionLayout = new Recruit.ApplicationModule.ApplicationVersionLayout({
model:ApplicationModule.applicationVersion
});
ApplicationModule.applicationVersion.fetch();
// Fake server responds to the request
ApplicationModule.server.respond();
Recruit.layout.main.show(applicationVersionLayout);
var applicationVersionCategories = new Recruit.ApplicationModule.ApplicationCategoriesView({
collection: new Backbone.Collection(ApplicationModule.applicationVersion.get('application_categories'))
});
applicationVersionLayout.applicationVersionCategories.show(applicationVersionCategories);
}
};
Marionette's CollectionView requires a valid Backbone.Collection, not a simple array. You need to create a Backbone.Collection from your array when passing it to the view:
new MyView({
collection: new Backbone.Collection(MyModel.Something.ArrayOfThings)
});
Having some issues with pulling calendar events from Google Calendar using Backbone.
When I call collection.fetch() I am only getting a length of 1 returned, when there are 13 objects in the json.
I had a look at the parse:function(response) method that I am overriding in the Collection, and it is returning all 13 objects. I had a look at the add method in backbone.js, and the issue appears to occur on line 591:
models = _.isArray(models) ? models.slice() : [models];
When I wrap the line with console.log to check the status of the models variable:
console.log(models);
models = _.isArray(models) ? models.slice() : [models];
console.log(models);
I get the following result:
[Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object] backbone.js:590
[child,undefined × 12]
I'm at a loss to explain why it would be failing on add. I have checked each model by changing the parse:function(response) method in the collection to return each object, and it works fine.:
parse: function(response) {
return response.feed.entry[5];
}
I have successfully parsed Google Calendar feeds with Backbone.js before, so I fear I am missing something really simple.
If I console.log response.feed the following is returned:
This is the full class:
/**
* Backbone
* #class
*/
var Gigs = Gigs || {};
Gigs.Backbone = {}
Gigs.Backbone.Model = Backbone.Model.extend();
Gigs.Backbone.Collection = Backbone.Collection.extend({
model: Gigs.Backbone.Model,
url: 'http://www.google.com/calendar/feeds/email#email.com/public/full?alt=json-in-script&orderby=starttime&callback=?',
sync: function(method, model, options) {
options.dataType = "jsonp";
return Backbone.sync(method, model, options);
},
parse: function(response) {
return response.feed.entry;
}
});
Gigs.Backbone.Controller = Backbone.View.extend({
initialize: function() {
var self = this;
this.collection = new Gigs.Backbone.Collection();
this.collection.on('reset', this.addElements, this);
this.collection.fetch();
},
addElements: function() {
log(this.collection);
}
});
var backbone = new Gigs.Backbone.Controller();
Apparently, Google Calendar provides its entries with an id wrapped in an object 1:
"id":{
"$t":"http://www.google.com/calendar/feeds/..."
}
which Backbone seems to dislike. A lot.
One simple solution would be to overwrite the id in your parse method:
parse: function(response) {
var entries=[];
_.each(response.feed.entry, function(entry,ix) {
entry.id=entry.id.$t;
entries.push(entry);
});
return entries;
}
And a Fiddle http://jsfiddle.net/bqzkT/
1 Check https://developers.google.com/gdata/docs/json to see how Google converts its XML data to JSON.
Edit : the problem comes from the way the data is returned with a straight XML to JSON conversion (requested via the alt=json-in-script parameter) wrapping the attributes in objects. Changing this parameter to alt=jsonc yields a much simpler JSON representation. Compare a jsonc output to the json-in-script equivalent.
See https://developers.google.com/youtube/2.0/developers_guide_jsonc#Comparing_JSON_and_JSONC for more info
I have two models (User and Task) which are instances of Backbone.RelationalModel.
The relation about these two models is the following:
// Task model
var Task = Backbone.RelationalModel.extend({
relations: [
{
type: 'HasOne',
key: 'user',
relatedModel: User
}
],
urlRoot: 'someUrl'
});
Then I have one collection which code looks like this:
var FollowerCollection = Backbone.Collection.extend({
initialize: function () {
_.bindAll(this);
}
model: User
});
var User = Backbone.RelationalModel.extend({
});
When I make a fetch on FollowerCollection I get the following error:
Uncaught TypeError: Cannot read property 'idAttribute' of undefined
on the line 1565 of backbone-relation.js of backbone-relation version 0.5.0
Here a piece of code of backbone-relation.js
if ( !( model instanceof Backbone.Model ) ) {
// Try to find 'model' in Backbone.store. If it already exists, set the new properties on it.
var existingModel = Backbone.Relational.store.find( this.model, model[ this.model.prototype.idAttribute ] );
The problem is related to _.bindAll(this) because if I comment it, it works properly.
Why? Any ideas?
Removing the _.bindAll does work.
It's a shame, because it's a really handy function. It must interact with some part of Backbone badly. I'm on v9.10
I use this method all the time, and issues only come up sometimes (like when you want to do a bulk add to a collection).
For me, The problem was in this Backbone.js method:
// Get a model from the set by id.
get: function(obj) {
if (obj == null) return void 0;
this._idAttr || (this._idAttr = this.model.prototype.idAttribute);
return this._byId[obj.id || obj.cid || obj[this._idAttr] || obj];
},
The code fails at this.model.prototype because prototype is undefined. What? Ya. For reals.
The problem is that when _.bindAll is called, it binds all properties of the collection, as #jakee says. This seems to include Collection.model, which is a bug I think.
The solution is to bind individual methods until this is fixed.
There's an existing, but closed issue on github: https://github.com/documentcloud/backbone/issues/2080
Seems like the current maintainers don't like the method, but I don't understand why.
Like my project is really big I had to create my custom bindAll. Here you have the code, it works with the lastest versions.
I bind all the properties of the instance "this" except the ones that has prototype with properties, like this.model in a Collection
https://gist.github.com/patrixd/8025952
//bindAll from underscore that allows 1 argument to bind all the functions from the prototype,
//or if there are more arguments they will be the only binded
_.originalBindAll = _.bindAll;
_.bindAll = function (that) {
var funcs = Array.prototype.slice.call(arguments, 1),
validKeys = [], fn;
if (funcs.length == 0) {
for (var i in that) {
fn = that[i];
if (fn && typeof fn == "function" && (!fn.prototype ||
_.keys(fn.prototype).length == 0))
validKeys.push(i);
}
_.originalBindAll.apply(_, [that].concat(validKeys));
}
else
_.originalBindAll.apply(_, arguments);
};