WP_List_Table – a step by step guide

Throughout WordPress the class WP_List_Table is used to display data, e.g. users, plugins, comments, or posts. The class contains almost all necessary methods for displaying, sorting, paginating, and searching data and and what is more obvious than to use it for your own plugins?

This article tries to give you a comprehensive walk-through all necessary steps to create a table tailored to your needs.

  1. Preliminary work
  2. Basics
  3. Sorting
  4. Actions
  5. Bulk actions
  6. Pagination
  7. Searching
  8. Screen options
  9. Styling the table
  10. Other customizations
  11. Weblinks

Preliminary work

For testing purposes we create a small plugin which adds a menu item:


My List Table Test/h2>

Basics

For a start we're creating a list table with only the basic functionality. First we have to make sure that the necessary class is available since the WP_List_Table isn't loaded automatically:

if( ! class_exists( 'WP_List_Table' ) ) {
    require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
}

To create a table to your needs you have to derive a class from WP_List_Table:

class My_List_Table extends WP_List_Table {
}

$myListTable = new My__List_Table();

For demonstration purposes we create some sample data. Usually this data would be read from the database:

var $example_data = array(
  array('ID' => 1,'booktitle' => 'Quarter Share', 'author' => 'Nathan Lowell',
        'isbn' => '978-0982514542'),
  array('ID' => 2, 'booktitle' => '7th Son: Descent','author' => 'J. C. Hutchins',
        'isbn' => '0312384378'),
  array('ID' => 3, 'booktitle' => 'Shadowmagic', 'author' => 'John Lenahan',
        'isbn' => '978-1905548927'),
  array('ID' => 4, 'booktitle' => 'The Crown Conspiracy', 'author' => 'Michael J. Sullivan',
        'isbn' => '978-0979621130'),
  array('ID' => 5, 'booktitle'     => 'Max Quick: The Pocket and the Pendant', 'author'    => 'Mark Jeffrey',
        'isbn' => '978-0061988929'),
  array('ID' => 6, 'booktitle' => 'Jack Wakes Up: A Novel', 'author' => 'Seth Harwood',
        'isbn' => '978-0307454355')
);

Before we can display the data in the table we have to define some methods and variables:

function get_columns(){
  $columns = array(
    'booktitle' => 'Title',
    'author'    => 'Author',
    'isbn'      => 'ISBN'
  );
  return $columns;
}

function prepare_items() {
  $columns = $this->get_columns();
  $hidden = array();
  $sortable = array();
  $this->_column_headers = array($columns, $hidden, $sortable);
  $this->items = $this->example_data;;
}

The method get_columns() is needed to label the columns on the top and bottom of the table. The keys in the array have to be the same as in the data array otherwise the respective columns aren't displayed.

prepare_items defines two arrays controlling the behaviour of the table:

  • $hidden defines the hidden columns (see Screen Options),
  • $sortable defines if the table can be sorted by this column.

Finally the method assigns the example data to the class' data representation variable items.

Before actually displaying each column WordPress looks for methods called column_{key_name}, e.g. function column_booktitle. There has to be such a method for every defined column. To avoid the need to create a method for each column there is column_default that will process any column for which no special method is defined:

function column_default( $item, $column_name ) {
  switch( $column_name ) { 
    case 'booktitle':
    case 'author':
    case 'isbn':
      return $item[ $column_name ];
    default:
      return print_r( $item, true ) ; //Show the whole array for troubleshooting purposes
  }
}

In our example the method will return the title for every column and if the column is not found it displays the content of the $item array for debugging purposes.

These are the essential ingredients to define a custom list table class. All you have to do now is to add an admin page to the backend, create an instance of our class, prepare the items and call display() to actually display the table:

function my_add_menu_items(){
    add_menu_page( 'My Plugin List Table', 'My List Table Example', 'activate_plugins', 'my_list_test', 'my_render_list_page' );
}
add_action( 'admin_menu', 'my_add_menu_items' );

function my_render_list_page(){
  $myListTable = new My_Example_List_Table();
  echo '

My List Table Test

'; $myListTable->prepare_items(); $myListTable->display(); echo '
'; }

This is the minimal version of a WP_List_Table possible:


Download minimal WP_List_Table example (gist)

Sorting

At the moment the items appear in the order they are defined in the code since the WP_List_Table class does not contain any code for sorting. What it does contain is some code to mark certain columns as sortable. In section "Basics" there already was a line $sortable = array(); which now will be changed to:

$sortable = $this->get_sortable_columns();

Additionally we need the method:

function get_sortable_columns() {
  $sortable_columns = array(
    'booktitle'  => array('booktitle',false),
    'author' => array('author',false),
    'isbn'   => array('isbn',false)
  );
  return $sortable_columns;
}

This way the above mentioned column headers are changed to links and display small triangles if the mouse hovers over them. The second parameter in the value array of $sortable_columns takes care of a possible pre-ordered column. If the value is true the column is assumed to be ordered ascending, if the value is false the column is assumed descending or unordered. This is needed for the small triangle beside the column name indicating the sort order to show in the correct direction:

If you click on the column header the page is reloaded and $_GET contains something like this:

array
  'page' => string 'my_list_test' (length=12)
  'orderby' => string 'booktitle' (length=5)
  'order' => string 'asc' (length=3)

With this information you can write a method for sorting our example data:

function usort_reorder( $a, $b ) {
  // If no sort, default to title
  $orderby = ( ! empty( $_GET['orderby'] ) ) ? $_GET['orderby'] : 'booktitle';
  // If no order, default to asc
  $order = ( ! empty($_GET['order'] ) ) ? $_GET['order'] : 'asc';
  // Determine sort order
  $result = strcmp( $a[$orderby], $b[$orderby] );
  // Send final sort direction to usort
  return ( $order === 'asc' ) ? $result : -$result;
}

To actually sort the data we have to extend prepare_items():

function prepare_items() {
  [..]
  usort( $this->example_data, array( &$this, 'usort_reorder' ) );
  $this->items = $this->example_data;
}

If you're retrieving the data from the database (which is most likely) it's of course best to use SQL's ORDERBY directly.

Actions

If you not only want to display the items but also want to manipulate them you have to define some actions:

function column_booktitle($item) {
  $actions = array(
            'edit'      => sprintf('Edit',$_REQUEST['page'],'edit',$item['ID']),
            'delete'    => sprintf('Delete',$_REQUEST['page'],'delete',$item['ID']),
        );

  return sprintf('%1$s %2$s', $item['booktitle'], $this->row_actions($actions) );
}

These actions will appear if the user hovers the mouse cursor over the table:

If you click on one of the action links the form will return for example the following data in $_GET:

array
  'page' => string 'my_list_test' (length=12)
  'action' => string 'delete' (length=6)
  'book' => string '2' (length=1)

Bulk actions

Bulk action are implemented by overwriting the method get_bulk_actions() and returning an associated array:

function get_bulk_actions() {
  $actions = array(
    'delete'    => 'Delete'
  );
  return $actions;
}

This only puts the dropdown menu and the apply button above and below the table:

The checkboxes for the rows have to be defined separately. As mentioned above there is a method column_{column} for rendering a column. The cb-column is a special case:

function column_cb($item) {
        return sprintf(
            '', $item['ID']
        );    
    }

This method currently will not be processed because we have to tell the class about the new column by extending the method get_columns():

function get_columns() {
  $columns = array(
    'cb'        => '',
[..]
}

This will also put the "select all" checkbox in the title bar:

If you don't want to display the checkbox in the title you simply set the value to an empty string. Nevertheless you still have to define the key/value pair otherwise no checkboxes are shown at all:

If "Apply" is pressed the form will return various variables: action and action2 contain the selected action or -1 if the user chose no action, and if any checkbox was selected the marked rows, in our case books, for example:

'action' => string 'delete' (length=6)
'book' => 
  array
    0 => string '2' (length=1)
    1 => string '6' (length=1)
'action2' => string '-1' (length=2)

action contains the selection from the upper select box, action2 the selection from the lower select box, and book the id of the selected rows, if any. You can use the method current_action() to query action/action2:

$action = $this->current_action();

It will return action if it's set, otherwise action2. If nothing is set the method returns FALSE.

Pagination

First things first: WordPress does not paginate your data in any way. It only contains a method to display a navigation bar on the top and bottom right of the table:

You have to tell the method how many items you have in total, how many items shall be displayed on a page, and most important, the data to be displayed on the page:

function prepare_items() {
  [...]
  $per_page = 5;
  $current_page = $this->get_pagenum();
  $total_items = count($this->example_data);

  // only ncessary because we have sample data
  $this->found_data = array_slice($this->example_data,(($current_page-1)*$per_page),$per_page);

  $this->set_pagination_args( array(
    'total_items' => $total_items,                  //WE have to calculate the total number of items
    'per_page'    => $per_page                     //WE have to determine how many items to show on a page
  ) );
  $this->items = $this->found_data;
}

As pointed out in the comment the array_slice is only necessary because we use sample data. If you're retrieving the data from a database you only need to load the necessary data by using SQL's LIMIT.

Searching

If you have a huge amount of data a search field will simplify accessing certain items:

$myListTable->search_box('search', 'search_id');

The button text search is defined by the first parameter, the id of the input by the second parameter. The method creates the following output:


The method will place the input field and the search button on the right side and style it correctly. The <form> element is not generated. You have to add it manually, in our case this would be:

search_box('search', 'search_id'); ?>

(The hidden element is needed to load the right page.)
To react to the search command you need to check the content of $_POST['s'] and filter your data accordingly before displaying the table.

Screen options

All core backend pages containing a WP_List_Table provide a "Screen Options" slide-in where the user can adjust the columns to be shown and the number of rows to be displayed.
To add options to your plugin you need to change your current code. First you have to make sure that the screen options are displayed only on the current page:

$hook = add_menu_page('My Plugin List Table', 'My List Table Example', 'activate_plugins', 'my_list_test', 'my_render_list_page');
add_action( "load-$hook", 'add_options' );

function add_options() {
  $option = 'per_page';
  $args = array(
         'label' => 'Books',
         'default' => 10,
         'option' => 'books_per_page'
         );
  add_screen_option( $option, $args );
}

This only displays the option field and apply button, saving and loading the data has to be defined separately. WordPress provides a filter called set-screen-option to take care of this:

add_filter('set-screen-option', 'test_table_set_option', 10, 3);
function test_table_set_option($status, $option, $value) {
  return $value;
}

The option is stored in the table usermeta in the database so each user has his own setting. To retrieve the option and adjust the table display accordingly the method prepare_items has to be altered (excerpt):

function prepare_items() {
[..]

  //paging
  $per_page = $this->get_items_per_page('books_per_page', 5);
  $current_page = $this->get_pagenum();

  [...]

Instead of simply assigning a number the user specified value is loaded. If the user hasn't changed the value there is no such option stored in the database and a default value is taken.

Adding the checkboxes for hiding/showing the columns is done by WordPress automatically. You just have to make sure that your derived class is instantiated before the screen option panel is rendered so that the parent class can retrieve the column names. To accomplish this the corresponding code is moved into the method add_options():

function add_options() {
    global $myListTable;
 
    $option = 'per_page';
    $args = array(
        'label' => 'Books',
        'default' => 10,
        'option' => 'books_per_page'
    );
    add_screen_option( $option, $args );

    $myListTable = new My_Example_List_Table;
}

The user's selections are automatically saved via Ajax functions. Nevertheless you have take care by yourself that the columns are hidden if the page is loaded initially. The method get_column_info() returns all, the hidden and the sortable columns. In the method prepare_items() instead of

$columns = $this->get_columns();
$hidden = array();
$sortable = $this->get_sortable_columns();
$this->_column_headers = array($columns, $hidden, $sortable);

it's now

$this->_column_headers = $this->get_column_info();

and the columns are set according to the screen options.

Annotation: you should avoid some strings as keynames since they are treated by WordPress specially:

$special = array('_title', 'cb', 'comment', 'media', 'name', 'title', 'username', 'blogname');

Your table would still work, but you won't be able to show/hide the columns.

Styling the table

Currently the table is styled to the WordPress defaults. To change this you have to adapt the CSS classes which are automatically assigned to each column. The class name consists of the string "column-" and the key name of the $columns array, e.g. "column-isbn" or "column-author". As an example the width of the columns will be redefined (for simplicity the style data is written directly into the HTML header):

function _construct() {
  [...]
  add_action( 'admin_head', array( &$this, 'admin_header' ) );
  [...]
}

function admin_header() {
  $page = ( isset($_GET['page'] ) ) ? esc_attr( $_GET['page'] ) : false;
  if( 'my_list_test' != $page )
    return; 

  echo '';
}

Other customizations

If there are no items in the list the standard message is "No items found." is displayed. If you want to change this message you can overwrite the method no_items():

function no_items() {
  _e( 'No books found, dude.' );
}

Download complete WP_List_Table example (gist) or see the Gist


Posted

in

by

Comments

27 responses to “WP_List_Table – a step by step guide”

  1. Bjorn van der Neut Avatar

    Thanks for the clear and useful information. I am going to use this example for sure when making my own plugin.

  2. Boone Gorges Avatar

    A nice tutorial. But beware: I once mentioned to one of WP’s core team that I was extending WP_List_Table in my own plugin, and I got scolded. The class is marked @access private, and while this privacy may not be enforced right now, (1) it could be at any moment, by marking some of the key methods ‘private’ in a way that would make extending quite difficult, and more importantly (2) the core team could muck around with the methods at any time, breaking your plugin without too much advanced warning. Not saying that they *will*, but I think that the ‘@access private’ line means that they *could*. So tread lightly 🙂

  3. kaiser Avatar

    Summed up pretty well. Thanks a lot!

  4. LarryK Avatar
    LarryK

    Any issues with using WP_List_Table for a regular WP page? (A page on the public site.)

    Thank you for the article.

  5. Yuuko Avatar
    Yuuko

    Your code is working properly at the display thank you ! But search form does not work. Do you know why?

  6. Latz Avatar

    @Yuuko: The actual search is not implemented in this example. The example just illustrated how to display the search form. Quote:

    To react to the search command you need to check the content of $_POST[‘s’] and filter your data accordingly before displaying the table.

  7. Latz Avatar

    @LarryK: Haven’t tried it myself but at least the table should be displayable in the frontend as long as you make sure the WP_Table_class is available. To use the form functionality you might have to experiment a bit with the code.

  8. Latz Avatar

    @Boone Gorges: Good point, I didn’t notice that remark until now.
    I think the class will stay derivable because all tables are based on it. Of course there is always the possibility that the code is changed in a way that derived classes will break but that’s a problem that occurred already before at other places. As a plugin developer you can never rely on WordPress’ API staying the same through major version changes.

    (Maybe a core developer can clarify why the class is marked as “@access private”?)

  9. Martin Herbst Avatar

    I am going to write a plugin where I will need a list. So this very good article came at the right time 🙂

  10. Andrew Nacin Avatar

    The classes are marked private because you should not be using them, as Boone said. Their original design was never meant to be an API for plugin developers. So a decent amount of this tutorial is recommending things that could very well break in a future release.

  11. Paul Avatar

    Nice article didn’t know about Wp_list_table class before this, thanks for the introduction.

  12. […] the original: WP_List_Table – a step by step guide – WP Engineer  Posted by jedwan  Tagged with: almost-all, and-searching, class, class-contains, […]

  13. […] WP_List_Table – a step by step guide […]

  14. Rilwis Avatar

    Thanks for this interesting code! It’s very cool. Just wondering about Nacin’s warning 😀

  15. Ben Avatar
    Ben

    Interesting, the codex entry for WP_List_Table specifically suggests that developers do extend it and make use of it – see http://codex.wordpress.org/Class_Reference/WP_List_Table (as at 29/3/12)

    Having said that when you try to use it for anything real it does feel very much like an early beta, so I wouldn’t be surprised if it changes significantly, there are a lot of little gotchas and rough edges to worry about when implementing it as an admin screen with a real data set.

  16. Frank Avatar

    @nacin: thanks for oyur comment and reply to this question; but do you have also an todo for an API for this requirement; maybe an release of this class for use on development from plugin authors?
    Best and thanks

  17. avinash Avatar
    avinash

    Great info. Thank you

  18. […] view within WordPress. I couldn’t have put this dashboard page together without the help of WP Engineer for creating my own WP_List_Table. I’ll eventually expand this too, giving the ability to […]

  19. David Higginbotham Avatar
    David Higginbotham

    Thanks for this, it’s in a book I’ve been reading recently and this covers a few of the areas it summarizes quickly; surprising for me. I’m glad I can finally utilize this in a fashion acceptable for my needs! =]

  20. […] WP_List_Table – a step by step guide – time to roll up your sleeves and get acquainted with WP_List_Table – a powerful class for sorting and presenting data. […]

  21. Richard Avatar
    Richard

    Wow! I can’t believe I didn’t look into seeing whether WP has a tabling class until now! For over a year now I’ve been manually trying to re-create the table without using the class, *smacks forehead*.

    This is a really detailed and comprehensive guide to extending to the class by the way, keep up the good work. Thanks!

  22. Onur Avatar
    Onur

    It looks amazing. Thanks!
    How can I use it with a database? I mean, I would like to get the data from the database. How should I create $example_data = array() ?

  23. Naveen Avatar
    Naveen

    Nice briefing. I would like to know, how to give edit link anchored to record title too ? as it is customary to click on the title to edit the record.

  24. Weerayut Teja Avatar

    It’s the great tutorial. It’s fine and easy to understand.
    Thank you.

  25. Kirill Fuchs Avatar

    Having some trouble with the $myListTable->search_box( ‘search’, ‘search_id’ );

    I’m getting

    Call to a member function search_box() on a non-object

    any help?

  26. Kirill Fuchs Avatar

    ok so in regards to my comment above I was getting the error message because the object in my script is not named $myListTable… So for anyone else who ran into this just double check and dont just copy paste 🙂

    DUHHHHH. lol