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();