Table — AJAX
Same component, remote dataset: add data-table-source and sorting, search, filters and paging become server-side — the table sends the query, the server returns one page. Link ng_table.js.
Everything documented on the Table page still applies — markup, modifiers, cell utilities, API. The difference: the tbody starts empty, and on init (and on every sort/search/filter/page change) the component fetches the page from the endpoint. Column toggle and row selection stay client-side. The demos here run against a static-array endpoint of this site — no database involved.
The contract
The component issues a GET (with X-Requested-With and Accept: application/json) carrying:
page_num / per_page | Requested page and size. |
|---|---|
sort_by / sort_dir | Active sort column key and asc/desc (absent when unsorted). |
search | Full-text query (absent when empty). |
<filterKey>=value | One param per active data-table-filter (e.g. status=Active). |
The endpoint answers with data[] (one object per row, keys = the data-col-key set; values may contain HTML) plus the paging meta:
{
"data": [
{ "id": 1, "name": "Alice Rossi", "mail": "alice.rossi1@email.com",
"orders": 13, "status": "<span class=\"ng-badge ng-badge-success\">Active</span>",
"joined": "02/02/2025" }
],
"total": 60,
"per_page": 5,
"current_page": 1,
"last_page": 12
}
Live: 60 records, server-side everything
Sort, search, filter and paging each trigger a fetch — watch the pagination recompute from total/last_page:
| ID | Name | Orders | Status | Joined |
|---|
<div class="ng-table" id="remote-table"
data-table-source="/en/api/docs/users"
data-table-sort="id:asc"
data-table-limit="5"
data-table-page-size="5,10,20"
data-table-page-size-position="bottom"
data-table-search-debounce="300">
<div class="ng-table-header">
<div class="ng-table-head-left">
<div class="ng-table-title">Users (remote)</div>
<div class="ng-table-subtitle">60 records server-side, 5 per page</div>
</div>
</div>
<div class="ng-table-filters">
<div class="ng-table-filters-left">
<div class="ng-filter">
<select class="ng-select ng-filter-select" data-table-filter="status">
<option value="">All statuses</option>
<option value="Active">Active</option>
<option value="Suspended">Suspended</option>
<option value="Blocked">Blocked</option>
</select>
</div>
</div>
<div class="ng-table-filters-right">
<span class="ng-badge ng-badge-primary-outline" data-table-count></span>
<input type="text" class="ng-input" placeholder="Search 60 users..." data-table-search>
</div>
</div>
<div class="ng-table-wrapper">
<table class="ng-table-table zebra row-hover">
<thead>
<tr>
<th class="ng-sortable ng-tcol-w-5 ng-tcol-center" data-col-key="id" data-sort-type="number">
<span class="ng-sort">ID <span class="ng-sort-indicator"></span></span>
</th>
<th class="ng-sortable ng-tcol-w-20" data-col-key="name" data-sort-type="string">
<span class="ng-sort">Name <span class="ng-sort-indicator"></span></span>
</th>
<th class="ng-col-grow" data-col-key="mail">Email</th>
<th class="ng-sortable ng-tcol-num ng-tcol-w-10" data-col-key="orders" data-sort-type="number">
<span class="ng-sort">Orders <span class="ng-sort-indicator"></span></span>
</th>
<th class="ng-tcol-w-10 ng-tcol-center" data-col-key="status">Status</th>
<th class="ng-sortable ng-tcol-w-15" data-col-key="joined" data-sort-type="date" data-sort-locale="eu">
<span class="ng-sort">Joined <span class="ng-sort-indicator"></span></span>
</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
Sub-table — expandable rows
Expandable rows work the same in remote mode: add data-table-expandable to the root. The detail is filled on the first ng:table:expand (cached — no re-fetch on re-render/sort/paging). This live remote table renders the detail from the row data; in a real app you'd fetch it per row:
| ID | Name | Status |
|---|
<div class="ng-table" id="remote-expand"
data-table-source="/en/api/docs/users"
data-table-limit="4"
data-table-expandable data-expand-single>
<div class="ng-table-wrapper">
<table class="ng-table-table row-hover">
<thead>
<tr>
<th class="ng-tcol-w-5 ng-tcol-center" data-col-key="id">ID</th>
<th class="ng-col-grow" data-col-key="name">Name</th>
<th class="ng-tcol-w-10 ng-tcol-center" data-col-key="status">Status</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
Lazy-fetch pattern:
// remote sub-table: fetch the detail per row on FIRST expand (cached)
table.addEventListener('ng:table:expand', async (e) => {
const { id, container } = e.detail;
container.textContent = 'Loading…';
const res = await fetch('/api/users/' + id + '/detail');
container.innerHTML = await res.text(); // your detail markup
});
Full attribute list on the local Table page.
Loading state — hooks
The root exposes lifecycle hooks (onBeforeFetch, onBeforeRender, onAfterRender, onFetchError). Pair onBeforeFetch with the is-loading modifier to dim the table during slow requests. Note: in remote mode onAfterRender fires when the request starts (the fetch is not awaited) — to detect "data arrived", watch the tbody:
| ID | Name | Status |
|---|
<div class="ng-table" id="slow-table"
data-table-source="/en/api/docs/users?delay=800"
data-table-limit="3">
<div class="ng-table-wrapper">
<table class="ng-table-table">
<thead>
<tr>
<th class="ng-tcol-w-5 ng-tcol-center" data-col-key="id">ID</th>
<th class="ng-col-grow" data-col-key="name">Name</th>
<th class="ng-tcol-w-10 ng-tcol-center" data-col-key="status">Status</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<script>
// onBeforeFetch fires before every request (this endpoint adds an
// artificial 800ms delay). The rows landing in the tbody mark the end.
const slow = document.getElementById('slow-table');
slow.onBeforeFetch = () => slow.classList.add('is-loading');
new MutationObserver(() => slow.classList.remove('is-loading'))
.observe(slow.querySelector('tbody'), { childList: true });
</script>
Error state
A failed fetch renders the error box row; the text is localizable with data-table-text-error (this demo points to a non-existing endpoint on purpose):
| ID | Name |
|---|
<div class="ng-table"
data-table-source="/en/api/docs/nowhere"
data-table-limit="5"
data-table-text-error="Could not load users — try again later.">
<div class="ng-table-wrapper">
<table class="ng-table-table">
<thead>
<tr>
<th class="ng-tcol-w-5 ng-tcol-center" data-col-key="id">ID</th>
<th class="ng-col-grow" data-col-key="name">Name</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
The endpoint (no DB needed)
Any backend works as long as it honours the contract. The demo endpoint of this page filters, sorts and slices a static PHP array:
// application/controller/docs_api_users.controller.php (excerpt)
// Static PHP array — filter, sort and slice server-side, no DB:
$total = count($users);
$lastPage = max(1, (int) ceil($total / $perPage));
$slice = array_slice($users, ($page - 1) * $perPage, $perPage);
echo json_encode([
'data' => $slice,
'total' => $total,
'per_page' => $perPage,
'current_page' => $page,
'last_page' => $lastPage,
]);
Options (remote-specific)
data-table-source | Endpoint URL — its presence switches the instance to remote mode. |
|---|---|
data-table-text-error | Error box text (default "Data Error Load"). |
root.onBeforeFetch / onFetchError | Request lifecycle hooks (functions assigned on the root element). |
root.onBeforeRender / onAfterRender | Render lifecycle hooks (both modes). |
table.reload() | In remote mode re-fetches the current page. |
Everything else (data-table-limit, -page-size, -sort, -search-debounce, -persist…) works as in local mode.
Anatomy (remote)
data-table-source page_num per_page sort_by sort_dir search data[] total per_page current_page last_page is-loading ng-table-error-box