There has to be something simple I am missing here.
http://jsfiddle.net/v9mdZ/
I am just learning Backbone and Underscore/loDash and am trying to get familiar with chain.
I have the following code, which works as expected:
var ids = _.pluck(collection.where({'is_checked':true}), 'id');
I attempted to refactor this, using chain like so:
var ids = collection.chain().where({'is_checked':true}).pluck('id').value();
Why doesn't the refactored code work? Am I using chain wrong?
Solution (details below)
Don't use where with chain.
The merging of some Underscore methods into collections is a little imperfect. When you say collection.some_mixed_in_underscore_method(), the collection unwraps some of the Backbone stuff behind your back so that the Underscore method is applied to the attributes inside the collection's models; it sort of works like this:
var ary = _(this.models).map(function(m) { return m.attributes });
return _(ary).some_mixed_in_underscore_method();
But collection.chain() doesn't work like that, chain just wraps the collection's models directly so if you do this:
console.log(collection.chain());
you'll see that chain is giving you an object that wraps an array of models. Your models won't have an is_checked property (i.e. there is no model.is_checked), they will have is_checked attributes though (i.e. there will be model.get('is_checked') and model.attributes.is_checked).
Now we can see where everything goes wrong:
collection.chain().where({'is_checked':true})
The models don't have is_checked properties. In particular, there won't be any models where is_checked is true and everything after the where is working with an empty array.
Now that we know where things go sideways, how do we fix it? Well, you could use filter instead of where so that you can easily unpack the models:
collection.chain()
.filter(function(m) { return m.get('is_checked') })
.pluck('id')
.value();
But, your models don't have ids yet as you didn't create them with ids and you haven't talked to a server to get ids so you're going to get an array of undefineds back. If you add some ids:
var collection = new App.OptionCollection([
{id: 1, 'is_checked': true},
{id: 2, 'is_checked': true},
{id: 3, 'is_checked': false}
]);
then you'll get the [1,2] that you're looking for.
Demo: http://jsfiddle.net/ambiguous/kRmaD/
Related
I am working on a backbone/signalr POC. I have very simple models working and I can create them client side and retrieve them via signalr.
The problem is this:
If I create a client side version of a model with a nested model I can access the attributes like this:
model.attributes.nestedModel.attributes.attributeName
When I retrieve the model from signalr via
model.fetch()
the model comes back but now to access the nested model properties I need to use
model.attributes.nestedModel.attributeName
the attributes level on the nested model is dropped, so this causes template rendering to fail
How do I get around this? Am I doing something wrong? I am new to signalr/backbone.
BTW, I am using the backbone.signalr nuget package.
Thanks.
this is because when you are using fetch(), the server returns only one JSON object with the model attributes and the attributes of the nested models. for example, the server returns:
{
id: "1",
name: "Model",
nestedModel: {
id: "12",
name: "nestedModel"
}
}
backbone is not smart enough to figure out that nestedModel is actually a "model". It treats "nestedModel" as an attribute on Model. (it's just a regular JSON object, not a backbone object)
that's why this:
model.attributes.nestedModel.attributes.attributeName
does not work.
to make it work, you have to instantiate nestedModel as a Backbone Model. so after fetch is done: (assuming your nestedModel is an instance of NestedModel)
model.fetch().done(function() {
model.set('nestedModel', new NestedModel(model.get('nestedModel')));
});
You can make backbone does this for you automatically by overwriting the parse() method.
in your model:
var NestedModel = Backbone.Model.extend({
//your nested model methods
});
var Model = Backbone.Model.extend({
//do other model stuff
parse: function(response) {
response.nestedModel = new NestedModel(response.nestedModel);
return response;
}
});
this should make your statement work.
but usually I'd use
model.get('nestedModel').get('attributeName')
for more info about parse, see here: http://backbonejs.org/#Model-parse
and to apply this pattern in all other models with more flexibility, you probably wanna read this:
http://www.devmynd.com/blog/2013-6-backbone-js-with-a-spine-part-2-models-and-collections
Let say you are defining a Backbone.js Model. From the documentation we have ...
new Model([attributes], [options])
This seems great for passing some default attributes to a model. When passed the model automatically inherits those attributes and their respective values. Nothing to do. Awesome!
On they other hand lets say we have a Collection of that model.
new Backbone.Collection([models], [options])
Okay, cool we can pass some initial models and some options. But, I have no initial models and no options I need to pass so let's continue. I am going to fetch the models from the server.
collection.fetch([options])
Well I don't have any options, but I want to pass some attributes to add to each models as it is fetched. I could do this by passing them as options and then adding them to the attributes hash in the initialize for the model, but this seems messy.
Is their a Backbone.js native way to do this?
You can pass the attributes as options to fetch and over-ride the collection's parse method to extend the passed options (attributes) on the response.
The solution would look like the following:
var Collection = Backbone.Collection.extend({
url:'someUrl',
parse:function(resp,options) {
if (options.attributesToAdd) {
for (var i=0;i<resp.length;i++)
_.extend(resp[i],options.attributesToAdd);
}
return resp;
}
});
Then to add attributes when you call fetch on the collection, you can:
var collection = new Collection();
collection.fetch({
attributesToAdd:{foo:'bar',more:'foobar'}
});
You may have to tweak the code a bit to work with your JSON structure, but hopefully this will get you started in the correct direction.
I am still new to backbone:
Here is my problem that I find hard to explain:
on initialisation there is a model something like this:
Model:
{
Id:xxx,
Questions:
[
{
Id: "yy",
Selections:
[
{OptionId:"aaa"},
...,
{OptionId:"zzz"}
]
},
....
]
}
there is a event method updates Selections Collection.
After event triggers I got two different result by with two code below:
window.pkg.Questions.get(this.Id).Selections.reset(selectedoptions);
console.log(window.pkg.Questions.get(this.Id).Selections.toJSON());
console.log(window.pkg.Questions.get(this.Id).toJSON().Selections);
The first log shows the updated model, however the later shows initial default value.
why it is working like this?
They are two different copies. Your QuestionModel has a SelectionsCollection property called Selections, and a Backbone attribute, also called Selections. I assume you want to use the SelectionsCollection, and the attribute will not stay in sync with that. The attribute is the original json which I assume was inadvertently added to the model.
Hard to say exactly how to fix it, because you did not show the code where you create and fetch these models and collections. Wherever you take the Selections JSON and and initially reset it into the collection, you can remove it from the JSON and/or unset it on the QuestionModel if it is already there....
This would also print the wrong, original data:
console.log(window.pkg.Questions.get(this.Id).get('Selections')); // print original JSON
You can remove it in QuestionModel's parse:
parse: function(data) {
// removing Selections from data here will prevent
// it from being added as an attribute.
delete data.Selections;
return data;
}
If you do this, and you want the Selections to be included in the toJSON output of QuestionModel, you would also need to override toJSON.
toJSON: function() {
// get json for Question
var json = Backbone.Model.prototype.toJSON.call(this);
// add updated json for selections
json.Selections = this.Selections.toJSON();
return json;
}
On the first attempt I wrote
this.collection.each(function(element){
element.destroy();
});
This does not work, because it's similar to ConcurrentModificationException in Java where every other elements are removed.
I tried binding "remove" event at the model to destroy itself as suggested Destroying a Backbone Model in a Collection in one step?, but this will fire 2 delete requests if I call destroy on a model that belongs to a collection.
I looked at underscore doc and can't see a each() variant that loops backwards, which would solve the removing every element problem.
What would you suggest as the cleanest way to destroy a collection of models?
Thanks
You could also use a good, ol'-fashioned pop destroy-in-place:
var model;
while (model = this.collection.first()) {
model.destroy();
}
I recently ran into this problem as well. It looks like you resolved it, but I think a more detailed explanation might also be useful for others that are wondering exactly why this is occurring.
So what's really happening?
Suppose we have a collection (library) of models (books).
For example:
console.log(library.models); // [object, object, object, object]
Now, lets go through and delete all the books using your initial approach:
library.each(function(model) {
model.destroy();
});
each is an underscore method that's mixed into the Backbone collection. It uses the collections reference to its models (library.models) as a default argument for these various underscore collection methods. Okay, sure. That sounds reasonable.
Now, when model calls destroy, it triggers a "destroy" event on the collection as well, which will then remove its reference to the model. Inside remove, you'll notice this:
this.models.splice(index, 1);
If you're not familiar with splice, see the doc. If you are, you can might see why this is problematic.
Just to demonstrate:
var list = [1,2];
list.splice(0,1); // list is now [2]
This will then cause the each loop to skip elements because the its reference to the model objects via models is being modified dynamically!
Now, if you're using JavaScript < 1.6 then you may run into this error:
Uncaught TypeError: Cannot call method 'destroy' of undefined
This is because in the underscore implementation of each, it falls back on its own implementation if the native forEach is missing. It complains if you delete an element mid-iteration because it still tries to access non-existent elements.
If the native forEach did exist, then it would be used instead and you would not get an error at all!
Why? According to the doc:
If existing elements of the array are changed, or deleted, their value as passed to callback will be the value at the time forEach visits them; elements that are deleted are not visited.
So what's the solution?
Don't use collection.each if you're deleting models from the collection. Use a method that will allow you to work on a new array containing the references to the models. One way is to use the underscore clone method.
_.each(_.clone(collection.models), function(model) {
model.destroy();
});
I'm a bit late here, but I think this is a pretty succinct solution, too:
_.invoke(this.collection.toArray(), 'destroy');
Piggybacking on Sean Anderson answer.
There is a direct access to backbone collection array, so you could do it like this.
_.invoke(this.collection.models, 'destroy');
Or just call reset() on the collection with no parameters, destroy metod on the models in that collection will bi triggered.
this.collection.reset();
http://backbonejs.org/#Collection-models
This works, kind of surprised that I can't use underscore for this.
for (var i = this.collection.length - 1; i >= 0; i--)
this.collection.at(i).destroy();
I prefer this method, especially if you need to call destroy on each model, clear the collection, and not call the DELETE to the server. Removing the id or whatever idAttribute is set to is what allows that.
var myCollection = new Backbone.Collection();
var models = myCollection.remove(myCollection.models);
_.each(models, function(model) {
model.set('id', null); // hack to ensure no DELETE is sent to server
model.destroy();
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="http://underscorejs.org/underscore-min.js"></script>
<script src="http://backbonejs.org/backbone-min.js"></script>
You don't need underscore and for loop for this.
this.collection.slice().forEach(element => element.destroy());
I'm attempting to learn backbone.js and (by extension) underscore.js, and I'm having some difficulty understanding some of the conventions. While writing a simpel search filter, I thought that something like below would work:
var search_string = new RegExp(query, "i");
var results = _.filter(this, function(data){
return search_string.test(data.get("title"));
}));
But, in fact, for this to work I need to change my filter function to the following:
var search_string = new RegExp(query, "i");
var results = _(this.filter(function(data){
return search_string.test(data.get("title"));
}));
Basically, I want to understand why the second example works, while the first doesn't. Based on the documentation (http://documentcloud.github.com/underscore/#filter) I thought that the former would have worked. Or maybe this just reflects some old jQuery habits of mine... Can anyone explain this for me?
I'd guess that you're using a browser with a native Array#filter implementation. Try these in your console and see what happens:
[].filter.call({ a: 'b' }, function(x) { console.log(x) });
[].filter.call([1, 2], function(x) { console.log(x) });
The first one won't do anything, the second will produce 1 and 2 as output (http://jsfiddle.net/ambiguous/tkRQ3/). The problem isn't that data is empty, the problem is that the native Array#filter doesn't know what to do when applied to non-Array object.
All of Underscore's methods (including filter) use the native implementations if available:
Delegates to the native filter method, if it exists.
So the Array-ish Underscore methods generally won't work as _.m(collection, ...) unless you're using a browser that doesn't provide native implementations.
A Backbone collection is a wrapper for an array of models, the models array is in c.models so you'd want to:
_.filter(this.models, function(data) { ... });
Backbone collections have several Underscore methods mixed in:
Backbone proxies to Underscore.js to provide 28 iteration functions on Backbone.Collection.
and one of those is filter. These proxies apply the Underscore method to the collection's model array so c.filter(...) is the same as _.filter(c.models, ...).
This mixing-in is probably what's confusing the "should I use the native method" checks that Underscore is doing:
if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
You can use _.filter on a plain old object (_.filter({a:'b'}, ...)) and get sensible results but it fails when you _.filter(backbone_collection, ...) because collections already have Underscore methods.
Here's a simple demo to hopefully clarify things: http://jsfiddle.net/ambiguous/FHd3Y/1/
For the same reason that $('#element') works and $#element doesn't. _ is the global variable for the underscore object just like $ is the global variable for the jQuery object.
_() says look in the _ object. _filter says look for a method named _filter.