# Tables and Record Lists

https://dev.wisecp.com/es/tables-and-record-lists

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

```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;
});
```

```html
<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.

## Related Articles

- [Interface Components](https://dev.wisecp.com/en/interface-components)
- [The Client Area](https://dev.wisecp.com/en/the-client-area)
