FE Master

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!

CrackIt Dev - Master Frontend Interviews | FE Master | System Design, DSA, JavaScript