Manual de Tablas Dinámicas con Filtros en JavaScript

Implementación moderna con tema Dracula

Introducción

Las tablas dinámicas con capacidades de filtrado son un componente esencial en muchas aplicaciones web modernas. Permiten a los usuarios visualizar, organizar y analizar grandes conjuntos de datos de manera eficiente.

Nota: Este manual cubre la implementación de una tabla dinámica completa con múltiples tipos de filtros, paginación y características avanzadas, todo con un diseño moderno usando el tema Dracula.

Características principales

Estructura Básica HTML

Comencemos con la estructura HTML necesaria para nuestra tabla dinámica:

<div class="demo-section">
    <div class="filter-controls">
        <!-- Controles de filtro -->
        <div class="filter-group">
            <label for="global-search">Búsqueda Global</label>
            <input type="text" id="global-search" placeholder="Buscar...">
        </div>
        <div class="filter-group">
            <label for="column-filter">Filtrar por Columna</label>
            <select id="column-filter">
                <option value="">Todas las columnas</option>
                <option value="0">Nombre</option>
                <option value="1">Edad</option>
                <option value="2">Departamento</option>
                <option value="3">Estado</option>
            </select>
        </div>
        <div class="filter-group">
            <label for="value-filter">Valor a Filtrar</label>
            <input type="text" id="value-filter" placeholder="Valor específico...">
        </div>
        <button id="reset-filters" class="btn btn-secondary">Resetear Filtros</button>
    </div>
    
    <div class="table-container">
        <table id="dynamic-table">
            <thead>
                <tr>
                    <th data-column="0">Nombre <span class="sort-icon">↕</span></th>
                    <th data-column="1">Edad <span class="sort-icon">↕</span></th>
                    <th data-column="2">Departamento <span class="sort-icon">↕</span></th>
                    <th data-column="3">Estado <span class="sort-icon">↕</span></th>
                </tr>
            </thead>
            <tbody>
                <!-- Los datos se cargarán dinámicamente -->
            </tbody>
        </table>
    </div>
    
    <div class="pagination">
        <button id="prev-page" class="btn btn-secondary">Anterior</button>
        <div id="page-numbers"></div>
        <button id="next-page" class="btn btn-secondary">Siguiente</button>
    </div>
</div>

Explicación de la estructura

La estructura se compone de tres partes principales:

  1. Controles de filtro: Un conjunto de inputs y selects que permiten al usuario filtrar los datos.
  2. Tabla: La tabla propiamente dicha, con cabeceras configurables y un cuerpo que se llenará dinámicamente.
  3. Paginación: Controles para navegar entre páginas de resultados.

Importante: Los atributos data-column en las cabeceras de la tabla son cruciales para el funcionamiento del filtrado por columnas y el ordenamiento.

Implementación JavaScript

Ahora implementaremos la lógica JavaScript que hará que nuestra tabla sea dinámica.

1. Datos de ejemplo

Primero, definimos un conjunto de datos de ejemplo para trabajar:

const sampleData = [
    { nombre: "Ana López", edad: 28, departamento: "Ventas", estado: "Activo" },
    { nombre: "Carlos Ruiz", edad: 35, departamento: "TI", estado: "Activo" },
    { nombre: "Beatriz García", edad: 42, departamento: "RRHH", estado: "Inactivo" },
    { nombre: "David Pérez", edad: 31, departamento: "Marketing", estado: "Activo" },
    { nombre: "Elena Castro", edad: 24, departamento: "Ventas", estado: "Activo" },
    { nombre: "Fernando Jiménez", edad: 39, departamento: "TI", estado: "Inactivo" },
    { nombre: "Gabriela Morales", edad: 27, departamento: "Marketing", estado: "Activo" },
    { nombre: "Héctor Silva", edad: 45, departamento: "RRHH", estado: "Activo" },
    { nombre: "Irene Vargas", edad: 33, departamento: "Ventas", estado: "Inactivo" },
    { nombre: "Javier Ortega", edad: 29, departamento: "TI", estado: "Activo" },
    { nombre: "Karen Méndez", edad: 36, departamento: "Marketing", estado: "Inactivo" },
    { nombre: "Luis Herrera", edad: 41, departamento: "Ventas", estado: "Activo" },
    { nombre: "María Navarro", edad: 26, departamento: "RRHH", estado: "Activo" },
    { nombre: "Nicolás Ríos", edad: 38, departamento: "TI", estado: "Inactivo" },
    { nombre: "Olivia Soto", edad: 30, departamento: "Marketing", estado: "Activo" }
];

2. Clase DynamicTable

Crearemos una clase para encapsular toda la funcionalidad de nuestra tabla dinámica:

class DynamicTable {
    constructor(tableId, data, itemsPerPage = 5) {
        this.table = document.getElementById(tableId);
        this.data = data;
        this.filteredData = [...data];
        this.itemsPerPage = itemsPerPage;
        this.currentPage = 1;
        this.sortColumn = null;
        this.sortDirection = 'asc';
        
        this.init();
    }
    
    init() {
        this.renderTable();
        this.setupEventListeners();
        this.renderPagination();
    }
    
    renderTable() {
        const tbody = this.table.querySelector('tbody');
        tbody.innerHTML = '';
        
        const startIndex = (this.currentPage - 1) * this.itemsPerPage;
        const endIndex = startIndex + this.itemsPerPage;
        const paginatedData = this.filteredData.slice(startIndex, endIndex);
        
        paginatedData.forEach(item => {
            const row = document.createElement('tr');
            
            row.innerHTML = `
                <td>${item.nombre}</td>
                <td>${item.edad}</td>
                <td>${item.departamento}</td>
                <td><span class="status-badge ${item.estado.toLowerCase()}">${item.estado}</span></td>
            `;
            
            tbody.appendChild(row);
        });
        
        this.updateTableInfo();
    }
    
    setupEventListeners() {
        // Filtro global
        document.getElementById('global-search').addEventListener('input', (e) => {
            this.applyFilters();
        });
        
        // Filtro por columna
        document.getElementById('column-filter').addEventListener('change', (e) => {
            this.applyFilters();
        });
        
        // Filtro por valor
        document.getElementById('value-filter').addEventListener('input', (e) => {
            this.applyFilters();
        });
        
        // Resetear filtros
        document.getElementById('reset-filters').addEventListener('click', () => {
            document.getElementById('global-search').value = '';
            document.getElementById('column-filter').value = '';
            document.getElementById('value-filter').value = '';
            this.applyFilters();
        });
        
        // Ordenar por columna
        this.table.querySelectorAll('th').forEach(th => {
            th.addEventListener('click', () => {
                const columnIndex = th.getAttribute('data-column');
                this.sortTable(columnIndex);
            });
        });
        
        // Paginación
        document.getElementById('prev-page').addEventListener('click', () => {
            if (this.currentPage > 1) {
                this.currentPage--;
                this.renderTable();
                this.renderPagination();
            }
        });
        
        document.getElementById('next-page').addEventListener('click', () => {
            const totalPages = Math.ceil(this.filteredData.length / this.itemsPerPage);
            if (this.currentPage < totalPages) {
                this.currentPage++;
                this.renderTable();
                this.renderPagination();
            }
        });
    }
    
    applyFilters() {
        const globalSearch = document.getElementById('global-search').value.toLowerCase();
        const columnFilter = document.getElementById('column-filter').value;
        const valueFilter = document.getElementById('value-filter').value.toLowerCase();
        
        this.filteredData = this.data.filter(item => {
            // Aplicar filtro global
            if (globalSearch) {
                const matches = Object.values(item).some(val => 
                    String(val).toLowerCase().includes(globalSearch)
                );
                if (!matches) return false;
            }
            
            // Aplicar filtro por columna específica
            if (columnFilter && valueFilter) {
                const columnIndex = parseInt(columnFilter);
                const columnKeys = ['nombre', 'edad', 'departamento', 'estado'];
                const columnKey = columnKeys[columnIndex];
                
                if (!String(item[columnKey]).toLowerCase().includes(valueFilter)) {
                    return false;
                }
            }
            
            return true;
        });
        
        this.currentPage = 1;
        this.renderTable();
        this.renderPagination();
    }
    
    sortTable(columnIndex) {
        const columnKeys = ['nombre', 'edad', 'departamento', 'estado'];
        const columnKey = columnKeys[columnIndex];
        
        // Determinar dirección de ordenamiento
        if (this.sortColumn === columnIndex) {
            this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
        } else {
            this.sortColumn = columnIndex;
            this.sortDirection = 'asc';
        }
        
        // Ordenar los datos
        this.filteredData.sort((a, b) => {
            let valueA = a[columnKey];
            let valueB = b[columnKey];
            
            // Para ordenamiento numérico si la columna es edad
            if (columnKey === 'edad') {
                valueA = parseInt(valueA);
                valueB = parseInt(valueB);
                return this.sortDirection === 'asc' ? valueA - valueB : valueB - valueA;
            }
            
            // Ordenamiento alfabético para otras columnas
            if (valueA < valueB) return this.sortDirection === 'asc' ? -1 : 1;
            if (valueA > valueB) return this.sortDirection === 'asc' ? 1 : -1;
            return 0;
        });
        
        // Actualizar iconos de ordenamiento
        this.table.querySelectorAll('th').forEach((th, index) => {
            const icon = th.querySelector('.sort-icon');
            if (index === columnIndex) {
                icon.textContent = this.sortDirection === 'asc' ? '↑' : '↓';
            } else {
                icon.textContent = '↕';
            }
        });
        
        this.renderTable();
    }
    
    renderPagination() {
        const pageNumbers = document.getElementById('page-numbers');
        pageNumbers.innerHTML = '';
        
        const totalPages = Math.ceil(this.filteredData.length / this.itemsPerPage);
        
        for (let i = 1; i <= totalPages; i++) {
            const button = document.createElement('button');
            button.textContent = i;
            button.className = 'btn btn-secondary';
            
            if (i === this.currentPage) {
                button.classList.add('active-page');
            }
            
            button.addEventListener('click', () => {
                this.currentPage = i;
                this.renderTable();
                this.renderPagination();
            });
            
            pageNumbers.appendChild(button);
        }
        
        // Actualizar estado de botones anterior/siguiente
        document.getElementById('prev-page').disabled = this.currentPage === 1;
        document.getElementById('next-page').disabled = this.currentPage === totalPages;
    }
    
    updateTableInfo() {
        // Puedes implementar aquí la actualización de información sobre los datos mostrados
        // Ejemplo: "Mostrando 1-5 de 15 registros"
    }
}

3. Inicialización de la tabla

Finalmente, inicializamos nuestra tabla dinámica cuando el DOM esté listo:

document.addEventListener('DOMContentLoaded', () => {
    const dynamicTable = new DynamicTable('dynamic-table', sampleData);
    
    // Opcional: puedes exponer la instancia para depuración
    window.dynamicTable = dynamicTable;
});

Demostración Funcional

A continuación puedes ver e interactuar con la implementación completa:

Nombre Edad Departamento Estado

Mejoras y Extensiones

La implementación básica puede extenderse con características adicionales:

1. Filtros Avanzados

// Ejemplo de filtro por rango de edad
addRangeFilter(minId, maxId, columnKey) {
    const minValue = document.getElementById(minId).value;
    const maxValue = document.getElementById(maxId).value;
    
    this.filteredData = this.filteredData.filter(item => {
        const value = parseInt(item[columnKey]);
        if (minValue && value < parseInt(minValue)) return false;
        if (maxValue && value > parseInt(maxValue)) return false;
        return true;
    });
}

2. Exportación de Datos

exportToCSV() {
    const headers = Object.keys(this.data[0]);
    const csvRows = [
        headers.join(','),
        ...this.filteredData.map(row => 
            headers.map(header => 
                `"${String(row[header]).replace(/"/g, '""')}"`
            ).join(',')
        )
    ];
    
    const csvContent = csvRows.join('\n');
    const blob = new Blob([csvContent], { type: 'text/csv' });
    const url = URL.createObjectURL(blob);
    
    const a = document.createElement('a');
    a.href = url;
    a.download = 'datos_filtrados.csv';
    a.click();
    
    URL.revokeObjectURL(url);
}

3. Carga de Datos desde API

async loadDataFromAPI(url) {
    try {
        const response = await fetch(url);
        if (!response.ok) throw new Error('Error al cargar datos');
        
        this.data = await response.json();
        this.filteredData = [...this.data];
        this.currentPage = 1;
        
        this.renderTable();
        this.renderPagination();
    } catch (error) {
        console.error('Error:', error);
        // Mostrar mensaje de error al usuario
    }
}

Consejo: Estas extensiones pueden implementarse como métodos adicionales de la clase DynamicTable y ser llamadas cuando sea necesario, manteniendo el código organizado y modular.

Conclusión

En este manual hemos creado una tabla dinámica completa con filtros avanzados, paginación y ordenamiento, todo con un diseño moderno basado en el tema Dracula. La implementación utiliza JavaScript moderno (ES6+) con un enfoque orientado a objetos que facilita el mantenimiento y la extensión del código.

Las tablas dinámicas son componentes poderosos que pueden mejorar significativamente la experiencia del usuario cuando se trabaja con grandes conjuntos de datos. La implementación presentada aquí sirve como base sólida que puede ser adaptada y extendida según las necesidades específicas de tu proyecto.

Recuerda: Siempre considera el rendimiento cuando trabajes con grandes volúmenes de datos. Para conjuntos de datos muy grandes (miles de registros), considera implementar:

  • Virtualización de listas para renderizar solo los elementos visibles
  • Paginación del lado del servidor
  • Web Workers para procesamiento en segundo plano