Backbone.js: Adding just the new models from JSON collection fetch - javascript

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.

Related

Filtering a collection of view models in using Knockback.js

I am currently creating a page using Knockback.js that displays an employee's tasks in a table. I have a view model for a task, which contains a boolean observable called isSelected. The view model for an employee contains a collection observable of a collection of task view models called 'tasks'.
I now want to add an attribute/function/observable called 'selectedTasks' which only exposes the selected tasks. I want to meet the following requirements:
Both the 'tasks' and 'selectedTasks' should give me view models, not models.
When I add a model to the original tasks collection, the 'tasks' observable should get updated.
When the user selects this newly added model, the 'selectedTasks' should get updated as well.
There should be only one view model for every task model. Otherwise I might get a view model that states task x is not selected while another view model states that x is selected.
To demonstrate it a bit more clearly, I created this jsfiddle: http://jsfiddle.net/drojoke/cg6d88Lp/14/
So far, I only managed to get everything working for the 'tasks' attribute using a collection observable, as seen here:
this.tasks = kb.collectionObservable(tasks, {
view_model: function (task) {
return new TaskViewModel(task);
}
});
I'm using the view_model option to turn every task in the tasks collection into a TaskViewModel. When I add a new task to the tasks collection, the CollectionObservable is updated as exptected.
I tried to create a selectedTasks attribute as a CollectionObservable with a filter option, like so:
this.selectedTasks = kb.collectionObservable(tasks, {
view_model: function (task) {
return new TaskViewModel(task);
}, filters: function (task) {
return task.isSelected(); // isSelected is undefined.
}
});
But unfortunately, the object that gets passed to the filters function is not a TaskViewModel, but just a task object, so I have no access to the isSelected observable.
I couldn't find a lot about filtering a collection of view models instead of models. How can I created a selectedTasks observable that filters view models and doesn't create additional view models?
You can simply use computed observables:
this.selectedTasks = ko.computed(function() {
return this.tasks().filter(function(task) {
return task.isSelected();
});
}, this);

Firebase + backbone: difference between collection.create() and collection.push()?

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.

The proper way of binding Backbone.js async fetch operation results to a view

I am wondering if there are any pointers on the best way of "fetching" and then binding a collection of data to a view within Backbone.js.
I'm populating my collection with the async fetch operation and on success binding the results to a template to display on the page. As the async fetch operation executes off the main thread, I one loses reference to the backbone view object (SectionsView in this case). As this is the case I cannot reference the $el to append results. I am forced to create another DOM reference on the page to inject results. This works but I'm not happy with the fact that
I've lost reference to my view when async fetch is executed, is there a cleaner way of implementing this ? I feel that I'm missing something...Any pointers would be appreciated.
SectionItem = Backbone.Model.extend({ Title: '' });
SectionList = Backbone.Collection.extend({
model: SectionItem,
url: 'http://xxx/api/Sections',
parse: function (response) {
_(response).each(function (dataItem) {
var section = new SectionItem();
section.set('Title', dataItem);
this.push(section);
}, this);
return this.models;
}
});
//Views---
var SectionsView = Backbone.View.extend(
{
tagName : 'ul',
initialize: function () {
_.bindAll(this, 'fetchSuccess');
},
template: _.template($('#sections-template').html()),
render: function () {
var sections = new SectionList();
sections.fetch({ success: function () { this.SectionsView.prototype.fetchSuccess(sections); } }); //<----NOT SURE IF THIS IS THE BEST WAY OF CALLING fetchSuccess?
return this;
},
fetchSuccess: function (sections) {
console.log('sections ' + JSON.stringify(sections));
var data = this.template({
sections: sections.toJSON()
});
console.log(this.$el); //<-- this returns undefined ???
$('#section-links').append(data); //<--reference independent DOM div element to append results
}
}
);
darthal, why did you re-implement parse? It seems to do exactly what Backbone does by default (receive an array of models from the AJAX call and create the models + add them to the collection).
Now on to your question... you are supposed to use the reset event of the Collection to do the rendering. You also have an add and remove when single instances are added or deleted, but a fetch will reset the collection (remove all then add all) and will only trigger one event, reset, not many delete/add.
So in your initialize:
this.collection.on("reset", this.fetchSuccess, this);
If you are wondering where the this.collection is coming from, it's a param you need to give to your view when you create it, you can pass either a model or a collection or both and they will automatically be added to the object (the view)'s context. The value of this param should be an instance of SectionList.
You'll also have to update fetchSuccess to rely on this.collection instead of some parameters. Backbone collections provide the .each method if you need to iterate over all the models to do stuff like appending HTML to the DOM.
In most cases you don't need a fetchSuccess, you should just use your render: when the collection is ready (on 'reset'), render the DOM based on the collection.
So to summarize the most general pattern:
The collection should be independent from the view: you give the collection as a param to the view creation, the collection shouldn't be created from a specific view.
You bind the View to the collection's reset event (+add, remove if you need) to run a render()
this.collection.on("reset", this.render, this);
You do a fetch on the collection, anytime (probably when you init your app).
A typical code to start the app would look something like this:
var sections = new SectionList();
var sectionsView = new SectionsView({collection: sections});
sections.fetch();
Because you bound the reset event in the view's initialize, you don't need to worry about anything, the view's render() will run after the fetch.

Undefined model prototype in Backbone Collection and Marionette CompositeView

Trying to populate a Collection from a list of values, I am getting an error about the Collection's model's prototype being undefined. Looking at this question about a similar problem, I have checked that the Model is actually created before the collection is instanced, to the best of my ability.
The error is being thrown in one of the event handlers of the Marionette CompositeView that holds the Collection, after fetching the data from the server and trying to reset the collection with the list of values from the data which should be populated into it.
Note: Using Backbone 0.9.10
The Model
MyItemModel = Backbone.Model.extend({});
The Collection
MyCollection = Backbone.Collection.extend({
model: MyItemModel
});
The CompositeView's relevant code
MyCompositeView = Backbone.Marionette.CompositeView.extend({
initialize: function(options) {
_.bindAll(this);
this.model = new MyCompositeViewModel();
this.collection = new MyCollection();
},
//This event handler gets properly fired and run.
on_event: function() {
var that = this;
// The data comes through fine with this `fetch`
this.model.fetch({success: function() {
var collection_results= that.model.get("collection_results");
// The error fires in this line
that.collection.reset(collection_results);
that.render();
});
}
})
The error
The error happens in the add function in Backbone, when doing a get for the model object, checking to see if it is a duplicate. The failing code is here:
// Get a model from the set by id.
get: function(obj) {
if (obj == null) return void 0;
// The error originates from this line
this._idAttr || (this._idAttr = this.model.prototype.idAttribute);
return this._byId[obj.id || obj.cid || obj[this._idAttr] || obj];
},
this._idAttr || (this._idAttr = this.model.prototype.idAttribute);
Here, the this.model.prototype.idAttribute fails because the prototype for the model is not defined.
Why is this happening, and how can it be fixed?
Thanks a lot!
The reason is, in Babkbone 0.9.10, if you call collection.reset(models) without options, the models will be passed to collection.add() which strictly needs real models as argument.
But, in fact, the arguments you passed are not real models. They are just an array of hash attributes.
Two options to fix:
Option 1: Call the reset with a parse option
that.collection.reset(collection_results, {parse: true});
Then reset will parse the array of hashes and set them as model.
Option 2: Upgrade to latest version Backbone 1.1.0.
Here reset() no longer pass responsibility to add() but use set() smartly. This option is recommended. And you don't need options here.
that.collection.reset(collection_results)
Another point
May I suggest you not to define model in CompositeView? CompositeView is for collection, not model. Of course I understand the model here is just to hold and fetch some data, but it would be really confusing for the code to be read by another developer, as well as your own maintaining.
To get bootstrapped data, you can load the data at first request and use conventional way to put it into collection. http://backbonejs.org/#FAQ-bootstrap

Implementing Backbone.Subset.js in Backbone.js to filter Models from a parent Collection

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

Categories

Resources