Las hojas de cálculo se han convertido en una parte integral de la computación moderna. Permiten a los usuarios organizar, manipular y analizar datos en un formato tabular. Aplicaciones como Google Sheets han establecido el estándar para hojas de cálculo potentes e interactivas.
En este artículo de blog, te guiaremos a través del proceso de construir una aplicación de hoja de cálculo utilizando JavaScript. Nos centraremos en conceptos clave de programación, exploraremos características de JavaScript e incluiremos fragmentos de código detallados con explicaciones.
Todo el código fuente está disponible aquí en mi Codepen.
¿Qué es Google Spreadsheet?
Google Spreadsheet es una aplicación basada en la web que permite a los usuarios crear, editar y colaborar en hojas de cálculo en línea. Proporciona características como fórmulas, validación de datos, gráficos y formato condicional.
Nuestro proyecto emula algunas características fundamentales de Google Spreadsheet, centrándose en:
- Celdas editables.
- Parseo y evaluación de fórmulas.
- Actualizaciones en tiempo real a través de un modelo Pub/Sub.
- Navegación por teclado y selección de celdas.
- Evaluación dinámica de dependencias entre celdas.
Características de este proyecto
- Celdas editables: Permite a los usuarios ingresar texto o ecuaciones en las celdas.
- Soporte para fórmulas: Procesa fórmulas que comienzan con
=
y evalúa expresiones. - Actualizaciones en vivo: Los cambios en celdas dependientes desencadenan actualizaciones utilizando un modelo de Pub/Sub.
- Navegación por teclado: Permite moverse entre celdas usando las teclas de flecha.
- Evaluación dinámica: Asegura actualizaciones en tiempo real para fórmulas dependientes de otras celdas.
- Manejo de errores: Proporciona mensajes de error significativos para entradas inválidas o dependencias circulares.
- Diseño escalable: Permite una fácil extensión para agregar más filas, columnas o características.
Componentes clave de la aplicación
1. Gestión de modos
const Mode = {
EDIT: 'edit',
DEFAULT: 'default'
};
Este enumerado define dos modos:
- EDICIÓN: Permite editar una celda seleccionada.
- POR DEFECTO: Permite la navegación e interacción sin editar.
¿Por qué usar modos?
Los modos simplifican la gestión del estado de la interfaz de usuario. Por ejemplo, en el modo POR DEFECTO, las entradas del teclado se mueven entre celdas, mientras que en el modo EDICIÓN, las entradas modifican el contenido de la celda.
2. Clase Pub/Sub
El modelo Pub/Sub maneja suscripciones y actualizaciones en vivo. Las celdas pueden suscribirse a otras celdas y actualizarse dinámicamente cuando cambian las dependencias.
class PubSub {
constructor() {
this.map = {};
}
get(source) {
let result = [];
let queue = [ (this.map[source] || [])];
while (queue.length) {
let next = queue.shift();
result.push(next.toUpperCase());
if (this.map[next]) queue.unshift(this.map[next]);
}
return result;
}
subscribeAll(sources, destination) {
sources.forEach((source) => {
this.map[source] = this.map[source] || [];
this.map[source].push(destination);
});
}
}
Características clave:
- Administración dinámica de dependencias: Rastrea las dependencias entre celdas.
- Propagación de actualizaciones: Actualiza celdas dependientes cuando cambian las celdas fuente.
- Búsqueda en anchura: Evita bucles infinitos rastreando todos los nodos dependientes.
Ejemplo de uso:
let ps = new PubSub();
ps.subscribeAll(['A1'], 'B1');
ps.subscribeAll(['B1'], 'C1');
console.log(ps.get('A1')); // Output: ['B1', 'C1']
3. Creación de Filas y Celdas
class Cell {
constructor(cell, row, col) {
cell.id = `${String.fromCharCode(col + 65)}${row}`;
cell.setAttribute('data-eq', '');
cell.setAttribute('data-value', '');
if (row > 0 && col > -1) cell.classList.add('editable');
cell.textContent = col === -1 ? row : '';
}
}
class Row {
constructor(row, r) {
for (let c = -1; c < 13; c++) {
new Cell(row.insertCell(), r, c);
}
}
}
Características clave:
- Generación de tablas dinámica: Permite añadir filas y columnas programáticamente.
- Identificación de celdas: Genera IDs basados en la posición (por ejemplo, A1, B2).
- Celdas editables: Las celdas son editables solo si son válidas (filas/columnas que no son encabezados).
¿Por qué usar filas y celdas dinámicas?
Este enfoque permite que el tamaño de la tabla sea escalable y flexible, soportando características como la adición de filas o columnas sin cambiar la estructura.
4. Manejo de Eventos para Interacción
addEventListeners() {
this.table.addEventListener('click', this.onCellClick.bind(this));
this.table.addEventListener('dblclick', this.onCellDoubleClick.bind(this));
window.addEventListener('keydown', this.onKeyDown.bind(this));
}
Características clave:
- Evento de clic: Selecciona o edita celdas.
- Evento de doble clic: Permite la edición de fórmulas.
- Evento de tecla presionada: Soporta navegación con las teclas de flecha.
5. Análisis y Evaluación de Fórmulas
function calcCell(expression) {
if (!expression) return 0;
return expression.split('+').reduce((sum, term) => {
let value = isNaN(term) ? getCellValue(term) : Number(term);
if (value === null) throw new Error(`Invalid cell: ${term}`);
return sum + Number(value);
}, 0);
}
Características clave:
- Cálculo dinámico: Calcula fórmulas que hacen referencia a otras celdas.
- Evaluación recursiva: Resuelve dependencias anidadas.
- Manejo de errores: Identifica referencias inválidas y dependencias circulares.
6. Manejo de Errores para la Entrada del Usuario
function isValidCell(str) {
let regex = /^[A-Z]{1}[0-9]+$/;
return regex.test(str);
}
Características clave:
- Validación: Asegura que las entradas hagan referencia a IDs de celdas válidas.
- Escalabilidad: Soporta la expansión dinámica de la tabla sin romper la validación.
Temas de JavaScript Cubiertos
1. Manejo de Eventos
Gestiona interacciones como clics y pulsaciones de teclas.
window.addEventListener('keydown', this.onKeyDown.bind(this));
2. Manipulación del DOM
Creación y modificación de elementos del DOM de manera dinámica.
let cell = document.createElement('td'); cell.appendChild(document.createTextNode('A1'));
3. Recursión
Procesa dependencias de manera dinámica.
function calcCell(str) { if (isNaN(str)) { return calcCell(getCellValue(str)); } }
4. Manejo de Errores
Detecta celdas no válidas y dependencias circulares.
if (!isValidCell(p)) throw new Error(`invalid cell ${p}`);
Conclusión
Este proyecto demuestra una poderosa hoja de cálculo utilizando JavaScript. Aprovecha el manejo de eventos, la recursión y los patrones Pub/Sub, sentando las bases para aplicaciones web complejas. Amplíelo agregando características como exportación de datos, gráficos o reglas de formato.
Referencias
Source:
https://dzone.com/articles/spreadsheet-application-javascript-guide