Backbone, WordPress Code Techniques

Syncing Backbone Models and Collections to admin-ajax.php

Backbone is framed around the assumption that your models and collections are backed by RESTful API’s. If this isn’t the case, life becomes difficult. WordPress provides an AJAX API for backend AJAX requests. These requests are sent through admin-ajax.php which is not RESTful.

I’ve written some code to force Backbone models and collections to play nicely with admin-ajax.php. First I’ll show code, then provide explanation:

/**
 * A mixin for collections/models
 */
var adminAjaxSyncableMixin = {
	url: ajaxurl,

	sync: function( method, object, options ) {
		if ( typeof options.data === 'undefined' ) {
			options.data = {};
		}

		options.data.nonce = localizedSettings.nonce;
		options.data.action_type = method;
		options.data.action = 'myaction';

		var json = this.toJSON();
		var formattedJSON = {};

		if ( json instanceof Array ) {
			formattedJSON.models = json;
		} else {
			formattedJSON.model = json;
		}

		_.extend( options.data, formattedJSON );

		options.emulateJSON = true;

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

/**
 * A model for all your syncable models to extend
 */
var BaseModel = Backbone.Model.extend( _.defaults( {
	parse: function( response ) {
		// Implement me depending on your response from admin-ajax.php!

		return response
	}

}, adminAjaxSyncableMixin ));

/**
 * A collection for all your syncable collections to extend
 */
var BaseCollection = Backbone.Collection.extend( _.defaults( {
	parse: function( response ) {
		// Implement me depending on your response from admin-ajax.php!

		return response
	}

}, adminAjaxSyncableMixin ));

The bulk of the action happens in our mixin:

var adminAjaxSyncableMixin = {
	url: ajaxurl,

	sync: function( method, object, options ) {}
};

We will extend the defining objects passed to our base model and collection. Therefore BaseCollection and BaseModel will have this objects properties (unless they are overwritten — more on that later).

The url property defines the location to which syncing will occur. In this case we are using ajaxurl which is a variable localized from WordPress by default containing a full URL to admin-ajax.php.

The sync property defines a function that will be called in front of Backbone.sync. Backbone.sync is called whenever we call Backbone.Model.save, Backbone.Model.destroy, Backbone.Model.fetch, Backbone.Collection.save, and Backbone.Collection.fetch. By providing a sync property to our base model and collection, we are forcing our sync function to be called instead of Backbone.sync.

adminAjaxSyncableMixin.sync has a few parameters:

sync: function( method, object, options ) {

}

By default, method is set by the function called. For example, calling Backbone.Model.save or Backbone.Collection.save will call Backbone.sync where method is create or patch (possibly update). method ultimately defines the type of HTTP method used (GET, POST, DELETE, PATCH, PUT, or OPTIONS). object is the model or collection object being synced. options lets us send associative arguments to our sync destination among other things.

Let’s look at the body of this function.

if ( typeof options.data === 'undefined' ) {
	options.data = {};
}

Setting the data property in options lets us manually overwrite the representational data sent to the server. We are going to use this so we make sure it’s defined, if it isn’t.

options.data.nonce = localizedSettings.nonce;
options.data.action_type = method;
options.data.action = 'myaction';

Now we are just setting up information to pass to admin-ajax.php. By default we pass a nonce that has been localized to our script. options.data.action should contain the action slug registered within WordPress using the wp_ajax_ hook. We will force our request to be an HTTP POST so we send our method along inside action_type for later use.

var json = this.toJSON();
var formattedJSON = {};

if ( json instanceof Array ) {
	formattedJSON.models = json;
} else {
	formattedJSON.model = json;
}
_.extend( options.data, formattedJSON );

This code sets up our model or collection data to be passed to the server. If the toJSON() representation of the current object is an Array, we know we have a collection. We extend the options.data object with the formattedJSON object we create.

options.emulateJSON = true;

This sends our data as application/x-www-form-urlencoded (classic form style) instead of application/json preventing us from having to decode JSON in our endpoint.

return Backbone.sync.call( this, 'create', object, options );

Finally, we call Backbone.sync in the current object context. We pass create as the method (forcing a POST request). object is simply passed along. We pass options having extended it with our own data. Essentially, our sync function is an intermediary between Backbone save/fetch and Backbone.sync.

var BaseModel = Backbone.Model.extend( _.defaults( {
	idAttribute: 'ID',
	parse: function( response ) {
		// Implement me depending on your response from admin-ajax.php!

		return response
	}
}, adminAjaxSyncableMixin ));

var BaseCollection = Backbone.Collection.extend( _.defaults( {
	parse: function( response ) {
		// Implement me depending on your response from admin-ajax.php!

		return response
	}
}, adminAjaxSyncableMixin ));

We define BaseModel by extending Backbone.Model and mixing in adminAjaxSyncableMixin. _.defaults returns an object filling in undefined properties of the first parameter object with corresponding property in the second parameter object. We define BaseCollection the same way extending Backbone.Collection and mixing in adminAjaxSyncableMixin.

Backbone.Model.parse and Backbone.Collection.parse intercept sync responses before they are processed into models and model data. Depending on how you write your admin-ajax.php endpoints, you may need to write some parsing code.

Finally, we can define, instantiate, and utilize new models and collections based on BaseModel and BaseCollection:

var myModel = BaseModel.extend({});
var myCollection = BaseCollection.extend({});

var modelInstance = new myModel( { ID: 1 } );
modelInstance.fetch();
modelInstance.set( 'key', 'value' );
modelInstance.save();

var collectionInstance = new myCollection();
collectionInstance.fetch();
collectionInstance.at( 0 ).set( 'key', 'value' );
collectionInstance.save();
Standard

9 thoughts on “Syncing Backbone Models and Collections to admin-ajax.php

    • Most implementations won’t require a special parse function. Is there something in particular you are trying to accomplish? A good example of where this is useful is a nested collection. parse would instantiate a new collection from an array of objects pulled from the endpoint.

  1. Hi Taylor.

    Thanks for taking the time to write this up.

    I’d really like to know how I would I customize this property in a child class.
    options.data.action = ‘myaction’;
    As I’d like to customize this per model.

      • No, I understand the wordpress side of things. I was unsure as to how to overide that property. I’m doing this now

        options.data.action = this.wpEndpoint;

        Then updating that in my client code with

        var myModel = BaseModel.extend({
        wpEndpoint:’custom-endpoint-here’
        });

        Is that how you would do it if you wanted to change that value in a child model ?

  2. Pius says:

    I am trying to create backbone app in the post-new.php. It does work to save a model into collection. But when I create a new post, the collection return empty again and model from previous post has gone. Whenever I reload the page, the collection will reset. I think the collection is not saved on the server. Please help

Leave a comment