Todo List App Powered By WordPress

WordPress is an awesome tool for setting up a blog. With its vast sea of plugins and themes, combined with an API that spans into every corner of the system, you can build nearly any kind of web application with it. This advances it from a content management system to a full-fledged platform for creating web applications.

In this tutorial, we are going to make a WordPress plugin that hooks into the API. It will then present a simple, AJAX-ed todo list application on the /todo URL of your WordPress site. The best thing is that this is a plugin and not a theme, which means you can use it on any WordPress site regardless of the theme. Let’s get started!

Your First WordPress Plugin

If you haven’t written a WordPress plugin before, here is what you need to know:

  • Plugins are PHP files that reside in the /wp-content/plugins folder;
  • Plugins can be either a single PHP file with a unique name, or a folder with that file inside it, along with additional includes and resources (read the getting started guide);
  • Plugins are described by a comment header in the main PHP file. You need this for your plugin to be recognized;
  • Plugins do their business by hooking up to specific events in the WordPress execution. There is a reference with all available filters and actions;
  • The documentation is your friend.

To develop a plugin, install a local copy of WordPress and create a new PHP file in the /wp-content/plugins folder. After this place the header comment you see below, and activate your plugin from the admin panel.

If you only wish to test out the Todo app we are writing today, you can simply grab the download zip, and install it from WordPress’ admin panel (choose Plugins->Upload).

Here is the main plugin file for our application:

tz-todoapp.php

/*
Plugin Name: Todo App
Plugin URI: http://tutorialzine.com
Description: This is a demo plugin for a Tutorialzine tutorial
Version: 1.0
Author: Martin Angelov
Author URI: http://tutorialzine.com
License: GPL2
*/

define('TZ_TODO_FILE', __FILE__);
define('TZ_TODO_PATH', plugin_dir_path(__FILE__));

require TZ_TODO_PATH.'includes/tzTodo.php';

new tzTodo();

You can see the header comment at the top. This is the description that the system uses to present on the plugin activation page. After this, we are defining two constants for the file of the plugin (it is used as an identifier in some function calls), and the folder path. After this we are including the tzTodo class and creating a new object.

Todo App Powered By WordPress

Todo App Powered By WordPress

The tzTodo Class

This class holds all of the functionality for the plugin. In its constructor, we are hooking to a number of actions: for initialization, ajax and layout of the custom post type we will be defining. In the body of the class we have methods that perform useful functionality when these actions are triggered and also define a custom post type ’tz_todo‘.

includes/tzTodo.php

class tzTodo {

	public function __construct(){

		add_action( 'init', array($this,'init'));

		// These hooks will handle AJAX interactions. We need to handle
		// ajax requests from both logged in users and anonymous ones:

		add_action('wp_ajax_nopriv_tz_ajax', array($this,'ajax'));
		add_action('wp_ajax_tz_ajax', array($this,'ajax'));

		// Functions for presenting custom columns on
		// the custom post view for the todo items

		add_filter( "manage_tz_todo_posts_columns", array($this, 'change_columns'));

		// The two last optional arguments to this function are the
		// priority (10) and number of arguments that the function expects (2):

		add_action( "manage_posts_custom_column", array($this, "custom_columns") , 10, 2 );
	}

	public function init(){

		// When a URL like /todo is requested from the,
		// blog we will directly include the index.php
		// file of the application and exit 

		if( preg_match('/\/todo\/?$/',$_SERVER['REQUEST_URI'])){
			$base_url = plugins_url( 'app/' , TZ_TODO_FILE);
			require TZ_TODO_PATH.'/app/index.php';
			exit;
		}

		$this->add_post_type();
	}

	// This method is called when an
	// AJAX request is made to the plugin

	public function ajax(){
		$id = -1;
		$data = '';
		$verb = '';

		$response = array();

		if(isset($_POST['verb'])){
			$verb = $_POST['verb'];
		}

		if(isset($_POST['id'])){
			$id = (int)$_POST['id'];
		}

		if(isset($_POST['data'])){
			$data = wp_strip_all_tags($_POST['data']);
		}

		$post = null;

		if($id != -1){
			$post = get_post($id);

			// Make sure that the passed id actually
			// belongs to a post of the tz_todo type

			if($post && $post->post_type != 'tz_todo'){
				exit;
			}
		}

		switch($verb){
			case 'save':

				$todo_item = array(
					'post_title' => $data,
					'post_content' => '',
					'post_status' => 'publish',
					'post_type' => 'tz_todo',
				);

				if($post){

					// Adding an id to the array will cause
					// the post with that id to be edited
					// instead of a new entry to be created.

					$todo_item['ID'] = $post->ID;
				}

				$response['id'] = wp_insert_post($todo_item);
			break;

			case 'check':

				if($post){
					update_post_meta($post->ID, 'status', 'Completed');
				}

			break;

			case 'uncheck':

				if($post){
					delete_post_meta($post->ID, 'status');
				}

			break;

			case 'delete':
				if($post){
					wp_delete_post($post->ID);
				}
			break;
		}

		// Print the response as json and exit

		header("Content-type: application/json");

		die(json_encode($response));

	}

	private function add_post_type(){

		// The register_post_type function
		// will make a new Todo item entry
		// in the wordpress admin menu

		register_post_type( 'tz_todo',
			array(
				'labels' => array(
					'name' => __( 'Todo items' ),
					'singular_name' => __( 'Todo item' )
				),
				'public' => true,
				'supports' => array('title')	// Only a title is allowed for this type
			)
		);
	}

	public function change_columns($cols){

		// We need to customize the columns
		// shown when viewing the Todo items
		// post type to include a status field

		$cols = array(
			'cb'       => '<input type="checkbox" />',
			'title'      => __( 'Task' ),
			'status' => __( 'Status' ),
			'date'     => __( 'Date' ),
		);

		return $cols;
	}

	public function custom_columns( $column, $post_id ) {

		// Add content to the status column

		switch ( $column ) {

			case "status":
				// We are requesting the status meta item

				$status = get_post_meta( $post_id, 'status', true);

				if($status != 'Completed'){
					$status = 'Not completed';
				}

				echo $status;

				break;
		}
	}

}

The most interesting method is probably the AJAX one. Here we are receiving AJAX requests that are sent from the jQuery frontend. Depending on the action that needs to be done, we create or delete an item of the tz_todo custom post type and attach or remove metadata to mark the task as completed. Credit goes to Joost de Valk for his useful snippets.

In the init() method, you can see a trick that I am using to serve the index.php file from the app folder of the plugin, when the /todo URL is requested. I am matching the $_SERVER['REQUEST_URI'] entry with a pattern. If the requested URL is the one we are interested in, the index.php file is included and the WordPress execution is stopped. Now when somebody visits the http://example.com/todo of your WordPress powered site, they will see the app.

Note: for this to work you need to have pretty URLs enabled from the WP settings. Otherwise a 404 error will be thrown.

The Todo List App

As you saw above, upon visiting the /todo URL, our plugin includes the /app/index.php. This is the file that presents the interface you see in the demo. It is shown below.

/app/index.php

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>Todo App Powered By WordPress | Tutorialzine Demo</title>

        <!-- This is important! It fixes the paths of the css and js files -->
        <base href="<?php echo $base_url ?>"></base>

        <!-- The stylesheets -->
        <link rel="stylesheet" href="assets/css/styles.css" />

        <script>

        	// This is the URL where we need to make our AJAX calls.
        	// We are making it available to JavaScript as a global variable.

        	var ajaxurl = '<?php echo admin_url('admin-ajax.php')?>';
        </script>
    </head>

    <body>

		<div id="todo">
			<h2>Todo List <a href="#" class="add"
				title="Add new todo item!">✚</a></h2>
			<ul>
				<?php

					$query = new WP_Query(
						array( 'post_type'=>'tz_todo', 'order'=>'ASC')
					);

					// The Loop
					while ( $query->have_posts() ) :
						$query->the_post();
						$done = get_post_meta(get_the_id(), 'status', true) ==
							'Completed';
					?>

						<li data-id="<?php the_id()?>"
							class="<?php echo ($done ? 'done' : '')?>">
							<input type="checkbox"
								<?php echo ($done ? 'checked="true"' : '')?> />
							<input type="text"
								value="<?php htmlspecialchars(the_title())?>"
								placeholder="Write your todo here" />
							<a href="#" class="delete" title="Delete">✖</a>
						</li>

					<?php endwhile; ?>
			</ul>
		</div>

        <!-- JavaScript includes.  -->
		<script src="http://code.jquery.com/jquery-1.8.2.min.js"></script>
		<script src="assets/js/script.js"></script>

    </body>
</html>

Here we are using the WP_Query class to request all the posts of the tz_todo type in ascending order starting with the oldest. An ordinary while loop follows that you may recognize if you’ve created WordPress themes.

At the bottom, we have the latest version of the jQuery library at the time of this writing, and our script.js file, which drives the front end of the app.

The jQuery Code

Our Todo app is nearly done! All we have to do is write the jQuery code that drives the interface:

/app/assets/js/script.js

$(function(){

	var saveTimer;
	var todoHolder = $('#todo');

	// Listen for the input event in the text fields:
	todoHolder.on('input','li input[type=text]', function(e){

		// This callback is run on every key press

		var todo = $(this),
			li = todo.closest('li');

		// We are clearing the save timer, so that
		// sending the AJAX request will only
		// happen once the user has stopped typing

		clearTimeout(saveTimer);

		saveTimer = setTimeout(function(){

			ajaxAction('save', li.data('id'), todo.val()).done(function(r){
				if(r.id != li.data('id')){
					// The item has been written to the database
					// for the first time. Update its id.
					li.data('id', r.id);
				}
			});

		}, 1000);

	});

	// Listen for change events on the checkboxes
	todoHolder.on('change', 'li input[type=checkbox]',function(e){

		var checkbox = $(this),
			li = checkbox.closest('li');

		li.toggleClass('done',checkbox.is(':checked'));

		if(checkbox.is(':checked')){
			ajaxAction('check', li.data('id'));
		}
		else{
			ajaxAction('uncheck', li.data('id'));
		}

	});

	// Listen for clicks on the delete link
	todoHolder.on('click', 'li .delete',function(e){

		e.preventDefault();

		var li = $(this).closest('li');

		li.fadeOut(function(){
			li.remove();
		});

		if(li.data('id') != 0){

			// No need to delete items if they are missing an id.
			// This would mean that the item we are deleting
			// does not exist in the database, so the AJAX
			// request is unnecessary.
			ajaxAction('delete', li.data('id'));
		}

	});

	// Clicks on the add new item button)
	todoHolder.on('click','a.add', function(e){
		e.preventDefault();

		var item = $('<li data-id="0">'+
			'<input type="checkbox" /> <input type="text" val="" placeholder="Write your todo here" />'+
			'<a href="#" class="delete">✖</a>'+
			'</li>');

		todoHolder.find('ul').append(item);

		// We are not running an AJAX request when creating elements.
		// We are only writing them to the database when text is entered.
	});

	// A help function for running AJAX requests
	function ajaxAction(verb, id, data){

		// Notice that we are returning a deferred

		return $.post(ajaxurl, {
			'action': 'tz_ajax',
			'verb':verb,
			'id': id,
			'data': data
		}, 'json');

	}
});

The code is heavily commented so it shouldn’t be difficult to grasp. I am using some of the newer jQuery features like deferreds and the on() syntax for registering events.

Done!

This application will give you a good idea of what developing WordPress plugins is like. As I said in the beginning, WP is turning into a platform for developing web applications. After all it is easy to set up, has a great admin area with user roles, and a multitude of high-quality plugins and themes.

via Tutorialzine http://tutorialzine.com/2012/10/todo-app-wordpress/

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s