Tables and Record Lists

10 Aufrufe Markdown

A theme never ships its own grid library: record lists run on the core Table component, and everything else is plain markup.

Overview

Two paths cover every table a theme needs. The question is not how many rows you have, but what the rows are.

Record list Services, invoices, domains, tickets, ledger entries, messages, activity. The visitor searches, filters and pages through them, and the set can be empty. Use Table with 'renderer' => 'list'.
Document table Invoice line items, a feature matrix, DNS records, an SMS report. It is read, not navigated, and it belongs to the page. Write plain markup in the template.

Prerequisites

  • Your theme is active, so Theme::active() resolves to its directory.
  • You know the component itself; this article only covers the client-side list mode.
  • A tables/ directory exists in your theme root.

Structure

Four layers, and each one owns a different file. The core controller already exists for the pages listed above, so a theme usually writes only the second and third.

Data and paging Core controller. Builds the table and hands over the two model closures.
Row markup Your preset, at templates/website/{Theme}/tables/{name}.php.
Container, toolbar, pager Your view, at views/account/{name}.tpl.
Interaction Your list.js: client search, filter, paging, counter text, empty state.

Step by Step

Adding a record list

  1. Copy the preset for the page from another theme into your own tables/ directory. The file name matches the table name the controller passes.
  2. Rewrite the markup inside setRowRender() in your own design language, and keep the data and the order untouched.
  3. Mirror that markup in the view's {foreach} branch, so the first page and the later pages look the same.
  4. Give the container its three text bridges, then load the page and page through it. The counter now reads in your language and the empty state appears when a filter matches nothing.

Reference

The list mode changes three things about the component.

Table::preset() Reads from two different roots. In the panel it is templates/admin/tables/; on the site it is Theme::active()->dir()."tables". A preset placed in the admin directory is never found.
setRowRender(callable) Your callback returns the whole row in $row["html"]. The column matrix under $row["data"] is unused in this mode.
buildList(string $format = ''): string Concatenates the rows and returns them. No columns, no slicing, and justBody returns the same thing as the full call.
Sorting There are no column headers to click, so the order comes from the model's allowlist.
Download Never offered here. The privilege check returns false for a customer session.

The controller finishes the work the template cannot do, because a theme template may not call the component under the sandbox:

build('justBody') Builds the rows ahead of time and hands them to the view as a ready string.
getAjax() The data address, and it is filled only when the account crossed the row threshold. The view writes it on the container as data-ajax.
ajaxControl(['baseLink' => …]) Answers a data request and returns a non-empty string when it did, so the controller returns that value untouched.
Small accounts never reach the server

Below the threshold every row is already in the first answer, data-ajax is absent and searching, filtering and paging all happen in the browser. Above it the same address serves one page at a time. Your markup is identical in both, which is why the row attributes below carry their own values.

The container carries the wording the interaction script needs:

data-noun The key inside the script, in English, and it is never shown.
data-txt-count · data-txt-nores · data-txt-noun The translated counter sentence, the no-match sentence and the word for the record. Leave one out and that text falls back to English.

Example

templates/website/MyTheme/tables/servicesList.php
/** @var \WISECP\Components\Table $table */
if (!isset($table)) return;

$table->setRowRender(function ($row) {
    $service = $row["model"];
    $name    = htmlspecialchars($service["name"]);
    $status  = $service["status"];
    $danger  = in_array($status, ['suspended', 'expired'], true) ? ' is-danger' : '';
    $link    = LinkGenerator::client("service-detail", [$service["id"]]);

    // The whole row, in your own markup. No <td> anywhere.
    $row["html"] = '<article class="list-item' . $danger . '">'
        . '<a href="' . $link . '">' . $name . '</a>'
        . '<span class="badge">' . $status . '</span>'
        . '</article>';

    return $row;
});
views/account/services.tpl — the container
<div class="list-rows" data-noun="services"
     data-txt-count="{lang key='website/index/list-count'}"
     data-txt-nores="{lang key='website/index/list-nores'}"
     data-txt-noun="{lang key='website/services/noun'}">
    {$rows nofilter}
</div>

Pitfalls

A preset in the wrong directory fails silently

The lookup asks whether the file exists and moves on when it does not. Nothing is logged, no error appears, and the list comes back empty. If your rows are missing, check the path before you check your code.

The first page and the later pages must match

The view builds page one, and the preset builds every page after it. When the two drift apart, the rows change shape as soon as the visitor pages forward.

Two empty states, not one

No records at all replaces the toolbar, the list and the pager together, and it may offer a way to create the first record. A filter that matches nothing lives inside the container and offers a way to clear the filter. Writing only one of them leaves the visitor stuck.

Every theme needs the file

Presets live in the theme, so each theme carries its own copy of all seven. A missing one costs that theme an empty list.

War das hilfreich?

Vielen Dank für Ihre Rückmeldung!

Brauchen Sie weitere Hilfe?

Unser Support-Team ist rund um die Uhr für Sie da, wenn Sie oben nicht fündig werden.