A high-performance virtual table component for vanilla JavaScript.
- High Performance - Virtual scrolling renders only visible rows, handling hundreds of thousands of rows smoothly
- Column Management - Sort, resize, reorder, and hide/show columns
- Multi-column Sorting - Sort by multiple columns simultaneously
- Flexible Column Widths - Mix absolute, relative, and auto-calculated widths
- Cell Preview - Hover tooltips for truncated cell content
- Sticky Columns - Pin columns to the start or end of the table
- RTL Support - Native right-to-left language support
- Variable Row Height - Support for rows with different heights
- Filtering - Built-in filtering with custom filter function support
- Web Worker Support - Load data asynchronously via Web Workers
npm install @danielgindi/dgtableimport DGTable from '@danielgindi/dgtable';
const table = new DGTable({
columns: [
{ name: 'id', label: 'ID', width: 80 },
{ name: 'name', label: 'Name', width: '30%' },
{ name: 'email', label: 'Email' },
],
height: 400,
virtualTable: true,
});
document.getElementById('container').appendChild(table.el);
table.setRows([
{ id: 1, name: 'John Doe', email: '[email protected]' },
{ id: 2, name: 'Jane Smith', email: '[email protected]' },
]);
table.render();If you're migrating from the older jQuery version:
- No
$elproperty - usetable.elinstead - No auto-clear of jQuery data
- Use
emit()instead oftrigger()for events - Event arguments are now always a single value/object
- DOM element properties:
'columnName'on cells,'index'/'vIndex'on rows
new DGTable(options?: DGTableOptions)| Option | Type | Default | Description |
|---|---|---|---|
el |
Element |
- | Optional existing element to use as container |
className |
string |
'dgtable-wrapper' |
CSS class for the wrapper element |
height |
number |
- | Table height in pixels |
width |
DGTable.Width |
NONE |
Width handling mode: NONE, AUTO, or SCROLL |
virtualTable |
boolean |
true |
Enable virtual scrolling (recommended for large datasets) |
estimatedRowHeight |
number |
40 |
Estimated row height for virtual scrolling calculations |
rowsBufferSize |
number |
3 |
Number of rows to render outside visible area |
| Option | Type | Default | Description |
|---|---|---|---|
columns |
ColumnOptions[] |
[] |
Array of column definitions |
minColumnWidth |
number |
35 |
Minimum column width in pixels |
resizableColumns |
boolean |
true |
Allow column resizing |
movableColumns |
boolean |
true |
Allow column reordering |
maxColumnsSortCount |
number |
1 |
Maximum number of columns to sort by |
allowCancelSort |
boolean |
true |
Allow cycling through asc → desc → none |
adjustColumnWidthForSortArrow |
boolean |
true |
Auto-expand columns for sort indicator |
relativeWidthGrowsToFillWidth |
boolean |
true |
Expand relative columns to fill space |
relativeWidthShrinksToFillWidth |
boolean |
false |
Shrink relative columns to fit |
convertColumnWidthsToRelative |
boolean |
false |
Convert auto widths to relative |
autoFillTableWidth |
boolean |
false |
Stretch columns to fill table width |
resizeAreaWidth |
number |
8 |
Width of resize drag area in pixels |
{
name: string; // Required: unique identifier
label?: string; // Header text (defaults to name)
width?: number | string; // number (px), '30%', or 0.3 (relative)
dataPath?: string | string[]; // Path to data (defaults to [name])
comparePath?: string | string[]; // Path for sorting (defaults to dataPath)
resizable?: boolean; // Allow resizing this column (default: true)
sortable?: boolean; // Allow sorting by this column (default: true)
movable?: boolean; // Allow moving this column (default: true)
visible?: boolean; // Column visibility (default: true)
sticky?: 'start' | 'end' | false | null; // Pin to start or end
cellClasses?: string; // Additional CSS classes for cells
ignoreMin?: boolean; // Ignore minColumnWidth for this column
order?: number; // Column order
}| Option | Type | Description |
|---|---|---|
cellFormatter |
(value: unknown, columnName: string, rowData: RowData) => string |
Custom cell HTML renderer |
headerCellFormatter |
(label: string, columnName: string) => string |
Custom header cell renderer |
filter |
(row: RowData, args: unknown) => boolean |
Custom filter function |
sortColumn |
string | string[] | ColumnSortOptions | ColumnSortOptions[] |
Initial sort configuration |
onComparatorRequired |
(columnName: string, descending: boolean, defaultComparator: ComparatorFunction) => ComparatorFunction |
Custom comparator provider |
customSortingProvider |
(data: RowData[], sort: (data: RowData[]) => RowData[]) => RowData[] |
Custom sorting implementation |
| Option | Type | Default | Description |
|---|---|---|---|
tableClassName |
string |
'dgtable' |
Base CSS class for the table |
cellClasses |
string |
'' |
Additional classes for all cells |
resizerClassName |
string |
'dgtable-resize' |
Class for resize handle |
cellPreviewClassName |
string |
'dgtable-cell-preview' |
Class for cell preview |
allowCellPreview |
boolean |
true |
Show preview on hover |
allowHeaderCellPreview |
boolean |
true |
Show preview for headers |
cellPreviewAutoBackground |
boolean |
true |
Match preview background to cell |
resizeAreaWidth |
number |
8 |
Width of resize drag area |
table.render() // Render the table
table.clearAndRender(render=true) // Force full re-rendertable.setColumns(columns, render=true) // Replace all columns
table.addColumn(columnData, before=-1, render) // Add a column
table.removeColumn(columnName, render=true) // Remove a column
table.setColumnLabel(column, label) // Update column label
table.moveColumn(src, dest, visibleOnly=true) // Reorder columns
table.setColumnVisible(column, visible) // Show/hide column
table.isColumnVisible(column) // Check visibility
table.setColumnWidth(column, width) // Set column width
table.getColumnWidth(column) // Get column width
table.getColumnConfig(column): ColumnOptions // Get column config
table.getColumnsConfig(): ColumnOptions[] // Get all columns configtable.sort(column: string, descending, add=false) // Sort by column
table.resort() // Re-apply current sort
table.setSortedColumns(columns: SerializedColumnSort[]) // Set current sort state
table.getSortedColumns(): SerializedColumnSort[] // Get current sort state
table.setMaxColumnSortCount(count) // Set max sortable columns
table.getMaxColumnSortCount(): number // Get max sortable columnstable.setRows(data, resort=false) // Replace all rows
table.addRows(data, at=-1, resort=false, render=true) // Add rows
table.removeRow(rowIndex, render=true) // Remove one row
table.removeRows(rowIndex, count, render=true) // Remove multiple rows
table.refreshRow(rowIndex, render=true) // Refresh a row
table.refreshAllVirtualRows() // Refresh all visible rows
table.getRowCount() // Total row count
table.getFilteredRowCount() // Filtered row count
table.getDataForRow(rowIndex) // Get row data by index
table.getDataForFilteredRow(filteredIndex) // Get filtered row data
table.getIndexForRow(rowData) // Find row index
table.getIndexForFilteredRow(rowData) // Find filtered row index
table.getRowElement(rowIndex) // Get row DOM element
table.getRowYPos(rowIndex) // Get row Y positiontable.setFilter(filterFn) // Set custom filter function
table.filter(args) // Apply filter with arguments
table.clearFilter() // Clear active filter
// Built-in filter example:
table.filter({ column: 'name', keyword: 'john', caseSensitive: false });table.setCellFormatter(fn) // Set cell formatter
table.setHeaderCellFormatter(fn) // Set header formatter
table.getHtmlForRowCell(rowIndex, columnName) // Get cell HTML
table.getHtmlForRowDataCell(rowData, columnName) // Get cell HTML from datatable.tableWidthChanged(forceUpdate=false, renderColumns=true) // Notify width change
table.tableHeightChanged() // Notify height change
table.setMinColumnWidth(width) // Set global min width
table.getMinColumnWidth() // Get global min widthtable.hideCellPreview() // Hide or prevent cell preview
table.abortCellPreview() // Alias for hideCellPreview()table.setMovableColumns(movable) // Enable/disable column moving
table.getMovableColumns() // Get movable state
table.setResizableColumns(resizable) // Enable/disable column resizing
table.getResizableColumns() // Get resizable statetable.setOnComparatorRequired(callback) // Set comparator provider
table.setCustomSortingProvider(provider) // Set custom sort providertable.isWorkerSupported() // Check Web Worker support
table.createWebWorker(url, start=true, resort=false) // Create worker
table.unbindWebWorker(worker) // Unbind worker
table.getUrlForElementContent(elementId) // Create blob URL from elementtable.el // The table wrapper element
table.getHeaderRowElement() // Get header row element// TypeScript users get full autocompletion and type checking!
table.on('rowclick', (data) => {
// data is typed as RowClickEvent
console.log(data.rowIndex, data.rowData);
});
// Custom events are also supported (for using table as event bus)
table.on('my-custom-event', (data) => {
// data is typed as `unknown` by default
console.log(data);
});
// You can specify the type for custom events
table.on<{ customField: string }>('my-typed-event', (data) => {
console.log(data.customField); // TypeScript knows the type
});
table.on(event, handler) // Add event listener (typed for built-in events)
table.once(event, handler) // Add one-time listener (typed)
table.off(event, handler) // Remove listener
table.emit(event, data) // Emit event (typed for built-in events)table.destroy() // Destroy table and free memory
table.close() // Alias for destroy()
table.remove() // Alias for destroy()Subscribe to events using table.on(eventName, handler).
Built-in events have fully typed handlers with autocompletion. You can also use the table's event system as an event bus for your own custom events.
| Event | Data Type | Description |
|---|---|---|
render |
undefined |
Table finished rendering |
renderskeleton |
undefined |
Table structure rebuilt |
| Event | Data Type | Description |
|---|---|---|
rowcreate |
RowCreateEvent |
Row element created |
rowclick |
RowClickEvent |
Row clicked |
rowdestroy |
HTMLElement |
Row element about to be removed |
interface RowCreateEvent {
filteredRowIndex: number; // Index in filtered data
rowIndex: number; // Index in original data
rowEl: HTMLElement; // The row DOM element
rowData: RowData; // The row data object
}
interface RowClickEvent {
event: MouseEvent; // The original mouse event
filteredRowIndex: number; // Index in filtered data
rowIndex: number; // Index in original data
rowEl: HTMLElement; // The row DOM element
rowData: RowData; // The row data object
}| Event | Data Type | Description |
|---|---|---|
cellpreview |
CellPreviewEvent |
Cell preview showing |
cellpreviewdestroy |
CellPreviewDestroyEvent |
Cell preview hiding |
interface CellPreviewEvent {
el: Element | null; // Preview element's first child
name: string; // Column name
rowIndex: number | null; // Row index (null for header)
rowData: RowData | null; // Row data (null for header)
cell: HTMLElement; // Original cell element
cellEl: HTMLElement; // Cell's inner element
}
interface CellPreviewDestroyEvent {
el: ChildNode | null; // Preview element's first child
name: string; // Column name
rowIndex: number | null; // Row index (null for header)
rowData: RowData | null; // Row data (null for header)
cell: HTMLElement | null; // Original cell element
cellEl: ChildNode | null; // Cell's inner element
}| Event | Data Type | Description |
|---|---|---|
headerrowcreate |
HTMLElement |
Header row created |
headercontextmenu |
HeaderContextMenuEvent |
Header right-click |
interface HeaderContextMenuEvent {
columnName: string; // Column that was right-clicked
pageX: number; // Mouse X position
pageY: number; // Mouse Y position
bounds: { // Cell bounds
left: number;
top: number;
width: number;
height: number;
};
}| Event | Data Type | Description |
|---|---|---|
addcolumn |
string |
Column name added |
removecolumn |
string |
Column name removed |
movecolumn |
MoveColumnEvent |
Column moved |
showcolumn |
string |
Column name shown |
hidecolumn |
string |
Column name hidden |
columnwidth |
ColumnWidthEvent |
Column resized |
interface MoveColumnEvent {
name: string; // Column name
src: number; // Original order position
dest: number; // New order position
}
interface ColumnWidthEvent {
name: string; // Column name
width: number; // New width
oldWidth: number; // Previous width
}| Event | Data Type | Description |
|---|---|---|
addrows |
AddRowsEvent |
Rows added |
sort |
SortEvent |
Data sorted |
filter |
unknown |
Filter applied (filter args) |
filterclear |
{} |
Filter cleared |
interface AddRowsEvent {
count: number; // Number of rows added
clear: boolean; // Whether table was cleared first
}
interface SortEvent {
sorts: SerializedColumnSort[]; // Current sort state
resort?: boolean; // True if re-sorting existing data
}// Row click handler
table.on('rowclick', (data) => {
console.log('Clicked row:', data.rowIndex, data.rowData);
});
// Column resize handler
table.on('columnwidth', (data) => {
console.log(`Column ${data.name} resized from ${data.oldWidth} to ${data.width}`);
});
// Sort handler
table.on('sort', (data) => {
console.log('Sorted by:', data.sorts);
});
// Cell preview handler
table.on('cellpreview', (data) => {
// Customize preview content
if (data.el) {
data.el.innerHTML += '<span class="custom-badge">Preview</span>';
}
});The library exports the following types for TypeScript users:
import type {
// Configuration types
DGTableOptions, // Constructor options
ColumnOptions, // Column definition
ColumnSortOptions, // Sort specification { column, descending? }
SerializedColumnSort, // Saved sort config
RowData, // Row data (Record<string, unknown>)
// Function types
CellFormatter, // Cell formatter function
HeaderCellFormatter, // Header cell formatter function
FilterFunction, // Filter function
ComparatorFunction, // Row comparator function
OnComparatorRequired, // Comparator provider callback
CustomSortingProvider, // Custom sorting function
// Event types
RowCreateEvent, // 'rowcreate' event data
RowClickEvent, // 'rowclick' event data
CellPreviewEvent, // 'cellpreview' event data
CellPreviewDestroyEvent, // 'cellpreviewdestroy' event data
HeaderContextMenuEvent, // 'headercontextmenu' event data
MoveColumnEvent, // 'movecolumn' event data
ColumnWidthEvent, // 'columnwidth' event data
AddRowsEvent, // 'addrows' event data
SortEvent, // 'sort' event data
// Event map (for advanced typing)
DGTableEventMap, // Maps event names to their data types
} from '@danielgindi/dgtable';The DGTableEventMap interface provides full autocompletion when using .on(), .once(), .off(), and .emit():
// Event names autocomplete, and handler receives correctly typed data
table.on('rowclick', (data) => {
// TypeScript knows: data.event, data.rowIndex, data.rowData, etc.
console.log(`Clicked row ${data.rowIndex}`);
});
table.on('columnwidth', (data) => {
// TypeScript knows: data.name, data.width, data.oldWidth
console.log(`Column ${data.name} resized to ${data.width}px`);
});# Install dependencies
npm install
# Build
npm run build
# Lint
npm run lintDaniel Cohen Gindi - [email protected]
Contributions are welcome! Please feel free to:
- Report bugs and issues
- Submit pull requests
- Improve documentation
- Share your use cases
MIT License - see LICENSE for details.
Copyright (c) 2013-present Daniel Cohen Gindi