Interface Components

6 views Markdown

Four building blocks the panel is assembled from. A screen you add looks and behaves like the ones that shipped with it.

Overview

A component is built in PHP and printed into the template: the controller describes what it wants, the component produces the markup. Sorting, filtering, paging and tab memory come with it.

All four live in WISECP\Components and are constructed with new. Every one takes its content through an options array, not positional arguments.

Reference

Table

WISECP\Components\Table
public function __construct(string $name, array $options = []);   // $name = preset file name

public function setColumns(array $columns): self;
public function setColumn(string $key, array $data): self;
public function deleteColumn(string $key): self;
public function setRows(array $rows): self;
public function setRowRender(callable $renderRow): self;
public function setFilters(array $filters): self;
public function setOptions(array $options): self;
public function getOptions(): array;

public function isRequestAjax(): bool;
public function ajaxControl(array $options = []): string;
public function allDataAjaxResponse(): string;
public function ajaxResponse(int $totalEntries = 0, int $totalSearchEntries = 0): string;

public function build(string $format = ''): string;        // '' = whole table, 'justBody' = rows only
public function buildList(string $format = ''): string;    // card rows instead of a grid
public function container(string $content, array $attributes = []): string;
public function exportButton(array $options = []): string; // ['label', 'formats', 'class']

// Public properties. The two models are closures the component calls itself.
public int $ajax_transition_limit = 1000;
public mixed $totalModel;   // fn (string $search = '', array $filters = []): int
public mixed $dataModel;    // fn (string $search = '', string $order = '', string $direction = '', int $start = 0, int $end = -1): array

Constructor options, all optional. The constructor includes the preset file itself, before the controller has assigned anything else. A preset reads options with getOptions() and never sees rows.

preset Load a preset file other than the table's own name, e.g. 'ticketList' for a widget reusing the main list's columns.
lazy true defers the first query until the tab is opened. Not for a detail tab; for expensive sources (a filesystem scan, a remote catalogue).
perPage · perPageOptions · search · info · pagination Toolbar switches. false drops the control; [10, 25, 50, 100, -1] sets the page sizes, -1 meaning all.
renderer 'list' switches to the card layout, the one the client area uses. Anything else gives a grid.
export · exportPrivilege · exportName Download is on by default. false removes it, a privilege key narrows it, the name becomes the file name. Set these in the controller: the export request exits before the template runs.
containerExtraAttributes Attribute map written on the wrapper, e.g. ['data-url-sync' => 'true'] to mirror filters into the browser address.

ajaxControl() takes its own array. It returns a non-empty string exactly when it has already answered the request, so the caller returns it untouched.

baseLink Address the data request is built from. For a subpage this must be the full subpage address: the controller root never reaches the page method, so the list comes back empty.
mode 'partial' (default) pages on the server; 'allData' hands the whole set over in one answer.
ajaxExtraParams Extra query values carried on every data request, e.g. ['type' => 'hosting'].
force Overrides the row-count threshold. Legitimate only when the filters shape the query itself; write the reason next to it.

The row your setRowRender() callback receives, and gives back, has three parts:

the shape of one row
$row = [
    // The record as the model returned it. Read-only input.
    'model' => ['id' => 5, 'name' => 'example.com', 'status' => 'active'],

    // One entry per column key. 'value' is the cell markup, 'attributes' land on the <td>.
    // 'data-value' is what sorting and client-side filtering compare, so it is the RAW value.
    'data' => [
        'id'     => ['value' => '<a href="...">#5</a>', 'attributes' => ['data-value' => 5]],
        'status' => ['value' => '<span class="badge">Active</span>', 'attributes' => ['data-value' => 'active']],
    ],

    // Attributes for the <tr>. Row-level filter values are read from here.
    'attributes' => ['class' => 'table-tr-bg-info', 'data-filter-status' => 'active'],
];

Tab and Accordion

WISECP\Components\Tab · WISECP\Components\Accordion
// Tab. $type is 'horizontal' or 'vertical'. add() takes TWO arguments:
// the panel key, then an options array. The panel body is $options['content'].
public function __construct($name = '', $type = 'horizontal');
public function add($name, $options = []): self;
public function set($name, $options = []): self;                 // same as add()
public function get($name = '');
public function remove($name = ''): self;
public function noUrl(bool $noUrl = true): self;
public function render(array $options = [], string $header = '', string $contents = ''): string;
public function header($options = []): string;
public function contents(array $options = []): string;

// Accordion. Same idea, typed, and no vertical/horizontal choice.
public function __construct(string $name = '');
public function add(string $name, array $options = []): self;
public function set(string $name, array $options = []): self;
public function get(string $name = '');
public function remove(string $name): self;
public function noUrl(): self;
public function render(array $options = []): string;
content The panel's markup. Both accept it here and only here; an empty panel stays empty, so supply your own empty state.
title Label on the tab or accordion header. Falls back to the key when absent, so an untranslated panel shows its key.
icon Icon class, e.g. 'bi bi-gear'. On a horizontal tab the spacing class comes from iconClass and defaults to 'me-2'.
badge · badgeClass Tab only: a count printed inside the tab after its label. Any non-empty value prints, including '0'.
subtitle · hideContentTitle Tab only, vertical layout: the second line in the rail, and a switch that suppresses the heading the pane otherwise repeats.
show Accordion only: true opens this section on load. Sections share a parent, so opening one closes the rest.
headerTag · titleClass · buttonClass · bodyClass Accordion only: heading element (default 'h2') and the classes on header, toggle (default 'bg-light', pass '' to clear) and body.

Both remember the open panel in the address, under the component's own name: a tab set named settings reads ?settings=general. Two sets on one screen need different names.

WISECP\Components\Modal
// Everything is given at construction; the setters exist to change it afterwards
// and they return void, so they do NOT chain.
public function __construct(array $params);
public function setTitle(string $title): void;
public function setBody(string $body): void;
public function setFooter(string $footer): void;
public function setForm(?array $formAttributes): void;   // attribute map, or null to unwrap
public function setHeaderClasses(array $classes): void;
public function render(): string;
id DOM id, and the target a trigger points at. Defaults to 'SampleModal', so two dialogs without an id collide.
title · body · bodyClass · footer Heading, content markup, extra classes on the body, and the footer. An empty footer is not rendered at all.
form Attribute map that wraps header, body and footer in a <form>: ['action' => $url, 'method' => 'POST']. Every pair is written as an attribute, so a footer submit button posts the dialog. Omit it for a read-only dialog.
headerClasses Picks the tone only: a value containing danger gives a red title, anything else the primary title. The shell is fixed, so this cannot paint a coloured bar.
modalDialogExtraClass · scrollable · centered Width class such as 'modal-lg', plus the two layout switches that both default to true.
attributes Raw attributes merged over the defaults on the outer element. The only supported way to lock a backdrop.

Example

A list takes three files: the controller wires the data, the preset declares the columns and builds each row out of the model, the page template prints it.

the controller
$table = new \WISECP\Components\Table('widgetList', ['exportName' => 'widgets']);

// Values the operator chose. setFilters carries them on the data request;
// the model only receives them while answering one.
$dynamic = [];
if ($status = Filter::init("REQUEST/filter/status", "route")) $dynamic['status'] = $status;

$table->setFilters($dynamic);
$filter = $table->isRequestAjax() ? $dynamic : [];

$table->totalModel = fn ($search = '', $filters = []) =>
    $this->model->list(true, array_merge($filter, $filters, ['word' => $search]));

$table->dataModel = fn ($search = '', $order = '', $direction = '', $start = 0, $end = -1) =>
    $this->model->list(false, array_merge($filter, ['word' => $search]), [$order => $direction], $start, $end);

// Non-empty means the request was already answered here.
if ($response = $table->ajaxControl(['baseLink' => $links["controller"]])) return $response;

$this->addData('table', $table);
templates/admin/tables/widgetList.php
/** @var \WISECP\Components\Table $table */
if (!isset($table)) return;

$table->setColumns([
    'id'     => ['title' => 'ID', 'attributes' => ['class' => 'text-center'], 'sortable' => true],
    'name'   => ['title' => Language::gc("admin/widgets/th-name"), 'sortable' => true],
    'status' => ['title' => Language::gc("admin/widgets/th-status"), 'sortable' => "asc"],
    'email'  => ['title' => Language::gc("admin/widgets/th-email"), 'exportOnly' => true],
]);

$table->setRowRender(function ($row) {
    $id = (int) $row["model"]["id"];

    $row["data"]["id"]["value"]                     = '<a href="?id=' . $id . '">#' . $id . '</a>';
    $row["data"]["id"]["attributes"]["data-value"]  = $id;

    $row["data"]["name"]["value"] = htmlspecialchars($row["model"]["name"] ?? '');

    // The raw value is what the filter compares; the cell shows the badge.
    $row["data"]["status"]["value"]                    = \WISECP\AdminComponents\Statuses::getInstance()->service($row["model"]["status"]);
    $row["data"]["status"]["attributes"]["data-value"] = $row["model"]["status"];

    // Not drawn on screen, present in the download.
    $row["data"]["email"]["value"] = $row["model"]["email"] ?? '';

    $row["attributes"]["data-filter-status"] = $row["model"]["status"];

    return $row;
});
templates/admin/widgets/list.php
// Nothing appears until the page template prints it. The preset already ran, inside
// the constructor, so this call only renders what the controller and the preset built.
if (isset($table)) {
    $table->setOptions(['containerExtraClass' => 'mt-3']);
    echo $table->build();
}

A tab set and a dialog, both fed by their options array:

tab set with a conditional panel, and a dialog that submits
// footer.php prints whatever is in $modals, so the template declares it once up here
// and every dialog on the page appends to it.
$modals = '';

$tab = new \WISECP\Components\Tab('widget-detail');

$tab->add('overview', [
    'title'   => Language::g('widgets-overview'),
    'icon'    => 'bi bi-grid-1x2',
    'content' => $overviewHtml,
]);

// The condition lives HERE, once. A reader of the panel then knows the tab is
// missing because of this rule and not because something failed.
if ((int) Config::get('options/reselling/status') === 1)
    $tab->add('reselling', [
        'title'   => Language::g('widgets-reselling'),
        'badge'   => $pendingCount,
        'content' => $resellingHtml,
    ]);

echo $tab->render();

$modal = new \WISECP\Components\Modal([
    'id'                    => 'widgetDeleteModal',
    'title'                 => Language::g('needs/delete'),
    'body'                  => '<p>' . Language::gc('admin/widgets/delete-confirm') . '</p>'
                             . '<input type="hidden" name="operation" value="delete_widget">',
    'footer'                => '<button type="submit" class="btn btn-danger">' . Language::g('needs/delete') . '</button>',
    'headerClasses'         => ['bg-danger'],
    'modalDialogExtraClass' => 'modal-lg',
    'form'                  => ['action' => $links["controller"], 'method' => 'POST'],
]);

$modals .= $modal->render();

Pitfalls

Tab and Accordion take a key and an options array, never a label and a body

Writing add('overview', 'Overview', $html) passes a string where the options array is expected, plus a third argument that does not exist. The panel appears with its key as the title and no content at all, without an error. The body is $options['content'].

The dialog's setters return nothing, so they cannot be chained

Every setter is typed void. Chaining one onto another is a fatal call on null. Pass everything in the constructor array; use the setters only to change a value you already set.

Let the row count decide where paging happens

The component switches to server-side paging by itself once the total passes its threshold. Forcing that decision at the call site either puts fifty thousand rows into one document or pays an extra round trip for twenty.

A nested tab set should stay out of the address

Two sets both writing to the address fight over it, and the operator returns to the wrong pair. The inner one calls noUrl().

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.