Dog Table / Documentation
Back to gallery Open a demo
Dog Table Docs v1.5.1-beta

Vanilla tables with the practical stuff already built in.

Dog Table gives you search, sorting, pagination, remote data loading, grouping, selection, CSV export, inline editing, localization, persistence, and live refresh without bringing in a framework, while keeping the internals split into focused modules. v1.5.1-beta adds performance optimizations: request debouncing & deduplication, memoized data pipeline, DOM diffing, rAF-batched renders, precomputed sort keys, virtual scrolling support, and CSS content-visibility for grouped rows.

1 constructor Create a table with new DogTable(container, options) and call init().
Remote-ready fetch() requests are abortable, deduplicated, and react to page, sort, and search state. Optional fetchDebounce prevents request spam.
Performance optimized Memoized data pipeline, DOM diffing, rAF-batched renders, precomputed sort keys, and virtual scrolling support for large datasets.

Installation

Use the package from npm, or load the source files directly in a browser module setup.

npm

npm install dog-table

Package entry points

  • dog-table - exports dist/data-table.js by default, with dog-table/min for the minified bundle
  • dog-table/css - exports dist/data-table.css, with dog-table/css/min for minified CSS
  • dog-table/locale/* - maps to dist/locale/*.js
  • dog-table/plugin/* - maps to dist/plugin/*.js
  • dog-table/utils - exports dist/utils/index.js
  • src/core/ contains internal support modules and is not exported as a package subpath

Quick Start

The basic flow is: load CSS, create a DogTable, pass data and columns, then call init().

Browser modules

<link rel="stylesheet" href="./src/data-table.css" />
<div id="dogs"></div>
<script type="module">
  import { DogTable } from "./src/data-table.js";

  const table = new DogTable("#dogs", {
    data: [
      { id: 1, name: "Mochi", age: 3, status: "ready" },
      { id: 2, name: "Pepper", age: 5, status: "pending" },
    ],
    columns: [
      { key: "name", label: "Name" },
      { key: "age", label: "Age" },
      { key: "status", label: "Status" },
    ],
  });

  table.init();
</script>

Bundlers

import { DogTable } from "dog-table";
import "dog-table/css";

const table = new DogTable("#dogs", {
  data,
  columns: [
    { key: "name", label: "Name" },
    { key: "breed", label: "Breed" },
    { key: "age", label: "Age" },
  ],
});

table.init();

Examples

Ten focused examples covering all major DogTable features.

1. Basic Local Table

const table = new DogTable("#app", {
  data: [
    { id: 1, name: "Mochi", breed: "Shiba Inu", age: 3 },
    { id: 2, name: "Pepper", breed: "Border Collie", age: 5 },
  ],
  columns: [
    { key: "name", label: "Name", sortable: true },
    { key: "breed", label: "Breed", sortable: true },
    { key: "age", label: "Age", type: "number" },
  ],
  pageSize: 5,
  searchable: true,
  pagination: true,
});

table.init();

2. Remote Data with API

const table = new DogTable("#app", {
  remote: {
    url: "https://api.example.com/dogs",
    dataKey: "results",
    totalKey: "count",
    headers: { Authorization: "Bearer TOKEN" },
    queryParams: {
      page: "page",
      pageSize: "limit",
      sort: "sort_by",
      order: "order",
      search: "q",
    },
  },
  columns: [
    { key: "name", label: "Name", sortable: true },
    { key: "breed", label: "Breed", sortable: true },
    { key: "age", label: "Age", type: "number" },
  ],
  pageSize: 10,
});

table.init();

3. Inline Editing

const table = new DogTable("#app", {
  data: [
    { id: 1, name: "Mochi", age: 3, status: "Ready" },
  ],
  columns: [
    { key: "name", label: "Name", editable: true },
    { key: "age", label: "Age", type: "number", editable: true },
    { key: "status", label: "Status", editable: true },
  ],
  remote: {
    url: "https://api.example.com/dogs",
    update: {
      url: "https://api.example.com/dogs/{id}",
      method: "PUT",
      headers: { Authorization: "Bearer TOKEN" },
      buildBody: (ctx) => JSON.stringify({ [ctx.field]: ctx.value }),
      mapResponse: (payload) => payload.data,
    },
  },
  hooks: {
    onUpdateSuccess: ({ rowId, field, value, row }) => {
      console.log(`Row ${rowId}: ${field} = ${value}`, row);
    },
  },
});

table.init();

4. Create New Records

const table = new DogTable("#app", {
  data: [],
  columns: [
    { key: "name", label: "Name", required: true },
    { key: "breed", label: "Breed", required: true },
    { key: "age", label: "Age", type: "number", required: true },
    {
      key: "status",
      label: "Status",
      inputType: "select",
      options: ["Ready", "Pending", "Adopted"],
      defaultValue: "Ready",
    },
  ],
  create: {
    triggerLabel: "Add Dog",
    title: "Register New Dog",
    submitLabel: "Save Dog",
    initialValues: { status: "Ready" },
    remote: {
      url: "https://api.example.com/dogs",
      method: "POST",
      headers: { Authorization: "Bearer TOKEN" },
      buildBody: (ctx) => JSON.stringify(ctx.data),
      mapResponse: (payload) => payload.data,
    },
    refetchAfterSubmit: true,
  },
});

table.init();

5. Row Selection & CSV Export

const table = new DogTable("#app", {
  data: [
    { id: 1, name: "Mochi", breed: "Shiba Inu", age: 3 },
    { id: 2, name: "Pepper", breed: "Border Collie", age: 5 },
  ],
  columns: [
    { key: "name", label: "Name" },
    { key: "breed", label: "Breed" },
    { key: "age", label: "Age", type: "number" },
  ],
  selectable: true,
  rowKey: "id",
  hooks: {
    onSelectionChange: (selectedData) => {
      console.log("Selected:", selectedData);
    },
  },
});

table.init();

// API usage
table.getSelectedData();
table.exportCSV("dogs-export.csv");

6. Data Formatting

const table = new DogTable("#app", {
  data: [
    { id: 1, name: "Mochi", price: 1500.50, birthDate: "2021-03-15" },
  ],
  columns: [
    { key: "name", label: "Name" },
    {
      key: "price",
      label: "Price",
      type: "currency",
      currency: "USD",
      formatOptions: { minimumFractionDigits: 2 },
    },
    {
      key: "birthDate",
      label: "Birth Date",
      type: "date",
      formatOptions: { year: "numeric", month: "long", day: "numeric" },
    },
  ],
});

table.init();

7. Grouped Rows

const table = new DogTable("#app", {
  data: [
    { id: 1, name: "Mochi", category: "Small", age: 3 },
    { id: 2, name: "Pepper", category: "Medium", age: 5 },
    { id: 3, name: "Buddy", category: "Large", age: 2 },
  ],
  columns: [
    { key: "name", label: "Name" },
    { key: "category", label: "Category" },
    { key: "age", label: "Age", type: "number" },
  ],
  groupBy: "category",
  groupLabel: (value, rows) => `${value} Dogs (${rows.length})`,
  initialSort: { key: "category", direction: "asc" },
});

table.init();

8. Expandable Row Details

const table = new DogTable("#app", {
  data: [
    {
      id: 1,
      name: "Mochi",
      breed: "Shiba Inu",
      description: "A loyal Shiba Inu.",
    },
  ],
  columns: [
    { key: "name", label: "Name" },
    { key: "breed", label: "Breed" },
  ],
  rowDetail: {
    render: (row) => `
      <div class="dog-details">
        <p><strong>Description:</strong> ${row.description}</p>
      </div>
    `,
    toggleLabel: (row, isExpanded) =>
      isExpanded ? "Hide Details" : "Show Details",
  },
});

table.init();

// Programmatic control
table.expandRowDetail(1);
table.collapseRowDetail(1);

9. State Persistence & Live Sync

const table = new DogTable("#app", {
  remote: {
    url: "https://api.example.com/dogs",
    dataKey: "results",
    totalKey: "count",
  },
  columns: [
    { key: "name", label: "Name", sortable: true },
    { key: "breed", label: "Breed", sortable: true },
    { key: "age", label: "Age", type: "number" },
  ],
  persistence: "local",
  persistenceKey: "my-dog-table",
  autoRefresh: 5000,
  hooks: {
    onBeforeRefresh: () => console.log("Refreshing..."),
  },
});

table.init();

// Live sync controls
table.live.start();
table.live.stop();
table.live.toggle();

10. Custom Rendering, Sorting & Hooks

const table = new DogTable("#app", {
  data: [
    { id: 1, name: "Mochi", age: 3, score: 85 },
    { id: 2, name: "Pepper", age: 5, score: 92 },
  ],
  columns: [
    {
      key: "name",
      label: "Name",
      sortable: true,
      render: (value, row) => `<a href="/dogs/${row.id}">${value}</a>`,
    },
    {
      key: "age",
      label: "Age",
      sortable: true,
      sortValue: (value) => value * 7,
      render: (value) => `${value} years`,
    },
    {
      key: "score",
      label: "Score",
      sortable: true,
      render: (value) => `<div class="progress"><span>${value}%</span></div>`,
    },
  ],
  initialSort: { key: "score", direction: "desc" },
  searchDebounce: 300,
  theme: "default",
  paginationGuard: { maxPage: 50, minPageSize: 5, maxPageSize: 100 },
  hooks: {
    onInit: (state) => console.log("Initialized", state),
    onUpdate: (processed) => console.log("Updated", processed),
    onPageChange: (page) => console.log("Page:", page),
    onSortChange: ({ sortKey, sortDirection }) =>
      console.log(`Sort: ${sortKey} (${sortDirection})`),
    onSearchChange: (query) => console.log("Search:", query),
    onFetchStart: () => console.log("Fetching..."),
    onFetchSuccess: (payload) => console.log("Loaded:", payload),
    onFetchError: (error) => console.error("Fetch failed:", error),
    onLoadingChange: (isLoading) => console.log("Loading:", isLoading),
    onDataUpdated: (rawData) => console.log("Data:", rawData.length, "rows"),
    onDestroy: () => console.log("Destroyed"),
  },
});

table.init();

// Programmatic API
table.setPage(2);
table.setSearch("Mochi");
table.setSort("age", "asc");
table.setPageSize(10);
table.clearSearch();
table.clearSort();
table.reset();
table.setData(newData);
table.setColumns(newColumns);
table.setTheme("bootstrap");
table.destroy();

Core Concepts

Local data

Client-side search, sort, and pagination happen automatically when you pass data and columns. Set pagination: false if you want to render all loaded rows without page controls.

Remote data

Add a remote config to fetch server data. Page, sort, and search state are converted into query params, while repeated query contexts keep their own pagination state.

Columns

Columns define labels, sort rules, formatting, search behavior, custom rendering, visibility, and editability.

Hooks

Lifecycle hooks expose init, fetch, update, sort, search, paging, row detail, selection, and loading changes.

Grouping and detail

Use groupBy, groupLabel, rowKey, and rowDetail.render() to build grouped tables with expandable rows.

Progressive enhancement

Persistence, CSV export, inline editing, locale switching, and live polling are already wired into the table instance.

Architecture

keeps the public DogTable API stable while separating state management, data processing, rendering, remote loading, event binding, and plugin bootstrapping. v1.5.1-beta adds performance optimizations across all layers.

DogTable

The main controller coordinates update flow, public methods, hooks, and module lifecycles. Uses requestAnimationFrame batching to prevent multiple renders per frame, and deduplicates in-flight fetch requests.

TableState

Owns page, page size, search query, sort state, error/loading state, and pagination guard constraints.

DataEngine

Handles filtering, sorting, pagination slicing, grouping, and cache reuse without touching the DOM. Precomputes sort keys before comparison (O(n) instead of O(n log n) property access), and memoizes buildDisplayRows() to avoid rebuilds when data hasn't changed.

RemoteAdapter

Wraps remote fetching, aliases identical datasets in memory, and keeps pagination metadata isolated per query context. Fetch requests are deduplicated so concurrent calls share the same promise.

TableRenderer

Uses DOM diffing to track row nodes via Map and only rebuilds changed rows instead of full innerHTML replacement. Preserves scroll position and reduces DOM churn.

Query cache guide

Use the dedicated guide for the full separation rules between shared remote data and per-query pagination state.

Renderers

TableRenderer, PaginationRenderer, and MetaRenderer keep UI output separate from business logic.

EventBinder

Centralizes DOM bind/unbind behavior for header sorting, pagination, row actions, search, and create/live controls.

VirtualScroller

Optional virtual scrolling module that renders only visible rows plus a buffer. Enable for large datasets with 100+ rows per page.

await table.update();
// -> fetch remote rows when needed (deduplicated + debounced)
// -> process state through DataEngine (memoized + precomputed sort keys)
// -> render header/body/meta/pagination (rAF-batched + DOM diffing)

          

Remote Example

const table = new DogTable("#dogs", {
  columns: [
    { key: "name", label: "Name" },
    { key: "age", label: "Age" },
  ],
  remote: {
    url: "/api/dogs",
    queryParams: {
      page: "page",
      pageSize: "limit",
      sort: "sort",
      order: "order",
      search: "q",
    },
    mapResponse(payload) {
      return {
        rows: payload.items,
        totalItems: payload.total,
      };
    },
  },
});

table.init();
const allRowsTable = new DogTable("#all-dogs", {
  data,
  pagination: false,
  columns: [
    { key: "name", label: "Name" },
    { key: "breed", label: "Breed" },
    { key: "age", label: "Age" },
  ],
});

allRowsTable.init();

Table Options

Option Type Default Purpose
data Array<object> [] Raw local rows.
columns Array<object> [] Column definitions used for rendering and behavior.
pagination boolean true Enables page slicing and pagination controls. Set false to render all loaded rows.
pageSize number 5 Rows shown per page.
paginationGuard boolean | { maxPage, minPageSize, maxPageSize } false Optional guardrails for page/page-size bounds. Use true for defaults (25 / 1 / 100). Ignored when pagination is false.
searchable boolean true Shows or hides the built-in search control.
searchDebounce number 250 Debounce delay for the search input.
fetchDebounce number 0 Debounce delay for remote fetch requests. Set to a positive value to prevent request spam during rapid state changes. Use table.fetchNow() to bypass.
language object built-in labels Overrides visible UI copy such as search, paging, empty, and loading text.
initialSort { key, direction } | null null Initial sort state.
theme string | object "default" Preset name or custom theme map.
classNames object {} Additional class name overrides merged with the active theme.
remote object | null null Enables server-driven data loading through fetch().
groupBy string | function | null null Groups rows before rendering.
groupLabel function | null null Builds custom labels for group header rows.
rowKey string | function | null null Stable id source for row detail and selection features.
rowDetail object | null null Enables expandable detail rows.
persistence "url" | "local" | "session" | null null Persists search, paging, sort, and page size state.
persistenceKey string | null null Custom storage or URL key prefix.
selectable boolean false Adds row selection checkboxes.
create object | null null Enables built-in creation workflows with modal forms and validation.
hooks object {} Lifecycle callbacks.

Column Definition

Field Type Purpose
key string Primary property used for value lookup, sort state, and default filtering.
label string Visible header label.
accessor string Alternate row property used for rendering and editing.
sortable boolean Disable built-in sorting for a column.
searchable boolean Exclude a column from local search matching.
visible boolean Hide a column while keeping it in state.
editable boolean Makes body cells editable through the editor plugin.
render(value, row) function Custom cell rendering. Can return a Node, string, or primitive.
sortValue(value, row) function Transforms values before local sorting.
filter({ value, row, query }) function Custom local-search matching for the column.
format or type string Used by the formatter plugin for built-in display formatting.
formatOptions object Options passed to the formatter logic.
locale string Locale override for formatted output.

API Methods

Method What it does
init() Builds the table UI, binds events, initializes live mode, and renders the first state.
update({ skipFetch }) Re-renders the table. Scheduled via requestAnimationFrame to batch multiple calls. Use updateSync() for immediate render.
updateSync({ skipFetch }) Synchronous version of update() that renders immediately without rAF batching. Use when you need instant visual feedback.
fetchData() Triggers a remote fetch. Respects fetchDebounce if configured. In-flight requests are deduplicated.
fetchNow() Bypasses fetch debounce and executes the fetch immediately. Cancels any pending debounced fetch.
loadState() / saveState() Manually interact with the persistence plugin.
setPage(pageNumber) Changes the active page and updates the table.
setSearch(query) Updates the search term. New queries start at page 1, while previously seen remote queries restore their cached pagination state.
clearSearch() Clears the search input and filter state.
setPageSize(size) Updates page size and resets to page 1.
setSort(key, direction) Applies a specific sort state and preserves pagination independently for each remote query context.
clearSort() Removes active sorting.
setData(data) Replaces local row data and clears error/detail state.
setColumns(columns) Replaces the current columns and rerenders.
setTheme(theme, classNames) Swaps theme mapping and rebuilds the DOM shell.
setLanguage(language) Merges language overrides and rebuilds visible UI text.
toggleRowDetail(rowId) Expands or collapses a detail row.
expandRowDetail(rowId) / collapseRowDetail(rowId) Explicit detail row control.
toggleRowSelection(rowId, isSelected) Selects or deselects a row when selection is enabled.
selectAll(isSelected) Selects or clears all rows in the current processed set.
toggleColumnVisibility(columnKey, isVisible) Shows or hides a column.
getSelectedData() Returns selected row objects.
getState() Returns a copy of the current table state.
setLoading(isLoading) Manually updates loading state and triggers onLoadingChange.
exportCSV(filename) Exports visible columns from the current raw data to CSV.
openCreateModal() Opens the record creation modal if the feature is enabled.
reset() Clears search, sort, errors, and expanded detail rows.
destroy() Removes event listeners, aborts fetches, stops live mode, and clears the container.

Hooks

Hook When it fires
onInit(state) After the first init() render completes.
onFetchStart(state) Before a remote request begins.
onFetchSuccess(payload) After remote data is mapped successfully.
onFetchError(error) When a remote request fails.
onDataUpdated(rows) After fresh remote rows are stored.
onLoadingChange(isLoading) Whenever loading state changes.
onPageChange(page) When the visible page changes.
onSortChange({ sortKey, sortDirection }) When sort state changes.
onSearchChange(query) When the normalized search query changes.
onRowToggle({ rowId, expanded }) When a detail row is opened or closed.
onSelectionChange(selectedRows) When row selection changes.
onUpdate(snapshot) After a successful render/update cycle.
onCreateSuccess(row) After a new record is successfully created and added.
onCreateError(error) When a create request fails.

Plugins and Modules

PersistencePlugin

Stores search, page, sort, and page size in url, localStorage, or sessionStorage.

SelectionPlugin

Tracks selected row ids and exposes getSelectedData(), toggleRow(), and selectAll().

ExportPlugin

Exports visible columns to CSV with toCSV(filename).

FormatterPlugin

Formats output values before render using column formatting hints.

EditorPlugin

Starts inline editing for cells marked editable in column definitions.

LivePlugin

Handles auto-refresh timers, visibility pausing, and adaptive backoff for remote tables.

CreatePlugin

Manages the built-in record creation workflow, field validation, and modal state.

DataFetcher

Builds request URLs from table state, supports query param remapping, and aborts stale requests.

PluginManager

Initializes persistence, selection, export, formatter, editor, live, and create plugins for the controller.

ThemeManager

Resolves built-in theme slots and class-name overrides used throughout the rendered UI.

utils

Exports escapeHtml(value) and debounce(callback, wait).

VirtualScroller

Optional virtual scrolling for large datasets. Enable with table.virtualScroller.enable({ rowHeight, bufferSize }) and disable with table.virtualScroller.disable().

Good Defaults to Know

pagination defaults to true pageSize defaults to 5 paginationGuard is false by default paginationGuard is ignored when pagination: false paginationGuard: true uses maxPage 25 and maxPageSize 100 Search is enabled by default Search input debounce is 250ms Fetch debounce is 0 (disabled) by default Sort direction starts at asc Remote responses expect data and total unless mapped Grouping labels fall back to Ungrouped (count)

Demo Map

Use the demos as working reference implementations for each feature area.