High-Performance Virtual Scrolling Data Table - dgtable

File Size: 403 KB
Views Total: 3093
Last Update:
Publish Date:
Official Website: Go to website
License: MIT
   
High-Performance Virtual Scrolling Data Table - dgtable

DGTable.js is a Vanilla JavaScript library (formerly a jQuery plugin) for creating high-performance virtual data tables that renders only the rows currently visible in the viewport. 

Features:

  • Renders large datasets with virtual scrolling.
  • Sorts one column or multiple columns.
  • Resizes, reorders, hides, and shows columns.
  • Pins columns to the table start or end.
  • Supports absolute, percentage, and relative column widths.
  • Shows hover previews for truncated cell text.
  • Filters rows with built-in and custom logic.
  • Supports right-to-left table layouts.
  • Handles variable row heights.
  • Loads data asynchronously through Web Workers.

Use Cases:

  • Admin dashboards that display large server log streams or audit trails get smooth scrolling from the virtual row engine.
  • Financial platforms that sort transaction records across multiple fields simultaneously use the multi-column sort configuration.
  • CRM tools that let users reorganize their workspace use column reordering and visibility controls.
  • Reporting UI with many columns uses sticky column pinning to keep key identifiers on screen during horizontal scroll.

Table Of Contents:

How to use it:

1. Install DGTable with NPM and import it into your JS project.

$ npm install @danielgindi/dgtable
import DGTable from '@danielgindi/dgtable';

2. Or load the UMD build directly in the browser:

<script src="/path/to/dist/dgtable.jslib.umd.min.js"></script>

3. Create a new DGTable instance and define the column data as follows.

// Define the table with a column configuration and a fixed height
const table = new DGTable({
  columns: [
    { name: 'userId',     label: 'User ID',  width: 90 },
    { name: 'username',   label: 'Username', width: '25%' },
    { name: 'role',       label: 'Role',     width: '20%' },
    { name: 'email',      label: 'Email' }, // auto width
  ],
  height: 450,
  virtualTable: true,        // Activate virtual scrolling for large datasets
  maxColumnsSortCount: 2,    // Allow sorting by up to 2 columns at once
});

// Attach the table's wrapper element to the page
document.getElementById('table-wrapper').appendChild(table.el);

// Load the initial dataset
table.setRows([
  { userId: 101, username: 'alice_w',  role: 'Admin',  email: '[email protected]' },
  { userId: 102, username: 'bob_m',    role: 'Editor', email: '[email protected]' },
  { userId: 103, username: 'carol_t',  role: 'Viewer', email: '[email protected]' },
]);

// Trigger the initial render
table.render();

4. Custom cell formatting:

const table = new DGTable({
  columns: [
    { name: 'status',  label: 'Status',  width: 110 },
    { name: 'product', label: 'Product' },
    { name: 'qty',     label: 'Qty',     width: 80 },
  ],
  height: 400,
  virtualTable: true,

  // Return an HTML string for each cell; receives value, column name, and the full row object
  cellFormatter: function(value, columnName, rowData) {
    if (columnName === 'status') {
      // Render a colored badge based on the status value
      const color = value === 'Active' ? '#2a9d5c' : '#999';
      return `<span style="color:${color}; font-weight:600;">${value}</span>`;
    }
    if (columnName === 'qty' && Number(value) < 5) {
      // Highlight low stock quantities in red
      return `<span style="color:#c0392b;">${value}</span>`;
    }
    return String(value);
  },
});

5. Web Worker integration.

// Check browser support before creating a worker
if (table.isWorkerSupported()) {
  // Bind a Web Worker to stream data into the table asynchronously
  const worker = table.createWebWorker('/workers/data-feed.js', true, false);

  worker.addEventListener('message', function(e) {
    if (e.data.done) {
      // Unbind the worker once the data transfer is complete
      table.unbindWebWorker(worker);
    }
  });
}

6. All configuration options.

Table Options

  • el (Element): An existing DOM element to use as the table's container.
  • className (string): CSS class applied to the wrapper element. Defaults to 'dgtable-wrapper'.
  • height (number): Table height in pixels.
  • width (DGTable.Width): Width handling mode. Accepts NONE, AUTO, or SCROLL. Defaults to NONE.
  • virtualTable (boolean): Activates virtual scrolling. Recommended for large datasets. Defaults to true.
  • estimatedRowHeight (number): Estimated row height used for virtual scroll calculations. Defaults to 40.
  • rowsBufferSize (number): Number of rows rendered outside the visible area as a scroll buffer. Defaults to 3.

Column Layout Options

  • columns (ColumnOptions[]): Array of column definition objects. Defaults to [].
  • minColumnWidth (number): Global minimum column width in pixels. Defaults to 35.
  • maxStickyColumnRelativeWidth (number): Maximum relative width for a sticky column.
  • resizableColumns (boolean): Activates drag-to-resize on all columns globally. Defaults to true.
  • movableColumns (boolean): Activates drag-to-reorder on all columns globally. Defaults to true.
  • maxColumnsSortCount (number): Maximum number of columns active in a multi-column sort. Defaults to 1.
  • allowCancelSort (boolean): Cycles sort direction through ascending, descending, and none. Defaults to true.
  • adjustColumnWidthForSortArrow (boolean): Auto-expands a column's width to accommodate the sort indicator. Defaults to true.
  • relativeWidthGrowsToFillWidth (boolean): Expands relative-width columns to fill available table space. Defaults to true.
  • relativeWidthShrinksToFillWidth (boolean): Shrinks relative-width columns to fit available table space. Defaults to false.
  • convertColumnWidthsToRelative (boolean): Converts auto-calculated widths to relative widths internally. Defaults to false.
  • autoFillTableWidth (boolean): Stretches all columns to fill the full table width. Defaults to false.
  • resizeAreaWidth (number): Width of the drag handle for column resizing, in pixels. Defaults to 8.

Column Definition Object

  • name (string): Required. Unique identifier for the column.
  • label (string): Text displayed in the column header. Defaults to name.
  • width (number | string): Column width as pixels (90), percentage ('25%'), or relative decimal (0.25).
  • dataPath (string | string[]): Path to the data property in each row object. Defaults to [name].
  • comparePath (string | string[]): Path used for sort comparisons. Defaults to dataPath.
  • resizable (boolean): Allows the user to resize this specific column. Defaults to true.
  • sortable (boolean): Allows the user to sort by this specific column. Defaults to true.
  • movable (boolean): Allows the user to reorder this specific column. Defaults to true.
  • visible (boolean): Sets the initial visibility of the column. Defaults to true.
  • sticky ('start' | 'end' | false | null): Pins the column to the left ('start') or right ('end') edge of the table.
  • cellClasses (string): Additional CSS classes applied to every body cell in this column.
  • ignoreMin (boolean): Skips the global minColumnWidth constraint for this column.
  • order (number): Sets the initial display order of the column.

Formatting and Filtering Options

  • cellFormatter (function): A function (value, columnName, rowData) => string that returns an HTML string for each body cell.
  • headerCellFormatter (function): A function (label, columnName) => string that returns an HTML string for each header cell.
  • filter (function): A default filter function (row, args) => boolean called when table.filter(args) executes.
  • sortColumn (string | string[] | ColumnSortOptions | ColumnSortOptions[]): Sets the initial sort column or columns at construction time.
  • onComparatorRequired (function): A callback (columnName, descending, defaultComparator) => ComparatorFunction that returns a custom comparator for a specific column.
  • customSortingProvider (function): A function (data, sort) => RowData[] that replaces the built-in sort algorithm entirely.

Styling Options

  • tableClassName (string): Base CSS class applied to the table element. Defaults to 'dgtable'.
  • cellClasses (string): Additional CSS classes applied to all body cells globally. Defaults to ''.
  • resizerClassName (string): CSS class for the column resize handle element. Defaults to 'dgtable-resize'.
  • cellPreviewClassName (string): CSS class for the cell preview tooltip element. Defaults to 'dgtable-cell-preview'.
  • allowCellPreview (boolean): Shows a tooltip on hover for truncated body cell content. Defaults to true.
  • allowHeaderCellPreview (boolean): Shows a tooltip on hover for truncated header cell content. Defaults to true.
  • cellPreviewAutoBackground (boolean): Copies the originating cell's background color to the preview element. Defaults to true.

7. API methods.

// ── Rendering ──

// Perform the initial render or re-render after a configuration change
table.render();

// Force a complete teardown and DOM rebuild of the entire table
table.clearAndRender();

// ── Column Management ──

// Replace the entire column set with a new array of column definitions
table.setColumns(columns, render);

// Insert a new column; second arg is the column name or index to insert before (-1 appends)
table.addColumn(columnData, before, render);

// Remove a column by its name property
table.removeColumn('columnName', render);

// Change the displayed header label for an existing column
table.setColumnLabel('columnName', 'New Label');

// Move a column from one position to another by index
table.moveColumn(srcIndex, destIndex, visibleOnly);

// Show or hide a column (the data remains in the model)
table.setColumnVisible('columnName', true);

// Check whether a column is currently visible
table.isColumnVisible('columnName');

// Set a column's width programmatically
table.setColumnWidth('columnName', 150);

// Get a column's current width
table.getColumnWidth('columnName');

// Retrieve the full configuration object for one column
table.getColumnConfig('columnName');

// Retrieve the full configuration array for all columns
table.getColumnsConfig();

// ── Sorting ── 

// Sort by a column; second arg = descending, third arg = append to existing sort
table.sort('columnName', false, false);

// Re-apply the current sort state to the existing data
table.resort();

// Restore a previously saved sort state
table.setSortedColumns([{ column: 'username', descending: false }]);

// Retrieve the current sort state as a serializable array
table.getSortedColumns();

// Set the maximum number of columns active in a multi-column sort
table.setMaxColumnSortCount(3);

// Get the current maximum column sort count
table.getMaxColumnSortCount();

// ── Data Management ──

// Replace the entire dataset; pass true to re-sort after loading
table.setRows(data, resort);

// Insert new rows at a specific index; use -1 to append
table.addRows(data, at, resort, render);

// Remove a single row by its index in the full dataset
table.removeRow(rowIndex, render);

// Remove multiple consecutive rows starting at an index
table.removeRows(rowIndex, count, render);

// Force a single row to re-render its cells after an external data mutation
table.refreshRow(rowIndex, render);

// Re-render all currently visible virtual rows
table.refreshAllVirtualRows();

// Get the total number of rows in the dataset
table.getRowCount();

// Get the number of rows that pass the active filter
table.getFilteredRowCount();

// Retrieve the data object for a row by its index in the full dataset
table.getDataForRow(rowIndex);

// Retrieve the data object for a row by its index in the filtered dataset
table.getDataForFilteredRow(filteredIndex);

// Find a row's index in the full dataset by its data object reference
table.getIndexForRow(rowData);

// Find a row's index in the filtered dataset by its data object reference
table.getIndexForFilteredRow(rowData);

// Get the DOM element for a rendered row by its dataset index
table.getRowElement(rowIndex);

// Get the vertical pixel position of a row within the table's scroll area
table.getRowYPos(rowIndex);

// ── Filtering ──

// Register a custom filter function; receives each row and the args object
table.setFilter(function(row, args) {
    return row.status === args.status;
});

// Execute the active filter with a set of arguments
table.filter({ status: 'Active' });

// Use the built-in column keyword filter
table.filter({ column: 'email', keyword: '@company.com', caseSensitive: false });

// Remove the active filter and display all rows
table.clearFilter();

// ── Formatters ──

// Set a global cell formatter for all body cells
table.setCellFormatter(function(value, columnName, rowData) {
    return `<span class="cell-${columnName}">${value}</span>`;
});

// Set a global formatter for all header cells
table.setHeaderCellFormatter(function(label, columnName) {
    return `<span class="hdr-${columnName}">${label}</span>`;
});

// Get the rendered HTML string for a cell by row index and column name
table.getHtmlForRowCell(rowIndex, 'columnName');

// Get the rendered HTML string for a cell from a row data object directly
table.getHtmlForRowDataCell(rowData, 'columnName');

// ── Layout ──

// Notify the table that its container width has changed so it recalculates column widths
table.tableWidthChanged(forceUpdate, renderColumns);

// Notify the table that its container height has changed
table.tableHeightChanged();

// Set the global minimum column width in pixels
table.setMinColumnWidth(40);

// Get the current global minimum column width
table.getMinColumnWidth();

// Set the maximum relative width for sticky columns
table.setMaxStickyColumnRelativeWidth(0.4);

// Get the maximum relative width for sticky columns
table.getMaxStickyColumnRelativeWidth();

// ── Cell Preview ──

// Hide the currently visible cell preview tooltip
table.hideCellPreview();

// Alias for hideCellPreview()
table.abortCellPreview();

// ── Column Feature Toggles ──

// Enable or disable interactive column moving globally
table.setMovableColumns(true);

// Check whether column moving is currently active
table.getMovableColumns();

// Enable or disable interactive column resizing globally
table.setResizableColumns(true);

// Check whether column resizing is currently active
table.getResizableColumns();

// ── Sorting Customization ──

// Register a callback that returns a custom comparator for a specific column
table.setOnComparatorRequired(function(columnName, descending, defaultComparator) {
    if (columnName === 'priority') {
        const rank = { High: 0, Medium: 1, Low: 2 };
        return function(a, b) {
            return rank[a.priority] - rank[b.priority];
        };
    }
    return defaultComparator;
});

// Replace the entire sort algorithm with a custom implementation
table.setCustomSortingProvider(function(data, sort) {
    // Call sort(data) to use the built-in sort, or return your own sorted array
    return sort(data);
});

// ── Web Workers ──

// Check whether the browser supports Web Workers
table.isWorkerSupported();

// Create and bind a Web Worker for async data loading
table.createWebWorker('/workers/table-feed.js', true, false);

// Detach a previously bound Web Worker
table.unbindWebWorker(worker);

// Generate a Blob URL from an inline script element for use as a worker source
table.getUrlForElementContent('worker-script-element-id');

// ── DOM Access ──

// Direct reference to the table's wrapper DOM element
table.el;

// Get the header row DOM element
table.getHeaderRowElement();

// ── Lifecycle ── 

// Destroy the table, remove all DOM nodes, and release all event listeners
table.destroy();

// Alias for destroy()
table.close();

// Alias for destroy()
table.remove();

8. Event handlers.

// Fires after the table completes a render cycle
table.on('render', function() {
  console.log('Table rendered');
});

// Fires when the table rebuilds its underlying DOM structure
table.on('renderskeleton', function() {
  console.log('Table skeleton rebuilt');
});

// Fires when a new row DOM element is created during virtual scrolling
// data: { filteredRowIndex, rowIndex, rowEl, rowData }
table.on('rowcreate', function(data) {
  data.rowEl.setAttribute('data-uid', data.rowData.userId);
});

// Fires when the user clicks a row
// data: { event, filteredRowIndex, rowIndex, rowEl, rowData }
table.on('rowclick', function(data) {
  console.log('Clicked row index:', data.rowIndex, 'Data:', data.rowData);
});

// Fires just before a virtual row's DOM element is removed from the DOM
table.on('rowdestroy', function(rowEl) {
  console.log('Row DOM element removed:', rowEl);
});

// Fires when a cell preview tooltip becomes visible
// data: { el, name, rowIndex, rowData, cell, cellEl }
table.on('cellpreview', function(data) {
  if (data.name === 'email' && data.el) {
      // Append a custom badge to the preview content
      data.el.innerHTML += '<span class="copy-hint">Click to copy</span>';
  }
});

// Fires just before a cell preview tooltip closes
// data: { el, name, rowIndex, rowData, cell, cellEl }
table.on('cellpreviewdestroy', function(data) {
  console.log('Preview closed for column:', data.name);
});

// Fires when the table creates its header row element
table.on('headerrowcreate', function(headerRowEl) {
  console.log('Header row element:', headerRowEl);
});

// Fires when the user right-clicks a header cell
// data: { columnName, pageX, pageY, bounds }
table.on('headercontextmenu', function(data) {
  console.log('Context menu at column:', data.columnName, 'X:', data.pageX, 'Y:', data.pageY);
  // data.bounds contains { left, top, width, height } of the header cell
});

// Fires after a column is added to the table
table.on('addcolumn', function(columnName) {
  console.log('Column added:', columnName);
});

// Fires after a column is removed from the table
table.on('removecolumn', function(columnName) {
  console.log('Column removed:', columnName);
});

// Fires after the user drags a column to a new position
// data: { name, src, dest }
table.on('movecolumn', function(data) {
  console.log(`Column "${data.name}" moved from position ${data.src} to ${data.dest}`);
});

// Fires when a hidden column becomes visible
table.on('showcolumn', function(columnName) {
  console.log('Column shown:', columnName);
});

// Fires when a visible column is hidden
table.on('hidecolumn', function(columnName) {
  console.log('Column hidden:', columnName);
});

// Fires after the user resizes a column by dragging
// data: { name, width, oldWidth }
table.on('columnwidth', function(data) {
  console.log(`Column "${data.name}" resized from ${data.oldWidth}px to ${data.width}px`);
});

// Fires after rows are added to the table
// data: { count, clear }
table.on('addrows', function(data) {
  console.log(`${data.count} row(s) added. Full dataset cleared first: ${data.clear}`);
});

// Fires after a sort operation completes
// data: { sorts, resort }
table.on('sort', function(data) {
  console.log('Current sort state:', data.sorts);
});

// Fires each time a filter is applied; receives the filter arguments
table.on('filter', function(args) {
  console.log('Active filter arguments:', args);
});

// Fires when the active filter is cleared
table.on('filterclear', function() {
  console.log('Filter cleared, all rows visible');
});

Alternatives:

  • Tabulator: A full-featured JavaScript table library with spreadsheet-style inline editing, tree data, and REST data source integration.
  • Handsontable: A spreadsheet-style data grid for JavaScript and major frameworks, optimized for cell-level editing workflows.

Changelog:

2025-06-21

  • v2.0.28

2025-01-27

  • v0.6.19: bugfix

2024-10-14

  • v0.6.17: bugfix

2023-11-30

  • v0.6.11: bugfix

2023-01-12

  • v0.6.10: [feature] allowCancelSort

2022-12-26

  • v0.6.9: bugfix

2022-12-04

  • v0.6.8: bugfix

2022-11-22

  • v0.6.7: bugfix

2022-11-20

  • v0.6.6: [fix] restore default behavior of refreshRow to immediately render changes

2022-11-17

  • v0.6.5: [fix] allowCellPreview setting was no treated correctly since virtual table support

2022-11-08

  • v0.6.3: bugfix

2022-11-07

  • v0.6.1: auto estimate estimatedRowHeight by default

2022-10-13

  • v0.5.59: corrected single sortColumn option support

2022-05-18

  • v0.5.58: [fix] do not trigger events after destroy

2022-04-03

  • v0.5.57: [fix] preview cell should inherit cursor style

2022-03-28

  • v0.5.56: [fix] emit rowclick events from cell preview

2022-03-16

  • v0.5.55: [fix] avoid index corruption when trigerring rowclick

This awesome jQuery plugin is developed by danielgindi. For more Advanced Usages, please check the demo page or visit the official website.