This WordPress tutorial shows how to implement a list table using the WP_List_Table class, with features like searching, sorting, and pagination. At Diversify India, we collect subscriber email addresses using a newsletter subscription form located at the bottom of our pages. To manage and display these email addresses, we utilize the WP_List_Table class. By following this tutorial, you’ll be able to easily implement a similar list table, as demonstrated in the following image.
In this tutorial, we will cover the following points:
- Screen option to enable or disable a specific column.
- Screen option to adjust the number of entries displayed per page.
- Implementing a search bar to filter data.
- Sortable columns to perform sorting in ascending or descending order.
- Bulk actions for performing bulk operations, such as deleting selected entries from the database.
- Custom action on hover for adding actions like deleting an entry from the database upon hovering.
- Adding custom actions in the extra table navigation section, such as clearing all data from the database.
- Implementing pagination for the list table.
Adding Code to the functions.php File
The most effective way to add the list table code is by creating a separate plugin or using the Code Snippets plugin, ensuring that the functionality persists even after theme updates. For simplicity, this tutorial will use the functions.php
file. Be sure to insert the code in the functions.php
file of a child theme to prevent losing your customizations when updating your theme.
Creating a Menu Page
First, we need to create a menu page to display our list table. This can be done using the admin_menu
hook. Add the following code to your functions.php
file to create a custom page within the WordPress admin area:
add_action('admin_menu', 'my_subscribers');
function my_subscribers() {
add_menu_page(__('Subscribers', 'theme-name'), __('Subscribers', 'theme-name'), 'manage_options', 'subscribers', 'init_subscribers', 'dashicons-email', 110);
}
function init_subscribers() {
echo "My Subscribers";
}
Inherit the WP_List_Table class
WordPress doesn’t load the WP_List_Table class automatically, so we need to ensure it’s available by including the class-wp-list-table.php
file in our code. This can be done by checking if the class exists before including the file. Then we have to extend this class and override the prepare_items
and get_columns
methods. Also, ensure that the get_columns
method returns an array.
// load WP_List_Table class
if( ! class_exists('WP_List_Table') ) {
require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
}
// extend WP_List_Table
class Subscribers_List_Table extends WP_List_Table {
public function __construct( $args = array() ) {
if(empty($args)) {
$args = array(
'plural' => __('subscribers', 'theme-name'),
'singular' => __('subscriber', 'theme-name'),
);
}
parent::__construct($args);
}
// override prepare_items() and get_columns() method
public function prepare_items() {}
public function get_columns() {
return array(); // Ensure this method returns an array
}
}
In the init_subscribers
method (created in the previous step), instantiate your class and pass an array with keys plural
and singular
. The values of these keys will be used for labels, CSS class names, and the objects being listed. Then, call the prepare_items
method to prepare the list of items and the display
method to render the table.
function init_subscribers() {
echo "My Subscribers";
// create an instance of class
$table = new Subscribers_List_Table(array(
'plural' => __('subscribers', 'theme-name'),
'singular' => __('subscriber', 'theme-name'),
));
$table->prepare_items();
$table->display();
}
If you run this program, you will see an empty table with the message No items found.
Displaying Columns
First of all, create a database table named $wpdb->prefix . 'subscribers'
with the following columns:
id | subscribers_email | date_of_subscription |
---|---|---|
1 | admin@example.com | 10-12-2023 |
2 | contact@example.com | 03-05-2024 |
To display column names in the header and footer of the table, return an associative array from the get_columns
method. If the checkbox is missing in the thead
and tfoot
sections, ensure the array key cb
is set in the get_columns
method, as this renders the checkbox. Replace the get_columns
method, which was overridden in the previous step, with the following code:
public function get_columns() {
return array(
'cb' => '<input type="checkbox" />', // display checkbox
'subscribers-email' => __('Subscribers Email', 'theme-name'),
'date' => __('Date', 'theme-name')
);
}
Adding Sortable Columns
To implement sorting, override the get_sortable_columns
method of the WP_List_Table class. The first parameter of an array should match the column name in your database, as it will be used in the ORDER BY
clause.
protected function get_sortable_columns() {
return array(
'date' => array('date_of_subscription', true, __( 'Date' ), __( 'Table ordered by Date.' ))
);
}
In the above code, date is the column name assigned to the date_of_subscription
column in our database. By passing true
as the second parameter, the initial sorting order for this column will be set to descending.
To display our columns, we need to set the value of the _column_headers
property of the WP_List_Table class. This property is configured in the prepare_items
method. The _column_headers
property accepts an array with three elements:
- An array of column names.
- An array of hidden columns, which can be set through screen options.
- An array of sortable columns.
Replace the prepare_items
method from step 3 with the following code:
public function prepare_items() {
$meta_key = 'managetoplevel_page_' . $_REQUEST['page'] . 'columnshidden';
// Retrieves preferences related to hidden columns from the usermeta column of the database
$hidden = (is_array(get_user_meta(get_current_user_id(), $meta_key, true))) ? get_user_meta(get_current_user_id(), $meta_key, true) : array();
$this->_column_headers = [
$this->get_columns(),
$hidden,
$this->get_sortable_columns(),
];
}
After refreshing the page, you should see the column names along with arrows indicating sortable columns.
Adding Support for Screen Options
To add support for screen options, you need to modify the my_subscribers
and init_subscribers
methods defined in the creating a menu page step.
function my_subscribers() {
// create global page variable
global $subscribers_page;
// store page value return from add_menu_page() into global page var
$subscribers_page = add_menu_page(__('Subscribers', 'theme-name'), __('Subscribers', 'theme-name'), 'manage_options', 'subscribers', 'init_subscribers', 'dashicons-email', 110);
add_action("load-$subscribers_page", "subscribers_screen_options");
}
function init_subscribers() {
// define $table as a global variable
global $table;
echo '<div class="wrap"><h1 class="wp-heading-inline">My Subscribers</h1>';
$table->prepare_items();
$table->display();
echo '</div>';
}
Then, define the subscribers_screen_options
callback method, used by the load-$subscribers_page
hook.
function subscribers_screen_options() {
// declare $subscribers_page and $table as a global variable
global $subscribers_page, $table;
// return if not on our settings page
$screen = get_current_screen();
if(!is_object($screen) || $screen->id !== $subscribers_page) {
return;
}
$args = array(
'label' => __('Subscribers per page', 'theme-name'),
'default' => 20,
'option' => 'subscribers_per_page'
);
add_screen_option('per_page', $args);
// create an instance of class
$table = new Subscribers_List_Table(array(
'plural' => __('subscribers', 'theme-name'),
'singular' => __('subscriber', 'theme-name'),
));
}
Now verify that the screen options and the show/hide columns functionality work correctly.
Implementing Pagination
Now we will add functionality to fetch data from the database with pagination support. To do this, call the get_table_data
function within the prepare_items
method. Paste the following code in step 3 and replace the prepare_items
method with it:
private function get_table_data($args = array()) {
global $wpdb;
$orderby = isset( $_REQUEST['orderby'] ) ? $_REQUEST['orderby'] : 'id';
$order = isset( $_REQUEST['order'] ) ? $_REQUEST['order'] : 'desc';
return $wpdb->get_results("SELECT * FROM " . $wpdb->prefix . "subscribers ORDER BY {$orderby} {$order} LIMIT {$args['offset']}, {$args['per_page']}");
}
private function get_total_items() {
global $wpdb;
return (int) $wpdb->get_var("SELECT COUNT(`id`) FROM " . $wpdb->prefix . "subscribers");
}
public function prepare_items() {
$meta_key = 'managetoplevel_page_' . $_REQUEST['page'] . 'columnshidden';
// Retrieves preferences related to hidden columns from usermeta column of database
$hidden = (is_array(get_user_meta(get_current_user_id(), $meta_key, true))) ? get_user_meta(get_current_user_id(), $meta_key, true) : array();
$this->_column_headers = [
$this->get_columns(),
$hidden,
$this->get_sortable_columns(),
];
// get per page value from screen options
$items_per_page = $this->get_items_per_page('subscribers_per_page');
// get total number of items from database
$total_items = $this->get_total_items();
// get current page number
// get_pagenum() method is available in WP_List_Table class
$page = $this->get_pagenum();
// calculate offset
$offset = ( $page - 1 ) * $items_per_page;
// call set_pagination_args() method to add pagination support
$this->set_pagination_args(
array(
'total_items' => $total_items,
'per_page' => $items_per_page,
)
);
// fetch data from database
$this->items = $this->get_table_data(array(
'offset' => $offset,
'per_page' => $items_per_page
));
}
If the table appears empty, override the column_default
method of the WP_List_Table class to fix this issue.
protected function column_default($item, $column_name) {
if($column_name === 'subscribers-email') {
// items key name should match with database column name
return $item->subscribers_email;
} elseif($column_name === 'date') {
return $item->date_of_subscription;
} else {
return $item->{$column_name};
}
}
Now that our data is showing up in the table, you may notice that the checkbox is missing. To display this checkbox, override the column_cb
method of the WP_List_Table class.
protected function column_cb($item) {
return '<input id="cb-select-' . $item->id . '" type="checkbox" name="subscribers[]" value="' . $item->id . '" />';
}
Now check whether all column’s data is displaying correctly and verify if pagination is working. If you notice that the pagination form is not functioning, enclose the table within a <form>
tag. Replace the init_subscribers
method from the create a menu page step with the following code:
function init_subscribers() {
// define $table as a global variable
global $table;
echo '<div class="wrap"><h1 class="wp-heading-inline">My Subscribers</h1>';
echo '<form id="subscribers-form" method="post">';
$table->prepare_items();
$table->display();
echo '</form></div>';
}
Adding Bulk Actions
To add bulk action support, override the get_bulk_actions
method of the WP_List_Table class.
protected function get_bulk_actions() {
return array('delete_all' => __('Delete permanently', 'theme-name'));
}
Adding Custom Buttons to Extra Table Navigation
To include an extra button in the table navigation, override the extra_tablenav
method.
protected function extra_tablenav( $which ) {
$page = $_REQUEST['page'];
echo '<div class="alignleft actions"><a href="?page=' . $page . '&action=clear&_wpnonce=' . esc_attr(wp_create_nonce("delete-table")) . '" aria-label="' . __('Clear this table data', 'theme-name') . '" role="button" class="button action clear-all" style="background-color: #cf2e2e; color: #ffffff;">' . __('Clear All', 'theme-name') . '</a></div>';
}
Adding Action Links to Columns
To display action links in any column, create a method named column_columnName
, which will display an action link when you hover over the row. In this example, we will enable an action link for the date column.
protected function column_date($item) {
$_wpnonce = esc_attr(wp_create_nonce("delete-entry"));
$actions = array(
'delete' => sprintf('<a href="?page=%s&action=%s&id=%s&_wpnonce=%s" aria-label="%s" role="button">%s</a>',
$_REQUEST['page'], 'delete', $item->id, $_wpnonce, __('Delete this contact permanently', 'theme-name'), __('Delete Permanently', 'theme-name')
)
);
return $item->date_of_subscription . $this->row_actions($actions);
}
Adding Search Bar Support
To add a search bar, call the search_box
method in the init_subscribers
method from the create a menu page step.
function init_subscribers() {
// define $table as a global variable
global $table;
echo '<div class="wrap"><h1 class="wp-heading-inline">My Subscribers</h1>';
echo '<form id="subscribers-form" method="post">';
$table->prepare_items();
$table->search_box('search', 'subscribers');
$table->display();
echo '</form></div>';
}
Then, modify the get_table_data
method from the pagination implementation step to return results based on the search query.
private function get_table_data($args = array()) {
global $wpdb;
$orderby = isset( $_REQUEST['orderby'] ) ? $_REQUEST['orderby'] : 'id';
$order = isset( $_REQUEST['order'] ) ? $_REQUEST['order'] : 'desc';
if(isset($_POST['s']) && $_POST['s']) {
return $wpdb->get_results("SELECT * FROM " . $wpdb->prefix . "subscribers WHERE subscribers_email LIKE '%{$_POST['s']}%'");
}
return $wpdb->get_results("SELECT * FROM " . $wpdb->prefix . "subscribers ORDER BY {$orderby} {$order} LIMIT {$args['offset']}, {$args['per_page']}");
}
Handling Actions
Finally, handle the delete actions by defining a new method called process_bulk_action
, which should be called within the prepare_items
method.
public function process_bulk_action() {
global $wpdb;
if(isset($_POST['_wpnonce']) && !empty($_POST['_wpnonce'])) {
$nonce = filter_input(INPUT_POST, '_wpnonce', FILTER_SANITIZE_STRING);
$action = 'bulk-' . $this->_args['plural'];
if(!wp_verify_nonce($nonce, $action)) {
wp_die(__('Security check failed!', 'theme-name'));
}
if((isset($_POST['action']) && $_POST['action'] === 'delete_all') || (isset($_POST['action2']) && $_POST['action2'] === 'delete_all')) {
$ids = $_POST['subscribers'];
foreach($ids as $id) {
$wpdb->delete($wpdb->prefix . "subscribers", array("id" => $id), "%d");
}
}
} else if(isset($_GET['action']) && (isset($_GET['_wpnonce']) && !empty($_GET['_wpnonce']))) {
$nonce = filter_input(INPUT_GET, '_wpnonce', FILTER_SANITIZE_STRING);
if($_GET['action'] === 'delete' && isset($_GET['id'])) {
if(!wp_verify_nonce($nonce, "delete-entry")) {
wp_die(__('Security check failed!', 'theme-name'));
}
$id = $_GET['id'];
$wpdb->delete($wpdb->prefix . "subscribers", array("id" => $id), "%d");
} else if($_GET['action'] === 'clear') {
if(!wp_verify_nonce($nonce, "delete-table")) {
wp_die(__('Security check failed!', 'theme-name'));
}
$wpdb->query("DELETE FROM " . $wpdb->prefix . "subscribers");
}
}
}
By implementing all these steps, you’ll have a fully functional list table with all the necessary features. You can further optimize the code or add additional features as needed.
Complete WP_List_Table Program
Here’s the complete code for creating an email list table using the WP_List_Table class:
add_action('admin_menu', 'my_subscribers');
function my_subscribers() {
// create global page variable
global $subscribers_page;
// store page value return from add_menu_page() into global page var
$subscribers_page = add_menu_page(__('Subscribers', 'theme-name'), __('Subscribers', 'theme-name'), 'manage_options', 'subscribers', 'init_subscribers', 'dashicons-email', 110);
add_action("load-$subscribers_page", "subscribers_screen_options");
}
function subscribers_screen_options() {
// declare $subscribers_page and $table as a global variable
global $subscribers_page, $table;
// return if not on our settings page
$screen = get_current_screen();
if(!is_object($screen) || $screen->id !== $subscribers_page) {
return;
}
$args = array(
'label' => __('Subscribers per page', 'theme-name'),
'default' => 20,
'option' => 'subscribers_per_page'
);
add_screen_option('per_page', $args);
// create an instance of class
$table = new Subscribers_List_Table(array(
'plural' => __('subscribers', 'theme-name'),
'singular' => __('subscriber', 'theme-name'),
));
}
function init_subscribers() {
// define $table as a global variable
global $table;
echo '<div class="wrap"><h1 class="wp-heading-inline">My Subscribers</h1>';
echo '<form id="subscribers-form" method="post">';
$table->prepare_items();
$table->search_box('search', 'subscribers');
$table->display();
echo '</form></div>';
}
// load WP_List_Table class
if( ! class_exists('WP_List_Table') ) {
require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
}
// extend WP_List_Table and override
// prepare_items() and get_columns() method
class Subscribers_List_Table extends WP_List_Table {
public function __construct( $args = array() ) {
if(empty($args)) {
$args = array(
'plural' => __('subscribers', 'theme-name'),
'singular' => __('subscriber', 'theme-name'),
);
}
parent::__construct($args);
}
public function prepare_items() {
$meta_key = 'managetoplevel_page_' . $_REQUEST['page'] . 'columnshidden';
// Retrieves preferences related to hidden columns from usermeta column of database
$hidden = (is_array(get_user_meta(get_current_user_id(), $meta_key, true))) ? get_user_meta(get_current_user_id(), $meta_key, true) : array();
$this->_column_headers = [
$this->get_columns(),
$hidden,
$this->get_sortable_columns(),
];
$this->process_bulk_action();
// get per page value from screen options
$items_per_page = $this->get_items_per_page('subscribers_per_page');
// get total number of items from database
$total_items = $this->get_total_items();
// get current page number
// get_pagenum() method is available in WP_List_Table class
$page = $this->get_pagenum();
// calculate offset
$offset = ( $page - 1 ) * $items_per_page;
// call set_pagination_args() method to add pagination support
$this->set_pagination_args(
array(
'total_items' => $total_items,
'per_page' => $items_per_page,
)
);
// fetch data from database
$this->items = $this->get_table_data(array(
'offset' => $offset,
'per_page' => $items_per_page
));
}
public function get_columns() {
return array(
'cb' => '<input type="checkbox" />', // display checkbox
'subscribers-email' => __('Subscribers Email', 'theme-name'),
'date' => __('Date', 'theme-name')
);
}
protected function get_sortable_columns() {
return array(
'date' => array('date_of_subscription', true, __( 'Date' ), __('Table ordered by Date.' ))
);
}
private function get_table_data($args = array()) {
global $wpdb;
$orderby = isset( $_REQUEST['orderby'] ) ? $_REQUEST['orderby'] : 'id';
$order = isset( $_REQUEST['order'] ) ? $_REQUEST['order'] : 'desc';
if(isset($_POST['s']) && $_POST['s']) {
return $wpdb->get_results("SELECT * FROM " . $wpdb->prefix . "subscribers WHERE subscribers_email LIKE '%{$_POST['s']}%'");
}
return $wpdb->get_results("SELECT * FROM " . $wpdb->prefix . "subscribers ORDER BY {$orderby} {$order} LIMIT {$args['offset']}, {$args['per_page']}");
}
private function get_total_items() {
global $wpdb;
return (int) $wpdb->get_var("SELECT COUNT(`id`) FROM " . $wpdb->prefix . "subscribers");
}
protected function column_default($item, $column_name) {
if($column_name === 'subscribers-email') {
// items key name should match with database column name
return $item->subscribers_email;
} elseif($column_name === 'date') {
return $item->date_of_subscription;
} else {
return $item->{$column_name};
}
}
protected function column_cb($item) {
return '<input id="cb-select-' . $item->id . '" type="checkbox" name="subscribers[]" value="' . $item->id . '" />';
}
protected function get_bulk_actions() {
return array('delete_all' => __('Delete permanently', 'theme-name'));
}
protected function extra_tablenav( $which ) {
$page = $_REQUEST['page'];
echo '<div class="alignleft actions"><a href="?page=' . $page . '&action=clear&_wpnonce=' . esc_attr(wp_create_nonce("delete-table")) . '" aria-label="' . __('Clear this table data', 'theme-name') . '" role="button" class="button action clear-all" style="background-color: #cf2e2e; color: #ffffff;">' . __('Clear All', 'theme-name') . '</a></div>';
}
protected function column_date($item) {
$_wpnonce = esc_attr(wp_create_nonce("delete-entry"));
$actions = array(
'delete' => sprintf('<a href="?page=%s&action=%s&id=%s&_wpnonce=%s" aria-label="%s" role="button">%s</a>',
$_REQUEST['page'], 'delete', $item->id, $_wpnonce, __('Delete this contact permanently', 'theme-name'), __('Delete Permanently', 'theme-name')
)
);
return $item->date . $this->row_actions($actions);
}
public function process_bulk_action() {
global $wpdb;
if(isset($_POST['_wpnonce']) && !empty($_POST['_wpnonce'])) {
$nonce = filter_input(INPUT_POST, '_wpnonce', FILTER_SANITIZE_STRING);
$action = 'bulk-' . $this->_args['plural'];
if(!wp_verify_nonce($nonce, $action)) {
wp_die(__('Security check failed!', 'theme-name'));
}
if((isset($_POST['action']) && $_POST['action'] === 'delete_all') || (isset($_POST['action2']) && $_POST['action2'] === 'delete_all')) {
$ids = $_POST['subscribers'];
foreach($ids as $id) {
$wpdb->delete($wpdb->prefix . "subscribers", array("id" => $id), "%d");
}
}
} else if(isset($_GET['action']) && (isset($_GET['_wpnonce']) && !empty($_GET['_wpnonce']))) {
$nonce = filter_input(INPUT_GET, '_wpnonce', FILTER_SANITIZE_STRING);
if($_GET['action'] === 'delete' && isset($_GET['id'])) {
if(!wp_verify_nonce($nonce, "delete-entry")) {
wp_die(__('Security check failed!', 'theme-name'));
}
$id = $_GET['id'];
$wpdb->delete($wpdb->prefix . "subscribers", array("id" => $id), "%d");
} else if($_GET['action'] === 'clear') {
if(!wp_verify_nonce($nonce, "delete-table")) {
wp_die(__('Security check failed!', 'theme-name'));
}
$wpdb->query("DELETE FROM " . $wpdb->prefix . "subscribers");
}
}
}
}
Read our article on how to create newsletter forms without using a plugin to collect the data used in the above table.
Frequently Asked Questions
How do I fix the column screen option not showing in the WP List Table?
The main cause of this problem is either not using the global $table
variable or creating a new list table object in the callback function used in the add_menu_page
function. If you are using the code provided above, ensure that you are not reinitializing the table object in the init_subscribers
function. Refer to the Adding Support for Screen Options section in the tutorial above.
How do I fix the responsiveness of a table created using the WP_List_Table class or the table overflowing on the right side?
To make tables created using the WP List Table class responsive, wrap them inside a <div>
tag with the class ‘wrap’.
echo '<div class="wrap"><form id="subscribers-form" method="post">';
$table->prepare_items();
$table->display();
echo '</form></div>';