Ascent

Save-Once Backbone and Collection.saveAll()

Backbone.js does a lot of things right when it comes to client-heavy interfaces. Client-side M.V.Collection, RESTful by design and structured code locations are just some of the benefits i point to 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, in theory, would be an ideal user experience.

The Problem...

Occasionally however, 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. You invariably find yourself writing some code like:

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.

Forms and saveAll()

The trick i've used in these cases is to lean on Backbone for the interface, and then fall back to standard HTML forms 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 server-side handling.

Get the latest posts delivered right to your inbox.
Author image
Written by Ben
Ben is the co-founder of Skyward. He has spent the last 10 years building products and working with startups.