Todo List App Powered By WordPress

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.

by Martin Angelov

Martin is a web developer with an eye for design from Bulgaria. He founded Tutorialzine in 2009 and publishes new tutorials weekly.

Tutorials

38 Comments

  1. Douglas says:

    Awesome guys!!

  2. good tutorial! Thanks)

  3. arifur rahman says:

    Hello, Martin really awesome tutorial.

  4. Linas says:

    Great tutorial! is it possible to use it without WordPress?

    1. Jakub says:

      No, you can't.
      This tutorial is basically a wordpress theme for todo lists. What are you looking for and why not wordpress?

      1. Linas says:

        I need something like this ToDo list to adapt to my own php based CMS system. Could be jQuery based (or Ajax) script that communicates with database. Any ideas? Thanks.

    2. Martin Angelov says:

      Like Jakub said, the tutorial is built around WordPress. Althoug it is a plugin not a theme. If you want to remove WP entirely, you will have to rewrite the code and handle database insertion for the todo item and metadata yourself.

      1. Linas says:

        Thanks. Already working on database side, hope to come up with something soon.

        1. Jack says:

          Did you ever do this? I'd love to get a copy.

  5. Armin says:

    Awesome tutorial! By the way, why dont you put sharing options at the end of the post, so I can share it AFTER I read it?! I scroll up all the page every single time to find them.

    1. Martin Angelov says:

      Thank you! I'll think of something.

  6. Pavel says:

    Thanks for share, great tutorial!

  7. FINESTGOL says:

    Very Good!!! Add in the front page of the site!

    1. FINESTGOL says:

      *Very Good!!!!!! Add on the admin page of the site!
      PS: sorry bad translator(

  8. David says:

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

    Why don't you use the native Rewrite-API of WP? There is a rewrite-parameter for registered post types. With this and the filter 'template_redirect' you can get your template without exit during the WP process. You should never exit WP! There comes a bunch of other hooks after 'init'. Your plugin won't be the only one in a typical WP-Setup!

    <!-- …It fixes the paths of the css and js files -->

    There would be nothing to fix, if you use the API for enqueing styles an scripts. See wp_enqueue_style(), wp_enqueue_script().

    Finally, with a simple POST-Request on your AJAX-Interface I can input arbitrarily 'todos' to your DB. You should impede this, by using nonces. See wp_create_nonce().

    1. Martin Angelov says:

      Thank you for the thorough comment! I chose this approach instead of the Rewrite API, as the API is not able to redirect requests outside of WP's index.php. In our case, the todo application's PHP file is separate from the rest of WordPress and is only hosted in the plugin folder for ease of deployment. There exists a way to rewrite requests outside of index.php by using external rules, but these write to the .htaccess file. The page also will not have access to the WordPress API. There is of course a way to work around the latter issue, but the resulting code will not be any better than it is now.

      You are right that I should have used wp_enqueue_* and nonces. I will keep it in mind for future WP work. Again thank you for your suggestions!

      1. Francesco says:

        Hi Martin,
        is there a way to add a parameter after /todo/ and to read it into your app?

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

        What I would like to do is to have a url like /todo/3 or /todo/4
        and read the number 3 or 4 as a variable inside your script.

        Is this something possible?

        Thanks a lot for the tutorial!

  9. David says:

    Don't mention it!

    I still have one question: You're writing a plugin. So why don't you put everything of the code in the plugins folder from beginning? The next thing is, you're dependent from the use of permalinks. Otherwise the Request on /todo would never trigger WP to run.
    And, again, it's a bad idea to interrupt the WP execution on 'init'.

  10. Rob Mayer says:

    Hi Martin,

    I agree that many people say that WordPress is easy to use for beginners.

    I'm reading your post and don't know if I can lol ... if I need to learn all of those codes, I'm giving up even before I install WordPress lol.

    Thanks for your post those
    Rob Mayer

    1. Adewale Olaore says:

      I agree with you Rob, even though this is a very good tutorial, it still leaves the beginners out of the while show. There should be an explanation of what each line of code is doing and why it has to be written the way it is.

      Martin, I really appreciate your efforts in putting forth these tutorials, I mean I have benefited from some of them, but when it comes to coding tutorials, please take time to explain.

  11. Matthew says:

    Is it possible to write this just into a webpage? I mean like, make this separate? Without wordpress?

    Thanks.

  12. Vladimir says:

    Very good work, I will use it carefully in my projects
    bigUp

  13. BoMaZeN says:

    cool tutorial , Thank you!

  14. Saleh Janahi says:

    this thing is AMAZING!

    Can i use it as widget, and allow only the admin to change it? Wolud look cool if it was on the home page :-)

  15. Your site has been around for a couple of years but I just found it now. The tutorials are really well done. I actually prefer the layout to those on Nettuts.

  16. Matt Morris says:

    Hi Martin, this is a fantastic tutorial, would it be possible to get a seperate rss feed from this app?

    1. Martin Angelov says:

      You can try with this plugin. However it will not include whether the task is completed in the feed.

  17. Youss says:

    Check this out: https://github.com/addyosmani/todomvc
    Something to reflect upon

    1. Martin Angelov says:

      So in a sense, todo apps have become the hello world of frameworks :)

  18. Matt Morris says:

    Thanks for your help guys :)

  19. Ajmal says:

    Hey there Martin!

    Really glad that I stumbled upon this tutorial! Really what I am looking for my next WordPress project.

    Does this plugin works on http://example.com/todo or I could actually pull it and insert it inside my index.php or homepage?

  20. So in a sense, todo apps have become the hello world of frameworks :)

  21. nitin says:

    Heloo there thanks for nice tutorial
    But is it Possible to use outside wordpress i mean for other cms like Drupal,Joomla etc...

  22. Amr says:

    how can I make the todo app only visible to logged in users or users depending in level??

  23. himu says:

    Hey, big thanks for this tutorial

  24. JimG says:

    Thanks Martin for the nice tutorial.

    Was wondering why when I click for the delete link (x) on one item it brings me to another page, wp-contents/# instead of capting it and deleting the item. Do you know how I can fix this?

    Thanks

  25. Awesome tutorial! I use it in my WordPress site.

  26. Justin says:

    Hello,
    I have a simple question. I followed your tutorial. I enjoyed it and also had created a plugin as you have explained, almost. I would like to change the title from Add a new Post to something like Add a new Todo. I do not know it is because if made any mistakes . But I think it would be great.

Add Comment

Add a Reply

HTML is escaped automatically. Surround code blocks with <pre></pre> for readability.
Perks:   **bold**   __italics__   [some text](http://example.com) for links