Save several Backbone models at once - javascript

I have a Backbone collection with a load of models.
Whenever a specific attribute is set on a model and it is saved, a load of calculations fire off and the UI rerenders.
But, I want to be able to set attributes on several models at once and only do the saving and rerendering once they are all set. Of course I don't want to make several http requests for one operation and definitely dont want to have to rerender the interface ten times.
I was hoping to find a save method on Backbone.Collection that would work out which models hasChanged(), whack them together as json and send off to the back end. The rerendering could then be triggered by an event on the collection. No such luck.
This seems like a pretty common requirement, so am wondering why Backbone doesn't implement. Does this go against a RESTful architecture, to save several things to a single endpoint? If so, so what? There's no way it's practical to make 1000 requests to persist 1000 small items.
So, is the only solution to augment Backbone.Collection with my own save method that iterates over all its models and builds up the json for all the ones that have changed and sends that off to the back end? or does anyone have a neater solution (or am I just missing something!)?

I have ended up augmenting Backbone.Collection with a couple of methods to handle this.
The saveChangeMethod creates a dummy model to be passed to Backbone.sync. All backbone's sync method needs from a model is its url property and toJSON method, so we can easily knock this up.
Internally, a model's toJSON method only returns a copy of it's attributes (to be sent to the server), so we can happily just use a toJSON method that just returns the array of models. Backbone.sync stringifies this, which gives us just the attribute data.
On success, saveChanged fires off events on the collection to be handled once. Have chucked in a bit of code that gets it firing specific events once for each of the attributes that have changed in any of the batch's models.
Backbone.Collection.prototype.saveChanged = function () {
var me = this,
changed = me.getChanged(),
dummy = {
url: this.url,
toJSON: function () {
return changed.models;
}
},
options = {
success: function (model, resp, xhr) {
for (var i = 0; i < changed.models.length; i++) {
changed.models[i].chnageSilently();
}
for (var attr in changed.attributes) {
me.trigger("batchchange:" + attr);
}
me.trigger("batchsync", changed);
}
};
return Backbone.sync("update", dummy, options);
}
We then just need the getChanged() method on a collection. This returns an object with 2 properties, an array of the changed models and an object flagging which attributes have changed:
Backbone.Collection.prototype.getChanged = function () {
var models = [],
changedAttributes = {};
for (var i = 0; i < this.models.length; i++) {
if (this.models[i].hasChanged()) {
_.extend(changedAttributes, this.models[i].changedAttributes());
models.push(this.models[i]);
}
}
return models.length ? {models: models, attributes: changedAttributes} : null;
}
Although this is slight abuse of the intended use of backbones 'changed model' paradigm, the whole point of batching is that we don't want anything to happen (i.e. any events to fire off) when a model is changed.
We therefore have to pass {silent: true} to the model's set() method, so it makes sense to use backbone's hasChanged() to flag models waiting to be saved. Of course this would be problematic if you were changing models silently for other purposes - collection.saveChanged() would save these too, so it is worth considering setting an alternative flag.
In any case, if we are doing this way, when saving, we need to make sure backbone now thinks the models haven't changed (without triggering their change events), so we need to manually manipulate the model as if it hadn't been changed. The saveChanged() method iterates over our changed models and calls this changeSilently() method on the model, which is basically just Backbone's model.change() method without the triggers:
Backbone.Model.prototype.changeSilently = function () {
var options = {},
changing = this._changing;
this._changing = true;
for (var attr in this._silent) this._pending[attr] = true;
this._silent = {};
if (changing) return this;
while (!_.isEmpty(this._pending)) {
this._pending = {};
for (var attr in this.changed) {
if (this._pending[attr] || this._silent[attr]) continue;
delete this.changed[attr];
}
this._previousAttributes = _.clone(this.attributes);
}
this._changing = false;
return this;
}
Usage:
model1.set({key: value}, {silent: true});
model2.set({key: value}, {silent: true});
model3.set({key: value}, {silent: true});
collection.saveChanged();
RE. RESTfulness.. It's not quite right to do a PUT to the collection's endpoint to change 'some' of its records. Technically a PUT should replace the entire collection, though until my application ever actually needs to replace an entire collection, I am happy to take the pragmatic approach.

You can define a new resource to accomplish this kind of behavior, you can call it MyModelBatch.
You need to implement a new resource in you server side that is able to digest an Array of models and execute the proper action: CREATE, UPDATE and DESTROY.
Also you need to implement a Model in your Backbone client side with one attribute which is the Array of Models and a special url that doesn't make use the id.
About the re-render thing I suggest you to try to have one View by each Model so there will be as much renders as Models have changed but they will be detail re-renders without duplication.

This is what i came up with.
Backbone.Collection.extend({
saveAll: function(models, key, val, options) {
var attrs, xhr, wait, that = this;
var transport = {
url: this.url,
models: [],
toJSON: function () {
return { models: this.models };
},
trigger: function(){
return that.trigger.apply(that, arguments);
}
};
if(models == null){
models = this.models;
}
// Handle both `"key", value` and `{key: value}` -style arguments.
if (key == null || typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
options = _.extend({validate: true}, options);
wait = options.wait;
// After a successful server-side save, the client is (optionally)
// updated with the server-side state.
if (options.parse === void 0) options.parse = true;
var triggers = [];
_.each(models, function(model){
var attributes = model.attributes;
// If we're not waiting and attributes exist, save acts as
// `set(attr).save(null, opts)` with validation. Otherwise, check if
// the model will be valid when the attributes, if any, are set.
if (attrs && !wait) {
if (!model.set(attrs, options)) return false;
} else {
if (!model._validate(attrs, options)) return false;
}
// Set temporary attributes if `{wait: true}`.
if (attrs && wait) {
model.attributes = _.extend({}, attributes, attrs);
}
transport.models.push(model.toJSON());
triggers.push(function(resp){
if(resp.errors){
model.trigger('error', model, resp, options);
} else {
// Ensure attributes are restored during synchronous saves.
model.attributes = attributes;
var serverAttrs = options.parse ? model.parse(resp, options) : resp;
if (wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
return false;
}
model.trigger('sync', model, resp, options);
}
});
// Restore attributes.
if (attrs && wait) model.attributes = attributes;
});
var success = options.success;
options.success = function(resp) {
_.each(triggers, function(trigger, i){
trigger.call(options.context, resp[i]);
});
if (success) success.call(options.context, models, resp, options);
};
return this.sync('create', transport, options);
}
});

Related

Different model types in one collection Backbone

Backbone 1.1.2
Underscore 1.7.0
jQuery 1.11.1
I have a single collection that holds messages.
My messages can be be of different types (and the endpoints in the api are different for each type, but I have an endpoint that allows me to do one request and get all the messages)
When Collection.fetch()
I need to be able to define which model to use when populating the collection based on existing properties.
I have tried as suggested here: A Backbone.js Collection of multiple Model subclasses
as well as the backbone documentation backbonejs.org
My code looks like this
model: function (attr, options) {
if(attr.hasOwnProperty('prop')){
return new PropModel(attr,options);
}
else if(attr.hasOwnProperty('another_prop')){
new AnotherPropModel(attr,options);
}
},
the attr value is just one big array of objects, so without traversing somehow this solution makes no sense to me and its obvious why it doesn't work.
Am I handling this correctly is there another way to do this?
---UPDATE----
I have also tried doing this in the Parse Function of the collection and my collection is just empty
parse: function (resp, options) {
_.each(resp, _.bind(function (r) {
console.log(this);
if(r.hasOwnProperty('prop')){
this.add(new PropModel(r));
}else{
this.add(new AnotherPropModel(r));
}
},this));
}
So the solution was a mix of using model function and return.
Here goes the explanation:
First off we have the parse function
which is just an entry point for us to alter a response that we receive from our server
parse: function (resp, options) {
return resp;
}
In my case, the server was returning an Object of Object as
{{1:data},{2:data}}
Firstly this is strange and obviously needs to be resolved.
The IMPORTANT POINT IS:
When backbone assess the response return from parse, it needs to decide where to break off for each model as in what defines a new model.
Backbone sees objects as a single model and as in my case, I had one big object, I was getting one big model... this is why the attrs argument in model function was one big mush of data.
So I simply altered my response in the parse function and Voila!! everything in model function worked as expected:
Here is the code:
model: function (attr, options) {
if(attr.hasOwnProperty('prop')){
return new PropModel(attr,options);
}
else if (attr.hasOwnProperty('anotherProp')){
return new AnotherPropModel(attr,options);
}
},
parse: function (resp, options) {
var response = [];
_.each(resp, _.bind(function (r) {
response.push(r);
},this));
return response;
}
Im sure there is a better way to resolve the object to array, but for now this works and I'm smiling again!!
This article lead me to the solution:
A collection of muppets
You could do something like the following - reacting to the different type (if possible) and then provide a different URL. Then once the JSON models are in the template, then you can render the HTML the way you like:
Example Json
"[{"id":1,"Type":"Person"},{"id":2,"Type":"Business"}]"
Example Model
var Person = Backbone.Model.extend({
keyTypes: {
Person: 'Person',
Business: 'Business'
},
url: function() {
// we are assuming only two types. Change logic if there are three or more types.
return this.get('Type') === this.keyTypes.Person ? '/api/people' : '/api/businesss';
}
});
Collection
var Collection = Backbone.Collection.extend({
model: Person
});
View
var View = Backbone.View.extend({
initialize: function() {
this.collection = new Collection()
.on('fetch', this.render, this);
},
bootstrap: function() {
this.collection.fetch();
}
render: function() {
this.$el.html(_.template({
models: this.collection.toJSON()
}));
}
})
** !! Update !! **
If you want to still use parse, it will could to look the following.
parse: function (data, options) {
var models = [];
_.each(data, function (entity) {
// this is IE8 Safe....
var model = Object.prototype.hasOwnProperty.call(entity,'prop') ? PropModel : AnotherPropModel;
models.push(new model(entity));
});
return models;
}

Add methods to a collection returned from an angular resource query

I have a resource that returns an array from a query, like so:
.factory('Books', function($resource){
var Books = $resource('/authors/:authorId/books');
return Books;
})
Is it possible to add prototype methods to the array returned from this query? (Note, not to array.prototype).
For example, I'd like to add methods such as hasBookWithTitle(title) to the collection.
The suggestion from ricick is a good one, but if you want to actually have a method on the array that returns, you will have a harder time doing that. Basically what you need to do is create a bit of a wrapper around $resource and its instances. The problem you run into is this line of code from angular-resource.js:
var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data));
This is where the return value from $resource is set up. What happens is "value" is populated and returned while the ajax request is being executed. When the ajax request is completed, the value is returned into "value" above, but by reference (using the angular.copy() method). Each element of the array (for a method like query()) will be an instance of the resource you are operating on.
So a way you could extend this functionality would be something like this (non-tested code, so will probably not work without some adjustments):
var myModule = angular.module('myModule', ['ngResource']);
myModule.factory('Book', function($resource) {
var service = $resource('/authors/:authorId/books'),
origQuery = service.prototype.$query;
service.prototype.$query = function (a1, a2, a3) {
var returnData = origQuery.call(this, a1, a2, a3);
returnData.myCustomMethod = function () {
// Create your custom method here...
return returnData;
}
}
return service;
});
Again, you will have to mess with it a bit, but that's the basic idea.
This is probably a good case for creating a custom service extending resource, and adding utility methods to it, rather than adding methods to the returned values from the default resource service.
var myModule = angular.module('myModule', []);
myModule.factory('Book', function() {
var service = $resource('/authors/:authorId/books');
service.hasBookWithTitle = function(books, title){
//blah blah return true false etc.
}
return service;
});
then
books = Book.list(function(){
//check in the on complete method
var hasBook = Book.hasBookWithTitle(books, 'someTitle');
})
Looking at the code in angular-resource.js (at least for the 1.0.x series) it doesn't appear that you can add in a callback for any sort of default behavior (and this seems like the correct design to me).
If you're just using the value in a single controller, you can pass in a callback whenever you invoke query on the resource:
var books = Book.query(function(data) {
data.hasBookWithTitle = function (title) { ... };
]);
Alternatively, you can create a service which decorates the Books resource, forwards all of the calls to get/query/save/etc., and decorates the array with your method. Example plunk here: http://plnkr.co/edit/NJkPcsuraxesyhxlJ8lg
app.factory("Books",
function ($resource) {
var self = this;
var resource = $resource("sample.json");
return {
get: function(id) { return resource.get(id); },
// implement whatever else you need, save, delete etc.
query: function() {
return resource.query(
function(data) { // success callback
data.hasBookWithTitle = function(title) {
for (var i = 0; i < data.length; i++) {
if (title === data[i].title) {
return true;
}
}
return false;
};
},
function(data, response) { /* optional error callback */}
);
}
};
}
);
Thirdly, and I think this is better but it depends on your requirements, you can just take the functional approach and put the hasBookWithTitle function on your controller, or if the logic needs to be shared, in a utilities service.

Check to see if something is a model or collection in backbone js

When you override backbone sync, both model/collection .save()/fetch() uses the same backbone sync method, so what is the best way to check if what Backbone.sync recieves is a model or a collection of models?
As an example:
Backbone.sync = function(method, model, options){
//Model here can be both a collection or a single model so
if(model.isModel()) // there is no isModel or isCollection method
}
I suppose I am looking for a "safe" best practice, I could of course check for certain attributes or methods that only a model or a collection have, but it seems hackish, shouldn't there be a better obvious way? And there probably is I just couldn't find it.
Thanks!
You could also try instanceof like so:
Backbone.sync = function(method, model, options) {
if (model instanceof Backbone.Model) {
...
} else if (model instanceof Backbone.Collection) {
...
}
}
#fiskers7's answer works with deep extension :
var Item = Backbone.Model.extend({
className : 'Item',
size :10
});
var VerySmallItem = Item.extend({
size :0.1
});
var item = new Item();
var verySmall = new VerySmallItem();
alert("item is Model ?" + (item instanceof Backbone.Model)); //true
alert("verySmall is Model ?" + (verySmall instanceof Backbone.Model)); //true
This is equally hackish, but a Backbone collection has a model property, and a model doesn't -- it is itself a model.
Perhaps a safer method is model.toJSON() and see if the result is an object or an array. You're probably going to model.toJSON() in your custom Backbone.sync anyway, so though this is pretty computationally expensive, it would happen anyway.
I'm not entirely sure how I feel about this because it seems a bit hackish, but I can't exactly think of why it would be super bad at the moment.
Definitely simple, and faster than an "instanceof" check (I'm assuming you won't name any other functions "isBBModel/Collection" on your objects?)
Backbone.Model.prototype.isBBCollection = function() { return false; }
Backbone.Model.prototype.isBBModel = function() { return true; }
Backbone.Collection.prototype.isBBCollection = function() { return true; }
Backbone.Collection.prototype.isBBModel = function() { return false; }
You could do something like this.
Backbone.Model.prototype.getType = function() {
return "Model";
}
Backbone.Collection.prototype.getType = function() {
return "Collection";
}
if(model.getType() == "Model") {}
if(model.getType() == "Collection"){}

How to make Backbone.js Collection items Unique?

Say I have these Backbone.js Model:
var Truck = Backbone.Model.extend({});
var truck1 = new Truck();
var truck2 = new Truck();
truck1.set("brand", "Ford");
truck2.set("brand", "Toyota");
truck3.set("brand", "Honda");
truck4.set("brand", "Ford");
Then, let's say we have a Backbone.js Collection:
var TruckList = Backbone.Collection.extend({
model: Truck,
comparator: function(truck) {
return truck.get("brand");
};
});
I'm a car collector, so time to add each car to my collection:
Trucks = new TruckList();
Trucks.add(truck1);
Trucks.add(truck2);
Trucks.add(truck3);
Trucks.add(truck4);
Just focusing on the brand attribute, truck4 is a duplicate of truck1. I can't have duplicates in my Collection. I need my collection to have unique values.
My question is, How do I remove duplicate items from my Backbone.js Collection?
Should I use Underscore.js for this? If so, can someone please provide a working/runnable example of how to do this.
Assume the following:
1.Collection is not sorted
Removal must be done on brand attribute value
Ajax call to populate each instance of a Truck. This means when adding to a collection, you don't have access to the Truck properties.
I would override the add method in your TruckList collection and use underscore to detect duplicates there and reject the duplicate. Something like.
TruckList.prototype.add = function(truck) {
// Using isDupe routine from #Bill Eisenhauer's answer
var isDupe = this.any(function(_truck) {
return _truck.get('brand') === truck.get('brand');
});
// Up to you either return false or throw an exception or silently ignore
// NOTE: DEFAULT functionality of adding duplicate to collection is to IGNORE and RETURN. Returning false here is unexpected. ALSO, this doesn't support the merge: true flag.
// Return result of prototype.add to ensure default functionality of .add is maintained.
return isDupe ? false : Backbone.Collection.prototype.add.call(this, truck);
}
The simplest way to achieve this is to make sure the models you are adding have unique ids. By default Backbone collections will not add models with duplicate ids.
test('Collection should not add duplicate models', 1, function() {
var model1 = {
id: "1234"
};
var model2 = {
id: "1234"
};
this.collection.add([model1, model2]);
equal(1, this.collection.length, "collection length should be one when trying to add two duplicate models");
});
Try this. It uses the any underscore method to detect the potential duplicate and then dumps out if so. Of course, you might want to dress this up with an exception to be more robust:
TruckList.prototype.add = function(newTruck) {
var isDupe = this.any(function(truck) {
return truck.get('brand') === newTruck.get('brand');
}
if (isDupe) return;
Backbone.Collection.prototype.add.call(this, truck);
}
As an aside, I would probably write a function on Truck to do the dupe checking so that the collection doesn't know too much about this condition.
var TruckList = Backbone.Collection.extend({
model : Truck,
// Using #Peter Lyons' answer
add : function(truck) {
// Using isDupe routine from #Bill Eisenhauer's answer
var isDupe = this.any(function(_truck) {
return _truck.get('brand') === truck.get('brand');
});
if (isDupe) {
// Up to you either return false or throw an exception or silently
// ignore
return false;
}
Backbone.Collection.prototype.add.call(this, truck);
},
comparator : function(truck) {
return truck.get("brand");
} });
VassilisB's answer worked great but it will override Backbone Collection's add() behavior. Therefore, errors might come when you try to do this:
var truckList = new TruckList([{brand: 'Ford'}, {brand: 'Toyota'}]);
So, I added a bit of a checking to avoid these errors:
var TruckList = Backbone.Collection.extend({
model : Truck,
// Using #Peter Lyons' answer
add : function(trucks) {
// For array
trucks = _.isArray(trucks) ? trucks.slice() : [trucks]; //From backbone code itself
for (i = 0, length = trucks.length; i < length; i++) {
var truck = ((trucks[i] instanceof this.model) ? trucks[i] : new this.model(trucks[i] )); // Create a model if it's a JS object
// Using isDupe routine from #Bill Eisenhauer's answer
var isDupe = this.any(function(_truck) {
return _truck.get('brand') === truck.get('brand');
});
if (isDupe) {
// Up to you either return false or throw an exception or silently
// ignore
return false;
}
Backbone.Collection.prototype.add.call(this, truck);
}
},
comparator : function(truck) {
return truck.get("brand");
}});
I'm doing a FileUpload thing with the same issue, and here's how I did it (coffeescript):
File = Backbone.Model.extend
validate: (args) ->
result
if !#collection.isUniqueFile(args)
result = 'File already in list'
result
Files = Backbone.Collection.extend
model: File
isUniqueFile: (file) ->
found
for f in #models
if f.get('name') is file.name
found = f
break
if found
false
else
true
... and that's it. The collection object is automatically referenced in File, and Validation is automatically called when an action is invoked on the collection which in this case is Add.
Underscore.js, a pre-req for backbone.js, provides a function for this: http://documentcloud.github.com/underscore/#uniq
Example:
_.uniq([1,1,1,1,1,2,3,4,5]); // returns [1,2,3,4,5]
Not sure if this is an update to either Backbone or underscore, but the where() function works in Backbone 0.9.2 to do the matching for you:
TruckList.prototype.add = function(truck) {
var matches = this.where({name: truck.get('brand')});
if (matches.length > 0) {
//Up to you either return false or throw an exception or silently ignore
return false;
}
Backbone.Collection.prototype.add.call(this, truck);
}
I would prefer override the add method like this.
var TruckList = Backbone.Collection.extend({
model : Truck,
// Using #Peter Lyons' answer
add : function(truck) {
// Using isDupe routine from #Bill Eisenhauer's answer
var isDupe = this.any(function(_truck) {
return _truck.get('brand') === truck.get('brand');
});
if (isDupe) {
// Up to you either return false or throw an exception or silently
// ignore
return false;
}
Backbone.Collection.prototype.add.call(this, truck);
},
comparator : function(truck) {
return truck.get("brand");
} });
It seems like an elegant solution would be to use _.findWhere so long as you have some unique attribute (brand in your case). _.findWhere will return a match which is a JavaScript object and therefore truthy or undefined which is falsey. This way you can use a single if statement.
var TruckList = Backbone.Collection.extend({
model: Truck,
add: function (truck) {
if (!this.findWhere({ brand: truck.get('brand') })) {
Backbone.Collection.prototype.add.call(this, truck);
}
}
});
Try this...
var TruckList = Backbone.Collection.extend({
model: Truck,
comparator: function(truck) {
return truck.get("brand");
},
wherePartialUnique: function(attrs) {
// this method is really only tolerant of string values. you can't do partial
// matches on arrays, objects, etc. use collection.where for that
if (_.isEmpty(attrs)) return [];
var seen = [];
return this.filter(function(model) {
for (var key in attrs) {
// sometimes keys are empty. that's bad, so let's not include it in a unique result set
// you might want empty keys though, so comment the next line out if you do.
if ( _.isEmpty(model.get(key).trim()) ) return false;
// on to the filtering...
if (model.get(key).toLowerCase().indexOf(attrs[key].toLowerCase()) >= 0) {
if (seen.indexOf( model.get(key) ) >= 0 ) return false;
seen.push(model.get(key));
return true;
} else {
return false;
}
}
return true;
});
}
});
A few things to remember:
this is based on the backbone.collection.where method and unlike that method, it will attempt partial matches on model attributes within a collection. If you don't want that, you'll need to modify it to only match exactly. Just mimic what you see in the original method.
it should be able to accept multiple attribute matches, so if you have model attributes of foo and bar, you should be able to do collection.wherePartialUnique({foo:"you",bar:"dude"}). I have not tested that though. :) I have only ever done one key/value pair.
i also strip out empty model attributes from consideration. I don't care about them, but you might.
this method doesn't require a collection of unique model properties that the comparator depends. It's more like a sql distinct query, but I'm not an sql guy so don't shoot me if that's a bad example :)
your collection is sorted by way of the comparator function, so one of your assumptions about it not being sorted is incorrect.
I believe this also addresses all of your goals:
Collection is not sorted
Removal must be done on brand attribute value
Ajax call to populate each instance of a Truck. This means when adding to a collection, you don't have access to the Truck properties.
I'm really unhappy with the accepted answer to this solution. It contains numerous errors. I've edited the original solution to highlight my concerns, but I am proposing the following solution assuming you're OK dirtying your duplicate's id/cid property:
TruckList.prototype.add = function(truckToAdd, options) {
// Find duplicate truck by brand:
var duplicateTruck = this.find(function(truck){
return truck.get('brand') === truckToAdd.get('brand');
});
// Make truck an actual duplicate by ID:
// TODO: This modifies truckToAdd's ID. This could be expanded to preserve the ID while also taking into consideration any merge: true options.
if(duplicateTruck !== undefined){
if(duplicateTruck.has('id')){
truckToAdd.set('id', duplicateTruck.get('id'), { silent: true });
}
else {
truckToAdd.cid = duplicateTruck.cid;
}
}
// Allow Backbone to handle the duplicate instead of trying to do it manually.
return Backbone.Collection.prototype.add.call(this, truckToAdd, options);
}
The only flaw with this one is that truckToAdd's ID/cid is not preserved. However, this does preserve all of the expected functionality of adding an item to a collection including passing merge: true.
I was not satisfied with the provided answers for several reasons:
Modifying the return value of add is unexpected.
Not supporting { merge: true } is unexpected.
I've provided a solution which I believe to be more robust. This solution clones given models if they have duplicates in the collection, updates the clones' ID to match the duplicates ID, and then passes the list of duplicates and non-duplicates onto the original add method so that it can do its magic. No unintended side-effects as far as I am aware.
add: function (models, options) {
var preparedModels;
if (models instanceof Backbone.Collection) {
preparedModels = models.map(this._prepareModelToAdd.bind(this));
}
else if (_.isArray(models)) {
preparedModels = _.map(models, this._prepareModelToAdd.bind(this));
} else if (!_.isNull(models) && !_.isUndefined(models)) {
preparedModels = this._prepareModelToAdd(models);
} else {
preparedModels = models;
}
// Call the original add method using preparedModels which have updated their IDs to match any existing models.
return Backbone.Collection.prototype.add.call(this, preparedModels, options);
},
// Return a copy of the given model's attributes with the id or cid updated to match any pre-existing model.
// If no existing model is found then this function is a no-op.
// NOTE: _prepareModel is reserved by Backbone and should be avoided.
_prepareModelToAdd: function (model) {
// If an existing model was not found then just use the given reference.
var preparedModel = model;
var existingModel = this._getExistingModel(model);
// If an existing model was found then clone the given reference and update its id.
if (!_.isUndefined(existingModel)) {
preparedModel = this._clone(model);
this._copyId(preparedModel, existingModel);
}
return preparedModel;
},
// Try to find an existing model in the collection based on the given model's brand.
_getExistingModel: function (model) {
var brand = model instanceof Backbone.Model ? model.get('brand') : model.brand;
var existingModel = this._getByBrand(brand);
return existingModel;
},
_getByBrand: function (brand) {
return this.find(function (model) {
return model.get('brand') === brand;
});
},
_clone: function (model) {
// Avoid calling model.clone because re-initializing the model could cause side-effects.
// Avoid calling model.toJSON because the method may have been overidden.
return model instanceof Backbone.Model ? _.clone(model.attributes) : _.clone(model);
},
// Copy the model's id or cid onto attributes to ensure Backbone.Collection.prototype.add treats attributes as a duplicate.
_copyId: function (attributes, model) {
if (model.has('id')) {
attributes.id = model.get('id');
} else {
attributes.cid = model.cid;
}
}

Creating methods on the fly

Hi I'm trying to author a jQuery plugin and I need to have methods accessible to elements after they are initialized as that kind of object, e.g.:
$('.list').list({some options}); //This initializes .list as a list
//now I want it to have certain methods like:
$('.list').find('List item'); //does some logic that I need
I tried with
$.fn.list = function (options) {
return this.each(function() {
// some code here
this.find = function(test) {
//function logic
}
}
}
and several other different attempts, I just can't figure out how to do it.
EDIT:
I'll try to explain this better.
I'm trying to turn a table into a list, basically like a list on a computer with column headers and sortable items and everything inbetween. You initiate the table with a command like
$(this).list({
data: [{id: 1, name:'My First List Item', date:'2010/06/26'}, {id:2, name:'Second', date:'2010/05/20'}]
});
.list will make the <tbody> sortable and do a few other initial tasks, then add the following methods to the element:
.findItem(condition) will allow you to find a certain item by a condition (like findItem('name == "Second"')
.list(condition) will list all items that match a given condition
.sort(key) will sort all items by a given key
etc.
What's the best way to go about doing this?
If you want these methods to be available on any jQuery object, you will have to add each one of them to jQuery's prototype. The reason is every time you call $(".list") a fresh new object is created, and any methods you attached to a previous such object will get lost.
Assign each method to jQuery's prototype as:
jQuery.fn.extend({
list: function() { .. },
findItem: function() { .. },
sort: function() { .. }
});
The list method here is special as it can be invoked on two occasions. First, when initializing the list, and second when finding particular items by a condition. You would have to differentiate between these two cases somehow - either by argument type, or some other parameter.
You can also use the data API to throw an exception if these methods are called for an object that has not been initialized with the list plugin. When ('xyz').list({ .. }) is first called, store some state variable in the data cache for that object. When any of the other methods - "list", "findItem", or "sort" are later invoked, check if the object contains that state variable in its data cache.
A better approach would be to namespace your plugin so that list() will return the extended object. The three extended methods can be called on its return value. The interface would be like:
$('selector').list({ ... });
$('selector').list().findOne(..);
$('selector').list().findAll(..);
$('selector').list().sort();
Or save a reference to the returned object the first time, and call methods on it directly.
var myList = $('selector').list({ ... });
myList.findOne(..);
myList.findAll(..);
myList.sort();
I found this solution here:
http://www.virgentech.com/blog/2009/10/building-object-oriented-jquery-plugin.html
This seems to do exactly what I need.
(function($) {
var TaskList = function(element, options)
{
var $elem = $(element);
var options = $.extend({
tasks: [],
folders: []
}, options || {});
this.changed = false;
this.selected = {};
$elem.sortable({
revert: true,
opacity: 0.5
});
this.findTask = function(test, look) {
var results = [];
for (var i = 0,l = options.tasks.length; i < l; i++)
{
var t = options['tasks'][i];
if (eval(test))
{
results.push(options.tasks[i]);
}
}
return results;
}
var debug = function(msg) {
if (window.console) {
console.log(msg);
}
}
}
$.fn.taskList = function(options)
{
return this.each(function() {
var element = $(this);
if (element.data('taskList')) { return; }
var taskList = new TaskList(this, options);
element.data('taskList', taskList);
});
}
})(jQuery);
Then I have
$('.task-list-table').taskList({
tasks: eval('(<?php echo mysql_real_escape_string(json_encode($tasks)); ?>)'),
folders: eval('(<?php echo mysql_real_escape_string(json_encode($folders)); ?>)')
});
var taskList = $('.task-list-table').data('taskList');
and I can use taskList.findTask(condition);
And since the constructor has $elem I can also edit the jQuery instance for methods like list(condition) etc. This works perfectly.
this.each isn't needed. This should do:
$.fn.list = function (options) {
this.find = function(test) {
//function logic
};
return this;
};
Note that you'd be overwriting jQuery's native find method, and doing so isn't recommended.
Also, for what it's worth, I don't think this is a good idea. jQuery instances are assumed to only have methods inherited from jQuery's prototype object, and as such I feel what you want to do would not be consistent with the generally accepted jQuery-plugin behaviour -- i.e. return the this object (the jQuery instance) unchanged.

Categories

Resources