SmartTable
A declarative, attribute-driven data table Web Component. Provide an api-url and a response-map and get full sorting, search, pagination, infinite scroll, skeleton loading, badge filters, and row deletion — zero JavaScript required.
<script src="smart-table.js"></script>
Live Playground
Adjust the controls on the left to see the real <smart-table> component update live.
Controls
<smart-table api-url="https://jsonplaceholder.typicode.com/posts" response-map='{"dataPath":"","totalPath":""}' page-size="10" hide-id ></smart-table>
Auto Columns
When no columns attribute is set, SmartTable reads the keys from the first row and builds headers automatically. Add hide-id to suppress the id field.
<!-- Minimal — columns auto-detected from API response --> <smart-table api-url="/api/posts/" response-map='{"dataPath":"","totalPath":""}' page-size="10" hide-id ></smart-table>
Custom Columns
Pass a JSON array to columns to control which fields appear, their labels, sort behaviour, and render type.
<smart-table api-url="/api/users/" response-map='{"dataPath":"","totalPath":""}' columns='[ {"field":"id", "hidden":true}, {"field":"name", "label":"Full Name"}, {"field":"username", "label":"Username"}, {"field":"email", "label":"Email"}, {"field":"phone", "label":"Phone"} ]' page-size="5" ></smart-table>
Badge & Date Types
Set type on a column to control rendering. "badge" maps string values to semantic colours and automatically adds clickable chip filters below the toolbar. "dateFormatted" formats values with toLocaleDateString(). Boolean values always render as Yes/No badges regardless of type.
<smart-table api-url="/api/users/" response-map='{"dataPath":"results","totalPath":"count"}' columns='[ {"field":"id", "hidden":true}, {"field":"name", "label":"Name"}, {"field":"status", "label":"Status", "type":"badge"}, {"field":"role", "label":"Role", "type":"badge"}, {"field":"joined", "label":"Joined", "type":"dateFormatted"}, {"field":"orders", "label":"Orders", "type":"integer"}, {"field":"active", "label":"Active"} ]' ></smart-table>
Automatic badge colour mapping — known keywords get fixed colours; everything else rotates a pastel palette:
Inline Objects
Use "type":"inline" on a column whose value is a nested JSON object. SmartTable renders it as a compact horizontal key/value grid inside the cell — one header row, one value row. Perfect for address fields, metadata, or any structured sub-object.
<!-- "address" and "company" are nested objects --> <smart-table api-url="/api/users/" response-map='{"dataPath":"","totalPath":""}' columns='[ {"field":"id", "hidden":true}, {"field":"name", "label":"Name"}, {"field":"email", "label":"Email"}, {"field":"address", "label":"Address", "type":"inline"}, {"field":"company", "label":"Company", "type":"inline"} ]' page-size="10" ></smart-table> // API shape — each key becomes a column header: // "address": { "street": "Kulas Light", "city": "Gwenborough" }
Delete Rows
Add delete-api-url to append a trash-icon column. Clicking fires a smart-confirm window event — <smart-modal> intercepts it if present, otherwise window.confirm() runs as a fallback. On confirm, DELETE /{id} fires, the row fades out, and a toast confirms the result.
<!-- Optional but recommended: branded modal + toast --> <smart-modal></smart-modal> <smart-toast position="bottom-right"></smart-toast> <smart-table api-url="/api/users/" response-map='{"dataPath":"results","totalPath":"count"}' columns='[ {"field":"id", "hidden":true}, {"field":"name", "label":"Name"}, {"field":"email", "label":"Email"} ]' delete-api-url="/api/users" page-size="10" ></smart-table>
Filter Bar
<smart-filter-bar> sits above the table and drives it via smart-table-filter window events — no direct DOM coupling between the two elements. Point its target attribute at the table's id. Add auto-apply to re-filter on every keystroke (debounced 300ms), or use the Apply / Reset buttons for manual control.
<!-- smart-filter-bar.js must be loaded --> <smart-filter-bar target="usersTable" auto-apply > <smart-input name="email" label="Email" type="text" placeholder="Filter by email…" ></smart-input> <smart-button action="apply">Apply</smart-button> <smart-button action="reset">Reset</smart-button> </smart-filter-bar> <smart-table id="usersTable" api-url="/api/users/" response-map='{"dataPath":"","totalPath":""}' columns='[...]' page-size="20" ></smart-table>
Filter behaviour depends on how much data the table has loaded:
| Table mode | What happens on filter dispatch |
|---|---|
| client / paginated | Filters the already-loaded rows in memory. No extra network request fires. |
| server / infinite | Filter values are appended as GET query params (or POST body) to every subsequent API call. Your backend receives them and filters at the database level. |
POST & CSRF
Use fetch-config to send requests as POST instead of GET, choose the body format, and attach custom headers. Set any header value to "auto" and SmartTable reads the CSRF token from a <meta name="csrf-token"> tag or the csrftoken cookie — no manual wiring needed.
<!-- Django POST with CSRF auto-read from cookie --> <smart-table api-url="/api/users/" response-map='{"dataPath":"results","totalPath":"count"}' fetch-config='{ "method": "POST", "bodyMode": "json", "headers": { "Content-Type": "application/json", "X-CSRFToken": "auto" } }' page-size="20" ></smart-table> <!-- FormData body instead of JSON --> <smart-table fetch-config='{ "method": "POST", "bodyMode": "form", "headers": { "X-CSRFToken": "auto" } }' api-url="/api/data/" response-map='{"dataPath":"results","totalPath":"count"}' ></smart-table>
Regardless of method, the table always includes page, limit, search, sort, order, and active filter values — as query params for GET, as body for POST.
Public API
All four public methods are available on any table element. Try them on the table below — each button calls one method and fires a confirmation toast.
const table = document.getElementById('myTable'); // Re-fetch from page 1 (server) or re-filter (client) table.refresh(); // Apply external filters — merged into every request/filter pass // Empty string or null clears that field's filter table.setFilters({ userId: '3', status: 'active' }); table.refresh(); // Clear all filters set via setFilters() // Badge chip filters and the search box are unaffected table.resetFilters(); table.refresh(); // Clear the search input and re-render table.clearSearch();
| Method | Description |
|---|---|
| refresh() | Re-fetches from page 1 in server/infinite mode, or re-applies all filters in client/paginated mode. |
| setFilters(obj) | Merges { field: value } into the active external filters. Empty string or null removes the filter on that field. Active filters are included in every fetch or client-side pass. |
| resetFilters() | Clears all external filters set via setFilters(). Badge chip filters and the search box are not affected. |
| clearSearch() | Empties the search input and triggers a fresh render. Equivalent to the user clearing the box manually. |
Response Map
response-map tells SmartTable where to find the rows array and total count inside your API's JSON. Use dot-notation for nested paths. Leave a path as "" when the value sits at the root.
// Flat array — API returns the array directly <smart-table response-map='{"dataPath":"","totalPath":""}'></smart-table> // Django REST Framework — { "results": [...], "count": 200 } <smart-table api-url="/api/posts/" response-map='{"dataPath":"results","totalPath":"count"}' ></smart-table> // Deeply nested — { "payload": { "items": [...], "meta": { "total": 500 } } } <smart-table response-map='{"dataPath":"payload.items","totalPath":"payload.meta.total"}' ></smart-table> // hasMore path — for APIs that signal whether more pages exist <smart-table response-map='{"dataPath":"data","totalPath":"total","hasMorePath":"has_next"}' ></smart-table>
Attributes
| Attribute | Type | Description | Default |
|---|---|---|---|
| api-url | string | Required. Endpoint to fetch data from. Query params page, limit, search, sort, order, and any active filter values are appended automatically. |
— |
| response-map | JSON | Required. Maps dataPath, totalPath, and optionally hasMorePath using dot-notation. |
— |
| columns | JSON | Array of column config objects. If omitted, all keys from the first API row are used automatically. | auto |
| page-size | number | Rows per page, or chunk size for infinite scroll. | 20 |
| hide-id | boolean | Suppress the id field when columns are auto-detected. |
false |
| delete-api-url | string | Base URL for row deletion. Sends DELETE {url}/{row.id} after confirmation. Fires smart-confirm event — <smart-modal> intercepts if present, else window.confirm() runs. |
— |
| fetch-config | JSON | Optional fetch overrides: method ("GET" | "POST"), bodyMode ("json" | "form"), headers object. Header values of "auto" resolve to the CSRF token from <meta name="csrf-token"> or the csrftoken cookie. |
GET, no headers |
| data-st-theme | string | Force colour scheme: "light" or "dark". Auto-detected from Bootstrap data-bs-theme, body.light-mode, or OS preference when omitted. |
auto |
Column Config
Each object in the columns JSON array accepts these properties:
| Property | Type | Description | Default |
|---|---|---|---|
| field | string | Key on each row object to read the value from. Required. | — |
| label | string | Header text. Defaults to auto-formatted field name. | auto |
| hidden | boolean | Exclude from the rendered table. Keeps the field available for delete URLs or JS access. | false |
| sortable | boolean | Show sort arrows and allow header-click sorting. Set to false for long text columns. |
true |
| type | string |
Cell render mode:badge — coloured label with auto chip filter bardateFormatted — toLocaleDateString()integer — right-aligned numberimage — thumbnail (34×34px)inline — nested object as horizontal key/value grid
|
— |
columns='[ {"field":"id", "hidden":true}, {"field":"avatar", "label":"Photo", "type":"image", "sortable":false}, {"field":"name", "label":"Customer"}, {"field":"status", "label":"Status", "type":"badge"}, {"field":"joined", "label":"Joined", "type":"dateFormatted"}, {"field":"orders", "label":"Orders", "type":"integer"}, {"field":"active", "label":"Active"}, {"field":"address", "label":"Address", "type":"inline", "sortable":false}, {"field":"notes", "label":"Notes", "sortable":false} ]'
Filter Bar Reference
<smart-filter-bar> lives in smart-filter-bar.js — a standalone file with no dependency on smart-table. Its child <smart-input> and <smart-button> are lightweight scoped versions, separate from the main form components.
smart-filter-bar
| Attribute | Type | Description | Default |
|---|---|---|---|
| target | string | Required. The id of the <smart-table> to drive. The filter bar dispatches smart-table-filter on window with this id. |
— |
| auto-apply | boolean | Re-filter on every input change, debounced 300ms. Omit to require an explicit Apply button click. | false |
smart-input (inside filter bar)
| Attribute | Type | Description |
|---|---|---|
| name | string | Filter key — must match the column field name you want to filter. |
| label | string | Label shown above the input. Defaults to name. |
| type | string | text (default) | date | select | number | email. |
| options | JSON | For select — array of {"value","label"}. First entry with empty value acts as "All". |
| placeholder | string | Input placeholder text. |
| value | string | Initial value. |
smart-button (inside filter bar)
| Attribute | Description |
|---|---|
| action="apply" | Dispatches the current filter values to the target table immediately. |
| action="reset" | Clears all inputs and dispatches an all-empty filter object, removing active filters. |
Public API
const bar = document.querySelector('smart-filter-bar'); // Current filter values as a plain object console.log(bar.getValues()); // → { email: "alice", status: "active", from_date: "" } // Programmatically dispatch current values bar.apply(); // Reset all inputs and clear table filters bar.reset();
Events
| Event | detail | Fires when |
|---|---|---|
| data-loaded | { data, total } | A fetch completes successfully. data is the current row array, total is the count from the API. |
| row-deleted | { id } | A row DELETE request succeeds and the row has been removed from the DOM. |
const table = document.querySelector('smart-table'); table.addEventListener('data-loaded', (e) => { console.log('Rows:', e.detail.data.length, 'Total:', e.detail.total); }); table.addEventListener('row-deleted', (e) => { console.log('Deleted id:', e.detail.id); });
Scroll Modes
SmartTable automatically selects the best rendering strategy based on the total returned by your API — no attribute needed.
| Mode | Condition | Behaviour |
|---|---|---|
| client | total ≤ page-size | All rows in one request. Search, sort, and filter-bar filters run entirely in memory — no further network calls. |
| paginated | page-size < total ≤ 1000 | Numbered page controls appear. Each sort, search, or filter-bar dispatch triggers a new API request. |
| infinite | total > 1000 | Rows append as the user scrolls via IntersectionObserver on a sentinel element. |
<!-- 50,000 rows — SmartTable auto-picks infinite scroll --> <smart-table api-url="/api/logs/" response-map='{"dataPath":"results","totalPath":"count"}' columns='[ {"field":"id", "hidden":true}, {"field":"timestamp", "label":"Time", "type":"dateFormatted"}, {"field":"level", "label":"Level", "type":"badge"}, {"field":"message", "label":"Message", "sortable":false} ]' page-size="50" ></smart-table>