Copying events to new instance - backbone.js

I have a collection of Tasks. I have added some filter methods to the collection declaration to return a subset of the collection in a new instance. What I want to do is create a model in the original collection and have the add (and any other) event filter down to any new instances of the Tasks collection so my view partials can update accordingly.
Here is a JSBin demonstrating my problem. Notice that when I add a new model to the original collection on the last line the list doesn't update because, obviously, the filter methods return new instances of that collection (which seems clean to me) therefore any listeners aren't fired - the view is using a different collection to the one that has had an item added to it.
How can I filter a collection semantically and cleanly, but keep any events bound to the set being filtered bound on the subset of models returned?
For example, the add event for notAsManyTasks isn't fired for the below code (the alert() never shows):
// Task model
var Task = Backbone.Model.extend();
// Tasks collection
var TasksCollection = Backbone.Collection.extend({
model: Task,
byProject: function(projectId) {
var matches = this.filter(function(task) {
return task.get('projectId') == projectId;
});
return new TasksCollection(matches);
},
complete: function(state) {
return new TasksCollection(this.where({ complete: state }));
}
});
// Example collection
var lotsOfTasks = new TasksCollection([
{ id: 1, projectId: 1, complete: false },
{ id: 2, projectId: 1, complete: true },
{ id: 3, projectId: 2, complete: false },
{ id: 4, projectId: 2, complete: true }
]);
var notAsManyTasks = lotsOfTasks.byProject(1);
notAsManyTasks.on('add', function() {
alert("Added");
});
// Does not fire `add` event on `notAsManyTasks` which is my problem, however
// it _does_ fire on `lotsOfTasks`, as it should
lotsOfTasks.add({ id: 5, projectId: 1, complete: false });
How can I get round this problem? Ideally I don't want to store the collection's original state, return a subset, then restore all models again.

Without doing some complicated things, you should be able to do something along those lines:
mySubset.listenTo(myCollection, 'all', function() {
this.trigger.apply(this, arguments);
});
You listen to all the events of your collection, and echo them with your subset. Though I guess that's only half a solution because your subset wouldn't be updated and you'd do the filter in the listener for all your subsets.
Another solution to do the job only once would be for your collection to listen to its own add event, filter the new model, and trigger a custom event so that only the subset(s) that actually need(s) to do something do something.

Related

BackboneJs - Retain events on a collection inside a model when model changes

i've a Collection inside a Model as illustrated below:
var itemModel = Backbone.Model.extend({
defaults:{
name:"",
brand:"",
priceCollection:[]
}
})
There are change listeners attached to the itemModel and also change listeners attached to collection as
this.listenTo(itemModel.get('priceCollection'),'change',this.dosomething) in a view.
The problem is that the change listeners on the collection work fine as long as the parent model hasn't changed , if the model is given a set of new attributes via itemModel.set(newattributes) the event bound on itemModel.get('priceCollection') is lost.
How do i retain this event? or should i rebind this event everytime the Model is change? or Should i move the listener on the collection from the view to the Model and trigger a custom Backbone event?
It should be noted that this model is singleton
Keep in mind that Backbone assumes a Collection and Model should be mapped 1:1 to a server side resource. It makes clear assumtions on the API layout and data structures - refer to Model.url, Model.urlRoot and Collection.url.
Proposal
You said the model is a singleton. In this case I'd suggest to maintain the model and collection separately.
Since a SomeModel is not accompanied by a certain collection SomeCollection which have a tight relationship it's not necessary to relate them on an attribute level. The effort needed to establish event listeners and sync the data is only at one place.
// some controller (app main)
var model = new SomeSingletonModel();
var collection = new SomeSingletonCollection();
var view = new SomeView({
model: model,
collection: collection
});
Probably the resource that is mapped to SomeSingletonModel will deliver an array.
What are the benefits of using a collection as model attribute (that's what model.get("name") is) over using a plain array? Syncing and change events. Both are probably only necessary when a View updates the Collection's Models. When the View only renders, a Collection does not provide any benefit in many cases.
If the data of that array needs to be updated, using a Collection is probably the right choice because of Backbone's synching mechanisms.
But how to keep the collection sync with the model (you ask)?
Your controller needs to listen to the model and update the collection on sync and reset:
model.on("sync reset", function() {
// "priceCollection" is a model attribute
collection.reset(model.get("priceCollection"));
// optionally unset "priceCollection" on the model
this.unset("priceCollection", { silent: true });
});
This will initialize the collection.
Any change to the Collection's Models will then only be part of the Collection's or Model's syncing mechanisms.
Foreword
Also see my other answer which is probably the better choice when the model is a singleton.
Note the very first statement on Backbone's assumptions on the API design on that answer.
Proposals using a coupling between Model and Collection
Note: if necessary, in all these implementations the Collection's url or Model's (the Collection's Model) url/rootUrl may get (re-)defined upon sync to control the syncing.
Update internal reference on change/sync/reset
This implementation removes the model attribute and updates an object attribute with its data.
The object attribute is one Collection instance that is only reset, not recreated, upon model change.
var CustomModel = Backbone.Model.extend({
defaults: {
// defaults go here - "children" may be contained here
},
// implement constructor to act before the parent constructor is able to
// call set() (L402 in v1.3.0) with the initial values
// See https://github.com/jashkenas/backbone/blob/1.3.0/backbone.js#L402
constructor: function() {
// create children collection as object attribute - replaces model attr.
this.children = new Backbone.Collection();
// listen to changing events to catch away that attribute an update the
// object attribute
this.listenTo(this, "change:children sync reset", this.onChangeColl);
// apply original constructor
Backbone.Model.apply(this, arguments);
},
onChangeColl: function() {
// check for presence since syncing will trigger "set" and then "sync",
// the latter would then empty the collection again after it has been updated
if (this.has("children")) {
// update "children" on syncing/resetting - this will trigger "reset"
this.children.reset(this.get("children"));
// remove implicitly created model attribute
// use silent to prevent endless loop due to change upon change event
this.unset("children", { silent: true });
}
}
});
Example usage when testing in a Fiddle or console:
var c = new CustomModel({ a: 1, children: [{ x: 1 }, { x: 5 }] });
c.set({a: 8, children: [{ x: 50 }, { x: 89 }]});
c.url = "/dummy"
// replace sync() only for fetch() demo - the implementation does what sync() would do on success
c.sync = function(method, coll, opts){ if (method == "read") { opts.success({ a: 100, children: [{ x: 42 }, { x: 47 }] }); } }
c.fetch();
Pro
listening to collection events is easier to implement since there's one instance through model lifetime
Contra
code is more complex
collection data is not part syncing without further implementations
Replace on change/sync/reset
This implementation intercepts model attribute changes and replaces its data with a Collection instance that has been initialized (reset) with the raw data.
var CustomModel = Backbone.Model.extend({
defaults: {
// this is optional
children: new Backbone.Collection()
},
initialize: function() {
// listen to model attribute changing events to swap the raw data with a
// collection instance
this.listenTo(this, "change:children sync reset", this.onChangeColl);
},
onChangeColl: function() {
if (this.has("children")) {
// use silent to prevent endless loop due to change upon change event
this.set("children", new Backbone.Collection(this.get("children")), { silent: true });
}
}
});
Example usage when testing in a Fiddle or console:
var c = new CustomModel({ a: 1, children: [{ x: 1 }, { x: 5 }] });
c.set({ a: 8, children: [{ x: 50 }, { x: 89 }] });
c.url = "/dummy";
// replace sync() only for fetch() demo - the implementation does what sync() would do on success
c.sync = function(method, coll, opts){ if (method == "read") { opts.success({ a: 100, children: [{ x: 42 }, { x: 47 }] }); } }
c.fetch();
Pro
straightforward implementation
Contra
data included in sync, excluding it takes more effort
listening to the Collection impractical: since all consumers would need to unbind/bind
Note: depending on your requirements and API design you may not want children being synced to the server automatically. In this case this solution is limited. You could overwrite toJSON() of the Model but this may limit its usage for other parts of the application (like feeding the data into a view).
Inverse relation: Collection has a Model
Maybe your primary data is actually the Collection. So decorating a Collection with additional data is another approach. This implementation provides one model along the Collection that will be updated upon collection sync.
This implementation is only best suited for fetch of collection data along with attributes (e.g. fetching directory contents with attributes of the directory itself).
var CustomCollection = Backbone.Collection.extend({
initialize: function() {
// maintain decorative attributes of this collection
this.attrs = new Backbone.Model();
},
parse: function(data, opts) {
// remove "children" before setting the remainder to the Model
this.attrs.set(_.omit(data, "children"));
// return the collection content only
return data.children;
}
});
Example usage when testing in a Fiddle or console:
var c = new CustomCollection({ a: 1, b: 2, children: [{ x: 2 }, { x: 3 }] }, { parse: true });
c.reset({ a: 9, b: 11, children: [{ x: 5 }, { x: 10 }] } , { parse: true });
// replace sync() only for fetch() demo - the implementation does what sync() would do on success
c.sync = function(method, coll, opts){ if (method == "read") { opts.success({ a: 100, b: 124, children: [{ x: 42 }, { x: 47 }] }) } }
c.fetch();
Pro
straight forward implementation
delegating model events is easier
Contra
collection data is not part syncing without further implementations
requires parse() to be implemented which
-- in turn requires parse: true to always be passed to reset() and set() and
-- requires parse() to be called with the collection as scope (this) (this could be circumvented by defining parse within initialize bound to this using `bind()ยด)

Collection fetch returns one model but response has all models

I'm new to Backbone and on fetching a collection, I can see the server return all 15 collections. The fetch success returns all 15 models in the response object but the collection object has only the last of the 15 models.
var BracketModel = Backbone.Model.extend({
defaults: {
id: '',
name: '',
title: ''
},
urlRoot: 'http://test.com/bracket/rest.php',
.....
}),
var BracketsCollection = Backbone.Collection.extend({
url: 'http://test.com/bracket/rest.php?op=list',
model: BracketModel,
}),
bracketCollection.fetch({
success: function (collection, response) {
// Collection.models only has one model, response has 15
var bracketsView = new BracketsView({collection: collection});
},
Try
var bracketsView = new BracketsView({collection: response});
Or
var bracketsView = new BracketsView({collection: collection.toJSON()});
I haven't tested it now, but if I remember well, both are equivalent.
The first parameter returns the collection object, which gives you access to different collection attributes. The second parameter returns 'an array containing the attributes hash of each model in the collection', which is likely the thing you are looking for.
The pattern that I usually go with for passing a collection to a view goes like this:
var bracketCollection = new BracketsCollection();
var view = new brackatsView({collection: bracketCollection});
brackCollection.fetch();
Then inside of your view's initialization method do this:
this.listenTo(this.collection, 'sync', this.render);
What this all is doing is creating your collection and your view, and then when you create the view you are telling it about the collection. Calling fetch on the collection is an asynchronous event that will fire a 'sync' even when it is done. The view will listen for this sync event, and when it happens will call the render function.

Backbone Collection Set method remove existing elements and then add all elements

I have a backbone collection.I want to add or remove some models dynamically in the collection. But if i am using collection.set() method then it is going to remove first all elements and then it will add all elements again.
What i want to do is trigger add event of collection for those whose are really new added and trigger remove events for those whose are removed from previous collection.
Here is a example [http://jsfiddle.net/PkJCx/2/]
From the docs
The set method performs a "smart" update of the collection with the
passed list of models. If a model in the list isn't yet in the
collection it will be added; if the model is already in the collection
its attributes will be merged; and if the collection contains any
models that aren't present in the list, they'll be removed.
It is also a good idea to provide `idAttribute' to the model so that the collection identifies that based on the id. Otherwise the collection would not know if the model is a new one or not.
So after setting the id and using set, you can see that is performs a smart update
$(function () {
var MyModel = Backbone.Model.extend({
// This attribute should be set as a default
defaults: {
Name: ''
},
// Set the id attribute so that the collection
// know that it is the old model
idAttribute: 'id'
});
var Coll = Backbone.Collection.extend({
model: MyModel
});
var models = [{
Name: 'A',
id: 1
}, {
Name: 'B',
id: 2
}];
var collection = new Coll(models);
collection.bind('add', function (model) {
alert('addb')
});
collection.bind('remove', function () {
alert('add')
});
models = [{
Name: 'A',
id :1
}, {
Name: 'B',
id: 2
}, {
Name: 'C',
id: 3
}];
collection.add(models);
});
Check Fiddle
It will not try to remove the other 2, but Backbone is smart enough to identify that 2 of them are old models and then just merges the newer one into the collection.

Handling Subsidiary Views in Backbone.js

I have a basic Backbone application which obtain an array of JSON objects from a remote service and displays them: all good so far. However, each JSON object has an array of tags and I want to display the tags in a separate area of the webpage.
My question is: what is the most Backbone-friendly way of doing this? I could parse the existing data again in a second view, which is cleaner but takes up more computation (processing the entire array twice).
An alternative is gathering up the tag information in the primary view as it is working through the array and then passing it along to the subsidiary view, but then I'm linking the views together.
Finally, I'd like to filter based on those tags (so the tags will become toggle buttons and turning those buttons on/off will filter the information in the primary view); does this make any difference to how this should be laid out?
Bonus points for code snippets.
Hm. I'm not sure if this is the Backbone-friendly way, but I'll put the logic to retrieve a list of tags (I think that's what you meant by "parse") in the collection.
Both the main view and the subview will "listen" to the same collection, and the subview will just call collection.getTags() to get a list of tags it needs.
// Model that represents the list data
var ListDataModel = Backbone.Model.extend({
defaults: function() {
return {
name: null,
tags: []
};
}
});
// Collection of list data
var ListDataCollection = Backbone.Collection.extend({
model: ListDataModel,
initialize: function() {
var me = this;
// Expires tag collection on reset/change
this.on('reset', this.expireTagCache, this);
this.on('change', this.expireTagCache, this);
},
/**
* Expires tag cache
* #private
*/
expireTagCache: function() {
this._cachedTags = null;
},
/**
* Retrieves an array of tags in collection
*
* #return {Array}
*/
getTags: function() {
if (this._cachedTags === null) {
this._cachedTags = _.union.apply(this, this.pluck('tags'));
}
return this._cachedTags;
},
sync: function(method, model, options) {
if (method === 'read') {
var me = this;
// Make an XHR request to get data for this demo
Backbone.ajax({
url: '/echo/json/',
method: 'POST',
data: {
// Feed mock data into JSFiddle's mock XHR response
json: JSON.stringify([
{ id: 1, name: 'one', tags: [ 'number', 'first', 'odd' ] },
{ id: 2, name: 'two', tags: [ 'number', 'even' ] },
{ id: 3, name: 'a', tags: [ 'alphabet', 'first' ] }
]),
},
success: function(resp) {
options.success(me, resp, options);
},
error: function() {
if (options.error) {
options.error();
}
}
});
}
else {
// Call the default sync method for other sync method
Backbone.Collection.prototype.sync.apply(this, arguments);
}
}
});
var listColl = new ListDataCollection();
listColl.fetch({
success: function() {
console.log(listColl.getTags());
}
});
I guess two reasons for handling this in the collection:
It keeps the View code cleaner (This is given that we are not doing very complex logic in the tag extraction - It's just a simple _.pluck() and _.union().
It has 0 business logic involved - It can arguably belong to the data layer.
To address the performance issue:
It does go through the collection twice - However, if the amont of data you are consuming is too much for the client to process even in this case, you may want to consider asking the Backend to provide an API endpoint for this. (Even 500 pieces of data with a total of 1000 tags shouldn't bee too much for a somewhat modern browser to handle nowadays.)
Hmm. Does this help?
JSFiddle to go with this with the collection and the model: http://jsfiddle.net/dashk/G8LaB/ (And, a log statement to demonstrate the result of .getTags()).

Backbone sorting procedure. UI vs Data

My question is about the proper way to show a list of records using backbone. Lets say you have a person model that you want to display to the user and allow them to sort by first name, last name, id....
The first instinct is to just have the view catch the event and re-render based on the users sort option. The problem with this method is that it is U.I. driven and not data driven.
Second thoughts are to set the sorting attributes in the model since the collection does not contain attributes (though that seems would be the best option). This method is at least data driven by setting the sorting attributes but is horribly redundant and if the sorting attributes are not stripped out on save they are sent to the server | local or...
Last thought is probably the correct one. Create a second model that would be a control model used contain sorting/displaying properties. My issues with this method is events and models can get very unruly. If you expand beyond just a person model and make this a fairly large app you have a LOT of models and events and gets hard to manage. The model-1 view has to catch the initial event, then have the collection trigger a custom event then the second model has to catch the custom event and render it.
Sorry for the long post, I am fairly new to backbone js and want to make sure I have the best practice grasp. Thanks in advance for the help. I hope I am at least on the correct track.
I just implemented this last night.
You can set a new collection comparator and then use the sort method on the collection. sort will fire a reset event, which you can use in your view to re-render the list.
Here is my view, which contains a select box that allows the user to choose how to sort the data:
App.HouseListView = Backbone.View.extend({
el: '.house-list',
initialize: function() {
App.houseCollection.bind('reset', this.populateList, this);
},
events: {
'change .sort':'sort',
},
populateList: function(collection) {
this.$('ul').html('');
_.each(collection.models, function(model) {
var view = new App.HouseListElemView({model:model});
this.$('ul').append(view.el);
});
},
sort: function(e) {
var sort_by = $(e.srcElement.selectedOptions[0]).attr('data-sort-by');
App.houseCollection.comparator = function(house) {
return house.get(sort_by);
}
App.houseCollection.sort();
},
});
Hope this helps
EDIT: Implemented #mu is too short's suggestion:
App.Houses = Backbone.Collection.extend({
model: App.House,
url: API_URL,
_sort_by: 'price',
sort_houses_by: function(sort_by) {
this._sort_by = sort_by;
this.sort();
},
comparator: function(house) {
return house.get(this._sort_by);
},
});
App.houseCollection = new App.Houses();
App.HouseListView = Backbone.View.extend({
el: '.house-list',
initialize: function() {
App.houseCollection.bind('reset', this.populateList, this);
},
events: {
'change .sort':'sort',
},
populateList: function(collection) {
this.$('ul').html('');
_.each(collection.models, function(model) {
var view = new App.HouseListElemView({model:model});
this.$('ul').append(view.el);
});
},
sort: function(e) {
var sort_by = $(e.srcElement.selectedOptions[0]).data('sort-by');
App.houseCollection.sort_houses_by(sort_by);
},
});

Resources