Build Your Own Data Table With Sorting Filtering
DifficultyHard
Estimated time: 2-3 hours
0 done
Description
# Build Your Own Data Table with Sorting and Filtering Data tables are essential for displaying and managing large datasets in web applications. This challenge will teach you how to build a performan...
Requirements
- **Column Sorting**: Click headers to sort ascending/descending
- **Filtering**: Search and filter by column values
- **Pagination**: Split large datasets into pages
- **Row Selection**: Select single or multiple rows
- **Expandable Rows**: Show additional details on click
- **Column Resizing**: Drag to resize column widths
- **Virtual Scrolling**: Handle thousands of rows efficiently
- **Export**: Download table data as CSV
Overview
Data tables are essential for displaying and managing large datasets in web applications. This challenge will teach you how to build a performant, feature-rich data table with sorting, filtering, pagination, and selection capabilities.
Implementation Guide
Build Your Own Data Table with Sorting and Filtering
Data tables are essential for displaying and managing large datasets in web applications. This challenge will teach you how to build a performant, feature-rich data table with sorting, filtering, pagination, and selection capabilities.
The Challenge - Building a Data Table
The functional requirements include:
- Column Sorting: Click headers to sort ascending/descending
- Filtering: Search and filter by column values
- Pagination: Split large datasets into pages
- Row Selection: Select single or multiple rows
- Expandable Rows: Show additional details on click
- Column Resizing: Drag to resize column widths
- Virtual Scrolling: Handle thousands of rows efficiently
- Export: Download table data as CSV
Step One: Create the DataTable Class
Example:
class DataTable {
constructor(element, options = {}) {
this.element = element;
this.options = {
data: [],
columns: [],
pageSize: 10,
sortable: true,
filterable: true,
selectable: false,
...options
};
this.currentData = [];
this.filteredData = [];
this.currentPage = 1;
this.sortColumn = null;
this.sortDirection = 'asc';
this.selectedRows = new Set();
this.init();
}
init() {
this.currentData = [...this.options.data];
this.filteredData = [...this.currentData];
this.render();
}
}
Step Two: Render the Table Structure
Example:
render() {
this.element.innerHTML = `
${this.options.filterable ? this.renderFilters() : ''}
<div class="table-container">
<table class="data-table">
<thead>${this.renderHeader()}</thead>
<tbody>${this.renderBody()}</tbody>
</table>
</div>
${this.renderPagination()}
`;
this.attachEvents();
}
renderHeader() {
return `
<tr>
${this.options.selectable ? '<th><input type="checkbox" class="select-all"></th>' : ''}
${this.options.columns.map(col => `
<th data-column="${col.field}" class="${this.options.sortable ? 'sortable' : ''}">
${col.label}
${this.renderSortIndicator(col.field)}
</th>
`).join('')}
</tr>
`;
}
renderBody() {
const start = (this.currentPage - 1) * this.options.pageSize;
const end = start + this.options.pageSize;
const pageData = this.filteredData.slice(start, end);
return pageData.map((row, index) => `
<tr data-index="${start + index}" ${this.selectedRows.has(start + index) ? 'class="selected"' : ''}>
${this.options.selectable ? `<td><input type="checkbox" class="row-select" ${this.selectedRows.has(start + index) ? 'checked' : ''}></td>` : ''}
${this.options.columns.map(col => `
<td>${this.formatCell(row[col.field], col)}</td>
`).join('')}
</tr>
`).join('');
}
formatCell(value, column) {
if (column.render) {
return column.render(value);
}
return value || '';
}
Step Three: Implement Sorting
Example:
sort(column) {
if (this.sortColumn === column) {
// Toggle direction
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
} else {
this.sortColumn = column;
this.sortDirection = 'asc';
}
const columnConfig = this.options.columns.find(col => col.field === column);
this.filteredData.sort((a, b) => {
let aVal = a[column];
let bVal = b[column];
// Custom sort function
if (columnConfig && columnConfig.sort) {
return columnConfig.sort(aVal, bVal, this.sortDirection);
}
// Default sorting
if (typeof aVal === 'string') {
aVal = aVal.toLowerCase();
bVal = bVal.toLowerCase();
}
if (aVal < bVal) return this.sortDirection === 'asc' ? -1 : 1;
if (aVal > bVal) return this.sortDirection === 'asc' ? 1 : -1;
return 0;
});
this.currentPage = 1;
this.render();
}
renderSortIndicator(column) {
if (this.sortColumn !== column) {
return '<span class="sort-icon">⇅</span>';
}
return this.sortDirection === 'asc'
? '<span class="sort-icon">↑</span>'
: '<span class="sort-icon">↓</span>';
}
Step Four: Add Filtering
Example:
renderFilters() {
return `
<div class="table-filters">
<input
type="search"
class="table-search"
placeholder="Search all columns..."
aria-label="Search table"
>
</div>
`;
}
filter(searchTerm) {
searchTerm = searchTerm.toLowerCase();
this.filteredData = this.currentData.filter(row => {
return this.options.columns.some(col => {
const value = String(row[col.field] || '').toLowerCase();
return value.includes(searchTerm);
});
});
this.currentPage = 1;
this.render();
}
// Column-specific filtering
filterByColumn(column, value) {
this.filteredData = this.currentData.filter(row => {
const rowValue = String(row[column] || '').toLowerCase();
return rowValue.includes(value.toLowerCase());
});
this.currentPage = 1;
this.render();
}
Step Five: Implement Pagination
Example:
renderPagination() {
const totalPages = Math.ceil(this.filteredData.length / this.options.pageSize);
if (totalPages <= 1) return '';
const pages = [];
for (let i = 1; i <= totalPages; i++) {
pages.push(`
<button
class="page-btn ${i === this.currentPage ? 'active' : ''}"
data-page="${i}"
>
${i}
</button>
`);
}
return `
<div class="table-pagination">
<button class="page-btn" data-page="prev" ${this.currentPage === 1 ? 'disabled' : ''}>
Previous
</button>
${pages.join('')}
<button class="page-btn" data-page="next" ${this.currentPage === totalPages ? 'disabled' : ''}>
Next
</button>
<span class="page-info">
Showing ${(this.currentPage - 1) * this.options.pageSize + 1}-${Math.min(this.currentPage * this.options.pageSize, this.filteredData.length)}
of ${this.filteredData.length}
</span>
</div>
`;
}
goToPage(page) {
const totalPages = Math.ceil(this.filteredData.length / this.options.pageSize);
if (page === 'next') {
page = Math.min(this.currentPage + 1, totalPages);
} else if (page === 'prev') {
page = Math.max(this.currentPage - 1, 1);
}
this.currentPage = page;
this.render();
}
Step Six: Add Row Selection
Example:
attachEvents() {
// Sort headers
this.element.querySelectorAll('th.sortable').forEach(th => {
th.addEventListener('click', () => {
const column = th.dataset.column;
this.sort(column);
});
});
// Search
const searchInput = this.element.querySelector('.table-search');
if (searchInput) {
let debounceTimer;
searchInput.addEventListener('input', (e) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
this.filter(e.target.value);
}, 300);
});
}
// Pagination
this.element.querySelectorAll('.page-btn').forEach(btn => {
btn.addEventListener('click', () => {
const page = btn.dataset.page;
this.goToPage(page === 'next' || page === 'prev' ? page : parseInt(page));
});
});
// Row selection
if (this.options.selectable) {
// Select all
const selectAll = this.element.querySelector('.select-all');
selectAll?.addEventListener('change', (e) => {
this.selectAll(e.target.checked);
});
// Individual rows
this.element.querySelectorAll('.row-select').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const index = parseInt(e.target.closest('tr').dataset.index);
this.toggleRowSelection(index);
});
});
}
}
toggleRowSelection(index) {
if (this.selectedRows.has(index)) {
this.selectedRows.delete(index);
} else {
this.selectedRows.add(index);
}
this.render();
}
selectAll(selected) {
const start = (this.currentPage - 1) * this.options.pageSize;
const end = start + this.options.pageSize;
for (let i = start; i < end && i < this.filteredData.length; i++) {
if (selected) {
this.selectedRows.add(i);
} else {
this.selectedRows.delete(i);
}
}
this.render();
}
getSelectedRows() {
return Array.from(this.selectedRows).map(index => this.filteredData[index]);
}
Step Seven: Add Column Resizing
Example:
makeColumnsResizable() {
this.options.columns.forEach((col, colIndex) => {
const th = this.element.querySelector(`th[data-column="${col.field}"]`);
if (!th) return;
const resizer = document.createElement('div');
resizer.className = 'column-resizer';
th.appendChild(resizer);
resizer.addEventListener('mousedown', (e) => {
e.preventDefault();
const startX = e.pageX;
const startWidth = th.offsetWidth;
const handleMouseMove = (e) => {
const width = startWidth + (e.pageX - startX);
th.style.width = `${width}px`;
};
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
});
});
}
Step Eight: Implement CSV Export
Example:
exportCSV() {
const headers = this.options.columns.map(col => col.label).join(',');
const rows = this.filteredData.map(row => {
return this.options.columns.map(col => {
let value = row[col.field];
// Escape commas and quotes
if (typeof value === 'string' && (value.includes(',') || value.includes('"'))) {
value = `"${value.replace(/"/g, '""')}"`;
}
return value;
}).join(',');
});
const csv = [headers, ...rows].join('\n');
// Download
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'table-data.csv';
link.click();
URL.revokeObjectURL(url);
}
Step Nine: Add Virtual Scrolling for Performance
Example:
class VirtualDataTable extends DataTable {
constructor(element, options) {
super(element, { ...options, virtualize: true });
this.rowHeight = 40;
this.visibleRows = Math.ceil(window.innerHeight / this.rowHeight);
this.scrollTop = 0;
}
renderBody() {
const startIndex = Math.floor(this.scrollTop / this.rowHeight);
const endIndex = startIndex + this.visibleRows;
const visibleData = this.filteredData.slice(startIndex, endIndex);
const totalHeight = this.filteredData.length * this.rowHeight;
const offsetY = startIndex * this.rowHeight;
return `
<div style="height: ${totalHeight}px; position: relative;">
<div style="transform: translateY(${offsetY}px);">
${visibleData.map(row => this.renderRow(row)).join('')}
</div>
</div>
`;
}
}
Step Ten: Build a Complete Demo
Create a demo with:
- Large dataset (1000+ rows)
- Multiple column types (text, number, date)
- Custom cell renderers
- Row actions (edit, delete)
- Bulk actions for selected rows
- Export functionality
The Final Step: Clean Up and Document
- Document all options and methods
- Create usage examples
- Add performance optimization notes
- Include accessibility features
Help Others by Sharing Your Solutions!
Powerful data tables improve data management UX. Share your component to help others build better admin interfaces!