The WordPress administration panels make extensive use of the WP_List_Table class to organize
and display data related to a wide variety of WordPress features. You may or may not know it's
possible to add custom columns to any of these tables. And even if you did know, there's a good
chance you don't know how it's done because the
Codex
is very sparse on this sort of information.
If you would like to know how custom columns are added to admin tables or how to alter other
table features, you came to the right place, we're going to dive into this topic right now.
Prerequisites
This article assumes you have reasonable PHP coding skills in hacking WordPress. You must have a
reasonable understanding of PHP, including array manipulation, Object Oriented Programming
and extended classes. You must understand how to
hook into
WordPress filters and actions,
including those that pass arrays, objects, or multiple parameters. You need to be familiar with
filter and action tags that use variable elements. You also need a basic understanding of HTML coding.
How WP_List_Tables Works
The WP_List_Table class is a basic table framework for displaying data in table form.
Administration panels for each data element, such as Posts or Links, extend this class, redefining
some properties and methods and creating others anew. These class definitions are all in the
wp-admin/includes/ folder. The class offers several standard elements to make interacting
with the table easy, and also has the effect of standardizing the user experience from panel to panel.
The main thing most developers are concerned with is adding custom columns to the main table. When
a new table class is instantiated, some basic data relating to the admin panel are initialized.
Next, prepare_items() is called to establish various counts of certain data elements. Finally,
display() is called to output the table.
The display() method first outputs the various elements that appear above the table, such
as views, bulk actions, and pagination. Then the table column labels are output by first calling
get_column_headers(). This is your opportunity to add custom columns before get_column_headers()
returns via the 'manage_{$screen->id}_columns' filter.
The {$screen->id} portion of the filter tag is a variable value depending on the context when
the filter is initiated. See the
Hook Summary
for values used for most contexts.
A query is made to get all the table elements using the view, filter, page, etc. selections to restrict
the query. Each element returned is stepped through in turn, outputting a single row per element
by calling single_row(). This function outputs a row by stepping through all the
column headers. Each column in the default table is represented in a switch/case structure.
When a particular header comes up in the loop, it is matched to a case and the associated code
is executed to output the actual table cell content. In the case of custom columns that were added,
no case match is found, so the default case code is called, which typically consists of
do_action('manage_{$screen->id}_custom_column', $name, $id);, though the exact code varies
by administration screen. Hooking into this action enables you to output appropriate content at
the correct time. to Contents
Define Your Custom Column
To add a custom column, you must first add its name to the array of column header names.
This is done by hooking into the 'manage_{$screen->id}_columns' filter. Your callback is
passed the current name array. Simply add another key/value pair to the array and return
the entire array. The key can be any arbitrary unique string and the value is the actual
column name that will appear in the table header. The following example defines a 'Metabox'
column to be added to a 'portfolio' custom post type table.
You can only sort columns that contain data from the database related to the table objects, as the
sort is achieved by re-querying the table contents. If your data cannot be related to the table
objects in a mySQL query, then it cannot be used for sorting of your column.
After defining your column you can make it sortable by hooking the sortable filter variant
'manage_{$screen->id}_sortable_columns' and adding your column name to that array as well.
This can often be the same callback as the one for adding the initial non-sortable column.
You then hook the 'request' filter and check the passed query variables array for your
column name under the 'orderby' key. If found, set query arguments that can be used to
query the table contents in the correct order (typically the orderby column) and merge into the
passed variables. Clicking the column head will toggle the query order between ASC and DESC.
The query arguments you set are the same arguments you define in a custom new WP_Query object.
You essentially define an arguments array as if for a new query object and merge it with the array
passed by the 'request' filter. The 'request' filter is initiated from the parse_request()
method of the WP class. It is defined in wp-includes/class-wp.php. Your callback is
passed $this->query_vars.
By default, the column order is predefined. So when you add your custom column, it appears at the
end of the list at the far right edge of the table. If you want your column to appear elsewhere,
or simply wish to rearrange the order of default columns, while you are hooked into the
'manage_{$screen->id}_columns' filter, instead of working with the array passed, copy
the elements in the desired order into a new array. Return this new array and it's order
will be used instead of the default. The following example moves our Metabox column to appear
before the Date column. As it only involves the last column, instead of redefining the entire array,
we leave the first part alone and only unset then reset the date column we want to move to the end. You
can var_dump($columns) to see the key names used for each column.
To output your custom column data into the table, hook the 'manage_{$screen->id}_custom_column'
action or whatever specific hook applies to the admin screen you are customizing. Your callback
must check the passed data to identify which column and row data should be output. Retrieve the
data as required and echo it directly to the remote client browser.
function bcw_cpt_column($colname, $cptid) {
if ($colname == 'metabox')
echo get_post_meta( $cptid, '_my_meta_value_key', true );
}
add_action('manage_portfolio_posts_custom_column', 'bcw_cpt_column', 10, 2);
Interactivity
You can make your column data interactive by using jQuery to capture mouse events such as onclick
or mouseover. Such events can initiate an AJAX request, and depending on the response from the
server, alter what is being displayed to the user. For example, if the custom column represented
a Boolean state, clicking on the data can toggle it between true and false. Concurrently, the
corresponding data in the DB is also updated to the new state via AJAX requests.
The details on implementing this are beyond the scope of this article, but check out my article
on WordPress AJAX techniques.
Row Action Links
These are the links that appear under the titles of each item on hover in a table. Each link
performs a particular action on that item. For example, the posts table has action links for
Edit, Quick Edit, Trash, View.
You can add your own action links to a table by hooking into the appropriate {$screen->id}_row_actions
filter and adding your own element to the array passed, then return the whole array. The added
element's key is an arbitrary label for your action link and the element's value is the HTML
string to be output at the appropriate spot in the table. The filter to use for various tables
is listed in the Hook Summary.
The row action filters cannot be added when your plugin or theme loads. To add your filter, the
add_filter() call must be done from within an action callback for the
'admin_menu' action.
The following code snippet adds another action link on hover to the comments table under each comment
listed. The action link output by this code would actually 404 if you clicked on it because
there is no file named act.php in a typical WordPress installation. The link HTML is just an
example, not a functional link. Note how the comment ID is included as an URL parameter so the
fictious php file would know which comment to act upon.
Hooks exist to alter other elements on the tables, but in reality what you can do with these hooks
is quite limited. The information is included mainly for the sake of completeness rather than
any desirable utility.
NEW! - Beginning with WordPress 4.7 (targeted release December 2016), developers will be able to register custom
bulk actions. Unlike other hooks mentioned below, this feature IS useful.
Views
These are the links at the top of the table that affords different views of the data, for
example, the Posts table offers views for All, Published, Trash. To add a view, study the source
code that outputs the current page and how it handles parameters that are passed for the existing
view options. A new view needs to work within this system, unless you are ready to develop an
entirely new view handling script. Hook into the appropriate 'views_{$screen->id}' filter and add
a new element to the passed array. The key is an arbitrary tag for your view and the value is the HTML
to be output at the appropriate time. The pipe '|' separator is added automatically, it does not need
to be part of your HTML. The HTML can be anything actually, but the idea is to cause the table to
reload with varying parameters, such as different post statuses for the posts table. Of course,
the destination page needs to be able to understand the URL parameters being sent to it. What
the page can understand and to what extent you can alter it's understanding will vary by the table type.
The following snippet adds a "Watch" link to the comments table. The link will not work because
the target page does not know what to do with a "watch" parameter. The point here is to illustrate
the code required to add a link, not how to work with the edit-comments.php page.
The filter dropdowns at the top of the table are called "extra tablenav" in source code. The
availability of hooks to alter these is limited to a few panels. It is not possible to alter
what is already offered. If any hook is provided at all, it is an action in which you can
output additional HTML or perform some other operation after the dropdown is output but
before the "Filter" button is output. Note the hooks are actions for the Filter dropdown,
not filters! When the Filter button is clicked, the form
is sent as a GET request to the same page as the one displaying the table in the first place,
so that page needs to know how to handle the any form fields that you added. The form fields
are used to construct a new query, so you could output hidden form fields that contain additional
query vars that further restrict the query.
Alternately, enqueue some jQuery to handle an added field's onchange event, bypassing the
Filter button and the table's page. The extra tablenav actions are listed in the Hooks Summary.
The following snippet adds "Click to" after the filter dropdown on the comments table. This
obviously serves no purpose. I leave more substantive content to others.
add_action('restrict_manage_comments', 'bcw_restrict');
function bcw_restrict($options) {
echo 'Click to';
}
The base list table and the default extensions have no filters to alter the search box.
Plugin extended classes may have additional filters or actions. Check the source code of
the applicable class for a search_box() method that may offer a hook of some sort.
Bulk Actions
Prior to version 4.7, there is no convenient way to add bulk actions to the existing options, only removing options
is possible. Short of hacking core code which is a serious no-no, about all you could do is extend
the base class with your own version of the table, remove the existing admin panel and replace it
with your own. This is almost as bad as hacking core code because you lose the benefit of that
panel being maintained by the core developers. The onus falls on you to keep up with any changes
to the original panel and migrate such changes to your own code as needed.
To remove a bulk action option, hook the appropriate 'bulk_actions-*' filter and unset
the desired item in the table passed. Return the revised table. The following code removes the
"Approve" bulk action so users must individually approve every comment.
add_filter('bulk_actions-edit-comments', 'bcw_remove');
function bcw_remove($actions) {
unset($actions['approve']);
return $actions;
}
Adding Bulk Actions
With the release of WordPress 4.7, you'll be able to register bulk actions that can be applied to
list table content. There are three filter hooks to use. First use the "bulk_actions-{$screen->id}" filter
to register your action. This is simlar to how columns are added. You add your bulk action to the passed array
of current bulk actions. Then use the "handle_bulk_actions-{$screen->id}" filter to handle the actual actions.
First your callback must verify the hook fired for your particular action (passed as $doaction)
because all bulk actions fire this same hook. You can then step through the passed object IDs to
execute the intended action on each object.
You should also add an URL parameter to the passed redirect URL so that an appropriate
message can be displayed to the user after the actions complete. The action 'admin_notices'
is then used to output an appropriate message based on the URL parameter added to the redirect URL. More
information and examples can be found at the WordPress core developer's
"Make" site.
View Switcher
These are the buttons that allow one to select either List or Excerpt views. The base list table
has no filters to alter the switch options. Plugin extended classes may have additional filters or
actions, but the default panels for WordPress do not. Check the source code of the applicable
class for a view_switcher() method that may offer a hook of some sort. to Contents
Hook Summary
The following is a comprehensive summary of action and filter hooks related to all the default
WordPress List Tables and administration screens. It is accurate for version 3.5.2, the latest
version at the time of this writing. The information is subject to change with subsequent versions.
It's best to confirm the hook still works as intended by examining the related source code for
the version you are using.
Column Definition, Views, & Bulk Actions
The column name filter is applied by the get_column_headers() function. The filter tag
has the form "manage_{$screen->id}_columns" and can be found in wp-admin/includes/screen.php.
Your filter callback is passed the column definition array. The values for $screen->id in the
various admin panels are listed below. It's easy to confuse this filter with similar actions or
filters used to output the table content. Note that this filter for column names always has the plural
form of 'columns' in its name. The 'column' in action and filter names for outputting table content
is always singular. Also note both underscores and hyphens could appear in screen IDs, they are not
interchangeable.
The sortable column name filter variant is applied by the get_column_info() method of the
WP_List_Table class. It has the form "manage_{$screen->id}_sortable_columns" and can
be found in wp-admin/includes/class-wp-list-table.php. Your filter callback is passed the sortable
column array. The values for $screen->id in the various admin panels are listed below.
The views filter is called by the views() method of the applicable table class. The tag has
the form of "views_{$screen->id}". The callback is passed the $views array.
The bulk actions filter is called by the bulk_actions() method of the applicable table class.
The tag has the form of "bulk_actions-{$screen->id}". Note the hyphen before the screen id.
The callback is passed the $actions array.
The filter that handles the bulk actions is "handle_bulk_actions-{$screen->id}",
available beginning with WordPress 4.7. Three parameters are passed, $redirect_to, $doaction,
$post_ids. This filter is applied from the corresponding edit object file, such as edit.php,
edit-comment.php, etc.
Panel
$screen->id
Links
'link-manager'
Link Categories
'edit-link_category'
Comments
'edit-comments'
MS Sites
'sites-network'
MS Themes
'themes-network' - individual blogs use 'themes'
MS Users
'users-network' - individual blogs use 'users'
Plugins
'plugins'*
Posts
'edit-post'
Pages
'edit-page'
Custom Post types
"edit-{$post->post_type}"
Categories
'edit-category'
Tags
'edit-post_tag'
Terms
"edit-{$this->screen->taxonomy}"
Themes
'themes'*
Media
'upload'
Users
'users'*
*Append -network to MS Admin screens, individual blog screens use the same IDs as regular sites. to Contents
Table Cell Content
These actions or filters are hooked to output the actual cell contents
of a custom column for the tables on various administrative panels. Note that while most of these
hooks are actions, a few are filters. The Filter
designation in the A/F column is always bold to remind you to use
add_filter() instead of the more common add_action().
The
hook tags in
all cases here end with 'column', the singular form. The hook tags for
defining custom columns always end with 'columns', the plural form.
Some of the parameters passed to your callback are listed below as ''.
This indicates two single quotes representing an empty string. It
appears to be one double quote, but that would not make sense as there
is no corresponding close quote. For filters, you must always return the first parameter passed.
The remaining parameters are available for your use if you need them.
The File column lists the source code file in which the table class is defined. In all cases the files
are found in /wp-admin/includes/ .
This table is very wide. Use the scroll bar at the bottom to view all the content of each row.
These filters are used to add action links that appear under the title
of each item on hover in a table. The filters cannot be added when your
plugin or theme loads. To add your filter, the add_filter()
call must
be done from within an action callback for the 'admin_menu'
action.
The following example adds a link under each comment that reports the comment to
a third party spammer identification service. Important! This code is obsolete because
the commenter's email is not verified, violating the SFS TOS. Do not use this script, it
is an example for adding row action links only, not for actually reporting spammers.
function bcw_report($actions, $comment) {
$uname = urlencode($comment->comment_author);
// email MUST be verified by commenter before submitting!
$email = urlencode($comment->comment_author_email);
$ip = $comment->comment_author_IP;
$evidence = "Profile Link: " . $comment->comment_author_url . "\nPost Content: " . $comment->comment_content;
$evidence = urlencode($evidence);
$apikey = "Get your own API key and place here";
$action = "<a title=\"Report to Stop Forum Spam (SFS)\"target=\"_stopspam\" href=\"http://www.stopforumspam.com/add.php?username=$uname&email=$email&ip_addr=$ip&evidence=$evidence&api_key=$apikey\">Report to SFS</a>";
$actions['report_spam'] = $action;
return $actions;
}
function bcw_init() {
add_filter('comment_row_actions','bcw_report', 10, 2);
}
add_action('admin_menu', 'bcw_init');
Panel
Fiter tag
Parameters
File
Links
n/a
Link Categories
'link_category_row_actions'
$actions, $tag
class-wp-terms-list-table.php
Comments
'comment_row_actions'
$actions, $comment
dashboard.php
MS Sites
'manage_sites_action_links'
$actions , $blog['blog_id'], $blogname
class-wp-ms-sites-list-table.php
MS Themes
'theme_action_links'
$actions, $theme, $context
class-wp-ms-themes-list-table.php
MS Users
'ms_user_row_actions'
$actions, $user
class-wp-ms-users-list-table.php
Plugins
*'plugin_action_links' or
*"plugin_action_links_{$plugin_file}"