Creating a table in the admin-style?

What is the recommended way of creating a page with a table, in the style of the tables showing posts or users in the admin area?

I am expanding the Cache Images plugin, and it contains a table with domains and a number of images from that domain. So there is no equivalent existing table that I can build upon (in the first version of this question, I asked about a table with posts, but there I could (maybe) expand the existing post table).

Read More

Should I just base myself on the post overview page, and start with a <table class="widefat">, or are there better functions that handle this now? Do you know a clean, empty example of a table with paging that I could base my work on?

Related posts

Leave a Reply

7 comments

  1. This is what I generally use:

    <table class="widefat fixed" cellspacing="0">
        <thead>
        <tr>
    
                <th id="cb" class="manage-column column-cb check-column" scope="col"></th> // this column contains checkboxes
                <th id="columnname" class="manage-column column-columnname" scope="col"></th>
                <th id="columnname" class="manage-column column-columnname num" scope="col"></th> // "num" added because the column contains numbers
    
        </tr>
        </thead>
    
        <tfoot>
        <tr>
    
                <th class="manage-column column-cb check-column" scope="col"></th>
                <th class="manage-column column-columnname" scope="col"></th>
                <th class="manage-column column-columnname num" scope="col"></th>
    
        </tr>
        </tfoot>
    
        <tbody>
            <tr class="alternate">
                <th class="check-column" scope="row"></th>
                <td class="column-columnname"></td>
                <td class="column-columnname"></td>
            </tr>
            <tr>
                <th class="check-column" scope="row"></th>
                <td class="column-columnname"></td>
                <td class="column-columnname"></td>
            </tr>
            <tr class="alternate" valign="top"> // this row contains actions
                <th class="check-column" scope="row"></th>
                <td class="column-columnname">
                    <div class="row-actions">
                        <span><a href="#">Action</a> |</span>
                        <span><a href="#">Action</a></span>
                    </div>
                </td>
                <td class="column-columnname"></td>
            </tr>
            <tr valign="top"> // this row contains actions
                <th class="check-column" scope="row"></th>
                <td class="column-columnname">
                    <div class="row-actions">
                        <span><a href="#">Action</a> |</span>
                        <span><a href="#">Action</a></span>
                    </div>
                </td>
                <td class="column-columnname"></td>
            </tr>
        </tbody>
    </table>
    

    Hope that helps.

  2. Use the Core API, not only its CSS

    Normally you just use an instance of the WP_List_Table class.

    Guides:

    Benefits?

    YES!

    You can add pagination, search boxes, actions and whatever magic you can imagine (and are able to code).

  3. There are many good options here. But there isn’t a quick’n’dirty one:

    <table class="widefat striped fixed">
        <thead>
            <tr>
                <th>Header1</th>
                <th>Header2</th>
                <th>Header3</th>
            </tr>
        </thead>
    
        <tbody>
            <tr>
                <td>Content1</td>
                <td>Content2</td>
                <td>Content3</td>
            </tr>
        </tbody>
    
        <tfoot>
            <tr>
                <th>Footer1</th>
                <th>Footer2</th>
                <th>Footer3</th>
            </tr>
        </tfoot>
    </table>
    

    Explanation

    • The widefat-class is used by the common.css, loaded in the admin, to make it look like a WP-table.
    • The striped-class makes it striped (what a surprise).
    • The fixed-class adds the CSS: table-layout: fixed;
  4. For those looking to implement WP_List_Table, please note that all guides I found are woefully out of date and will either have write redundant code or actually ask you to do things that no longer work.

    Here is a minimal example that works to some degree. It should be easy to understand without a “guide” and will get you started.

    Includes:

    • quick filters (views)
    • search box
    • row actions

    Missing:

    • page size configuration (I actually haven’t seen a WordPress page use this)
    • bulk actions
    • pulldown filters
    class My_List_Table extends WP_List_Table {
    
        function __construct() {
            parent::__construct([
                'singular' => 'employee',
                'plural' => 'employees',
            ]);
        }
    
        function get_columns() {
            return [
                'name'      => __('Name'),
                'employer' => __('Employer'),
                'rank'     => __('Rank'),
                'phone'    => __('Telephone'),
                'joined'   => __('Join Date'),
            ];
        }
    
        /* Optional - without it no column is sortable */
        public function get_sortable_columns() {
            return [
               // keys are "column_name" like above
               // values are "order" field names as per what your data model needs
                'name'     => 'name',
                'employer' => 'employer',
                'rank'     => 'rank',
                'joined'   => 'joined',
            ];
        }
    
        public function prepare_items() {
           // support the search box
            $search = @$_REQUEST['s'] ? wp_unslash(trim($_REQUEST['s']))) : '';
           // get number of records per page setting from option storage
            $per_page = $this->get_items_per_page('my_list_table_per_page');
           // fill data array with your model items. In my implementation these
           // are StdClass instances with various fields, but it can be anything
           // we'll see in a minute how.
            $this->items = get_model_items([
                'offset' => ($this->get_pagenum()-1)*$per_page,
                'count' => $per_page,
                'orderby' => @$_GET["orderby"] ?: 'id', // default order field, if not specified
                'order' => @$_GET["order"] ?: 'ASC', // default order direction
                'search' => $search, // pass search field if set
                'status' => @$_REQUEST['status'] // pass view filter, if set [see get_views()]
            );
            $this->set_pagination_args([
                "total_items" => get_model_item_count(),
                "per_page" => $per_page,
            ]);
           // `get_model_item_count` should be the total number of records after
           // filtering (views and search) but before paging. This may be hard/inefficient
           // to do with MySQL. If you want to put the results of `COUNT(*)` here,
           // no one will blame you.
        }
    
        public function column_default($item, $column_name) {
            // default column presentation
            // Most of my object fields are printable as is, so we have a generic
            // method to handle that.
            return $item->$column_name;
        }
    
        /* Optional, unless you have data that requires special formatting */
        public function column_joined($item) {
            // The 'joined' field is a DateTime object and can't be implicitly
           // converted to string by the built-in logic, so we'll need to do it
            return $item->joined->format("Y-m-d");
        }
    
       /* Optional - draw quick filters on top of the table */
        public function get_views() {
            $makelink = function($filter_val, $name) { // DRYing tool for view makers
                $filter_name = 'status';
                return '<a href="'
                    . esc_url(add_query_arg($filter_name, $filter_val)) . '" ' .
                    (@$_REQUEST[$filter_name]==$filter_val ? 
                        'class="current" aria-current="page"' : ''). ">" .
                    $name . "</a>";
            };
            return [
                'all' => $makelink(false, __('All')),
                'green' => $makelink('green', __('Newbs')),
                'pros' => $makelink('pro', __('Experts')),
                'bofh' => $makelink('veteran', __('Crusty fellows')),
            ];
        }
    
        /* Optional: row actions */
        public function handle_row_actions($item, $column_name, $primary) {
            $out = parent::handle_row_actions($item, $column_name, $primary);
            if ($column_name === $primary)
                $out .= $this->row_actions([
                    'edit' => sprintf('<a href="%s">%s</a>',
                        add_query_arg('employee_id', $item->id, admin_url('admin.php?page=edit-employee')),
                        __("Edit")),
                    'delete' => sprintf('<a href="%s">%s</a>',
                        add_query_arg('employee_id', $item->id, admin_url('admin.php?page=delete-employee')),
                        __("Delete")),
                ]);
            return $out;
        }
    
    }
    

    Then the admin page function (for add_menu_page/add_submenu_page) may look like this:

    function drawAdminPage() {
        $my_list_table = new My_List_Table();
        $my_list_table->prepare_items();
        ?>
        <div class="wrap">
        <h1 class="wp-heading-inline"><?php _e('Admin Page Title')?></h1>
        <hr class="wp-header-end">
        <?php $my_list_table->views() ?>
        <form id="employee-filter" method="get">
        <input type="hidden" name="page" value="<?php echo $_REQUEST['page']?>">
        <?php $my_list_table->search_box(__('Search'), 'employee') ?>
        <?php $my_list_table->display(); ?>
        </form>
        </div>
        <?php
    }