Welcome to the second part of Using Backbone Within the WordPress Admin. In the first part, we set up the 'back-end' of our plugin and now in the second part we'll finish off by adding our 'client-side' or 'front end' functionality. For an overview of what we're building in this tutorial along with our folder structure and files, please review the first part.
1. Create the Template File
Within the src folder, create another one called Templates and a file inside that one called metabox.templ.php. This is where we will be putting the HTML needed for our meta box. It's also a great opportunity to output the JSON data needed for our Answers.
Your folders and files should now look like this.
Create the Template for a Single Answer
Let's take another look at what we are creating. You can think of each Answer as a Model of data and because we'll be using client-side templates to generate a view for each one, that view can react to changes within the model. This allows us to be very specific when binding events to the UI and naturally leads to an easier workflow - once you've gotten your head around it, that is.
Inside our newly created metabox.templ.php, this is the template that we'll be using for each of our models. You can see that we are basically wrapping some HTML in a script tag. We give the script tag the attribute type="text/template"
so that the browser does not render it to the page. This small chunk of HTML is to be used later to generate the markup needed for each view. We will be using Underscore's built-in template capabilities so values wrapped like this will be replaced by data in our models later on.
<!-- src/templates/metabox.templ.php --> <!-- Template --> <script type="text/template" id="inputTemplate"> <label for="<%= answer_id %>"><%= index %>:</label> <input id="<%= answer_id %>" class="answers" size="30" type="text" name="<%= answer_id %>" value="<%= answer %>" placeholder="Answer for Question <%= index %> Here"> <button disabled="true">Save</button> </script> <!-- End template -->
Base HTML
Still inside of src/templates/metabox.templ.php - here we are just laying down the containers that will be populated with the inputs from the template above. This happens after Backbone has parsed the JSON data needed for the model, so for now this is all we need to do here.
<!-- src/templates/metabox.templ.php --> <p>Enter the Answers below</p> <div id="answerInputs"></div> <div id="answerSelect"> <span>Correct Answer:</span> </div> <p> <input name="save" type="submit" class="button button-primary button-small" value="Save all"> </p>
Output the JSON
The final thing needed inside the src/templates/metabox.templ.php file, is the JSON data that represents each answer. Here we are creating an object on the Global Namespace and then assigning the values that we sent through with the $viewData
array. I also like to save references to the containers we will be using later on so that I don't have IDs in two seperate files.
<!-- src/templates/metabox.templ.php --> <script> window.wpQuiz = {}; var wpq = window.wpQuiz; wpq.answers = <?= $answers ?>; wpq.answers.correct = <?= $correct ?>; wpq.answerSelect = '#answerSelect'; wpq.answerInput = '#answerInputs'; wpq.inputTempl = '#inputTemplate'; wpq.post_id = <?= $post->ID ?>; </script>
2. The JavaScript
Ok, if you have gotten this far, you have successfully set-up your plugin to allow the use of Backbone.js and your meta box is outputting the required mark-up and JSON data. Now it's time to bring it all together and use Backbone.js to organise our client-side code. It's time to cover:
- Creating a collection of models from the JSON data
- Using client-side templates to construct a view for each
- Watching for click, key up and blur events within each view
- Saving a model back to the database
Create the File admin.js and Place It Into the js Folder
Your final directory structure and files should look like this.
First of all we'll wrap everything we do in an immediately called function and pass in jQuery to be used with the $
sign, I won't show this wrapper in any more snippets, so ensure you put everything below within it.
/* js/admin.js */ (function($) { /** Our code here **/ }(jQuery));
Next we need to access our data stored on the global namespace and also create a new object that will store our Backbone objects.
/* js/admin.js */ var Quiz = { Views:{} }; var wpq = window.wpQuiz;
The Model
The model represents a single answer. Within its constructor we are doing a couple of things.
- Setting a default value for correct as
false
- Setting the URL that Backbone requires to save the model back to the database. We can access the correct URL thanks to WordPress proving the
ajaxurl
variable that is available on every admin page. We also append the name of our method that handles the ajax request - Next we are overwriting the
toJSON
method to append the current post's ID to each model. This could've been done server-side, but I've put it in here as an example of how you can override what is saved to the server (This can come in very handy which is why I've included it here) - Finally in the initialize method, we are checking if the current model is the correct answer by comparing its ID to the ID of the correct answer. We do this so that later on we know which answer should be selected by default
/* js/admin.js */ Quiz.Model = Backbone.Model.extend({ defaults : { 'correct' : false }, url : ajaxurl+'?action=save_answer', toJSON : function() { var attrs = _.clone( this.attributes ); attrs.post_id = wpq.post_id; return attrs; }, initialize : function() { if ( this.get( 'answer_id' ) === wpq.answers.correct ) { this.set( 'correct', true ); } } });
The Collection
A Collection is essentially just a wrapper for a bunch of models and it makes working with those models a breeze. For our small example, we won't be modifying the collection, other than specifying which model it should use.
/* js/admin.js */ Quiz.Collection = Backbone.Collection.extend({ model: Quiz.Model });
The Inputs Wrapper
Our first view can be considered a wrapper for the individual input fields. We don't need to declare a template or which HTML element we want Backbone to create for us in this case, because later on when we instantiate this view, we'll pass it the ID of a div
that we created in the meta box file. Backbone will then simply use that element as its container. This view will take a collection and for each model in that collection, it will create a new input
element and append it to itself.
/* js/admin.js */ Quiz.Views.Inputs = Backbone.View.extend({ initialize:function () { this.collection.each( this.addInput, this ); }, addInput : function( model, index ) { var input = new Quiz.Views.Input({ model:model }); this.$el.append( input.render().el ); } });
A Single Input
This next view represents a single model. In the interest of showing off the types of things you can do when coding JavaScript this way, I've tried to provide a few different interaction techniques and show how to react to those with Backbone.
Note that we are specifying a 'tagName
' here along with a template. In our case, this is going to grab that template we looked at earlier, parse it using data from the model, and then wrap everything in a p
tag (which will give us a nice bit of margin around each one).
Also note how events are bound to elements within a view. Much cleaner than your average jQuery callback and what's even better is the ability to use a jQuery selector like this this.$('input')
within our views knowing that they are automatically scoped within the view. This means that jQuery is not looking at the entire DOM when trying to match a selector.
In this view, we'll be able to:
- Know when an input field has been changed
- Update the model associated with it automatically (which will be used to automatically update the select field below it)
- Enable the save button at the side of the input that was changed
- Perform the save back to the database
/* js/admin.js */ Quiz.Views.Input = Backbone.View.extend({ tagName: 'p', // Get the template from the DOM template :_.template( $(wpq.inputTempl).html() ), // When a model is saved, return the button to the disabled state initialize:function () { var _this = this; this.model.on( 'sync', function() { _this.$('button').text( 'Save' ).attr( 'disabled', true ); }); }, // Attach events events : { 'keyup input' : 'blur', 'blur input' : 'blur', 'click button' : 'save' }, // Perform the Save save : function( e ) { e.preventDefault(); $(e.target).text( 'wait' ); this.model.save(); }, // Update the model attributes with data from the input field blur : function() { var input = this.$('input').val(); if ( input !== this.model.get( 'answer' ) ) { this.model.set('answer', input); this.$('button').attr( 'disabled', false ); } }, // Render the single input - include an index. render:function () { this.model.set( 'index', this.model.collection.indexOf( this.model ) + 1 ); this.$el.html( this.template( this.model.toJSON() ) ); return this; } });
The Select Element
This select element is where the user can choose the correct answer. When this view in instantiated, it will receive the same collection of models that the input's wrapper did. This will come in handy later because we'll be able to listen for changes to the model in the input fields and automatically update the corresponding values within this select element.
/* js/admin.js */ Quiz.Views.Select = Backbone.View.extend({ initialize:function () { this.collection.each( this.addOption, this ); }, addOption:function ( model ) { var option = new Quiz.Views.Option({ model:model }); this.$el.append( option.render().el ); } });
A Single Option View
Our final view will create an option element for each model and will be appended to the select element above. This time I've shown how you can dynamically set attributes on the element by returning a hash from a callback function assigned to the attributes property. Also note that in the initialize()
method we have 'subscribed' to change events on the model (specifically, the answer
attribute). This basically just means: any time this model's answer attribute is changed, call the render()
method (which in this case, will just update the text). This concept of 'subscribing' or 'listening' to events that occur within a model is really what make Backbone.js and the many other libraries like it so powerful, useful and a joy to work with.
/* js/admin.js */ Quiz.Views.Option = Backbone.View.extend({ tagName:'option', // returning a hash allows us to set attributes dynamically attributes:function () { return { 'value':this.model.get( 'answer_id' ), 'selected':this.model.get( 'correct' ) } }, // Watch for changes to each model (that happen in the input fields and re-render when there is a change initialize:function () { this.model.on( 'change:answer', this.render, this ); }, render:function () { this.$el.text( this.model.get( 'answer' ) ); return this; } });
Instantiate Collection and Views
We are so close now, all we have to do is instantiate a new collection and pass it the JSON it needs, then instantiate both of the 'wrapper' views for the select element and for the inputs. Note that we also pass the el
property to our views. These are references to the div and select element that we left blank earlier in the meta box.
/* js/admin.js */ var answers = new Quiz.Collection( wpq.answers ); var selectElem = new Quiz.Views.Select({ collection:answers, el :wpq.answerSelect }); var inputs = new Quiz.Views.Inputs({ collection:answers, el:wpq.answerInput });
3. Activate the Plugin
If you have made it to the end, you should now have a fully working example of how to incorporate Backbone JS into a WordPress plugin. If you go ahead and take a look at the source files, you'll notice that the actual amount of code needed to incorporate Backbone is relatively small. Much of the code we went over here was the PHP needed for the plugin. Working with Backbone on a daily basis for the last 6 weeks has really given me a new found respect for front end code organisation and I hope that you can appreciate the benefits that will surely come from working in this manner.
Within the WordPress community I can envision some of the more complex and high-quality plugins out there really benefiting from using Backbone and I am honoured to have been able to share with you a technique for doing exactly that.
Comments