Implementación moderna con TypeScript y tema Dracula
TypeScript es un superset tipado de JavaScript que compila a JavaScript puro. Al usar TypeScript para nuestras tablas dinámicas, obtenemos:
Nota: Este manual asume que estás familiarizado con los conceptos básicos de TypeScript. Si no es así, te recomendamos revisar la documentación oficial primero.
Definamos primero nuestras interfaces y tipos para la tabla dinámica:
// Definición de tipos
interface TableData {
nombre: string;
edad: number;
departamento: string;
estado: 'Activo' | 'Inactivo';
}
type SortDirection = 'asc' | 'desc';
class DynamicTable {
private table: HTMLTableElement;
private data: TableData[];
private filteredData: TableData[];
private itemsPerPage: number;
private currentPage: number;
private sortColumn: number | null;
private sortDirection: SortDirection;
constructor(tableId: string, data: TableData[], itemsPerPage: number = 5) {
const tableElement = document.getElementById(tableId);
if (!tableElement || !(tableElement instanceof HTMLTableElement)) {
throw new Error(`Elemento con ID ${tableId} no encontrado o no es una tabla`);
}
this.table = tableElement;
this.data = data;
this.filteredData = [...data];
this.itemsPerPage = itemsPerPage;
this.currentPage = 1;
this.sortColumn = null;
this.sortDirection = 'asc';
this.init();
}
private init(): void {
this.renderTable();
this.setupEventListeners();
this.renderPagination();
}
// ... otros métodos
}
Al definir nuestras interfaces y tipos:
TableData
TableData
documenta claramente qué estructura deben tener los datosPrimero, configura tu proyecto TypeScript (tsconfig.json
):
{
"compilerOptions": {
"target": "ES6",
"module": "ESNext",
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Aquí está la implementación completa con TypeScript:
// src/dynamic-table.ts
interface TableData {
nombre: string;
edad: number;
departamento: string;
estado: 'Activo' | 'Inactivo';
}
type SortDirection = 'asc' | 'desc';
type ColumnKey = keyof TableData;
export class DynamicTable {
private table: HTMLTableElement;
private data: TableData[];
private filteredData: TableData[];
private itemsPerPage: number;
private currentPage: number;
private sortColumn: number | null;
private sortDirection: SortDirection;
private columnKeys: ColumnKey[];
constructor(tableId: string, data: TableData[], itemsPerPage: number = 5) {
const tableElement = document.getElementById(tableId);
if (!tableElement || !(tableElement instanceof HTMLTableElement)) {
throw new Error(`Elemento con ID ${tableId} no encontrado o no es una tabla`);
}
this.table = tableElement;
this.data = this.validateData(data);
this.filteredData = [...this.data];
this.itemsPerPage = itemsPerPage;
this.currentPage = 1;
this.sortColumn = null;
this.sortDirection = 'asc';
this.columnKeys = ['nombre', 'edad', 'departamento', 'estado'];
this.init();
}
private validateData(data: any[]): TableData[] {
return data.map((item, index) => {
if (typeof item.nombre !== 'string') {
console.warn(`Ítem ${index}: nombre no es string`);
}
if (typeof item.edad !== 'number') {
console.warn(`Ítem ${index}: edad no es número`);
}
if (typeof item.departamento !== 'string') {
console.warn(`Ítem ${index}: departamento no es string`);
}
if (item.estado !== 'Activo' && item.estado !== 'Inactivo') {
console.warn(`Ítem ${index}: estado no es 'Activo' o 'Inactivo'`);
}
return {
nombre: String(item.nombre),
edad: Number(item.edad),
departamento: String(item.departamento),
estado: item.estado === 'Activo' ? 'Activo' : 'Inactivo'
};
});
}
private init(): void {
this.renderTable();
this.setupEventListeners();
this.renderPagination();
}
private renderTable(): void {
const tbody = this.table.querySelector('tbody');
if (!tbody) return;
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();
}
private setupEventListeners(): void {
const globalSearch = document.getElementById('global-search');
const columnFilter = document.getElementById('column-filter');
const valueFilter = document.getElementById('value-filter');
const resetFilters = document.getElementById('reset-filters');
const prevPage = document.getElementById('prev-page');
const nextPage = document.getElementById('next-page');
if (globalSearch instanceof HTMLInputElement) {
globalSearch.addEventListener('input', () => this.applyFilters());
}
if (columnFilter instanceof HTMLSelectElement) {
columnFilter.addEventListener('change', () => this.applyFilters());
}
if (valueFilter instanceof HTMLInputElement) {
valueFilter.addEventListener('input', () => this.applyFilters());
}
if (resetFilters instanceof HTMLButtonElement) {
resetFilters.addEventListener('click', () => this.resetFilters());
}
this.table.querySelectorAll('th').forEach((th, index) => {
th.addEventListener('click', () => this.sortTable(index));
});
if (prevPage instanceof HTMLButtonElement) {
prevPage.addEventListener('click', () => this.prevPage());
}
if (nextPage instanceof HTMLButtonElement) {
nextPage.addEventListener('click', () => this.nextPage());
}
}
private applyFilters(): void {
const globalSearch = document.getElementById('global-search');
const columnFilter = document.getElementById('column-filter');
const valueFilter = document.getElementById('value-filter');
const globalSearchValue = globalSearch instanceof HTMLInputElement ? globalSearch.value.toLowerCase() : '';
const columnFilterValue = columnFilter instanceof HTMLSelectElement ? columnFilter.value : '';
const valueFilterValue = valueFilter instanceof HTMLInputElement ? valueFilter.value.toLowerCase() : '';
this.filteredData = this.data.filter(item => {
// Filtro global
if (globalSearchValue) {
const matches = Object.values(item).some(val =>
String(val).toLowerCase().includes(globalSearchValue)
);
if (!matches) return false;
}
// Filtro por columna específica
if (columnFilterValue && valueFilterValue) {
const columnIndex = parseInt(columnFilterValue);
if (isNaN(columnIndex) return true;
const columnKey = this.columnKeys[columnIndex];
if (!columnKey) return true;
if (!String(item[columnKey]).toLowerCase().includes(valueFilterValue)) {
return false;
}
}
return true;
});
this.currentPage = 1;
this.renderTable();
this.renderPagination();
}
private resetFilters(): void {
const globalSearch = document.getElementById('global-search');
const columnFilter = document.getElementById('column-filter');
const valueFilter = document.getElementById('value-filter');
if (globalSearch instanceof HTMLInputElement) globalSearch.value = '';
if (columnFilter instanceof HTMLSelectElement) columnFilter.value = '';
if (valueFilter instanceof HTMLInputElement) valueFilter.value = '';
this.applyFilters();
}
private sortTable(columnIndex: number): void {
const columnKey = this.columnKeys[columnIndex];
if (!columnKey) return;
// 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) => {
const valueA = a[columnKey];
const valueB = b[columnKey];
// Para ordenamiento numérico si la columna es edad
if (columnKey === 'edad') {
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 (!icon) return;
if (index === columnIndex) {
icon.textContent = this.sortDirection === 'asc' ? '↑' : '↓';
} else {
icon.textContent = '↕';
}
});
this.renderTable();
}
private renderPagination(): void {
const pageNumbers = document.getElementById('page-numbers');
const prevPage = document.getElementById('prev-page');
const nextPage = document.getElementById('next-page');
if (!pageNumbers || !prevPage || !nextPage) return;
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.toString();
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);
}
if (prevPage instanceof HTMLButtonElement) {
prevPage.disabled = this.currentPage === 1;
}
if (nextPage instanceof HTMLButtonElement) {
nextPage.disabled = this.currentPage === totalPages || totalPages === 0;
}
}
private prevPage(): void {
if (this.currentPage > 1) {
this.currentPage--;
this.renderTable();
this.renderPagination();
}
}
private nextPage(): void {
const totalPages = Math.ceil(this.filteredData.length / this.itemsPerPage);
if (this.currentPage < totalPages) {
this.currentPage++;
this.renderTable();
this.renderPagination();
}
}
private updateTableInfo(): void {
// Implementación opcional para mostrar información sobre los datos mostrados
const startItem = (this.currentPage - 1) * this.itemsPerPage + 1;
const endItem = Math.min(startItem + this.itemsPerPage - 1, this.filteredData.length);
console.log(`Mostrando ${startItem}-${endItem} de ${this.filteredData.length} registros`);
}
// Métodos públicos para manipulación externa
public updateData(newData: TableData[]): void {
this.data = this.validateData(newData);
this.filteredData = [...this.data];
this.currentPage = 1;
this.renderTable();
this.renderPagination();
}
public setItemsPerPage(count: number): void {
this.itemsPerPage = Math.max(1, count);
this.currentPage = 1;
this.renderTable();
this.renderPagination();
}
}
Para usar la tabla dinámica en tu aplicación:
// src/main.ts
import { DynamicTable } from './dynamic-table';
// Datos de ejemplo con tipo TableData
const sampleData: TableData[] = [
{ 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" },
// ... más datos
];
document.addEventListener('DOMContentLoaded', () => {
try {
const dynamicTable = new DynamicTable('dynamic-table', sampleData);
// Opcional: exponer para depuración
(window as any).dynamicTable = dynamicTable;
} catch (error) {
console.error('Error al inicializar la tabla:', error);
// Mostrar mensaje de error al usuario
}
});
Implementación completa con TypeScript compilado a JavaScript:
Nombre | Edad | Departamento | Estado |
---|
Con TypeScript podemos asegurar que los datos cumplan con la estructura esperada:
interface TableData {
nombre: string;
edad: number;
departamento: string;
estado: 'Activo' | 'Inactivo';
}
// El compilador TypeScript mostrará errores si los datos no coinciden
const badData: TableData[] = [
{ nombre: "Test", edad: "30", departamento: "TI", estado: "Activo" }, // Error: edad debe ser number
{ nombre: "Test 2", edad: 30, departamento: "TI", estado: "Pendiente" } // Error: estado no válido
];
Los editores modernos proporcionan autocompletado basado en los tipos:
// Al escribir "item." el editor sugerirá: nombre, edad, departamento, estado
this.filteredData.forEach(item => {
console.log(item.nombre); // El editor sabe que nombre es string
console.log(item.edad.toFixed(2)); // Sabe que edad es number
});
Si cambias la estructura de los datos, TypeScript te indicará todos los lugares que necesitan actualización:
// Si añadimos un nuevo campo a TableData:
interface TableData {
// ...
fechaIngreso: Date; // Nuevo campo
}
// TypeScript mostrará errores en todos los lugares donde se usan TableData
// hasta que se proporcione el nuevo campo
Consejo Profesional: Para proyectos grandes, considera usar typescript-eslint
con reglas estrictas para mantener una alta calidad de código.
La implementación de tablas dinámicas con TypeScript ofrece numerosas ventajas sobre JavaScript puro:
El ejemplo presentado en este manual proporciona una base sólida que puedes adaptar a tus necesidades específicas, añadiendo más características como:
Recurso Adicional: Para llevar esta implementación al siguiente nivel, considera usar librerías como: