47

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).

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?

34

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.

| improve this answer | |
  • is it also to have automatic pagination inserted like this? (e.g. showing post 1-20) – Michiel Standaert Dec 7 '11 at 14:50
  • @MichielStandaert No. – kaiser Jun 14 '14 at 11:19
  • @MichielStandaert if you want a paginated result you can use paginate_links – tiltdown Sep 26 '14 at 22:09
  • Thanks ! (But I still asking why they didn't use :odd for row instead of let us add a class each two rows ...) – Zachary Dahan Mar 15 '16 at 15:57
  • There are obviously "better" solutions (like the other answers here), but for a quick, basic table, this is exactly what I was after. Thank you! – rinogo Nov 1 '19 at 20:06
27

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).

| improve this answer | |
  • 1
    Small hint as link to see markup, classes for the admin interface, without only the goal to create tables: github.com/bueltge/WordPress-Admin-Style – bueltge Nov 4 '13 at 13:33
  • 2
    >This class's access is marked as private. That means it is not intended for use by plugin and theme developers as it is subject to change without warning in any future WordPress release. If you would still like to make use of the class, you should make a copy to use and distribute with your own project, or else use it at your own risk. – Austin Pray May 20 '17 at 1:16
  • 1
    @AustinPray A copy ? No, please do not do that. There are betas, RCs and other pre-releases of WP available. Just update your implementation/ extension. If you really have to go sideways, just write something better on your own. The core code isn't that good. – kaiser May 20 '17 at 23:13
  • @kaiser Don't shoot the messenger, those aren't my words. I was quoting from the WP Codex. Although signing yourself up for perpetual regression testing with every beta and RC doesn't sound much better than copying the class. I agree writing your own simple class is a better way forward. – Austin Pray May 22 '17 at 19:15
  • 1
    Unfortunately, both of the guides are woefully out of date, as of Wordpress 5, which I found the hard way while breaking things (Also the Smashing Managzine one has glaring syntax errors - someone failed to master copy&paste). I haven't found an up to date alternative (the custom-list-table-example is only slightly better) and the Codex has a lot of things missing. I managed to write a simple implementation by looking at Wordpress itself, especially class-wp-users-list-table.php which is almost fully featured but simpler than other WP classes. – Guss Jun 7 at 15:06
13

Use this example (written as a plugin) to create your admin tables:

http://wordpress.org/extend/plugins/custom-list-table-example/

It uses the built-in WP_List_Table class.

| improve this answer | |
4

Also you can use this small plugin for view the possibilities of the backend in WP: https://github.com/bueltge/WordPress-Admin-Style

| improve this answer | |
1

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
}
| improve this answer | |
  • There goes your upvote. – kaiser Jun 9 at 9:21
0

You might want to consider adding a filter to your custom post type list in the admin? The linked answer below shows how to do it with a taxonomy but you could easily use other criteria in your restrict_manage_posts hook:

Let me know if you have more questions.

| improve this answer | |
  • I apologize for the unclear question. In my first example it was a table of posts, and indeed, I could try to use the existing post table for that (even if I only want to show the post titles and then all custom columns?). But now I have edited my question with a concrete example: I have a table of domains, so there is no equivalent existing table that I can expand. – Jan Fabry Sep 7 '10 at 7:50
  • @Jan: Ah. Yes, I think you have found the truth, that there is not good encapsulated way to do this other than writing (duplicated) HTML. I've often had the same issue. Maybe create a ticket on trac asking for this enhancement and link the URL/ticket# here so we can support it. – MikeSchinkel Sep 7 '10 at 23:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.