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: https://tutorialzine.com Description: This is a demo plugin for a Tutorialzine tutorial Version: 1.0 Author: Martin Angelov Author URI: https://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.
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.
Bootstrap Studio
The revolutionary web design tool for creating responsive websites and apps.
Learn more
Awesome guys!!
good tutorial! Thanks)
Hello, Martin really awesome tutorial.
Great tutorial! is it possible to use it without WordPress?
No, you can't.
This tutorial is basically a wordpress theme for todo lists. What are you looking for and why not wordpress?
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.
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.
Thanks. Already working on database side, hope to come up with something soon.
Did you ever do this? I'd love to get a copy.
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.
Thank you! I'll think of something.
Thanks for share, great tutorial!
Very Good!!! Add in the front page of the site!
*Very Good!!!!!! Add on the admin page of the site!
PS: sorry bad translator(
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().
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 wpenqueue* and nonces. I will keep it in mind for future WP work. Again thank you for your suggestions!
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!
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'.
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
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.
Is it possible to write this just into a webpage? I mean like, make this separate? Without wordpress?
Thanks.
Very good work, I will use it carefully in my projects
bigUp
cool tutorial , Thank you!
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 :-)
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.
Hi Martin, this is a fantastic tutorial, would it be possible to get a seperate rss feed from this app?
You can try with this plugin. However it will not include whether the task is completed in the feed.
Check this out: https://github.com/addyosmani/todomvc
Something to reflect upon
So in a sense, todo apps have become the hello world of frameworks :)
Thanks for your help guys :)
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?
So in a sense, todo apps have become the hello world of frameworks :)
Heloo there thanks for nice tutorial
But is it Possible to use outside wordpress i mean for other cms like Drupal,Joomla etc...
how can I make the todo app only visible to logged in users or users depending in level??
Hey, big thanks for this tutorial
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
Awesome tutorial! I use it in my WordPress site.
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.