Last Updated: February 25, 2016
·
7.43K
· liquidise

Save-Once Backbone and Collection.saveAll()

read more at: Ben Roux's Blog

Backbone.js does a lot of things right when it comes to client-heavy interfaces. Client-side M.V.Controller, RESTful by design and structured code locations are just some of the benefits i point out as reasons for its use. But, just as the version number (0.9.2 as of this writing) suggests, there are still some things to come.

Saving in Backbone.Js

Backbone.js is designed for incremental object saves. That is to say, when a user makes a change, or a series of changes to the site, those changes are posted back to the server asynchronously and are usually invisible to the user. In my opinion, this is a great concept: users never forget to press the save button and they hardly, if ever, wait for pages to reload. It makes for what feels like an ideal user experience.

The Problem...

Occasionally, mockups or systems call for the more traditional workflow: users interact with a page for some time, and then press the standard Submit button at the bottom. If the interface is client-side heavy, i typically look to Backbone. The problem arises when it comes to save the objects. Where you find yourself is a place where you write some code:

var PostsCollection = Backbone.Collection.extend({
    model: post,

    saveAll: function( ) {
        // Loop over my collection...
        _(this.models).each( function(post) {
            // And POST for each object in the collection
            post.save();
        } );
    }
}

There are some clear disadvantages to this. The largest of which is the danger of data loss. The second largest is user confusion of what is happening while their system takes the time to POST an arbitrary number of times to the server.

A Less Than Ideal Solution

The trick i've used in these cases is to lean on Backbone for the interface, and then fall back to standard HTML for the rest. To do this, you simply rewrite your saveALL function to behave differently:

saveAll: function( ) {
    // Create the submission form
    var form = document.createElement( 'form' );
    form.setAttribute( "method", 'POST' )
    form.setAttribute( 'action', window.location.pathname );

    // Jsonify all of the models in the collection
    var formData = []
    _(this.models).each( function(post) {
        formData.push( post.toJSON() );
    } );
    formData = JSON.stringify( formData );

    // Store the array in a hidden field
    var hidden = document.createElement( 'input' );
    hidden.setAttribute( 'type', 'hidden' );
    hidden.setAttribute( 'name', 'data' );
    hidden.setAttribute( 'value', formData );
    form.appendChild( hidden );

    // Save to the server.
    form.submit();
}

Here we have a more standard user experience. When we call the saveAll on a Submit click, they see their browser loading and the page behaving as if it is a normal form. No need for extra UI elements or indicators that the page is "doing something". No need for trying to validate you have received all of the user's POSTs.

Next Steps

As i mentioned, this solution is less than ideal. Really, i would like to forego creating dom elements and really work exclusively with lighter weight objects like XMLHttpRequest and FormData. However, testing methods with these objects seemed to result in convoluted POST bodies, which made server side handling pretty ugly. Cleaning up the request would be a big plus when it came to simplifying the javascript.

I am very open to ideas if anyone else has come across this issue and solved it in a different manner.

3 Responses
Add your response

To get a response, we may be able to determine the target form post to a hidden iframe. Then, we listen the iframe load event, then parsing the contents of the iframe using native javascript like this or whatever:

var response = {
responseText: ''
};

var doc = frame.contentWindow.document || frame.contentDocument,
contentNode;

if (doc && doc.body) {
if ((contentNode = doc.body.firstChild) && /pre/i.test(contentNode.tagName)) {
response.responseText = contentNode.textContent;
}
else if ((contentNode = doc.getElementsByTagName('textarea')[0])) {
response.responseText = contentNode.value;
}
else {
response.responseText = doc.body.textContent || doc.body.innerText;
}
}
</pre>

over 1 year ago ·

I've had luck saving an entire collection at once without sending X number of HTTP requests by wrapping my collection in a model.

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
}

});

Check here for the original answer:
http://stackoverflow.com/questions/6879138/how-to-save-an-entire-collection-in-backbone-js-backbone-sync-or-jquery-ajax

over 1 year ago ·

I used your solution with a few minor improvements (used jQuery.ajax and sent the data as a list of objects to the server, and added a data param to the function, through which you can pass custom data to the server), and it seems pretty clean to me.

The data looks like this:

items[0][id]:35 items[0][student_id]:1 items[1][id]:36 items[1][student_id]:2</code>

That's pretty OK to handle server-side.

over 1 year ago ·