Implementación moderna con tema Dracula
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.
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>
La estructura se compone de tres partes principales:
Importante: Los atributos data-column
en las cabeceras de la tabla son cruciales para el funcionamiento del filtrado por columnas y el ordenamiento.
Ahora implementaremos la lógica JavaScript que hará que nuestra tabla sea dinámica.
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" }
];
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"
}
}
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;
});
A continuación puedes ver e interactuar con la implementación completa:
Nombre | Edad | Departamento | Estado |
---|
La implementación básica puede extenderse con características adicionales:
// 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;
});
}
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);
}
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.
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: