backbone
backbone copied to clipboard
collection.prototype.chain() doesn't proxy requests to model.attributes
As far as I can tell, it looks like most of the underscore methods attached to collections are proxied so that when you call them they act on the models' .attributes
object. This is great because it lets us do things like:
collection.where({ foo: 'bar' }); // Yay! Looks at model.attributes.foo
Unfortunately, it seems like all of this breaks when you call .chain()
collection.chain().where({ foo: 'bar' }).value(); // Boo :( Looks at model.foo
Obviously this example isn't very useful, but if you want to do more than one operation on the collection you can't use any of the shortcuts for things like where
, pluck
, etc and need to use iterator functions which is so much less clean.
That's because where
is special cased. It's an unfortunate side effect of the way chaining works in Underscore with Backbone. Do you have a better solution?
Seems that chain()
could be special-cased, and the chain object (this
in the chained methods) could detect when the next call is no longer returning Backbone models and basically become a normal Underscore chain context object at that point?
this.collection.chain() // "Special" chain context object, currently holding array of models
.where({ foo: "bar" }) // Should still be array of models
.where({ baz: "quuz" }) // Same here
.invoke("toJSON") // Definitely not an array of models
.map(_.keys) // This is now normal Underscore chain
.value(); // Here as well
Of course, this could get ugly when factoring in methods that take in method refs (e.g., "invoke"), which could honestly go either way. One way that could be handled is to augment the Underscore chain object with a method, asCollection()
(or something like that), which basically goes back to collection-aware chain mode. That method would be regarded as "use at your own risk".
Example of that (assume that the collection contains models, where the "Address" attribute is itself another model):
this.collection.chain() // Collection-aware
.pluck("Address") // In general, not collection-aware...
.asCollection() // ...but in reality this is an array of models
.where({ foo: "bar" }) // This should now work as it would have done on an actual collection
.value();
Honestly, this is looking pretty ugly even to me, but I wanted to throw it out there in the hopes that it will inspire more discussion and ultimately a workable solution.
I suspect the only viable way to do this without duplicating the logic which handles whether to provide a model or attributes is to chain by default
I haven't been able to come up with a clean solution really. My workaround so far is to just always follow up .chain()
with either .pluck('attributes')
or .invoke('toJSON')
. This lets me filter/manipulate the objects as I'd like, but unfortunately I lose their Backbone.Model
instances in the process. The invoke
method has the benefit of working with my Backbone.Mutator properties, while the pluck
method keeps the references to the original attributes
objects (maybe a good or a bad thing since you could modify it?).
Perhaps one option would be to track if all methods called in the chain simply filtered the collection (e.g where
, find
, findWhere
, filter
, reject
, sortBy
, shuffle
, sample
, toArray
, etc). If that's the case we could have a cleanup operation that happens on the output of the chain: looking for models whose .attributes matched that output, then swaping the attribute objects out for their model instances... Alternatively, if the models have id's we could match that way (which would probably be much faster since there is an index there...). That would basically look like automatically calling this:
chainResults.map(function (attributes) {
return collection.get(attributes.id);
});
It gets trickier with methods that rearrange the collection (e.g. groupBy
, indexBy
, 'partition', etc). Maybe those methods cause cleanup to happen immediately and from then on you're on your own?