backbone.js - handling model relationships in a RESTful way - javascript

I'm using backbone.js
For example, let's suppose we have a "products" model and a "categories" model which have a many-to-many relationship. In one of my views, say I need to retrieve a list of all categories and know whether or not each one is related to the current product model.
Do I set up a "category" collection and have it be a property of my model and somehow give it access to the id of the model so that when it is fetched, it only gets the categories that are related? And then I could fetch all categories and cross examine them to see which ones are related while still having the ones which are not?
I have no idea what the best way to do this would be. I'm used to using an ORM which makes it easy on the server-side.

Check out backbone-relational.

There is a simple & customizable solution for it, although it may not be as robust as backbone-relational.
Backbone.ModelWithRelationship = Backbone.Model.extend({
mappings: {},
set: function(attributes, options) {
_.each(this.mappings, function(constructor, key) {
var RelationshipClass = stringToFunction(constructor);
      var model = new RelationshipClass();
/* New relational model */
  if (!this.attributes[key]) {
this.attributes[key] = (model instanceof Backbone.Collection) ? model : null;
  }
  /* Update relational model */
  if (attributes[key] && !(attributes[key] instanceof Backbone.Model || attributes[key] instanceof Backbone.Collection)) {
if (model instanceof Backbone.Model) {
this.attributes[key] = model;
this.attributes[key].set(attributes[key], options);
} else if (model instanceof Backbone.Collection) {
this.attributes[key].reset(attributes[key], options);
}
delete attributes[key];
  }
}, this);
return Backbone.Model.prototype.set.call(this, attributes, options);
}
});
You can declare the mapping just by creating a subclass of Backbone.ModelWithRelationship.
Models.Post = Backbone.ModelWithRelationship.extend({
mappings: {
'comments': 'Collection.CommentCollection',
'user': 'Models.User'
}
});

http://pathable.github.com/supermodel/ is fantastic. It let's you do stuff like:
Post.has().many('comments', {
collection: Comments,
inverse: 'post'
});
Comment.has().one('post', {
model: Post,
inverse: 'comments'
});
var post = Post.create({
id: 1,
comments: [{id: 2}, {id: 3}, {id: 4}]
});
post.comments().length; // 3
var comment = Comment.create({id: 5, post_id: 1});
post.comments().length; // 4
comment.post() === post; // true :D

Assuming you are using a join table on the backend:
Create a collection and model containing all the rows on your join table and add the following methods to the collection: productsByCategory and categoriesByProduct (using [join collection].where(...).)
Having data in Backbone mirroring your data in the backend seems to help keep things simple and you won't have to do anything complicated when setting URLs.

Related

Cannot delete array from object populated by sails.js

I cannot delete or change the value of books attribute of library object.
Library.findOne(12).populate('books').populate('createdBy').exec(
function(err,library) {
delete library.createdBy;
//worked
delete library.name;
//worked
delete library.books;
//no effect
library.books = [];
//worked
library.books = [{a:'any val'}];
//just like library.books=[]
console.log(library);
});
My model for library for books and createdBy is like
createdBy: {
model: "createdBy"
},
books: {
collection: "books",
via: "library",
dominant: true
}
I cannot figured out what is happening here.
delete library.books; does not work because associations are not fields in the model object. Associations actually live in an associations object and read/write operations are done through custom getters/setters. You can see more about this behaviour in waterline/model/lib/internalMethods/defineAssociations.js#L109:
Define.prototype.buildHasManyProperty = function(collection) {
var self = this;
// Attach to a non-enumerable property
this.proto.associations[collection] = new Association();
// Attach getter and setter to the model
Object.defineProperty(this.proto, collection, {
set: function(val) { self.proto.associations[collection]._setValue(val); },
get: function() { return self.proto.associations[collection]._getValue(); },
enumerable: true,
configurable: true
});
};
Hope that helps.
Is this causing a problem? This can be avoided by not populating associations in the first place. Doing model.toObject() or model.toJSON() and delete the association field afterwards should also work.

How can I fake Collection data?

What I mean by this is I want to create it artificially.
This is for testing purposes.
But for models, it is quite simple. I just set defaults I instantiate the model object and from there I can use this.model.toJSON() to grab the created data.
I want to use this same trick with collections. Is there a similar way to do this with collections? What I would want to do is have the collection create x ( 8 in this case ) copies of Model defaults.
Basically what I was doing before for models but a little bit more complex as it applies to Collections.
Here is the actual use case. It should be simple.
/**Model
**/
// name, picture, time, tweet, h_file
var FeedRow = Backbone.Model.extend({
Name: 'FeedRow',
defaults: {
name: "default",
picture: 0,
time: "0",
tweet: "default",
h_file: "default"
}
});
/**Collection
**/
var FeedTable = Backbone.Collection.extend({
Name: 'FeedTable',
model: FeedRow
});
When your FeedTable collection is constructed you could set the model on it multiple times in the initialize method.
var FeedTable = Backbone.Collection.extend(
{
Name: 'FeedTable',
model: FeedRow,
initialize: function()
{
model = this.model;
models = [];
_.times(8, function(n)
{
models.push(new model({id: (n + 1)}));
});
this.set(models);
}
});

Fetching Models to a Collection

I am pretty new to Backbone and just came across this confusing issue. I am trying to fetch models in to my collection in a Express node.js server with the following code :
app.get('/tweet', function(req,res){
res.send([{ name: 'random_name' }, {name: 'diren_gezi'}] );
});
and my backbone code looks like this:
var PostsApp = new (Backbone.View.extend({
Collections: {},
Models: {},
Views: {},
}
start: function(){
var data = {
posts: [
{name:"gorkem"},
{name: "janish"},
{name: "akash"}
]
};
var posts = new PostsApp.Collections.Posts(data.posts);
var postsView = new PostsApp.Views.Posts({collection: posts});
posts.url = "/tweet";
posts.fetch();
console.log(posts.length);
console.log(posts);
}
}))({el : document.body});
I would expect console.log(posts.length) to return 5, because I am adding 3 when I initialize and 2 more when I fetch. Or even if the fetch method erases the collection and re-populates, I would expect posts.length to return 2. However it returns 3, but when I look into the post object from the console I only see two models, the ones coming from the fetch() method. What is the reason fror this ?
It is because , when you do a fetch the collection is reset
So the previous contents are removed and refreshed with the 2 new ones. That is the reason you see a length of 2.
Pass merge: true, to see a length of 5 , where in it effectively merges the collection
posts.fetch({merge: false});
because you already have declared data, and when you do fetch you return 2 more.
var data = {
posts: [
{name:"gorkem"},
{name: "janish"},
{name: "akash"}

Backbone.js model with collection

I have 2 models and one collection. JobSummary is a model, JobSummaryList is a collection of JobSummary items, and then I have a JobSummarySnapshot model that contains a JobSummaryList:
JobSummary = Backbone.Model.extend({});
JobSummaryList = Backbone.Collection.extend({
model: JobSummary
});
JobSummarySnapshot = Backbone.Model.extend({
url: '/JobSummaryList',
defaults: {
pageNumber: 1,
summaryList: new JobSummaryList()
}
});
When I call fetch on the JobSummarySnapshot object, it gets everything... Except when I move through the summaryList collection they are all of type object and not JobSummary.
I suppose this makes sense since other than the defaults object, it doesn't know that the summaryList should be of type JobSummaryList. I can go through each item and convert it to a JobSummary object, but I was hoping there was a way to do it without having to do it manually.
Here's my test code (working jsfiddle here):
var returnData = {
pageNumber: 3,
summaryList: [
{
id: 5,
name: 'name1'},
{
id: 6,
name: 'name2'}
]
};
var fakeserver = sinon.fakeServer.create();
fakeserver.respondWith('GET', '/JobSummaryList', [200,
{
'Content-Type': 'application/json'},
JSON.stringify(returnData)]);
var callback = sinon.spy();
var summarySnapshot = new JobSummarySnapshot();
summarySnapshot.bind('change', callback);
summarySnapshot.fetch();
fakeserver.respond();
var theReturnedList = callback.getCall(0).args[0].attributes.summaryList;
_.each(theReturnedList, function(item) {
console.log('Original Item: ');
console.log(item instanceof JobSummary); // IS FALSE
var convertedItem = new JobSummary(item);
console.log('converted item: ');
console.log(convertedItem instanceof JobSummary); // IS TRUE
});
UPDATE:
It occurred to me that I could override the parse function and set it that way... I have this now:
JobSummarySnapshot = Backbone.Model.extend({
url: '/JobSummaryList',
defaults: {
pageNumber: 1,
summaryList: new JobSummaryList()
},
parse: function(response) {
this.set({pageNumber: response.pageNumber});
var summaryList = new JobSummaryList();
summaryList.add(response.summaryList);
this.set({summaryList: summaryList});
}
});
This works so far. Leaving the question open in case someone has comment on it....
Your parse() function shouldn't set() anything, its a better practice to just return the attributes, Backbone will take care of setting it. e.g.
parse: function(response) {
response.summaryList = new JobSummaryList(response.summaryList);
return response;
}
Whatever you return from parse() is passed to set().
Not returning anything (which is like returning undefined) is the same as calling set(undefined), which could cause it not to pass validation, or some other unexpected results if your custom validate()/set() methods expects to get an object. If your validation or set() method fails because of that, the options.success callback passed to Backbone.Model#fetch() won't be called.
Also, to make this more generic, so that set()ing to a plain object from other places (and not only from the server response) also effects it, you might want to override set() instead:
set: function(attributes, options) {
if (attributes.summaryList !== undefined && !(attributes.summaryList instanceof JobSummaryList)) {
attributes.summaryList = new JobSummaryList(attributes.summaryList);
}
return Backbone.Model.prototype.set.call(this, attributes, options);
}
You might also find Backbone-relational interesting - it makes it much easier to deal with collections/models nested inside models.
edit I forgot to return from the set() method, the code is now updated

backbone.js parse 1 element (the Id)

For a id on a model in backbone, its just id and all lower cased. What if my Id on the server is called UserId. In the parse method for backbone, how do I change UserId to id and use the same names for all other properties?
For eg.
window.User = Backbone.Model.extend({
defaults:
{
UserId: 0, // <--can i just tell backbone that this is my id?
Name: '',
Age: 0
}
parse: function(response){
var model = response;
model.id = response.UserId;
return model;
}
});
Is there a better way to do this?
How about telling backbone model that my id is of type UserId.
You have to say Backbone what property is your id using the idAttribute in the model:
window.User = Backbone.Model.extend({
idAttribute: "UserId",
...
})
and everything works fine :). Backbone will create a id property for you and collections of this model will get() by your UserId.
Do it like so:
parse: function(response) {
var attrs = {};
attrs.id = response.UserId;
return attrs;
}
Parse has the responsibility to return an attributes hash, not a model. As such, you need only transform the response into an attributes hash versus a model as you are doing.
To set the id i would do as mauromartini says. To change any other property you don't need to create any private variables, response from the server is just an object so manipulate it and return it.
window.User = Backbone.Model.extend({
defaults: { UserId: 0 },
parse: function(response){
response.id = response.UserId;
return response;
}
});
you could also add the following before you return the object if you want to keep the model clean:
delete response.UserId;

Categories

Resources