Quantcast
Channel: WP EngineerWordPress Plugins
Viewing all articles
Browse latest Browse all 11

WP_List_Table – a step by step guide

$
0
0

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:

<?php
/*
Plugin Name: My List Table Example
*/
?>
<div class="wrap">
<div id="icon-users" class="icon32"></div>
<h2>My List Table Test/h2>
</div>
<?php

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 '<div class="wrap"><h2>My List Table Test</h2>'; 
  $myListTable->prepare_items(); 
  $myListTable->display(); 
  echo '</div>'; 
}

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('<a href="?page=%s&action=%s&book=%s">Edit</a>',$_REQUEST['page'],'edit',$item['ID']),
            'delete'    => sprintf('<a href="?page=%s&action=%s&book=%s">Delete</a>',$_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(
            '<input type="checkbox" name="book[]" value="%s" />', $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'        => '<input type="checkbox" />',
[..]
}

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:

<p class="search-box">
<label class="screen-reader-text" for="search_id-search-input">
search:</label> 
<input id="search_id-search-input" type="text" name="s" value="" /> 
<input id="search-submit" class="button" type="submit" name="" value="search" />
</p>

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:

<form method="post">
  <input type="hidden" name="page" value="my_list_test" />
  <?php $this->search_box('search', 'search_id'); ?>
</form>

(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 '<style type="text/css">';
  echo '.wp-list-table .column-id { width: 5%; }';
  echo '.wp-list-table .column-booktitle { width: 40%; }';
  echo '.wp-list-table .column-author { width: 35%; }';
  echo '.wp-list-table .column-isbn { width: 20%; }';
  echo '</style>';
}

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


© WP Engineer Team, All rights reserved (Digital Fingerprint: WPEngineer-be0254ce2b4972feb4b9cb72034a092d)

Viewing all articles
Browse latest Browse all 11

Trending Articles