So there's a piece of functionality with which I've been struggling for a while now. I'm using .where() method in order to retrieve an array of objects from the Collection and then I reset this Collection with this array.
# Fetch the collection
collection = App.request(collection:entites)
console.log collection
>collection {length: 25, models: Array[25] ... }
When the event fires it passes options for .where() method and starts reset process:
# Get new models
new_models = collection.where(options)
# Reset collection with the new models
collection.on 'reset', (model, options) ->
console.log options.previousModels
return
collection.reset(new_models)
console.log collection
>collection {length: 5, models: Array[5] ... }
In the View responsible of rendering this collection I listen to the 'reset' event and render the View accordingly.
initialize: ->
#listenTo(#collection, 'reset', #render)
It works just as expected: event fires, collections undergoes reset and the View re-renders reseted collection. But when the event fires second time the collection doesn't sync with the server and new_models = collection.where(options) receives a collection that was already reseted in a previous event run and returns an empty array.
What are my options here? Each event run I need an initial collection of all models to work with. Should I just request new instance of the collection on the each run or can I make it in a more cleaner manner, i.e. save original state somewhere and pass it for the event run instead of fetching new collection from the server? Please advise.
Yes. Another way to achieve it is, when you filter the collection, using .where(), you can trigger Backbone.Events custom event which you view can listen to. This way, the original collection does not reset, there only is a change in array which will trigger the custom event.
A sample code for using custom events in backbone is:
var object = {};
_.extend(object, Backbone.Events);
object.on("collectionFiltered", function(arrayOfFilteredModels) {
// do stuff to render your view with the new set of array.
// You can use underscore templating to traverse the array for rendering view.
});
object.trigger("collectionFiltered", collection.where(options);
Related
I have a model that has collections within it. Within my web app I have a project model, and in that I have an attribute called "collaborators" this attribute is actually a collection of users.
In the initialize of my view I have the following,
this.model.get('collaborators').on( 'change', alert("!!"), this);
When the view firsts loads I get an alert, firther into the view I have an event, that fires the following,
var newModel = new app.User({ id: this.clickedElement.data('userId') });
console.log(this.model.get(this.formSection)); // shows that the collection has 4 models
newModel.fetch({
success: function() {
that.model.get(that.formSection).add(newModel);
console.log(that.model.get(that.formSection)); //shows that the collection has 5 models
}
});
As you can see in the code above, I am logging my collaborators collection and it shows a length of 4, after the fetch in the success method, am I adding the model I just fetched to that collection, and then logging the collection again, this time it returns a length of 5.
This to me means that the add is successful, so is the event listener for "add" not firing after the initial page view?
The trouble is that you should pass some function as a second argument of on method. But you're firing alert right at the event binding moment and passing alert's result instead of function.
This should work correctly:
this.model.get('collaborators').on('change', function(){
alert("!!");
});
Update: But you should clarify which events you want to catch. You can find all events fired during fetching (or any other process) by launching search for trigger method calls across backbone.js file.
Update: Also you can use this code to catch all of events fired:
collaborators.on('all', function(){
/**
* `arguments` object contains all of arguments passed to the function.
* #see http://backbonejs.org/#Events-on for more details about `all`
* catching.
*/
console.log(arguments);
});
When you binding to the collaborators' change event, you should use Backbone's listenTo event.
this.listenTo(this.model.get('collaborators'), "change", function(){alert("!!"), this);
http://backbonejs.org/#Events-listenTo
The advantage of using this.listenTo is when the view gets closed/removed, Backbone will automatically invoke this.stopListening causing your view to automatically unbind the event. This will help you clean up events and prevent phantom views.
http://backbonejs.org/#Events-stopListening
Currently i have a call to server that returns two objects and one of the objects i use to set a collection. (i poll this every 10 seconds, and soon will be using socket.io for this).
I am noticing that my models initialize function is called every time i use set collection with object. I thought the set was smart and only added/changed attr or removed models and for ones unchanged just did nothing.
// All Orders Collection
var AllOrders = Backbone.Collection.extend({
model: Order,
url: '/venues/orders'
});
var Order = Backbone.DeepModel.extend({
idAttribute: 'orderid',
url: '/venues/orders',
initialize : function(){
// this is called everytime i use set even if model is in collection
// do stuff
}
})
*****************
app.allOrders.set( app.getOrders.get('orders') );
If you look at the backbone source,
When merging models into a collection the _prepareModel method is called before merging which creates a new model from the passed in attributes.
Here is the set code,
if (!(model = this._prepareModel(attrs = models[i], options))) continue;
// If a duplicate is found, prevent it from being added and
// optionally merge it into the existing model.
if (existing = this.get(model)) {
if (remove) modelMap[existing.cid] = true;
if (merge) {
attrs = attrs === model ? model.attributes : options._attrs;
existing.set(attrs, options);
if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
}
So what happens is,
A new model is created from your passed in attributes
The collection is queried for an existing model with a same id as the new one.
If the merge options is true it pushes the new attributes to the existing model.
This depends on how you write your client and services. By default the GET /venues/orders returns the whole order list. If you want to fetch the changes only you have to write your custom sync for the order collection which keeps the current orders, and fetches the changes only from the server (e.g. you send the current ids, or you send the timestamp of the last call). Btw. the initialize will be called by every fetched model because they are created from json and collection.set cannot compare json objects with backbone models, it can only compare backbone models with backbone models.
Possible solutions:
Override set to compare json with backbone models.
Override your app.getOrders.get('orders') & server side code to pull down only the changes, or manually remove the already existing objects from the pullled json.
Move your code from initialize to another place for example to collection.add event handler.
I have a form that create a model for a collection. That fires an add event to the collection. I have that binded to a method:
this.collection.bind('add', this.addOne, this)
fires...
addOne: function(tenant) {
var self = this
var collection = this.collection
var view = new TenantView({model: tenant,collection:collection});
self.$el.append(view.render().el);
}
The create syncs it to the database but, the new appended view still isNew to backbone since it hasn't fetched the collection and grabbed the id for the new model.
My question, how can I grab the synced model from the server (that has the id and isn't isNew) without fetching the entire collection then append it?
Use the sync event instead of add...
this.collection.bind('sync', this.addOne, this);
The add event gets fired immediately when calling create; but sync gets fired once the server has responded to the create method, so it should include the correct model id.
You can also wait for the server's response, before adding the model to the collection -- to do that use wait: true in create's options hash: collection.create({ ... }, { wait: true }).
Creating a model will cause an immediate "add" event to be triggered on the collection, as well as a "sync" event, once the model has been successfully created on the server. Pass {wait: true} if you'd like to wait for the server before adding the new model to the collection.
I have an "index" view and an accompanying "pagination" view. On initialization, the index view fetches the relevant collection. The initially fetched collection is limited to 100 models and contains the count of all values in the collection. The count is passed to the pagination view and it accordingly produces page numbers. After the 10th page (10 records/page) The next 100 models are fetched, and so on the pattern continues.
Keeping the aforementioned in mind, when I add one or more models to the collection the model count will need to be re-fetched from the server (so the pages can be re-calculated), even though I do:
#collection.add [new_model]
However, in the event of changing a value in a model, I simply want the collection to be re-rendered.
With the following initialization code, I'm able to have the collection re-rendered after a change. But in the event of a "add" nothing happens. How can I construct the view to re-fetch from the server the new collection and count?
Note: I'm using fetch(add: true)
initialize: ->
#collection = new MyApp.MyCollection()
#collection.on('add', #render, #)
#collection.on('change', #render, #)
#collection.fetch(add: true)
To clarify the events logic:
reset: will be triggered after a fetch from the server. A fetch drops all content of the collection and just inserts everything in the collection that comes from the server
add: will be triggered after a model has been added to the collection. ATTENTION: add will not be triggered by a fetch, unless you use the add:True option.
change: will be triggered after the content/field of a model has changed. ATTENTION: change will not be triggered by fetch nor by adding a model.
So the events do hardly overlap but all serve a specific use case. You have that in mind when binding the events to methods, especially the render method.
Here I have a Backbone.js Model - Contact and a Collection - Contacts with Contact as the Model. I've two views where in one I use collection to list all the contacts and in the second the model directly to show a single contact. But when I'm using the model directly I'm able to get the 'change' event fired while using with the collection the 'change' event, (even 'all' events of the model) is not fired. Am I missing some extra bind here to make it work with collection?
var Contact = Backbone.Model.extend({
defaults: {
id: 0,
urlDisplayPicBig: '',
urlDisplayPicSmall: ''
},
initialize: function () {
this.bind('change', this.doSomething);
},
generateUrls: function () { //earlier doSomething()
...
}
...
});
var Contacts = Backbone.Collection.extend({
model: Contact,
...
});
Update
While using both collection & single model instances I have to run generateUrls() to update urlDisplayPicBig & urlDisplayPicSmall based on the 'id' of the model.
When you do fetch on a collection:
the collection will reset
and that will
replace a collection with a new list of models (or attribute hashes), triggering a single "reset" event at the end. [...] Using reset with no arguments is useful as a way to empty the collection.
So a fetch on a collection will remove all the models that are currently in the collection and replace them with brand new model instances. No "change" events will occur, there will only be a single "reset" event.
When you do a fetch on a model:
A "change" event will be triggered if the server's state differs from the current attributes.
So calling fetch is pretty much the same loading the data from the server manually and calling set to change the model.
Summary: Collection#fetch doesn't trigger any "change" events, just a "reset" event; Model#fetch will trigger a "change" event if something changes.
If you just want to add a couple new attributes when creating new model instances then you can add new attributes to the incoming JSON using Model#parse.