I have several models which have their own url/api they can fetch.
I would like to keep them in a collection.
does it sound like a good idea?
how would you fetch them? (I'm thinking just to get the model I want to update from the collection then fetch it)
Let me know what you think, if you have any theoretical reading/advise.
A collection can contain any raw objects or any models derived from a Backbone.Model. Fetching a collection makes sense only if you have an API endpoint which returns an array of objects.
If you want to fetch a specific model, you can keep a reference to it, or just get it inside your collection, and then call fetch on it.
It can cause problems when you have id collisions, where identical ids are considered the same model and are merged together.
var book = new Book({ id: 1, title: "My Book" }),
note = new Note({ id: 1, title: "note test" });
var collection = new Backbone.Collection([book, note]);
console.log(collection.length); // 1
Ways to avoid id collisions:
don't use ids for these models if possible, Backbone will use their cid.
use GUIDs.
make a custom id attribute composed of identifying data, like prepending the type attribute. (book1, note1).
A way to make a multi model collection is to use the model property as a function. Though it doesn't prevent id collision by default.
var BooksAndNotes = Backbone.Collection.extend({
/**
* Different models based on the 'type' attribute.
* #param {Object} attrs currently added model data
* #param {Object} options
* #param {Backbone.Model} subclass dependant of the 'type' attribute.
*/
model: function ModelFactory(attrs, options) {
switch (attrs.type) {
case "book":
return new Book(attrs, options);
case "note":
return new MTextSession(attrs, options);
default:
return new Backbone.Model(attrs, options);
}
},
// fixes this.model.prototype.idAttribute and avoids duplicates
modelId: function(attrs) {
return attrs.id;
},
});
var collection = new BooksAndNotes([{
title: "My Book",
type: 'book'
}, {
title: "note test",
type: 'note'
}]);
See similar questions about multiple models in a collection:
A Backbone.js Collection of multiple Model subclasses
Backbone Collection with multiple models?
Related
I'm learning Backbone.js and reading through the docs I have a hard time understanding the below:
"If a model property is defined, you may also pass raw attributes objects, and have them be vivified as instances of the model."
Looking at Collection#model, they provided the example:
var Library = Backbone.Collection.extend({
model: Book
});
But how do I go on from there and "pass raw attributes objects"?
********Edit***********
Ok looking back now I think what this means is that if I have added the model "Book" to the Library collection, now I can do -
var lib = new Library;
lib.add([
{name: "Curious George"},
{name: "Harry Potter"}
]);
And this will have created 2 models in the lib collection with the corresponding names, right?
Raw object in this case is just a simple object (Not a already constructed model)
In this example, the collection is of type Book (which is a model which might have some predefined attributes and default values)
var Library = Backbone.Collection.extend({
model: Book
});
So Library is a collection Book models.
If you want to create a new Book as part of the Library, you can go about in 2 ways.
1.) Create a Model first and add it to the collection
var book1 = new Book({
id: 1,
name: 'abc'
});
Library.add(book1);
2.) Pass in the Raw attributes to the Collection directly.
Library.add([{id: 1, name: 'abc'}]);
I have a webapp based on Backbone.js with a list. The entries of the list are coming from a REST API. This list (JSON array) updates from time to time. I want to update my list in the frontend too, without reloading the page.
I thought about using a poller to update the file list with every new object returned by the API. However, the poller is not the problem here, I first need a function to add just the new models to the file list.
The API returns a JSON list, based on this model:
Xff = Backbone.Model.extend({
defaults: {
id: null,
name: "",
language: "en",
timestamp: 0,
status: null,
progress: 10,
duration: 0
}
});
This is the collection. restUri points to the REST API and with /files it gets the complete file list.
XffCollection = Backbone.Collection.extend({
model: Xff,
comparator: function(a, b) {
return (a.get("timestamp") > b.get("timestamp") ? -1 : 1);
},
url: restUri + "files"
});
This is the AppView object. It uses the XffCollection, as explained above.
app = new AppView({
collection: new XffCollection()
});
AppView is a regular backbone view...
AppView = Backbone.View.extend({ .... })
Using app.collection.fetch() I can fire the request (visible in firebug), but the list is not updated. I also have a addAll() function, but then it just appends the new file list to the old file list.
The addAll:
addAll: function() {
this.collection.chain().sort().each(this.addOne, this);
}
This is the addOne:
addOne: function(xff) {
var v = new XffView({model: xff});
this.xffViews.push(v);
$("#xffs").append(v.render().el);
}
How can I add just the new entries to the list?
UPDATE
While kindasimples anwser works now, the filelist in the frontend is not sorted anymore using the comparator defined in the collection (with the timestamp). Using addAll() in the bottom of the comparator, the list is sorted.
To provide additional information, here are more parts of the overall backbone code: http://pastebin.com/rR39x3Y1
From the backbone.js docs:
collection.sort([options])
Force a collection to re-sort itself. You don't need to call this under normal circumstances, as a collection with a comparator will sort itself whenever a model is added. To disable sorting when adding a model, pass {sort: false} to add. Calling sort triggers a "sort" event on the collection.
But it does not sort itself. Also calling app.collection.sort() right after the fetch does not help.
UPDATE 2
I fixed it by sorting the array in the API before returning it. That's not how it was meant to be but at least it works now.
You have the right idea. addOne() will render individual items when you do your initial setup after a fetch to populate items. You can add a listener to the collection events to add the new items. Collection.Fetch does what you want by adding new models to the collection and leaving the old in tact (as long as you don't pass the {reset:true} flag as a parameter)
So, on your view add the listener to the initialize hook
initialize: function() {
this.listenTo(this.collection, "add", this.addOne)
}
You will probably want to define the idAttribute on your Xff Model so that backbone can identify new items properly.
Here is what I am trying to understand.
Often times I find myself writing backbone like this:
var CallModel = Backbone.Model.extend({
});
var CallsCollection = Backbone.Collection.extend({
model: CallModel,
url: 'url/to/external/json'
});
It is a very basic example but as you can see, there is nothing really in the model all the data is coming into the Collection via an external url call to a json file that is build from a database.
So whats the purpose of the model? I am sure that I am probably not using backbone.js to its fullest extent which is why I am here asking you guys.
First of all, "there is nothing really in the model all the data is coming into the Collection via an external url call" - this is not true.
Let's assume you've the following:
//Model
var CallModel = Backbone.Model.extend({
defaults: {
cost:0,
duration:0
}
});
(without custom attributes or methods, there is no point in extending the original Backbone.Model)
//Collection
var CallsCollection = Backbone.Collection.extend({
model: CallModel,
url: 'url/to/external/json'
});
And the json data returned from service, probably something like:
//Response
{
callSummary: {
missed: 2,
received: 3,
totalCalls:5
totalDuration: 20
}
calls: [{
id:001,
caller:"Mr.A",
callee:"Mr.B",
cost:1,
duration:5
},{
id:002,
caller:"Mr.X",
callee:"Mrs.Y",
cost:1,
duration:7
},{
id:003,
caller:"Mr.A",
callee:"Mrs.B",
cost:1,
duration:8
}],
//and more additional information from your db
}
Now you populate your collection with data by calling it's fetch method:
CallsCollection.fetch();
Your collection should look something like:
{
models: [{
attributes: {
callSummary: {},
calls: [{},{},{}],
...
},
...
}],
length:1,
url: "url/to/external/json",
...
}
The data will be added to a model's attribute hash. If you don't specify a particular model, as Bart mentioned in his answer, backbone will populate the collection with a Backbone.Model instance: Which is still not much useful - Wew... A collection with single model having entire response data inside it's attributes as it is...
At this point, you're wondering why did I even bother creating a model, and then a collection..?
The problem here is Collections are derived from Arrays, while Models are derived from Objects. In this case, our root data structure is an Object (not an Array), so our collection tried to parse the returned data directly into a single model.
What we really want is for our collection to populate its models from the "calls" property of the service response. To address this, we simply add a parse method onto our collection:
var CallsCollection = Backbone.Collection.extend({
model: CallModel,
url: 'url/to/external/json',
parse: function(response){
/*save the rest of data to corresponding attributes here*/
return response.calls; // this will be used to populate models array
}
});
Now your collection will be something like the following:
{
models: [{
...
attributes: {
...
id:001,
caller:"Mr.A",
callee:"Mr.B",
cost:1,
duration:5
}
},{
...
attributes: {
...
id:002,
caller:"Mr.X",
callee:"Mrs.Y",
cost:1,
duration:7
}
},{
...
attributes: {
...
id:003,
caller:"Mr.A",
callee:"Mrs.B",
cost:1,
duration:8
}
}],
length:3,
url: "url/to/external/json",
...
}
This - is what we want! : Now it is very easy to handle the data: You can make use of the add, remove, find, reset and handful of other collection methods effectively.
You can pass this models array into your templating library of choice, probably with two way bindings: When the respective view for one of the call model changes, the particular model will be updated, events will propagate from your models to the collection, and the particular model will be passed into the handler functions.
You can now call fetch, save, destroy, clear and a lot of other methods with ease on single unit's of data (each model), rather than hurdle with the entire data saved in a single model - which is pretty much useless, you've to iterate through the response data manually and perform CRUD and similar operations by your own, and in most cases: re-render the entire collection view. which is very, very bad and totally unmaintainable.
To conclude: If your data source doesn't return an array of objects, or you don't parse the response and return an array of objects from which n number of models are to be populated - Then defining a collection is pretty much useless.
Hopefully, now you get the idea.
Very helpful source of info:
Backbone, The Primer: Models and Collections
Developing Backbone.js Applications
backbonejs.org
You don't need to specify a model. A Backbone collection will default to using Backbone.Model if you don't specify this option. The following would work equally well if you don't need the models of the collection to be of a particular instance.
var CallsCollection = Backbone.Collection.extend({
url: 'url/to/external/json'
});
Reference
EDIT
In essence, specifying the model option within a collection is just a way to ensure that objects added to this collection will be instances of that particular model class. If the models being added to your collection don't have any custom behaviour outside of what is available to Backbone.Model, you don't need to create and specify a model as Backbone collections will default to using an instance of Backbone.Model as I have already mentioned. If, however, you wanted to ensure that models added to a particular collection were of a particular type and shared customized behaviour (e.g. validations, defaults, etc.), you would create your own model class by extending Backbone.Model and specifying this in the collection. I hope this clears things up for you.
Sounds Weird but this is the way.
Every collection in backbone, must represent a model, so basically a collections is a list of models.
Even if your model has no data, you need to indicate it when you create a Collection.
This is how backbone works for collections.
I see a difference using create() and push() on collections using Backfire and wonder if this is a misunderstanding on my part, or a bug.
I have an Animal model and Animals collection as below. Normally, the collection is created with an options object containing a zoo_id which is then used to populate the zoo_id in new models. It's a fixed value for this example.
var Animal = Backbone.Model.extend({
initialize: function(attributes, options) {
console.log("model initializing", attributes, options)
}
}),
Animals = Backbone.Firebase.Collection.extend({
firebase: myFirebaseUrl + "/animal",
initialize: function(models, options) {
this.model = function(attrs, opts) {
return new Animal(_.extend(attrs, {zoo_id: 4}))
};
this.on("add", function(model, collection, options) {
console.log("adding", model, collection, options, model.attributes)
})
}
})
var a= new Animals()
If there's data in Firebase, all of the retrieved animal models in a[] have zoo_id = 4, as expected.
When I push a new model
a.push({name: "racoon"})
all of the attribute objects logged to the console have zoo_id = 4. However, the returned object does not have a zoo_id, nor is zoo_id present for the new entry in the Forge.
When I create a new model
a.create({name: "ape"})
all of the attribute objects logged to the console have zoo_id = 4, the returned object has zoo_id = 4, and the new entry has zoo_id = 4 is in the Forge.
If I remove the Firebase extensions and just use a regular Backbone model and collection in the same manner, push returns an object with a zoo_id, and create fails as there's no url set up (as expected).
thanks in advance for clarification!
Push is not part of the functionality overridden by the Backfire API. It pretty much sticks to the same contract as Backbone.Collection. Thus, push simply appends a record to the end of the array without syncing it to any back end.
You could probably create the desired behavior by calling sync after push, as would normally be done with a Backbone collection. I'm not sure how the id would work here, you might need to add one onto the object before it can be synchronized.
However, it's probably simplest to use create/add instead, which are part of BackFire's API and handle server synchronization.
In this stackoverflow post i read about filtering backbone collections and using subsets.
One answer (by sled) recommends using backbone.subset.js (usage example).
I could not find any further resources on backbone.subset.js and I failed implementing it into my project.
It seems like backbone.subset.js is the perfect solution for what i'm trying to achieve.
(Having one "parent" collection that holds all models at all times, and depending on user input filtering the relevant models from the parent collection into a backbone.subset collection.)
My "parent" collection, holding all tasks:
var TasksAll = Backbone.Collection.extend({
url: '/tasks', // the REST url to retrieve collection data
model: Task // the models of which the collection consists of
});
var allTasks = new TasksAll();
Now i want to create a subset collection for e.g. tasks where task.status = 0:
var TasksTrash = new Backbone.Subset({
superset: allTasks,
filter: function(Task) {
return Task.isTrash();
}
});
var trashTasks = new TasksTrash();
Whereas inside the Task model, the method "isTrash" returns true if:
this.get('status') == 0
a) Are there any more resources on backbone.subset.js?
b) How do I implement above scenario?
c) Can I pass 'superset' and 'filter' options as params to the Backbone.Subset init function?
d) I looked into the backbone.subset.js code, when I 'reset' my parent Collection my subset Collections should be updated straight away, right?
PS: I'm fairly new to Backbone. Thanks for your help.
Looking at the source for backbone-subset, it looks as though there is a pre-initialization hook which you could utilize in order to make the 'sieve' or filter available as an option or argument:
https://github.com/masylum/Backbone.Subset/blob/master/backbone.subset.js#L50
As for providing parent as an argument, there is an outstanding patch to add that exact functionality:
https://github.com/masylum/Backbone.Subset/pull/5
With it, you can pass in parent as an option, if it is not an option the library will fall back to looking for it on the object Prototype