Multiple collections tied to one base collection with filters and eventing - backbone.js

I have a complex model served from my back end, which has a bunch of regular attributes, some nested models, and a couple of collections.
My page has two tables, one for invalid items, and one for valid items. The items in question are from one of the nested collections. Let's call it baseModel.documentCollection, implementing DocumentsCollection.
I don't want any filtration code in my Marionette.CompositeViews, so what I've done is the following (note, duplicated for the 'valid' case):
var invalidDocsCollection = new DocumentsCollection(
baseModel.documentCollection.filter(function(item) {
return !item.isValidItem();
})
);
var invalidTableView = new BookIn.PendingBookInRequestItemsCollectionView({
collection: app.collections.invalidDocsCollection
});
layout.invalidDocsRegion.show(invalidTableView);
This is fine for actually populating two tables independently, from one base collection. But I'm not getting the whole event pipeline down to the base collection, obviously. This means when a document's validity is changed, there's no neat way of it shifting to the other collection, therefore the other view.
What I'm after is a nice way of having a base collection that I can have filter collections sit on top of. Any suggestions?

I fleshed out my previous attempt and have come up with an extension to Backbone.Collection that does what I need.
collections.FilteredCollection = Backbone.Collection.extend({
initialize: function(items, options) {
if (_.isUndefined(options.baseCollection))
throw "No base collection to watch";
if (!_.isFunction(options.filterFunc)) {
throw "No filter to apply";
}
_.extend(this, options);
this.listenTo(this.baseCollection, 'all', this.reraise);
},
reraise: function (event) {
this.reset(this.baseCollection.filter(this.filterFunc), { silent: true });
var args = [].slice.call(arguments, 1);
this.trigger(event, args);
}
});
The one small issue I have with this is that I have to manually apply filterFunc to the baseCollection, then pass that in as the items parameter when instantiating a FilteredCollection, but that's something I can live with.
The below code is what I'm using to instantiate. Note that there's another almost-exact copy which is for the collection of ONLY VALID items, but any filters can be applied.
var allDocs = theModel.get('Documents');
var invalidOptions = {
baseCollection: allDocs,
filterFunc: function(item) {
return !item.isValidItem();
}
};
var invalidDocs = allDocs.filter(invalidOptions.filterFunc);
var invalidDocsCollection = new collections.FilteredCollection(
invalidDocs, invalidOptions
);

Related

backbone.js set in model initialize not effecting models in collection

While performing a fetch() on my backbone collection, and instantiating models as children of that collection, I want to add one more piece of information to each model.
I thought that I could do this using set in the model initialize. (My assumption is that fetch() is instantiating a new model for each object passed into it. And therefore as each initialize occurs the extra piece of data would be set.
To illustrate my problem I've pasted in four snippets, first from my collection class. Second the initialize function in my model class. Third, two functions that I use in the initialize function to get the needed information from the flickr api. Fourth, and finally, the app.js which performs the fetch().
First the collection class:
var ArmorApp = ArmorApp || {};
ArmorApp.ArmorCollection = Backbone.Collection.extend({
model: ArmorApp.singleArmor,
url: "https://spreadsheets.google.com/feeds/list/1SjHIBLTFb1XrlrpHxZ4SLE9lEJf4NyDVnKnbVejlL4w/1/public/values?alt=json",
//comparator: "Century",
parse: function(data){
var armorarray = [];
var entryarray = data.feed.entry;
for (var x in entryarray){
armorarray.push({"id": entryarray[x].gsx$id.$t,
"Filename": entryarray[x].gsx$filename.$t,
"Century": entryarray[x].gsx$century.$t,
"Date": entryarray[x].gsx$date.$t,
"Country": entryarray[x].gsx$country.$t,
"City": entryarray[x].gsx$city.$t,
"Type": entryarray[x].gsx$type.$t,
"Maker": entryarray[x].gsx$maker.$t,
"Recepient": entryarray[x].gsx$recipient.$t,
"Flickrid": entryarray[x].gsx$flickrid.$t,
"FlickrUrl": "", //entryarray[x].gsx$flickrurl.$t,
"FlickrUrlBig": ""//entryarray[x].gsx$flickrurlbig.$t,
});
}
return armorarray;
}
});
Second, the initialization in my model.
initialize: function(){
//console.log("A model instance named " + this.get("Filename"));
item = this;
var flickrapi = "https://api.flickr.com/services/rest/?&method=flickr.photos.getSizes&api_key=<my_apikey>&photo_id=" + this.get("Flickrid") + "&format=json&jsoncallback=?";
sources = getFlickrSources(flickrapi);
sources.then(function(data){
sourceArray = parseFlickrResponse(data);
FlickrSmall = sourceArray[0].FlickrSmall;
console.log (FlickrSmall);
item.set("FlickrUrl", FlickrSmall);
console.log(item);
});
Notice here how I'm getting the "Flickrid" and using to get one more piece of information and then trying to add it back into the model with item.set("FlickrUrl", FlickerSmall);
console.log confirms that the property "FlickrUrl" has been set to the desired value.
Third, these are the functions my model uses to get the information it needs for the flicker api.
var getFlickrSources = function(flickrapi){
flickrResponse = $.ajax({
url: flickrapi,
// The name of the callback parameter, as specified by the YQL service
jsonp: "callback",
// Tell jQuery we're expecting JSONP
dataType: "jsonp"})
return flickrResponse;
}
var parseFlickrResponse = function(data){
flickrSourceArray = []
if (data.stat == "ok"){
sizeArray = data.sizes.size;
for (var y in sizeArray){
if (sizeArray[y].label == "Small"){
flickrSourceArray.push({"FlickrSmall": sizeArray[y].source});
}
else if (sizeArray[y].label == "Large"){
flickrSourceArray.push({"FlickrLarge": sizeArray[y].source});
}
}
}
return flickrSourceArray
}
But, fourth, when I try to perform the fetch and render the collection, I only get objects in my collection without the FlickrUrl property set.
//create an array of models and then pass them in collection creation method
var armorGroup = new ArmorApp.ArmorCollection();
armorGroup.fetch().then(function(){
console.log(armorGroup.toJSON());
var armorGroupView = new ArmorApp.allArmorView({collection: armorGroup});
$("#allArmor").html(armorGroupView.render().el);
});
var armorRouter = new ArmorApp.Router();
Backbone.history.start();
The console.log in this last snippet prints out all the objects/models supposedly instantiated through the fetch. But none of them include the extra property that should have been set during the initialization.
Any ideas what is happening?
What is this function ? getFlickrSources(flickrapi)
Why are you using this.get in the initialize function. Honestly it looks over-complicated for what you are trying to do.
If you want to set some parameter when you instantiate your model then do this var model = new Model({ param:"someparam", url:"someurl",wtv:"somewtv"});
If the point is to update your model just write an update function in your model something like update: function (newparam) { this.set;... etc and call it when you need it.
If I read you well you just want to set some params when your model is instantiated, so just use what I specified above. Here is some more doc : http://backbonejs.org/#Model-constructor
I hope it helps.
edit:
Put your call outside your model, you shouldn't (imo) make call inside your model this way it seems kinda dirty.
Sources.then(function(flickrdata) {
var mymodel = new Model({flicker:flickrdata.wtv});
});
It would be cleaner in my opinion.

How to define a Backbone.Collection to be a property but not an attribute of a Backbone.Model?

Say I have a web application which allows to filter a set of items and say that I'm using backbone.js in the web frontend.
Quite naturally I ended up creating a Filter which extends Backbone.Model and a SearchResultList extending Backbone.Collection. The Filter has some attributes like searchTerm, dataFrom, dateTo... Filter also has a method called applyFilter. Filter.applyFilter should call searchResultList.fetch, thus updating/filtering the search results.
The question is, how to best initialize searchResultList as a property of Filter instead of being an attribute in Backbone terms?
I would not like to create the SearchResultList in Filter.initialize as SearchResultList is not necessarily "owned" by Filter.
For now I ended up passing the searchResultList object as an option to Filter.initialize, but that feels a little bit awkward to me.
I doubt that it is a good idea to let Filter.applyFilter call SearchResultList.fetch. However, there needs to be a method of Filter which when called triggers a new request somehow. (Listening to change events of Filter is not an option either as the user is supposed to change filter options in multiple steps and decides to apply the filter manually via a button click)
I have a Collection for performing searches. Most of my collections extend this Search Collection.
It has an object in it holding parameters for searching. The user of the collection can call setSearchParam, and I have a method buildSearchString that will build the search part of the URL to call (I use the Matrix parameters in my REST urls).
window.SearchCollection = Backbone.Collection.extend({
initialize : function() {
this.params = {}; // holds parameters for searching
},
// Set a search param
setSearchParam: function(param, value) {
if (typeof value === 'undefined') return;
if (typeof this.params === 'undefined') this.params = {};
this.params[param] = value;
},
setSearchParams : function(paramMap) {
_.extend(this.params, paramMap);
},
// Build Matrix params for search. This could just as easily be a standard URL string
buildSearchString: function() {
var search = [];
var i = 0;
for (var key in this.params) {
var value = this.params[key];
if (utils.isEmpty(value)) continue;
search[i++] = ";";
search[i++] = key;
search[i++] = "=";
search[i++] = value;
}
return search.join('');
}
});
Then when I create a collection, I extend it:
window.BookingCollection = window.SearchCollection.extend({
model: Booking,
url: function(){
var urlString = BASE_REST_URL + "bookings/";
// Build the matrix params for the REST URL
urlString += this.buildSearchString();
return urlString;
}
});
Then, a user of this collection can use it like so:
var bookings = new BookingCollection();
bookings.setSearchParam("name", "hello");

In Backbone.js how do you handle Ordinal Numbering and changing numbering based on jQuery Sortable in the view?

Here is my situation. I have a bunch of "Question" model inside a "Questions" collection.
The Question Collection is represented by a SurveyBuilder view.
The Question Model is represented by a QuestionBuilder view.
So basically you have an UL of QuestionBuilder views. The UL has a jQuery sortable attached (so you can reorder the questions). The question is once I'm done reordering I want to update the changed "question_number"s in the models to reflect their position.
The Questions collection has a comparator of 'question_number' so collection should be sorted. Now I just need a way to make their .index() in the UL reflect their question_number. Any ideas?
Another problem is DELETEing a question, I need to update all the question numbers. Right now I handle it using:
var deleted_number = question.get('question_number');
var counter = deleted_number;
var questions = this.each(function(question) {
if (question.get('question_number') > deleted_number) {
question.set('question_number', question.get('question_number') - 1);
}
});
if (this.last()) {
this.questionCounter = this.last().get('question_number') + 1;
} else {
this.questionCounter = 1;
}
But it seems there's got to be a much more straighforward way to do it.
Ideally whenever a remove is called on the collection or the sortstop is called on the UL in the view, it would get the .index() of each QuestionuBuilder view, update it's models's question_number to the .index() + 1, and save().
My Models,Views, and Collections: https://github.com/nycitt/node-survey-builder/tree/master/app/js/survey-builder
Screenshot: https://docs.google.com/file/d/0B5xZcIdpJm0NczNRclhGeHJZQkE/edit
More than one way to do this but I would use Backbone Events. Emit an event either when the user clicks something like done sorting, hasn't sorted in N seconds, or as each sort occurs using a jQuery sortable event such as sort. Listen for the event inside v.SurveyBuilder.
Then do something like this. Not tested obviously but should get you there relatively easily. Update, this should handle your deletions as well becuase it doesn't care what things used to be, only what they are now. Handle the delete then trigger this event. Update 2, first examples weren't good; so much for coding in my head. You'll have to modify your views to insert the model's cid in a data-cid attribute on the li. Then you can update the correct model using your collection's .get method. I see you've found an answer of your own, as I said there are multiple approaches.
v.SurveyBuilder = v.Page.extend({
template: JST["app/templates/pages/survey-builder.hb"],
initialize: function() {
this.eventHub = EventHub;
this.questions = new c.Questions();
this.questions.on('add', this.addQuestion, this);
this.eventHub.on('questions:doneSorting', this.updateIndexes)
},
updateIndexes: function(e) {
var that = this;
this.$('li').each(function(index) {
var cid = $(this).attr('data-cid');
that.questions.get(cid).set('question_number', index);
});
}
I figured out a way to do it!!!
Make an array of child views under the parent view (in my example this.qbViews maintains an array of QuestionBuilder views) for the SurveyBuilder view
For your collection (in my case this.questions), set the remove event using on to updateIndexes. That means it will run updateIndexes every time something is removed from this.questions
In your events object in the parent view, add a sortstop event for your sortable object (in my case startstop .question-builders, which is the UL holding the questionBuilder views) to also point to updateIndexes
In updateIndexes do the following:
updateIndexes: function(){
//Go through each of our Views and set the underlying model's question_number to
//whatever the index is in the list + 1
_.each(this.qbViews, function(qbView){
var index = qbView.$el.index();
//Only actually `set`s if it changed
qbView.model.set('question_number', index + 1);
});
},
And there is my full code for SurveyBuilder view:
v.SurveyBuilder = v.Page.extend({
template: JST["app/templates/pages/survey-builder.hb"],
initialize: function() {
this.qbViews = []; //will hold all of our QuestionBuilder views
this.questions = new c.Questions(); //will hold the Questions collection
this.questions.on('add', this.addQuestion, this);
this.questions.on('remove', this.updateIndexes, this); //We need to update Question Numbers
},
bindSortable: function() {
$('.question-builders').sortable({
items: '>li',
handle: '.move-question',
placeholder: 'placeholder span11'
});
},
addQuestion: function(question) {
var view = new v.QuestionBuilder({
model: question
});
//Push it onto the Views array
this.qbViews.push(view);
$('.question-builders').append(view.render().el);
this.bindSortable();
},
updateIndexes: function(){
//Go through each of our Views and set the underlying model's question_number to
//whatever the index is in the list + 1
_.each(this.qbViews, function(qbView){
var index = qbView.$el.index();
//Only actually `set`s if it changed
qbView.model.set('question_number', index + 1);
});
},
events: {
'click .add-question': function() {
this.questions.add({});
},
//need to update question numbers when we resort
'sortstop .question-builders': 'updateIndexes'
}
});
And here is the permalink to my Views file for the full code:
https://github.com/nycitt/node-survey-builder/blob/1bee2f0b8a04006aac10d7ecdf6cb19b29de8c12/app/js/survey-builder/views.js

Proper way to sort a backbone.js collection on the fly

I can successfully do this:
App.SomeCollection = Backbone.Collection.extend({
comparator: function( collection ){
return( collection.get( 'lastName' ) );
}
});
Which is nice if I want to have a collection that is only sorted by 'lastName'. But I need to have this sorting done dynamically. Sometimes, I'll need to sort by, say, 'firstName' instead.
My utter failures include:
I tried passing an extra variable specifying the variable to sort() on. That did not work. I also tried sortBy(), which did not work either. I tried passing my own function to sort(), but this did not work either. Passing a user-defined function to sortBy() only to have the result not have an each method, defeating the point of having a newly sorted backbone collection.
Can someone provide a practical example of sorting by a variable that is not hard coded into the comparator function? Or any hack you have that works? If not, a working sortBy() call?
Interesting question. I would try a variant on the strategy pattern here. You could create a hash of sorting functions, then set comparator based on the selected member of the hash:
App.SomeCollection = Backbone.Collection.extend({
comparator: strategies[selectedStrategy],
strategies: {
firstName: function () { /* first name sorting implementation here */ },
lastName: function () { /* last name sorting implementation here */ },
},
selectedStrategy: "firstName"
});
Then you could change your sorting strategy on the fly by updating the value of the selectedStrategy property.
EDIT: I realized after I went to bed :) that this wouldn't quite work as I wrote it above, because we're passing an object literal to Collection.extend. The comparator property will be evaluated once, when the object is created, so it won't change on the fly unless forced to do so. There is probably a cleaner way to do this, but this demonstrates switching the comparator functions on the fly:
var SomeCollection = Backbone.Collection.extend({
comparator: function (property) {
return selectedStrategy.apply(myModel.get(property));
},
strategies: {
firstName: function (person) { return person.get("firstName"); },
lastName: function (person) { return person.get("lastName"); },
},
changeSort: function (sortProperty) {
this.comparator = this.strategies[sortProperty];
},
initialize: function () {
this.changeSort("lastName");
console.log(this.comparator);
this.changeSort("firstName");
console.log(this.comparator);
}
});
var myCollection = new SomeCollection;
Here's a jsFiddle that demonstrates this.
The root of all of your problems, I think, is that properties on JavaScript object literals are evaluated immediately when the object is created, so you have to overwrite the property if you want to change it. If you try to write some kind of switching into the property itself it'll get set to an initial value and stay there.
Here's a good blog post that discusses this in a slightly different context.
Change to comparator function by assigning a new function to it and call sort.
// Following example above do in the view:
// Assign new comparator
this.collection.comparator = function( model ) {
return model.get( 'lastname' );
}
// Resort collection
this.collection.sort();
// Sort differently
this.collection.comparator = function( model ) {
return model.get( 'age' );
}
this.collection.sort();
So, this was my solution that actually worked.
App.Collection = Backbone.Collection.extend({
model:App.Model,
initialize: function(){
this.sortVar = 'firstName';
},
comparator: function( collection ){
var that = this;
return( collection.get( that.sortVar ) );
}
});
Then in the view, I have to M-I-C-K-E-Y M-O-U-S-E it like this:
this.collections.sortVar = 'lastVar'
this.collections.sort( this.comparator ).each( function(){
// All the stuff I want to do with the sorted collection...
});
Since Josh Earl was the only one to even attempt a solution and he did lead me in the right direction, I accept his answer. Thanks Josh :)
This is an old question but I recently had a similar need (sort a collection based on criteria to be supplied by a user click event) and thought I'd share my solution for others tackling this issue. Requires no hardcoded model.get('attribute').
I basically used Dave Newton's approach to extending native JavaScript arrays, and tailored it to Backbone:
MyCollection = Backbone.Collection.extend({
// Custom sorting function.
sortCollection : function(criteria) {
// Set your comparator function, pass the criteria.
this.comparator = this.criteriaComparator(criteria);
this.sort();
},
criteriaComparator : function(criteria, overloadParam) {
return function(a, b) {
var aSortVal = a.get(criteria);
var bSortVal = b.get(criteria);
// Whatever your sorting criteria.
if (aSortVal < bSortVal) {
return -1;
}
if (aSortVal > bSortVal) {
return 1;
}
else {
return 0;
}
};
}
});
Note the "overloadParam". Per the documentation, Backbone uses Underscore's "sortBy" if your comparator function has a single param, and a native JS-style sort if it has two params. We need the latter, hence the "overloadParam".
Looking at the source code, it seems there's a simple way to do it, setting comparator to string instead of function. This works, given Backbone.Collection mycollection:
mycollection.comparator = key;
mycollection.sort();
This is what I ended up doing for the app I'm currently working on. In my collection I have:
comparator: function(model) {
var methodName = applicationStateModel.get("comparatorMethod"),
method = this[methodName];
if (typeof(method === "function")) {
return method.call(null, model);
}
}
Now I can add few different methods to my collection: fooSort(), barSort(), and bazSort().
I want fooSort to be the default so I set that in my state model like so:
var ApplicationState = Backbone.Model.extend({
defaults: {
comparatorMethod: "fooSort"
}
});
Now all I have to do is write a function in my view that updates the value of "comparatorMethod" depending upon what the user clicks. I set the collection to listen to those changes and do sort(), and I set the view to listen for sort events and do render().
BAZINGA!!!!

How can I pull any class by its cid in Backbone?

In using Backbone.js, I've noticed that both views and models are given cids. I understand that if these classes are part of a collection, I can pull any of them by collection.getByCid. Is it at all possible to pull any class, outside of a collection, given its cid, using Backbone?
For example, if I have MyObject.Views.Tree = Backbone.View.extend({ });, I can create a new Tree view from var tree = new MyObject.Views.Tree();. Calling tree.cid returns a specific cid--something like view231. Is there any way to reference my tree view given only its cid? A global Backbone.getByCid method, perhaps?
ExtJS spoiled me and I felt the need to recreate something similar for Backbone. Maybe this will help you out too? I haven't tested it too much, but it's a very simple change. Just be careful of creating lots of things and not removing them, or you'll have a bunch of registered objects eating up memory.
Backbone.View.Registry = {
items: {},
register: function (id, object) {
this.items[id] = object;
},
lookup: function (id) {
return this.items[id] || null;
},
remove: function (id) {
delete this.items[id];
}
}
Backbone.RegisteredView = Backbone.View.extend({
initialize: function () {
Backbone.View.prototype.initialize.apply(this);
this.cid = this.options.id || this.cid; //just in case you want to assign a unique ID and lookup from that.
Backbone.View.Registry.register(this.cid, this);
},
remove: function () {
Backbone.View.prototype.remove.apply(this);
Backbone.View.Registry.remove(this.cid);
return this;
}
});
test = Backbone.RegisteredView.extend({
intialize: function () {
return $("<div></div>"); //Just return something for this example
}
});
div1 = new test({id: 'header_pane'});
div2 = new test();
console.log(Backbone.View.Registry.items); //Will have the header_pane and a cid based obj in it
ref = Backbone.View.Registry.lookup('header_pane');
console.log(ref); //Will be the header_pane object
div1.remove();
console.log(Backbone.View.Registry.items); //Will only have the cid based object in it
No.
I think you have a slight misunderstanding of the backbone programming model, as well as JavaScript in general. Backbone doesn't keep track of what you create; it only helps you create objects with specific prototypes (Models, Collections, etc.). It doesn't care at all what you do with them. The CID is just a convenience method you can use for indexing and cross-referencing, but you have to write the indices and cross-references yourself.
So if you create an object and don't keep a reference to it somewhere (in a collection, in your router, in another object), it becomes inaccessible and the JavaScript VM will eventually garbage collect it.

Resources