stable 2.3

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_pageRequested page and size.
sort_by / sort_dirActive sort column key and asc/desc (absent when unsorted).
searchFull-text query (absent when empty).
<filterKey>=valueOne 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:

response.json
{
    "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:

Users (remote)
60 records server-side, 5 per page
ID Name Email Orders Status Joined
table-ajax.html
<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
table-ajax.html
<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:

app.js
// 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
table-ajax.html
<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
table-ajax.html
<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:

docs_api_users.controller.php
// 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-sourceEndpoint URL — its presence switches the instance to remote mode.
data-table-text-errorError box text (default "Data Error Load").
root.onBeforeFetch / onFetchErrorRequest lifecycle hooks (functions assigned on the root element).
root.onBeforeRender / onAfterRenderRender 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