🟦 CRUD en TypeScript con formulario de productos
El gemelo del CRUD en VBA con UserForm: la misma hoja Productos, los mismos 7 campos y los mismos 5 botones, ahora con TypeScript y JavaScript en una página web.
📖 Teoría paso a paso
<input> y <button>) que se abre en cualquier navegador, también en el celular.localStorage: cada objeto es una fila y cada propiedad es una columna.cantidad es un número y el compilador te avisa si intentas guardar un texto. Los tipos se borran al compilar: el navegador ejecuta JavaScript.En VBA, la fila 1 tiene los encabezados (ID, Nombre, Descripcion, IdCategoria, Entradas, Salidas, Cantidad). En TypeScript eso se declara con una interfaz: la forma que debe tener cada producto.
interface Producto {
id: number;
nombre: string;
descripcion: string;
idCategoria: number;
entradas: number;
salidas: number;
cantidad: number;
}
const CLAVE = "productos"; // nombre de la "hoja" donde se guardan los datos
Los id de cada control son los mismos nombres del UserForm (txtNombre, btnCrear…), así el código se lee igual en los dos lenguajes.
<form onsubmit="return false">
<label>ID <input id="txtID" type="number"></label>
<label>Nombre <input id="txtNombre"></label>
<label>Descripción <input id="txtDescripcion"></label>
<label>ID Categoría <input id="txtIdCategoria" type="number"></label>
<label>Entradas <input id="txtEntradas" type="number"></label>
<label>Salidas <input id="txtSalidas" type="number"></label>
<label>Cantidad <input id="txtCantidad" type="number"></label>
<button id="btnCrear">Crear</button>
<button id="btnBuscar">Buscar</button>
<button id="btnActualizar">Actualizar</button>
<button id="btnEliminar">Eliminar</button>
<button id="btnLimpiar">Limpiar</button>
<p id="mensaje"></p>
</form>
<table>
<thead><tr><th>ID</th><th>Nombre</th><th>Descripción</th><th>IdCategoria</th><th>Entradas</th><th>Salidas</th><th>Cantidad</th></tr></thead>
<tbody id="tablaProductos"></tbody>
</table>
<script src="app.js"></script>
Las mismas dos ideas de VBA (BuscarFilaPorID y LimpiarCampos), más leer y guardar la lista, leer un campo y el equivalente de Val().
function leerProductos(): Producto[] {
return JSON.parse(localStorage.getItem(CLAVE) ?? "[]") as Producto[];
}
function guardarProductos(lista: Producto[]): void {
localStorage.setItem(CLAVE, JSON.stringify(lista));
}
function campo(id: string): HTMLInputElement {
return document.getElementById(id) as HTMLInputElement;
}
// Igual que Val() de VBA: texto vacío o inválido -> 0
function val(texto: string): number {
const n = parseFloat(texto);
return isNaN(n) ? 0 : n;
}
// Igual que BuscarFilaPorID: devuelve la posición o -1 si no existe
function buscarIndicePorID(lista: Producto[], id: number): number {
return lista.findIndex(p => p.id === id);
}
function limpiarCampos(): void {
["txtID", "txtNombre", "txtDescripcion", "txtIdCategoria",
"txtEntradas", "txtSalidas", "txtCantidad"].forEach(id => campo(id).value = "");
campo("txtNombre").focus();
}
JSON.stringifyconvierte la lista en texto para guardarla;JSON.parsela vuelve a convertir en lista.?? "[]": si todavía no hay nada guardado, empieza con una lista vacía.findIndexrecorre la lista como elFor i = 2 To ultimaFilade VBA, pero en una línea. Devuelve-1si no encuentra (VBA devolvía0).
El código completo también trae mostrarMensaje() (en lugar de MsgBox) y pintarTabla(), que redibuja la tabla; los verás en el archivo completo del simulador.
function crear(): void {
if (campo("txtNombre").value.trim() === "") {
mostrarMensaje("Escribe un nombre antes de guardar.", "error");
return;
}
const lista = leerProductos();
const nuevoId = lista.length === 0 ? 1 : Math.max(...lista.map(p => p.id)) + 1;
lista.push({
id: nuevoId,
nombre: campo("txtNombre").value.trim(),
descripcion: campo("txtDescripcion").value.trim(),
idCategoria: val(campo("txtIdCategoria").value),
entradas: val(campo("txtEntradas").value),
salidas: val(campo("txtSalidas").value),
cantidad: val(campo("txtCantidad").value)
});
guardarProductos(lista);
mostrarMensaje("Registro creado con ID " + nuevoId + ".");
limpiarCampos();
pintarTabla();
}
returnsale de la función, comoExit Sub.lista.push({...})agrega una fila nueva al final, como escribir en la primera fila vacía.- Mejora frente al VBA: el ID nuevo es el mayor ID + 1. En VBA se usaba
nuevaFila - 1, que puede repetir un ID después de eliminar una fila.
function buscar(): void {
if (campo("txtID").value.trim() === "") {
mostrarMensaje("Escribe el ID que quieres buscar.", "error");
return;
}
const lista = leerProductos();
const i = buscarIndicePorID(lista, val(campo("txtID").value));
if (i === -1) {
mostrarMensaje("No existe ningún registro con ese ID.", "error");
return;
}
const p = lista[i];
campo("txtNombre").value = p.nombre;
campo("txtDescripcion").value = p.descripcion;
campo("txtIdCategoria").value = String(p.idCategoria);
campo("txtEntradas").value = String(p.entradas);
campo("txtSalidas").value = String(p.salidas);
campo("txtCantidad").value = String(p.cantidad);
mostrarMensaje("Registro encontrado.");
}
<input> siempre guarda texto. TypeScript te obliga a convertir el número; VBA lo hacía sin avisar.function actualizar(): void {
if (campo("txtID").value.trim() === "") {
mostrarMensaje("Primero busca un registro por su ID.", "error");
return;
}
const lista = leerProductos();
const i = buscarIndicePorID(lista, val(campo("txtID").value));
if (i === -1) {
mostrarMensaje("Ese ID no existe. No hay nada que actualizar.", "error");
return;
}
lista[i] = {
id: lista[i].id, // el ID nunca se cambia
nombre: campo("txtNombre").value.trim(),
descripcion: campo("txtDescripcion").value.trim(),
idCategoria: val(campo("txtIdCategoria").value),
entradas: val(campo("txtEntradas").value),
salidas: val(campo("txtSalidas").value),
cantidad: val(campo("txtCantidad").value)
};
guardarProductos(lista);
mostrarMensaje("Registro actualizado.");
pintarTabla();
}
function eliminar(): void {
const lista = leerProductos();
const i = buscarIndicePorID(lista, val(campo("txtID").value));
if (i === -1) {
mostrarMensaje("Ese ID no existe.", "error");
return;
}
if (confirm("¿Seguro que quieres eliminar este registro?")) {
lista.splice(i, 1);
guardarProductos(lista);
mostrarMensaje("Registro eliminado.");
limpiarCampos();
pintarTabla();
}
}
confirm() es el MsgBox ... vbYesNo del navegador. splice(i, 1) quita 1 elemento en la posición i, como ws.Rows(fila).Delete.En VBA cada botón tiene su evento btnCrear_Click. En TypeScript se conectan con addEventListener:
document.getElementById("btnCrear")!.addEventListener("click", crear);
document.getElementById("btnBuscar")!.addEventListener("click", buscar);
document.getElementById("btnActualizar")!.addEventListener("click", actualizar);
document.getElementById("btnEliminar")!.addEventListener("click", eliminar);
document.getElementById("btnLimpiar")!.addEventListener("click", limpiarCampos);
pintarTabla();
El navegador no entiende TypeScript: hay que compilarlo a JavaScript. Con Node.js instalado:
npm install -g typescript tsc app.ts --target es2020 --lib dom,es2020 --strict
Eso crea app.js junto a app.ts. Abre index.html y el CRUD funciona. En la sección del simulador puedes descargar los tres archivos listos.
getElementById("btnCrear")! le dice a TypeScript "confía en mí, este elemento existe". Si el id está mal escrito, el error aparece al cargar la página.| Síntoma | Causa probable |
|---|---|
| "Cannot read properties of null (reading 'value')" | Un id del HTML no coincide con el del código (por ejemplo txtnombre en vez de txtNombre). Equivale al error de VBA de un control con otro nombre. |
| Los datos desaparecen al cambiar de navegador | localStorage guarda en ese navegador y ese equipo. Para compartir datos entre personas hace falta un servidor o una base de datos. |
| Error de TypeScript "Type 'string' is not assignable to type 'number'" | Se intentó guardar el texto de un input en un campo numérico sin convertirlo con val(). |
Los cambios en app.ts no se ven | No se volvió a compilar: la página carga app.js, no app.ts. |
| Se crean registros duplicados | Se presionó Crear en vez de Actualizar sobre un registro existente (igual que en VBA). |
🧪 Simulador: el CRUD funcionando
Este formulario ejecuta de verdad el código JavaScript que produjo el compilador de TypeScript con el archivo app.ts de esta lección. Crea, busca, actualiza y elimina productos: se guardan en tu navegador.
| ID | Nombre | Descripción | IdCategoria | Entradas | Salidas | Cantidad |
|---|
Archivo completo app.ts
JavaScript que genera el compilador (app.js)
Descarga el proyecto listo para abrir en Visual Studio Code:
Guarda los cuatro archivos en la misma carpeta y abre index.html: funciona sin instalar nada. Si editas app.ts, compila con tsc (usa el tsconfig.json) para regenerar app.js.
🔁 De VBA a TypeScript: tabla de equivalencias
| Idea | VBA (UserForm) | TypeScript (página web) |
|---|---|---|
| Procedimiento | Private Sub btnCrear_Click() … End Sub | function crear(): void { … } |
| Variable con tipo | Dim fila As Long | const i: number = … |
| Estructura de una fila | Encabezados en la fila 1 | interface Producto { … } |
| Dónde viven los datos | ThisWorkbook.Sheets("Productos") | localStorage.getItem("productos") |
| Leer un control | Me.txtNombre.Value | campo("txtNombre").value |
| Texto a número | Val(Me.txtCantidad.Value) | val(campo("txtCantidad").value) |
| Quitar espacios | Trim(x) | x.trim() |
| Buscar por ID | For i = 2 To ultimaFila … Next | lista.findIndex(p => p.id === id) |
| "No encontrado" | 0 | -1 |
| Agregar fila | Cells(nuevaFila, 1).Value = … | lista.push({ … }) |
| Borrar fila | ws.Rows(fila).Delete | lista.splice(i, 1) |
| Salir antes | Exit Sub | return |
| Mensaje | MsgBox "…" | mostrarMensaje("…") o alert("…") |
| Confirmar | MsgBox(…, vbYesNo) = vbYes | confirm("…") |
| Evento de botón | btnCrear_Click | addEventListener("click", crear) |
| Igualdad | = | === (compara valor y tipo) |
| Abrir el formulario | frmProductos.Show | Abrir index.html en el navegador |
| Guardar el archivo | Libro .xlsm | app.ts compilado a app.js |
📊 El mismo CRUD dentro de Excel con Office Scripts
Office Scripts es TypeScript que corre en Excel para la web (Automatizar → Nuevo script). No puede mostrar un formulario como el UserForm, así que los datos llegan como parámetros: Excel los pide al ejecutar el script, o se los pasa un flujo de Power Automate. Trabaja sobre la misma hoja Productos del CRUD en VBA, con los encabezados en A1:G1.
/**
* CRUD de la hoja "Productos" con Office Scripts (Excel para la web).
* accion: "crear" | "buscar" | "actualizar" | "eliminar"
*/
function main(
workbook: ExcelScript.Workbook,
accion: string,
id?: number,
nombre?: string,
descripcion?: string,
idCategoria?: number,
entradas?: number,
salidas?: number,
cantidad?: number
): string {
const hoja = workbook.getWorksheet("Productos");
if (!hoja) {
return "No existe la hoja Productos.";
}
// Todas las celdas usadas; la fila 0 son los encabezados
const datos = hoja.getUsedRange().getValues();
const fila = datos.findIndex((f, i) => i > 0 && Number(f[0]) === (id ?? 0)); // -1 si no existe
const valores = [nombre ?? "", descripcion ?? "", idCategoria ?? 0, entradas ?? 0, salidas ?? 0, cantidad ?? 0];
switch (accion) {
case "crear": {
if ((nombre ?? "").trim() === "") {
return "Escribe un nombre antes de guardar.";
}
const ids = datos.slice(1).map(f => Number(f[0]));
const nuevoId = ids.length === 0 ? 1 : Math.max(...ids) + 1;
// datos.length es la primera fila vacía (las filas empiezan en 0)
hoja.getRangeByIndexes(datos.length, 0, 1, 7).setValues([[nuevoId, ...valores]]);
return "Registro creado con ID " + nuevoId;
}
case "buscar": {
if (fila === -1) {
return "No existe ningún registro con ese ID.";
}
return "Encontrado: " + datos[fila].join(" | ");
}
case "actualizar": {
if (fila === -1) {
return "Ese ID no existe. No hay nada que actualizar.";
}
// Columnas B a G de esa fila; el ID (columna A) no se toca
hoja.getRangeByIndexes(fila, 1, 1, 6).setValues([valores]);
return "Registro actualizado.";
}
case "eliminar": {
if (fila === -1) {
return "Ese ID no existe.";
}
hoja.getRangeByIndexes(fila, 0, 1, 7).delete(ExcelScript.DeleteShiftDirection.up);
return "Registro eliminado.";
}
default:
return "Acción no válida. Usa crear, buscar, actualizar o eliminar.";
}
}
| Diferencia | Página web (TypeScript) | Office Scripts |
|---|---|---|
| Dónde corre | Cualquier navegador | Excel para la web (Microsoft 365) |
| Formulario | Formulario HTML propio | No tiene: los datos llegan como parámetros |
| Dónde se guardan los datos | localStorage (o un servidor) | La hoja Productos del libro |
| Equivalente de MsgBox | mostrarMensaje() / confirm() | return "texto" (Excel lo muestra al terminar) |
| Automatización | Eventos de botones | Botón en la hoja o Power Automate |
¿Quieres más Office Scripts? Mira Office Scripts en Excel (40 scripts con su equivalente en VBA).
📚 Diccionario
🎬 Videos de apoyo
CRUD con LocalStorage y JavaScript (Bluuweb)
Cómo hacer un CRUD en JavaScript con LocalStorage, parte 1 (YamDevs)
TypeScript: tutorial desde cero (midudev)
Excel graba macros VBA y scripts de TypeScript a la vez (EXCELeINFO)
📝 Cuestionario
10 preguntas aleatorias de un banco de 50. Calificación de 0 a 100.
Tu calificación: