Advertisement

Using Backbone Within the WordPress Admin: The Back End

by

The rumours are true! The WordPress Admin Panel is now using Underscore and Backbone! This means that with minimal effort, we can begin to utilise these fantastic JavaScript libraries in our own plugins. This tutorial will show you exactly how you can do that. We'll create the Admin part of a Quiz plugin. We'll use a simple Custom Post Type to save Questions, and then within each question we'll add a meta box that will allow us to enter up to 4 answers and select which is the correct one. We'll be going through how to use templates, how to hook into click and key-up events, how to save data back to the WordPress database and most importantly, how to 'get your truth out of the Dom', as the creator Jeremy Ashkenas likes to put it.

I will say up front, that the plugin we are building in this tutorial may seem overly verbose for what it accomplishes. It will however, give you an excellent peek into the world of using Backbone and should you come across a project in the future that requires a complex user interface with a lot of JavaScript, you will be well armed and ready to bring much needed organisation to the party.


What We'll Do

In this first part, we'll set up the back end of our plugin. This will involve setting up the files and folders as well as implementing all of the features our plugin requires in PHP. We'll need to:

  1. Register a Custom Post Type - for our Questions
  2. Add a meta box that will allow us to enter answers on the same page
  3. Save information from the meta boxes when the post is saved
  4. Save information from our ajax requests (via Backbone)

Then in the Second Part...

Once we have our back end set up, we'll then proceed to output the required HTML for our meta box along with the data for each answer in JSON format. We'll also write the JavaScript that ties everything together. We'll cover:

  1. Outputting base HTML for the meta box
  2. Outputting a client-side template along with the answers in JSON
  3. The JavaScript needed to tie it all together

I hope this small series sounds interesting to you, and I'm looking forward to helping you get up and running with using Backbone.js within a WordPress plugin.


What We'll Build

This small plugin will use a Custom Post Type to save Questions. Then in a meta box, we'll create four inputs that will allow users to enter possible answers to the current question and select which of those is the correct answer. When an answer is changed, the corresponding save button will become active. When clicked, we'll use Backbone's built in model.save() method to save the data back to the WordPress database. Also, when the answers are being written in the inputs, the select box below it will automatically update its values as it will be looking out for changes to the models. All of these things are relatively trivial to do with Backbone and after reading this tutorial, you'll be able to start taking your plugins to the next level by using them within WordPress.

answers01

There's a lot to cover, so let's get started!


1. Create the Plugin

We need to do all the usual first steps involved with any plugin - create the files folders.

  1. Create a folder called wp_quiz
  2. Create a PHP file inside with the same name
  3. Create a folder called js
  4. Create a folder called src

Your folder structure should look like this.

files01

2. Add the Plugin Header

Inside of wp_quiz.php.

/*
Plugin Name: WP Quiz
Plugin URI: http://wp.tutsplus.com/author/shaneosbourne/
Description: An example of using Backbone within a plugin.
Author: Shane Osbourne
Version: 0.1
Author URI: http://wp.tutsplus.com/author/shaneosbourne/
*/

3. Add Hooks to Instantiate the Plugin

Still inside of wp_quiz.php, we need to do the following things:

  1. Include our main plugin class
  2. Create a function that will create an instance of the class
  3. Add a hook to only call the function when the user is an admin
/** wp_quiz.php **/

include 'src/WpQuiz.php'; // Class File

// Create an instance of the Plugin Class
function call_wp_quiz() {
	return new WpQuiz( 'admin' );
}

// Only when the current user is an Admin
if ( is_admin )
	add_action( 'init', 'call_wp_quiz' );

// Helper function
if ( ! function_exists( 'pp' ) ) {
	function pp() {
		return plugin_dir_url( __FILE__ );
	}
}

Putting the helper function pp() within this file will allow us to reference other files relative to the root of the plugin folder (you'll see that in action shortly).


4. Create the Plugin Class

Inside of the src folder, create a file called WpQuiz.php.

In this plugin class, we'll be needing a few different methods to accomplish all of the following:

  1. Register the Custom Post Type
  2. Add a meta box
  3. Retrieve the content for the meta box and output both HTML and some JSON data into it
  4. Listen for PUT requests and save data to the database
  5. Save our meta box data upon regular 'save' actions

Before we write the methods though, we're going to be storing some information as class properties. We store this information at the top of our class file so that modifications are easier to make later on. The answerIds array contains the keys that we'll be using throughout this plugin to save data using the built-in add_post_meta().

Add the Properties

/** src/WpQuiz.php  **/
class WpQuiz {

	// Names of Custom Post Type
	public $postTypeNameSingle = 'Question';
	public $postTypeNamePlural = 'Questions';

	// Meta Box Stuff
	public $metaBoxTitle = 'Answers';
	public $metaBoxTempl = 'templates/metabox.templ.php';

	// Question Id's
	public $answerIds = array( 'quiz-a-1', 'quiz-a-2', 'quiz-a-3', 'quiz-a-4' );

	// Javascript
	public $jsAdmin = '/js/admin.js';

}

Add the Constructor

  1. First we register the Custom Post Type using another helper method (which will be seen later on)
  2. Then we are registering a hook to load our meta box
  3. We also need a separate method for accepting our ajax requests
  4. Finally, when a page is loaded we'll want to save info from our meta box
/** src/WpQuiz.php  **/

public function __construct( $type ) {
	switch ( $type ) {
		case 'admin' :
			// Register the Post Type
			$this->registerPostType(
				$this->postTypeNameSingle,
				$this->postTypeNamePlural
			);

			// Add the Meta Box
			add_action( 'add_meta_boxes', array( $this, 'addMetaBox' ) );

			// Accept an Ajax Request
			add_action( 'wp_ajax_save_answer', array( $this, 'saveAnswers' ) );

			// Watch for Post being saved
			add_action( 'save_post', array( $this, 'savePost' ) );
	}
}

Add the Meta Box

  1. Add the JavaScript files needed for this plugin - again using a helper method (seen later)
  2. Create a unique ID for this plugin based on the name of the post type
  3. Add the meta box using the properties we set earlier
/** src/WpQuiz.php  **/

public function addMetaBox() {

	// Load the Javascript needed on this admin page.
	$this->addScripts();

	// Create an id based on Post-type name
	$id = $this->postTypeNameSingle . '_metabox';

	// Add the meta box
	add_meta_box(
		$id,
		$this->metaBoxTitle,
		array( $this, 'getMetaBox' ), // Get the markup needed
		$this->postTypeNameSingle
	);

}

Get the Meta Box Content

Here we are looping through our Answer IDs and constructing an array that contains post meta fetched with our helper method getOneAnswer. We make this new array so that we can encode it and send it to our template in JSON format - just the way Backbone likes it. We send data to our template using the $viewData array seen below. This keeps all of the HTML out of harm's way and allows us to work on it in a separate file. We'll take a quick look at the getTemplatePart method later on, but if you want an in-depth explanation about why I use it, please check out Improving Your Work-Flow – Separate Your Mark-Up From Your Logic!

/** src/WpQuiz.php  **/

public function getMetaBox( $post ) {

	// Get the current values for the questions
	$json = array();
	foreach ( $this->answerIds as $id ) {
		$json[] = $this->getOneAnswer( $post->ID, $id );
	}

	// Set data needed in the template
	$viewData = array(
		'post' => $post,
		'answers' => json_encode( $json ),
		'correct' => json_encode( get_post_meta( $post->ID, 'correct_answer' ) )
	);

	echo $this->getTemplatePart( $this->metaBoxTempl, $viewData );

}

Get a Single Answer - Helper

We are just returning an array of the data needed in our template. You can think of this as creating a single model that is needed on the front end.

/** src/WpQuiz.php  **/

public function getOneAnswer( $post_id, $answer_id ) {
	return array(
		'answer_id' => $answer_id,
		'answer' => get_post_meta( $post_id, $answer_id, true )
	);
}

Save Post

When a user clicks to save a post that our meta box is currently in, we need to do a couple of checks to ensure we are saving our Custom Post Type and that the current user has the correct permissions - if both checks are ok then we save the four answers from the meta box and the correct answer.

/** src/WpQuiz.php  **/

public function savePost( $post_id ) {
	// Check that we are saving our Custom Post type
	if ( $_POST['post_type'] !== strtolower( $this->postTypeNameSingle ) ) {
		return;
	}

	// Check that the user has correct permissions
	if ( ! $this->canSaveData( $post_id ) ) {
		return;
	}

	// Access the data from the $_POST global and create a new array containing
	// the info needed to make the save
	$fields = array();
	foreach ( $this->answerIds as $id ) {
		$fields[$id] = $_POST[$id];
	}

	// Loop through the new array and save/update each one
	foreach ( $fields as $id => $field ) {
		add_post_meta( $post_id, $id, $field, true );
		// or
		update_post_meta( $post_id, $id, $field );
	}

	// Save/update the correct answer
	add_post_meta( $post_id, 'correct_answer', $_POST['correct_answer'], true );
	// or
	update_post_meta( $post_id, 'correct_answer', $_POST['correct_answer'] );

}

Save Answers From Ajax Requests

This is where we will receive data passed to the server from Backbone. We need to:

  1. Access data sent as a PUT request. As it will be in JSON format, we need to decode it
  2. Again check that the current user has relevant permissions
  3. Go ahead and attempt the save
  4. If either Add or Update was successful, we can simply return the newly saved data and Backbone will view this as a successful save
  5. If neither were successful, we just return 0 to indicate a failure
/** src/WpQuiz.php  **/

public function saveAnswers() {
	// Get PUT data and decode it
	$model = json_decode( file_get_contents( "php://input" ) );

	// Ensure that this user has the correct permissions
	if ( ! $this->canSaveData( $model->post_id ) ) {
		return;
	}

	// Attempt an insert/update
	$update = add_post_meta( $model->post_id, $model->answer_id, $model->answer, true );
	// or
	$update = update_post_meta( $model->post_id, $model->answer_id, $model->answer );

	// If a save or update was successful, return the model in JSON format
	if ( $update ) {
		echo json_encode( $this->getOneAnswer( $model->post_id, $model->answer_id ) );
	} else {
		echo 0;
	}
	die();
}

The Helper Methods

Here are the four helpers mentioned in the above snippets.

  1. canSaveData() - This just ensures that the current user has the relevant permissions to edit / update this post.
  2. addScripts() - Note that here we are ensuring that we pass the 5th param to the wp_register_script() function. This will load our custom JavaScript into the footer and will ensure that our JSON data is available. Also, if you are using the WordPress editor in your plugin, you do not need to specify Backbone as a dependency as it will already be available to you. I'm including it here for the sake of the example though.
  3. registerPostType() - This is something I use often in plugins. It just makes life easier when adding a new Custom Post Type. It accepts both singular and plural versions of the name because it's not always as easy as just adding an 's'.
  4. getTemplatePart() - I've never been fond of having mark-up inside of my methods. This little helper will allow the use of a separate template file.
/** src/WpQuiz.php  **/

/**
* Determine if the current user has the relevant permissions
*
* @param $post_id
* @return bool
*/
private function canSaveData( $post_id ) {
	if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
		return false;
	if ( 'page' == $_POST['post_type'] ) {
		if ( ! current_user_can( 'edit_page', $post_id ) )
			return false;
	} else {
		if ( ! current_user_can( 'edit_post', $post_id ) )
			return false;
	}
	return true;
}

private function addScripts() {
	wp_register_script( 'wp_quiz_main_js', pp() . $this->jsAdmin , array( 'backbone' ), null, true );
	wp_enqueue_script( 'wp_quiz_main_js' );
}

/**
* Register a Custom Post Type
*
* @param $single
* @param $plural
* @param null $supports
*/
private function registerPostType( $single, $plural, $supports = null ) {

	$labels = array(
		'name' => _x( $plural, 'post type general name' ),
		'singular_name' => _x( "$single", 'post type singular name' ),
		'add_new' => _x( "Add New $single", "$single" ),
		'add_new_item' => __( "Add New $single" ),
		'edit_item' => __( "Edit $single" ),
		'new_item' => __( "New $single" ),
		'all_items' => __( "All $plural" ),
		'view_item' => __( "View $single" ),
		'search_items' => __( "Search $plural" ),
		'not_found' => __( "No $plural found" ),
		'not_found_in_trash' => __( "No $single found in Trash" ),
		'parent_item_colon' => '',
		'menu_name' => $plural
	);
	$args = array(
		'labels' => $labels,
		'public' => true,
		'publicly_queryable' => true,
		'show_ui' => true,
		'show_in_menu' => true,
		'query_var' => true,
		'rewrite' => true,
		'capability_type' => 'post',
		'has_archive' => true,
		'hierarchical' => false,
		'menu_position' => null,
		'supports' => ( $supports ) ? $supports : array( 'title', 'editor', 'page-attributes' )
	);
	register_post_type( $single, $args );
}

/**
* Render a Template File
*
* @param $filePath
* @param null $viewData
* @return string
*/
public function getTemplatePart( $filePath, $viewData = null ) {

	( $viewData ) ? extract( $viewData ) : null;

	ob_start();
	include ( "$filePath" );
	$template = ob_get_contents();
	ob_end_clean();

	return $template;
}

5. On to the Front End

At this point, we've set up everything needed for our back end. It's time to take a break and prepare for the next part where we'll be getting down and dirty with client-side templates, JavaScript and Backbone.js. I hope to see you there - it's going to be a good one.

Advertisement