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:

[code language=”javascript”]
/**
* 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 ));

[/code]

The bulk of the action happens in our mixin:

[code language=”javascript”]
var adminAjaxSyncableMixin = {
url: ajaxurl,

sync: function( method, object, options ) {}
};
[/code]

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:

[code language=”javascript”]
sync: function( method, object, options ) {

}
[/code]

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.

[code language=”javascript”]
if ( typeof options.data === ‘undefined’ ) {
options.data = {};
}
[/code]

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.

[code language=”javascript”]
options.data.nonce = localizedSettings.nonce;
options.data.action_type = method;
options.data.action = ‘myaction’;
[/code]

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.

[code language=”javascript”]
var json = this.toJSON();
var formattedJSON = {};

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

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.

[code language=”javascript”]
options.emulateJSON = true;
[/code]

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.

[code language=”javascript”]
return Backbone.sync.call( this, ‘create’, object, options );
[/code]

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.

[code language=”javascript”]
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 ));
[/code]

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:

[code language=”javascript”]
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();
[/code]


Comments

9 responses to “Syncing Backbone Models and Collections to admin-ajax.php”

  1. Can you give an example of what would go parsing functions?
    // Implement me depending on your response from admin-ajax.php!

    1. 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. Never-mind, I figured it out. Just a typo on my part in my this.collection.on(‘add’, this.addOne, this); handler. Thanks.

  2. 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.

    1. options.data.action = ‘myaction'; corresponds to action in wp_ajax_$youraction. See codex. Does that help?

      1. 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 ?

        1. That would work but you would need an initialize method like so:

          initialize: function( options ) {
          this.wpEndpoint = options.wpEndpoint;
          }

  3. 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 Reply

Your email address will not be published. Required fields are marked *