<smart-table>

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.

Auto Columns Sorting Pagination Infinite Scroll Filter Bar Delete Rows POST + CSRF
<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:

active / yes / success → green inactive / error / banned → red pending / draft / review → amber info / new / scheduled → blue anything else → pastel rotation

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.

Apply Reset
<!-- 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 modeWhat 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();
MethodDescription
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

AttributeTypeDescriptionDefault
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:

PropertyTypeDescriptionDefault
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 bar
dateFormattedtoLocaleDateString()
integer — right-aligned number
image — 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

AttributeTypeDescriptionDefault
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)

AttributeTypeDescription
namestringFilter key — must match the column field name you want to filter.
labelstringLabel shown above the input. Defaults to name.
typestringtext (default) | date | select | number | email.
optionsJSONFor select — array of {"value","label"}. First entry with empty value acts as "All".
placeholderstringInput placeholder text.
valuestringInitial value.

smart-button (inside filter bar)

AttributeDescription
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

EventdetailFires 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.

ModeConditionBehaviour
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>