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

47,781

Solution 1

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

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

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

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

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);
    }
});
Share:
47,781

Related videos on Youtube

PhD
Author by

PhD

Updated on January 13, 2020

Comments

  • PhD
    PhD over 4 years

    I am well aware it can be done and I've looked at quite a few places (including: 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.

    • Edward M Smith
      Edward M Smith almost 13 years
      Can your server-side code take this as a single request? In other words, the entire top level collection, all models, and nested collections as a single JSON packet? Or, do you need to save each model individually? Edit: Ah, read closer, the server IS capable of "bulk update/create"
    • PhD
      PhD almost 13 years
      @Edward: Yup! Had made that explicit since it's usually a point of concern, but not in this case :)
    • Edward M Smith
      Edward M Smith almost 13 years
      So, what is the structure of the data the server is expecting to receive?
    • PhD
      PhD almost 13 years
      @Edward: Does the structure of the data matter? Formatting is not possible in a comment but it's like this: [{postId: 1, labels:[{id:1,name:"a"},{id:2,name:"b"}]}] Basically each "postId" can have a set/array of labels which themselves are objects. There can be many such posts...I don't think the data format has got anything to do with the issue at hand, unless I'm missing something
    • philfreo
      philfreo almost 12 years
  • PhD
    PhD almost 13 years
    I looked through the source code and it seems that Collections don't have a save method, so the overriding should not be a problem (if it did have a save method world would be a LOT easier :)
  • PhD
    PhD almost 13 years
    +1 for the wrapper idea - clean and sweet. Didn't think of it at all. I was thinking that maybe I'll have to call Backboard.sync directly and just pass the collection in place of the "model" argument. Would need to specify a url for the model for it to work though...any thoughts? Since the sync method internally just calls getURL(model) on the model argument and doesn't perform any typeof comparisons either...seems to be intentional by design
  • Scott Switzer
    Scott Switzer about 11 years
    Agree - very elegant. However, I am running into a problem with this solution: my wrapper model is always new, thus when I save() it is always a POST. I have already retrieved my data, so when I save() it should be a PUT. I suppose I can hard code isNew() = false, or set a fake ID, but this does not seem like an elegant solution. Do you have any suggestions?
  • Anthony
    Anthony about 11 years
    Really clean answer, it would be nice to see how you'd instantiate the CollectionWrapper.
  • yves amsellem
    yves amsellem about 11 years
    Seems right. May be adding return Backbone.sync.. is more Backbonish.
  • inf3rno
    inf3rno almost 11 years
    I think - for the type - update would be better than create... Btw. you can override the toJSON of the model or the collection, so you can regulate what to send to the server... (usually just the id attribute needed) In Backbone.Relational you can set as well what to add to the json format.
  • Joe
    Joe almost 11 years
    Yes, It worked for me also and +1 for the good effort but if i want to send two collections to server, how do i go about?
  • Sergey Kamardin
    Sergey Kamardin over 10 years
    Attribute of options "url" is not neccessary here =)
  • hacklikecrack
    hacklikecrack over 10 years
    Backbone.sync is expecting "create", see backbonejs.org/docs/backbone.html#section-141
  • Jason M
    Jason M almost 10 years
    I think the problem with this solution is that the model's state is never updated
  • developerbmw
    developerbmw about 9 years
    You would be better off calling Backbone.ajax (it proxies to jQuery anyway, but it makes it more maintainable)
  • styler
    styler over 8 years
    I was wondering if you could help solve my collection save issue, right now jsfiddle.net/kyllle/f1h4cz7f/3 toJSON() updates each model but then save doesn't seem to have any data?
  • Matt Fletcher
    Matt Fletcher over 8 years
    Makes sense too to just pass options in, like save: function (options) { Backbone.sync('save', this, options); }
  • zeros-and-ones
    zeros-and-ones over 7 years
    Uncaught TypeError: Cannot read property 'toJSON' of undefined