Custom Post Types with a Drag & Drop Interface
WordPress custom post types have revolutionised the way WP works and how it has been implemented across the Internet. With many themes now registering post types it’s always nice to know how to do a little more. In the following tutorial we take you through registering a post type and create an interface whereby you can sort them. In our example we will be creating a very basic shop with products.
Registering the Custom Post Type
To begin with, we start with a fresh install of WP, using the default twenty eleven theme. Let’s open the theme in our favorite code editor. To setup the post type we need to use the correct hook. As stated in the post type codex entry we should use ‘init’ hook as follows:
// Let wordpress know we need to register the product
add_action( ‘init’, ‘sneek_register_product’ );
// The callback to register the product
function sneek_register_product() {
$labels = array(
'name' => _x('Products', 'post type general name'),
'singular_name' => _x('Product', 'post type singular name'),
'add_new' => _x('Add New', 'product'),
'add_new_item' => __('Add New Product'),
'edit_item' => __('Edit Product'),
'new_item' => __('New Product'),
'view_item' => __('View Product'),
'search_items' => __('Search Products'),
'not_found' => __('No products found'),
'not_found_in_trash' => __('No products found in Trash'),
'parent_item_colon' => '',
'all_items' => 'All products',
'menu_name' => 'Products'
);
$args = array(
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'query_var' => true,
'rewrite' => true,
'capability_type' => 'page',
'has_archive' => true,
'hierarchical' => false,
'menu_position' => null,
'supports' => array( 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'comments', 'custom-fields', 'page-attributes' )
);
register_post_type( 'product', $args );
}
The $labels variable simply holds all of the different variations of text for messages, button text etc. While they are not all required it’s best practice to fill them out. The $args variable holds the main parameters for the register_post_type() method. They are discussed in more details on the register_post_type() codex page.
Now time to test it, and input some content.
Bonus Points
For extra bonus points, using the relevant hook and add some CSS to style the icon from the default ‘post’ icon. Answer below:
add_action( 'admin_print_styles', 'sneek_admin_print_styles' );
function sneek_admin_print_styles()
{
?>
<style>
#adminmenu #menu-posts-product div.wp-menu-image {
background: url(<?php echo get_stylesheet_directory_uri() . '/images/product-icon-sprite.png'; ?>) no-repeat left bottom;
}
#adminmenu #menu-posts-product:hover div.wp-menu-image,
#adminmenu #menu-posts-product.wp-has-current-submenu div.wp-menu-image {
background-position: left top;
}
</style>
<?php
}
Displaying the Products
While the WP template hierarchy shows us that each post type has it’s own set of templates to use we will simply duplicate the page.php and create a template from it. We will create a new WP_Query() object then create a new loop. Not seen the WP_Query class before? Take a look at the codex article.
<?php $products = new WP_Query( array( 'post_type' => 'product', 'posts_per_page' => -1, 'orderby' => 'menu_order', 'order' => 'ASC' ) ); ?> <?php if( $products->have_posts() ) : ?> <ol id="products"> <?php while( $products->have_posts() ) : $products->the_post(); ?> <div id="post-<?php the_ID(); ?>" <?php post_class( 'clearfix' ); ?>> <h2 class="entry-title"><?php the_title(); ?></h2> <?php if( has_post_thumbnail() ) : ?> <div class="alignright"> <?php the_post_thumbnail( 'thumbnail' ); ?> </div> <?php endif; ?> <div class="content"> <?php the_content(); ?> </div><!-- .content --> </div><!-- #post-<?php the_ID(); ?> --> <?php endwhile; ?> </ol><!-- #products --> <?php endif; ?> <?php wp_reset_postdata(); // Always reset data after a custom loop! ?>
So create a page and apply the template; if you have added some products then you should see them in a nice list format.
Building the drag and drop interface
What if we wanted to sort the products to promote one over another? Lets create an interface that allows the user to drop and drop sort the products.
To do this we need to think about how we are going to do this. Let’s make a list of things to setup:
- Add a sub menu item to the product menu.
- Create an interface showing each product with a handle to sort.
- We’re going to be using jQuery UI for this, so we will need to enqueue that script.
- Write a little JavaScript to sort and send an AJAX update.
- Register and write the AJAX callback function to actually update the posts.
- Test!
1. Add a sub menu to the product menu
Adding a submenu page is very simple, like every astute developer you would have already opened the codex and read the add_submenu_page() method page, haven’t you?
add_action( 'admin_menu', 'sneek_register_product_menu' );
function sneek_register_product_menu() {
add_submenu_page(
'edit.php?post_type=product',
'Order Slides',
'Order',
'edit_pages', 'product-order',
'sneek_product_order_page'
);
}
So now we have a menu link below the product menu, however we need to create the function ‘sneek_product_order_page’ to display the interface.
2. Create an interface showing each product with a handle to sort
Now for a similar loop to the product template page, we will output it in a table to keep things consistent with the WordPress UI.
function sneek_product_order_page() {
?>
<div class="wrap">
<h2>Sort Products</h2>
<p>Simply drag the product up or down and they will be saved in that order.</p>
<?php $products = new WP_Query( array( 'post_type' => 'product', 'posts_per_page' => -1, 'order' => 'ASC', 'orderby' => 'menu_order' ) ); ?>
<?php if( $products->have_posts() ) : ?>
<table class="wp-list-table widefat fixed posts" id="sortable-table">
<thead>
<tr>
<th class="column-order">Order</th>
<th class="column-thumbnail">Thumbnail</th>
<th class="column-title">Title</th>
</tr>
</thead>
<tbody data-post-type="product">
<?php while( $products->have_posts() ) : $products->the_post(); ?>
<tr id="post-<?php the_ID(); ?>">
<td class="column-order"><img src="<?php echo get_stylesheet_directory_uri() . '/images/move-icon.png'; ?>" title="" alt="Move Icon" width="30" height="30" class="" /></td>
<td class="column-thumbnail"><?php the_post_thumbnail( 'thumbnail' ); ?></td>
<td class="column-title"><strong><?php the_title(); ?></strong><div class="excerpt"><?php the_excerpt(); ?></div></td>
</tr>
<?php endwhile; ?>
</tbody>
<tfoot>
<tr>
<th class="column-order">Order</th>
<th class="column-thumbnail">Thumbnail</th>
<th class="column-title">Title</th>
</tr>
</tfoot>
</table>
<?php else: ?>
<p>No products found, why not <a href="post-new.php?post_type=product">create one?</a></p>
<?php endif; ?>
<?php wp_reset_postdata(); // Don't forget to reset again! ?>
<style>
/* Dodgy CSS ^_^ */
#sortable-table td { background: white; }
#sortable-table .column-order { padding: 3px 10px; width: 50px; }
#sortable-table .column-order img { cursor: move; }
#sortable-table td.column-order { vertical-align: middle; text-align: center; }
#sortable-table .column-thumbnail { width: 160px; }
</style>
</div><!-- .wrap -->
<?php
}
Note: When adding a new page like this, or in your plugins, you should add the <div class=”wrap”> to keep it consistent with the other WordPress pages. Why it doesn’t do this automagically I don’t know!
3. Enqueue the jQuery UI
Time to load up jQuery UI, another hook here, the admin_enqueue_scripts hook.
add_action( 'admin_enqueue_scripts', 'sneek_admin_enqueue_scripts' );
function sneek_admin_enqueue_scripts() {
wp_enqueue_script( 'jquery-ui-sortable' );
wp_enqueue_script( 'sneek-admin-scripts', get_template_directory_uri() . '/js/sneek-admin-scripts.js' );
}
4. Make the table sortable and udpate their order via AJAX
Time to create the JavaScript file, ‘sneek-admin-scripts.js’, we’ll add some simple code to enable the jQuery UI Sortable on the table.
jQuery(function($) {
$('#sortable-table tbody').sortable({
axis: 'y',
handle: '.column-order img',
placeholder: 'ui-state-highlight',
forcePlaceholderSize: true,
update: function(event, ui) {
var theOrder = $(this).sortable('toArray');
var data = {
action: 'sneek_update_post_order',
postType: $(this).attr('data-post-type'),
order: theOrder
};
$.post(ajaxurl, data);
}
}).disableSelection();
});
The above enables the table to be sortable, with a few paraters. There is a nice ‘update’ callback function which allows us to the use an AJAX request to send back to WordPress to update the products.
Sortable? Great, let’s create the AJAX callback function.
Note: I have added a ‘data-post-type’ attribute to the table to enable this to be reused, allowing us to write the JS & AJAX methods only once.
5.Register and write the AJAX callback function to actually update the posts.
Now we have to let WordPress know we are going to use an AJAX callback method. WordPress has two different kinds of AJAX methods, one with privilages and one without. I.E Logged in vs Logged out. As this is an admin function we want to make sure the user can only update the posts if they are verified (WordPress will do the hard work for us)
add_action( 'wp_ajax_sneek_update_post_order', 'sneek_update_post_order' );
function sneek_update_post_order() {
global $wpdb;
$post_type = $_POST['postType'];
$order = $_POST['order'];
/**
* Expect: $sorted = array(
* menu_order => post-XX
* );
*/
foreach( $order as $menu_order => $post_id )
{
$post_id = intval( str_ireplace( 'post-', '', $post_id ) );
$menu_order = intval($menu_order);
wp_update_post( array( 'ID' => $post_id, 'menu_order' => $menu_order ) );
}
die( '1' );
}
Note: While there is next to no validation, this demonstrates the overall structure on how to update the posts. Make sure you validate everything!
The above is a simple loop updating each post with the relevant attribute. The method wp_update_post is really nice and simple to use, why not read the documentation?
6. Testing
Tested and working? No? Darn, we hope it works, seems to on this end. Have you looked into firebug for development.
Bonus Points
Make sure it’s all working before attempting this challenge. It’s great we have a working sortable list, however there isn’t much feedback to the user once an update has been performed. Why not create a loading message? Also give some feedback to the user to say if it updated or not!
Let us know how we did
Here at Sneek, we’re pretty new to the tutorial writing business. Did this article help? Perhaps you have a nice few suggestions, either comment below or send us an email! We love hearing from you guys.
Here are some kittens so your head doesn’t explode!


James Parsons
Thanks for this!
Was looking around for a way to do this without the need for a plugin (less likely to break) …
:)
March 3rd, 2012 //
Sneek
Hey James,
Thanks for the comment, I’m hoping to make this into a simple plugin actually. However there are a couple of nice ones out there already I think.
Was it easy enough to follow?
Cris
March 5th, 2012 //
Weblusive
Just decided to step in to say thank you for this awesome tutorial! Was searching for something like this for quite a long time now.
April 16th, 2012 //
Matt
Thanks for this however the main code under
2. Create an interface showing each product with a handle to sort
Is all jacked up. Can you clean it up so it can be copied properly?
thanks,
Matt
October 10th, 2012 //
Sneek
Hi Matt,
Thanks for pointing this out, not sure what has happened so I reverted to an earlier revision :)
October 11th, 2012 //
Roc
One of the easiest and nicest tutorials to follow. Thank you!
How do you feel of future WP updates affecting this tutorial? Runs great on 3.5 btw!
March 3rd, 2013 //
Sneek
Thanks, I think it needs a few areas fixing but overall does the job (even if you can get a plugin and use it straight away!). I don’t think there should be many / any changes to make moving forwards unless core features change (which they won’t). Maybe improve on the JS with a new framework or something.
Thanks, Cris
March 4th, 2013 //
Roc
Hi Cris!
Just came to a road block when I was creating the reorder table. Is there a way to display custom metabox data in a separate column? For instance, the reorder table will have Title, Thumbnail, and Actors (custom post-meta).
Thanks!
Roc.
March 8th, 2013 //
Roc
Just found the solution after posting above:
To display custom meta for a post type, you can create a new column and add this:
ID , '-metaname' , true); ?>
March 8th, 2013 //
Roc
Sorry – it didn’t post correctly. Here is a pastebin URL.
March 8th, 2013
Roc
Wow, twice in a row. Here is the link: http://pastebin.com/AN4NDJGK
March 8th, 2013
Cristian
This tutorial is over a year old, there are many improvements to be made to it!
The meta is nice, I wouldn’t use the global $post, instead use the function get_the_ID().
Research:
1. WP List table – http://wpengineer.com/2426/wp_list_table-a-step-by-step-guide/
2. Better AJAX response – http://codex.wordpress.org/Class_Reference/WP_Ajax_Response
:)
March 8th, 2013 //