I am trying to add a dynamic sort by to my collection using backbone.js.
At initialization the collection has the default sorting and the view is rendered. I made a button to test how to change the sorting. It calls the following function:
app.productList.comparator = function(product) {
return parseFloat(product.get("price"));
};
app.productList.sort();
If I understand correctly the Collection should now be sorted but the view still needs to be refreshed. I read in the documentation and in this topic to listen for the sort event
window.ProductCollection = Backbone.Collection.extend({
model:Product,
localStorage: new Backbone.LocalStorage("ProductCollection"),
events:{
"sort":"test"
},
test:function(){
alert('test');
}
});
For testing purposes I added a simple alert but this is not displayed. So it seems the sort event was not triggered.
Any ideas what I'm doing wrong here?
Backbone.Collection doesn't take into account a events hash like Backbone.View does, you have to bind events yourself. You could use the initialize method to do so, something like
var ProductCollection = Backbone.Collection.extend({
model:Product,
initialize: function() {
this.on('sort', this.test);
},
test: function(){
console.log('test');
}
});
And a demo http://jsfiddle.net/nikoshr/fTwpf/
By default there is no comparator for a collection. If you define a comparator, it will be used to maintain the collection in sorted order. This means that as models are added, they are inserted at the correct index in collection.models. A comparator can be defined as a sortBy (pass a function that takes a single argument), as a sort (pass a comparator function that expects two arguments), or as a string indicating the attribute to sort by.
"sortBy" comparator functions take a model and return a numeric or string value by which the model should be ordered relative to others. "sort" comparator functions take two models, and return -1 if the first model should come before the second, 0 if they are of the same rank and 1 if the first model should come after.
Note how even though all of the chapters in this example are added backwards, they come out in the proper order:
var Chapter = Backbone.Model;
var chapters = new Backbone.Collection;
chapters.comparator = function(chapter) {
return chapter.get("page");
};
chapters.add(new Chapter({page: 9, title: "The End"}));
chapters.add(new Chapter({page: 5, title: "The Middle"}));
chapters.add(new Chapter({page: 1, title: "The Beginning"}));
alert(chapters.pluck('title'));
Collections with a comparator will not automatically re-sort if you later change model attributes, so you may wish to call sort after changing model attributes that would affect the order.
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.
Related
I want to add attribute to a JS object, but in a custom place, After a given attribute.
var me = {
name: "myname",
age: "myage",
bday: "mybday"
};
me["newAt"] = "kkk"; //this adds at the end of the object
Is there a way to specify the object (me), an attribute(age) in it and add a new attribute(newAt) right after the specified one? A better way than doing string operations?
var newMe = {
name: "myname",
age: "myage",
newAt: "newAttr",
bday: "mybday"
}
UPDATE: (Since people are more focused on why I'm asking this than actually answering it)
I'm working on a drawable component based on user input - which is a JS object. And it has the ability to edit it - so when the user adds a new property based on "add new node" on the clicked node, and I was thinking of adding the new node right after it. And I want to update the data accordingly.
JavaScript object is an unordered list of properties. The order is not defined and may vary when using with an iterator like for in. You shouldn't base your code on the order of properties you see in debugger or console.
JavaScript objects do, as of ES2015, have an order to their properties, although that order is only guaranteed to be used by certain operations (Object.getOwnPropertyNames, Reflect.ownKeys, etc.), notably not for-in or Object.keys for legacy reasons. See this answer for details.
But you should not rely on that order, there's no point to it, it's more complicated than it seems initially, and it's very hard to manipulate (you basically have to create a new object to set the order of its properties). If you want order, use an array.
Re your edit:
I'm working on a drawable component based on user input - which is a JS object. And it has the ability to edit it - so when the user adds a new property based on "add new node" on the clicked node, and I was thinking of adding the new node right after it. And I want to update the data accordingly.
The best way to do that is, if you want a specific order, keep the order of keys in an array and use that to show the object.
While you could use ES2015's property order for it, to do so you'd have to:
Require your users use a truly ES2015-compliant browser, because this cannot be shimmed/polyfilled
Destroy the object and recreate it adding the properties in the specific order you want each time you add a property
Forbid properties that match the specification's definition of an array index
It's just much more work and much more fragile than keeping the order in an array.
The simplest solution I could find was to iterate through the keys of the parent and keep pushing them to form a clone of the parent. But to additionally push the new object if the triggered key is met.
var myObj = {
child1: "data1",
child2: "data2",
child3: "data3",
child4: "data4"
};
var a = (function addAfterChild(data, trigChild, newAttribute, newValue) {
var newObj = {};
Object.keys(data).some(function(k) {
newObj[k] = data[k];
if (k === trigChild) {
newObj[newAttribute] = newValue;
}
});
return newObj;
})(myObj, "child3", "CHILD", "VALUE");
document.getElementById("result").innerHTML = JSON.stringify(a);
<p id="result"></p>
I have DocPad documents that look like this:
---
categories: [{slug: ""}, {slug: ""}, ...]
---
Document content.
How can I query all documents which have a predefined slug value in the meta.category array?
There's a few ways we can go about this.
Via a Template Helper and Query Engine's setFilter
https://gist.github.com/4556245
The most immediate way is via a getDocumentsWithCategory template helper that utilises Query Engine's setFilter call that allows us to specify a custom function (our filter) that will be executed against each model in the collection and based on it's boolean return value keep the model or remove it from the collection.
The downsides of this solution are:
We have to redefine our category information in each post
We have no immediate method of being able to get information about all the categories available to us
Via Template Helpers and the parseAfter event
https://gist.github.com/4555732
If we wanted to be able to get information on all the categories available to us, but still had the requirement of defining our categories every single time for each post, then we can use the parseAfter event to hook into the meta data for our documents, extract the categories out into a global categories object, and then update our document's categories with id references instead.
Downside is that we still have to have redundant category information.
Via Template Helpers and a global categories listing
https://gist.github.com/4555641
If we wanted to only define our category information once, and then just reference the categories ids within our posts, then this solution is most ideal.
I manage to create a "foo" collection for a given slug value "bar in docpad.coffee:
collections:
foo: (database) ->
database.findAllLive().setFilter("search", (model, value) ->
categories = model.get('categories')
return false unless Array.isArray categories
for category in categories
if (category.slug and category.slug == value)
return true
return false
).setSearchString("bar")
but when I try to create a helper function that returns a collection given the array meta ("categories"), the key ("slug") and value ("bar") of the object :
createCollection: (meta, key, value) ->
result = #getDatabase().createLiveChildCollection()
.setFilter("search2", (model, searchString) ->
objects = model.get(meta)
return false unless Array.isArray objects
for object in objects
if (object[key] and object[key] == searchString)
return true
return false
).setSearchString(value)
i get an empty collection when i try to call it.
I don't have a drop in solution, but have you seen the source code for the blog of docpad's author? There are some examples of querying for the existence of a metadata object attribute. Hope that helps!
I'm working with several backbone collections and sometimes I need to access parts of them based on some criteria.
METHOD 1
As already stated in this question, using filter() on the collection itself returns an array of models and not another collection. This can work in simple cases, but it has the effect of losing collection's method concatenation as a plain array of models won't have all methods defined in the collection.
METHOD 2
The answer to that question suggested creating a new collection passing the array of models to the constructor. This works but has the side effect of calling the collection's constructor every time, so any event binding that might be defined there gets stacked on every time you filter the collection.
So what's the correct way to create a sub-collection based on some filter criteria?
Should I use method 1 and create more filtering methods instead on relying on method chaining?
Should I go with method 2 and avoid binding events in the collection's constructor?
Personally I would create more filtering methods on the collection, because it has the additional benefit of encapsulating logic inside the collection.
You could also try to reuse the existing collection. I was toying around with the idea, and arrived at something like this:
var Collection = Backbone.Collection.extend({
//Takes in n arrays. The first item of each array is the method you want
//to call and the rest are the arguments to that method.
//Sets the collection.models property to the value of each successive filter
//and returns the result of the last. Revers the collection.models to its original value.
chainFilters: function(/*args..*/) {
var models = this.models;
try {
filters = _.toArray(arguments);
_.each(filters, function(filter) {
this.models = filter[0].apply(this, _.rest(filter));
}, this);
} catch(err) {
this.models = models;
throw err;
}
var filtered = this.models;
this.models = models;
return filtered;
}
});
Usage:
var results = collection.chainFilters(
[ collection.filter, function(model) { return model.get('name') === 'foo'; } ],
[ collection.someMethod, 'someargument' ],
[ collection.someOtherMethod ]
);
Here's a working sample. It's a bit peculiar, I know.
It depends on the use case. If you want those models to update a view then you probably want a new collection as otherwise you don't get the nice reactive template updates. If you simply wanted the models to iterate through or manipulate data without worrying about the data updating then use the array + underscore.js.
Try it with the arrays and if you find yourself writing a lot of boiler plate code with features already in a collection but not in underscore.js, just start using a collection.
I have a parent Backbone Model that contains two objects.
(1) An array of Backbone Models
(2) A string
If I bind to the parent, setting the value of the string does trigger the change event, however calling set on an attribute of one of the models in the array of models does not trigger the change event on the parent.
How do I fix this so that any change to any of the models in the array triggers the parents change event?
EDIT -- Added Code by request
var myModel = Backbone.Model.extend(
{
defaults : {
models : [],
aString: 'foobar'
}
}
);
var foo = new myModel();
var arrayElement = Backbone.Model.extend({x: 7});
var arrayElement1 = new arrayElement({x: 7});
foo.set('models', [arrayElement1]);
foo.bind('change', function() { console.log('changed!')});
arrayElement1.set('x', 10); //Does not trigger console log
foo.set('aString', 'barfoo'); //Does trigger console log
Backbone models don't bind anything to their attributes so foo has no way of knowing that you are changing one of its attributes behind its back. So, when you do this:
foo.set('models', [some_other_model]);
some_other_model.set(...);
you haven't actually changed foo at all, all you've done is changed one of foo's attributes directly. A model's attributes can be anything, the model simply treats them as opaque blobs. You'll have similar problems with something like this:
o = { a: 'b' };
m.set('p', o);
o.a = 'c';
In both cases, you're directly changing a model's attribute through a reference rather than through the model's interface.
Collections, on the other hand, do listen for events on their models. Collections are collections of models so they expect their members to be models and behave accordingly.
If you want a contained model to propagate 'change' events then you'll have to do it yourself by, perhaps, overriding set to manually bind change handlers to propagate the events. You could also use an internal collection instead of an array to make propagating the events easier.
You also have a hidden bug in your defaults. The defaults are copied to new model instances but the copy is a shallow copy so your models will end up sharing the same reference to the array unless an explicit set is done to replace the reference. For example, this:
var M = Backbone.Model.extend({
defaults: {
a: []
}
});
var m1 = new M();
m1.get('a').push('pancakes');
console.log(M.prototype.defaults.a);
var m2 = new M();
console.log(m2.get('a'));
will put two ['pancakes'] in the console because m1.get('a') will return M.prototype.defaults.a rather than a new empty array that is specific to m1: http://jsfiddle.net/ambiguous/AraCu/
I'm taking my first steps with Backbone.js, and one of those involves being able to remove an item from a collection, and more importantly, retrieve that item. The Backbone.Collection.remove method simply returns the original collection with the item removed, so at the moment I'm obtaining a reference to the desired item prior to removal:
var Collection = Backbone.Collection.extend(...array of Backbone.Models...),
removedItem = Collection.get(3);
console.log(Collection.remove(3));//same collection sans #3
My question is if there is a short hand method for retrieving the remove item?
Edit: JFTR, I've read a fair bit of the source, and know that the original method returns a reference to the collection -
remove: function(models, options) {
// <snip for brevity>
// chain pattern incoming
return this;
},
It seemed odd to me that it didn't return the removed item., so I was just wondering if there was another method I'm missing, or a common way of achieving this pattern. Wouldn't be the first time I've used a long workaround when the API had some secret doohickey up it's sleeve...as it is I'll probably extend the class.
You could add a function to the Backbone.Collection 'type' and use removeModel on every collection you create.
Backbone.Collection.prototype.removeModel(model) {
var _model = this.get(model);
this.remove(item);
return _model;
}
var removedModel = collection.removeModel(model);