Marionette collectionEvents are not firing when the collection is defined inside the initialize function.
ex:
collectionEvents: {
update: 'onCollectionUpdate',
change: 'onCollectionChange',
error: 'onCollectionError',
},
initialize(opts) {
this.collection = new ListCollection({...});
}
but it works fine when the collection is defined as a property on the compositeView class
collection: new ListCollection({...}),
collectionEvents: {
update: 'onCollectionUpdate',
change: 'onCollectionChange',
error: 'onCollectionError',
},
Is there a reason for this I need to define the collection in the initialize function, because i have data being passed to the constructor that the collection needs
You can just pass in an empty collection to the view so that event bindings happen properly while creating view instance, then populate data in collection. Or just pass in collection including data.
const view = new CollectionView({
collection: new ListCollection(/*data you pass to initialize*/),
});
or in view's initialize
initialize(opts) {
this.collection.reset(options.data);
}
I wouldn't expect marionette to track when you are adding collection property to view at random point in time and then create event bindings accordingly.
Related
So I'm looking into backboneJS and I'm trying to figure out when to use the collection attribute and when to use this.collection inside the initialize function? Is there a difference? Here is an example.
Backbone.View.extend({
collection: myCollection,
initialize: function(collectionData) {
this.collection = new app.Library(collectionData);
}
});
When you use "this" in a method of a object, it is set to the object the method called on. So this.collection will always equal to collection property (initialize method will be invoked by a view instance right?).
In my opinion, I always assign the default collection in view option declaration and access the collection reference by this.collection in the method of view object.
//set the default collection to a view object
var CustomView = Backbone.View.extend({
collection: myCollection
});
//or set collection when you instantitate a view
var myView = new CustomView({collection: myCollection});
//and get the reference of view's collection for event registering
initialize:function(){
//refresh view when new data is added to collection
this.listenTo(this.collection,"add",this.render);
}
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.
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
For the following code, the add event bound in the view fires twice (more if you add more elements to the collection at once).
http://jsfiddle.net/radu/GnG66/
App = window.App || {};
var Model = Backbone.Model.extend();
var Collection = Backbone.Collection.extend();
App.collection = new Collection({ model: Model });
var View = Backbone.View.extend({
events: {
'click': function() {
console.log('click');
App.collection.add([{
foo: 'foo'
}, {
bar: 'bar'
}]);
}
},
initialize: function() {
App.collection.on('add', function() {
console.log('Something has been added to the collection')
}, this);
}
});
$(function() {
App.view = new View({ el: '#test' });
});
If instead of adding an array to the collection, you just pass several objects as arguments (basically just remove the square brackets), the event only fires once.
Is this by design and is there a way to override this behaviour without passing { silent : true } as an option?
The add event is fired once for each model added.
Collection.add can take an array of models, or a single model and some options.
In your example above, you are passing an array of two models in. Since the add event gets fired once for each model added, it fires twice.
When you pass in several objects, Backbone thinks the first object is a model and the second is a hash of options. That means only one model is being added, so it fires the add event once.
Sorry to resurrect this question from the dead, but I was having this problem too and wanted to post how I solved it. The problem with having 'add' trigger so many times for me was because I had a complex render function in my view that was listening for 'add'. This was causing serious performance issues.
I resolved it by creating a temporary collection using backbone's handy collection.clone() method on it, adding the new models to it, and then resetting the original collection with the temp collection's models property. The code looks like this:
// Create a temporary copy of searchResults
var temp = App.searchResults.clone();
// Add the new results
temp.add(newResults.models);
// Copy the new data over the old data
App.searchResults.reset(temp.models);
// Added this since reset triggers 'reset' and my view is listening for 'change add remove'
this.get('filtered_flights').trigger('change');
This sets off only ONE 'change' event instead of several 'add' events.
I'm creating a collection without using fectch(), but with JSON data already available.
this.displays = new Displays(jQuery.parseJSON($('#temp_json').html()));
I need for each model of that collections to have a 'position' value setup, which should be the position of this model in this collection.
What I'm trying to do is to catch an event, where in the initial load each model is constructed from the JSON and added to the collection.
I'll then make something like:
theModel.set('position', this.length);
Unfortunately I can't find that event I should bind my collection to.
Also this collections's models contains other collection (...) where same should be done.
This might seems weird but I have to do this as later on in my view I'll peform things like:
var prototypeNames = [this.model.get('display').get('position'), this.model.get('position')];
Hope this is clear enough.
Thanks!
The Collection.add-function fires a add-event, which you can listen to with the on-function. The event passes the added model AND an options object, which contains some useful attributes (like the index where the model was added), as parameters. (documentation here)
So you'll do something like this:
collection.on('add', function(model, options) {
model.set('position', options.index);
});
if you want to find out more about the parameters the add-event passes, just log them, because the existing documentation is scant at best.
collection.on('add', function(model, options) {
console.log(options);
});
Hope this helps!
Solution that worked for me:
I used backboneRelational, which is awesome when working with actual model relationships.
I'm listening to "relational:add" which is triggered for each item of the collection when constructing it.
I then set some values on my item and trigger a new event 'postAdd' that can be listened to from my view.
window.Displays = Backbone.Collection.extend({
model: Display,
initialize: function(data, options){
this.on("relational:add", function(relModel){
relModel.set('pos',this.indexOf(relModel));
this.trigger('postAdd', relModel);
}, this);
},
});
You can override the default 'add' implementation of the collection to trigger a custom event which won't be silenced by Backbone.
var MyCollection = Backbone.Collection.extend({
// Override the default 'add' implementation...
add: function( models, options ) {
// Call the default implementation first...
Backbone.Collection.prototype.add.apply( this, arguments );
// Fire our custom events on the models...
while (model = models.shift()) {
model.trigger('customAdd', model, this, options);
}
return this;
}
});
You can now listen to the 'customAdd' event triggered on the model. This one will always be triggered, event when silent = true.
As a consequence, when an item is added an with {silent: false} two events will be triggered: 'add' and 'customAdd'.
I've used this technique to fire an event when a new collection has initially loaded and all models are created. Since the initialize method will be called before the models are created, I needed to override the 'reset' function to trigger my code to execute when the models were created.