Interface Components
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
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.
'ticketList' for a widget reusing the main list's columns.
true defers the first query until the tab is opened. Not for a detail tab; for expensive sources (a filesystem scan, a remote catalogue).
false drops the control; [10, 25, 50, 100, -1] sets the page sizes, -1 meaning all.
'list' switches to the card layout, the one the client area uses. Anything else gives a grid.
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.
['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.
'partial' (default) pages on the server; 'allData' hands the whole set over in one answer.
['type' => 'hosting'].
The row your setRowRender() callback receives, and gives back, has three parts:
$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
// 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;
'bi bi-gear'. On a horizontal tab the spacing class comes from iconClass and defaults to 'me-2'.
'0'.
true opens this section on load. Sections share a parent, so opening one closes the rest.
'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.
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;
'SampleModal', so two dialogs without an id collide.
<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.
danger gives a red title, anything else the primary title. The shell is fixed, so this cannot paint a coloured bar.
'modal-lg', plus the two layout switches that both default to true.
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.
$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);
/** @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;
});
// 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:
// 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
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'].
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.
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.
Two sets both writing to the address fight over it, and the operator returns to the wrong pair. The inner one calls noUrl().
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.