"How" to save an entire collection in Backbone.js - Backbone.sync or jQuery.ajax?

JqueryAjaxbackbone.js

Jquery Problem Overview


I am well aware it can be done and I've looked at quite a few places (including: https://stackoverflow.com/questions/5014216/best-practice-for-saving-an-entire-collection). But I'm still not clear "exactly how" is it written in code? (the post explains it in English. It'd be great to have a javascript specific explanation :)

Say I have a collection of models - the models themselves may have nested collections. I have overridden the toJSON() method of the parent collection and I am getting a valid JSON object. I wish to "save" the entire collection (corresponding JSON), but backbone doesn't seem to come in-built with that functionality.

var MyCollection = Backbone.Collection.extend({
model:MyModel,

//something to save?
save: function() {
   //what to write here?
 }

});

I know somewhere you have to say:

Backbone.sync = function(method, model, options){
/*
 * What goes in here?? If at all anything needs to be done?
 * Where to declare this in the program? And how is it called?
 */
}

Once the 'view' is done with the processing it is responsible for telling the collection to "save" itself on the server (capable of handling a bulk update/create request).

Questions that arise:

  1. How/what to write in code to "wire it all together"?
  2. What is the 'right' location of the callbacks and how to specify a "success/error" callback? I mean syntactically?I'm not clear of the syntax of registering callbacks in backbone...

If it is indeed a tricky job then can we call jQuery.ajax within a view and pass the this.successMethod or this.errorMethod as success/error callbacks?? Will it work?

I need to get in sync with backbone's way of thinking - I know I'm definitely missing something w.r.t., syncing of entire collections.

Jquery Solutions


Solution 1 - Jquery

My immediate thought is not to override the method on save method on Backbone.Collection but wrap the collection in another Backbone.Model and override the toJSON method on that. Then Backbone.js will treat the model as a single resource and you don't have to hack the way backone thinks too much.

Note that Backbone.Collection has a toJSON method so most of your work is done for you. You just have to proxy the toJSON method of your wrapper Backbone.Model to the Backbone.collection.

var MyCollectionWrapper = Backbone.Model.extend({
url: "/bulkupload",

//something to save?
toJSON: function() {
	return this.model.toJSON(); // where model is the collection class YOU defined above
 }

});

Solution 2 - Jquery

A very simple...

Backbone.Collection.prototype.save = function (options) {
    Backbone.sync("create", this, options);
};

...will give your collections a save method. Bear in mind this will always post all the collection's models to the server regardless of what has changed. options are just normal jQuery ajax options.

Solution 3 - Jquery

I ended up just having a 'save' like method and called $.ajax within it. It gave me more control over it without the need to add a wrapper class as @brandgonesurfing suggested (although I absolutely love the idea :) As mentioned since I already had the collection.toJSON() method overridden all I landed up doing was using it in the ajax call...

Hope this helps someone who stumbles upon it...

Solution 4 - Jquery

This really depends on what the contract is between the client and server. Here's a simplified CoffeeScript example where a PUT to /parent/:parent_id/children with {"children":[{child1},{child2}]} will replace a parent's children with what's in the PUT and return {"children":[{child1},{child2}]}:

class ChildElementCollection extends Backbone.Collection
  model: Backbone.Model
  initialize: ->
    @bind 'add', (model) -> model.set('parent_id', @parent.id)

  url: -> "#{@parent.url()}/children" # let's say that @parent.url() == '/parent/1'
  save: ->
    response = Backbone.sync('update', @, url: @url(), contentType: 'application/json', data: JSON.stringify(children: @toJSON()))
    response.done (models) => @reset models.children
    return response

This is a pretty simple example, you can do a lot more... it really depends on what state your data's in when save() is executed, what state it needs to be in to ship to the server, and what the server gives back.

If your server is ok with a PUT of [{child1},{child2], then your Backbone.sync line could change to response = Backbone.sync('update', @toJSON(), url: @url(), contentType: 'application/json').

Solution 5 - Jquery

The answer depends on what you want to do with the collection on server side.

If you have to send additional data with the post you might need a wrapper model or a relational model.

With the wrapper model you always have to write your own parse method:

var Occupants = Backbone.Collection.extend({
	model: Person
});

var House = Backbone.Model.extend({
	url: function (){
		return "/house/"+this.id;
	},
	parse: function(response){
		response.occupants = new Occupants(response.occupants)
		return response;
	}
});

Relational models are better I think, because you can configure them easier and you can regulate with the includeInJSON option which attributes to put into the json you send to your rest service.

var House = Backbone.RelationalModel.extend({
	url: function (){
		return "/house/"+this.id;
	},
	relations: [
		{
			type: Backbone.HasMany,
			key: 'occupants',
			relatedModel: Person,
			includeInJSON: ["id"],
			reverseRelation: {
				key: 'livesIn'
			}
		}
	]
});

If you don't send additional data, you can sync the collection itself. You have to add a save method to your collection (or the collection prototype) in that case:

var Occupants = Backbone.Collection.extend({
	url: "/concrete-house/occupants",
	model: Person,
	save: function (options) {
		this.sync("update", this, options);
	}
});

Solution 6 - Jquery

I was also surprised that Backbone collections don't have a built in save. Here's what I put on my backbone collection to do it. I definitely don't want to iterate through each model in the collection and save independently. Also, I am using Backbone on the backend using Node so am overridding the native Backbone.sync to save to a flat file on my small project—but the code should pretty much be the same:

    save: function(){                                                                                                                                                                                                                                                                                                                                                     
      Backbone.sync('save', this, {                                                                                                                                                                                                                                                                                                                                     
        success: function(){                                                                                                                                                                                                                                                                                                                                          
          console.log('users saved!');                                                                                                                                                                                                                                                                                                                              
        }                                                                                                                                                                                                                                                                                                                                                             
      });                                                                                                                                                                                                                                                                                                                                                               
    }

Solution 7 - Jquery

Old thread i know, what i ended up doing is the following:

Backbone.Collection.prototype.save = function (options) {
            // create a tmp collection, with the changed models, and the url
            var tmpCollection = new Backbone.Collection( this.changed() );
            tmpCollection.url = this.url;
            // sync
            Backbone.sync("create", tmpCollection, options);
        };
        Backbone.Collection.prototype.changed = function (options) {
            // return only the changed models.
            return this.models.filter( function(m){
                return m.hasChanged()
            });
        };
// and sync the diffs.
self.userCollection.save();

Pretty straint foreward :)

Solution 8 - Jquery

Here's a simple example:

var Books = Backbone.Collection.extend({
model: Book,
url: function() {
  return '/books/';
},
save: function(){
  Backbone.sync('create', this, {
    success: function() {
      console.log('Saved!');
    }
  });
 }
});

When you call the save() method on your collection, it will send a PUT method request to the defined URL.

Solution 9 - Jquery

I would try something like:

var CollectionSync = function(method, model, [options]) {
    // do similar things to Backbone.sync
}

var MyCollection = Backbone.Collection.extend({
    sync: CollectionSync,
    model: MyModel,
    getChanged: function() {
        // return a list of models that have changed by checking hasChanged()
    },
    save: function(attributes, options) {
        // do similar things as Model.save
    }
});

( https://stackoverflow.com/a/11085198/137067 )

Solution 10 - Jquery

The accepted answer is pretty good, but I can go one step further and give you code that will ensure the proper events are fired for your listeners while also allowing you to pass in option ajax event callbacks:

save: function( options ) {
  var self = this;

  var success = options.success;
  var error = options.error;
  var complete = options.complete;

  options.success = function( response, status, xhr ) {
    self.trigger('sync', self, response, options);
    if (success) return success.apply(this, arguments);
  };

  options.error = function( response, status, xhr ) {
    self.trigger('error', self, response, options);
    if (error) return error.apply(this, arguments);
  };

  options.complete = function( response, status, xhr ) {
    if (complete) return complete.apply(this, arguments);
  }

  Backbone.sync('create', this, options);
}

Solution 11 - Jquery

For anyone who is still using backbone.js in 2017, the accepted answer is not working.

Try removing the toJSON() override in the wrapper model and calling toJSON on the collection when you instantiate the model wrapper.

new ModelWrapper(Collection.toJSON());

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPhDView Question on Stackoverflow
Solution 1 - JquerybradgonesurfingView Answer on Stackoverflow
Solution 2 - JqueryhacklikecrackView Answer on Stackoverflow
Solution 3 - JqueryPhDView Answer on Stackoverflow
Solution 4 - JquerycarpeliamView Answer on Stackoverflow
Solution 5 - Jqueryinf3rnoView Answer on Stackoverflow
Solution 6 - JqueryMauvis LedfordView Answer on Stackoverflow
Solution 7 - JqueryRene WetelingView Answer on Stackoverflow
Solution 8 - JqueryGaurav GuptaView Answer on Stackoverflow
Solution 9 - JqueryphilfreoView Answer on Stackoverflow
Solution 10 - JqueryThrottleheadView Answer on Stackoverflow
Solution 11 - Jqueryzeros-and-onesView Answer on Stackoverflow