🟦 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íaCódigo copiableSimulador funcionalVBA → TypeScriptOffice ScriptsQuiz 10/50

📖 Teoría paso a paso

El formularioEn VBA es un UserForm dentro de Excel. Aquí es un formulario HTML (<input> y <button>) que se abre en cualquier navegador, también en el celular.
La "hoja" ProductosEn VBA los datos viven en celdas. Aquí viven en una lista de objetos guardada en el navegador con localStorage: cada objeto es una fila y cada propiedad es una columna.
TypeScriptEs JavaScript con tipos: le dices que 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.
CRUDLas mismas 4 operaciones: Crear, Leer (buscar), Actualizar y Eliminar (Create, Read, Update, Delete), más un botón Limpiar.
¿Y dentro de Excel? Excel para la web también usa TypeScript, con Office Scripts. No muestra formularios, pero puede hacer el mismo CRUD sobre la hoja Productos: está en la sección Office Scripts.

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.

TypeScript
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
⚠️ Igual que en VBA: el nombre "productos" es como el nombre de la hoja. Si un archivo guarda con un nombre y otro lee con otro, los datos "desaparecen".

Los id de cada control son los mismos nombres del UserForm (txtNombre, btnCrear…), así el código se lee igual en los dos lenguajes.

HTML
<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>
💡 onsubmit="return false" evita que el formulario recargue la página al presionar Enter.

Las mismas dos ideas de VBA (BuscarFilaPorID y LimpiarCampos), más leer y guardar la lista, leer un campo y el equivalente de Val().

TypeScript
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.stringify convierte la lista en texto para guardarla; JSON.parse la vuelve a convertir en lista.
  • ?? "[]": si todavía no hay nada guardado, empieza con una lista vacía.
  • findIndex recorre la lista como el For i = 2 To ultimaFila de VBA, pero en una línea. Devuelve -1 si no encuentra (VBA devolvía 0).

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.

TypeScript
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();
}
  • return sale de la función, como Exit 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.
TypeScript
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.");
}
💡 String(...): un <input> siempre guarda texto. TypeScript te obliga a convertir el número; VBA lo hacía sin avisar.
TypeScript
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();
}
⚠️ Diferencia con Crear: Crear agrega un objeto nuevo con push; Actualizar reemplaza el que está en la posición i. Ninguno de los dos deja cambiar el ID.
TypeScript
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();
    }
}
❌ Regla de oro (igual que en VBA): nunca elimines sin preguntar. 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:

TypeScript
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:

Terminal
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.

💡 El signo ! en 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íntomaCausa 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 navegadorlocalStorage 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 venNo se volvió a compilar: la página carga app.js, no app.ts.
Se crean registros duplicadosSe 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.

frmProductos (formulario web)✕

📗 "Hoja" Productos (localStorage)
IDNombreDescripciónIdCategoriaEntradasSalidasCantidad
Bitácora: aquí verás qué función del código se ejecutó.
Prueba esto: carga los ejemplos, escribe 2 en ID y presiona Buscar; cambia la Cantidad y presiona Actualizar; después elimina el ID 1 y crea un producto nuevo: verás que recibe el ID 4 y no repite el 3.

Archivo completo app.ts

TypeScript · app.ts

            

JavaScript que genera el compilador (app.js)

JavaScript · 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

IdeaVBA (UserForm)TypeScript (página web)
ProcedimientoPrivate Sub btnCrear_Click() … End Subfunction crear(): void { … }
Variable con tipoDim fila As Longconst i: number = …
Estructura de una filaEncabezados en la fila 1interface Producto { … }
Dónde viven los datosThisWorkbook.Sheets("Productos")localStorage.getItem("productos")
Leer un controlMe.txtNombre.Valuecampo("txtNombre").value
Texto a númeroVal(Me.txtCantidad.Value)val(campo("txtCantidad").value)
Quitar espaciosTrim(x)x.trim()
Buscar por IDFor i = 2 To ultimaFila … Nextlista.findIndex(p => p.id === id)
"No encontrado"0-1
Agregar filaCells(nuevaFila, 1).Value = …lista.push({ … })
Borrar filaws.Rows(fila).Deletelista.splice(i, 1)
Salir antesExit Subreturn
MensajeMsgBox "…"mostrarMensaje("…") o alert("…")
ConfirmarMsgBox(…, vbYesNo) = vbYesconfirm("…")
Evento de botónbtnCrear_ClickaddEventListener("click", crear)
Igualdad==== (compara valor y tipo)
Abrir el formulariofrmProductos.ShowAbrir index.html en el navegador
Guardar el archivoLibro .xlsmapp.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.

Importante: este código se ejecuta en Excel para la web con una cuenta de Microsoft 365; aquí solo se muestra para copiarlo.
Office Scripts (TypeScript)
/**
 * 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.";
    }
}
DiferenciaPágina web (TypeScript)Office Scripts
Dónde correCualquier navegadorExcel para la web (Microsoft 365)
FormularioFormulario HTML propioNo tiene: los datos llegan como parámetros
Dónde se guardan los datoslocalStorage (o un servidor)La hoja Productos del libro
Equivalente de MsgBoxmostrarMensaje() / confirm()return "texto" (Excel lo muestra al terminar)
AutomatizaciónEventos de botonesBotó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

TypeScriptJavaScript con tipos. Se compila a JavaScript para que el navegador lo ejecute.
CompilarTraducir app.ts a app.js con el comando tsc, revisando que los tipos sean correctos.
interfaceDescribe la forma de un objeto: qué propiedades tiene y de qué tipo.
number / string / booleanTipos básicos: número, texto y verdadero/falso.
voidIndica que una función no devuelve ningún valor, como un Sub de VBA.
Arreglo (array)Lista ordenada de elementos: Producto[] es una lista de productos.
ObjetoConjunto de propiedades con nombre, como una fila con sus columnas.
localStorageAlmacén del navegador que guarda texto aunque se cierre la página. Solo vive en ese navegador.
JSONFormato de texto para guardar objetos y listas. JSON.stringify guarda; JSON.parse lee.
DOMLa página vista como objetos que el código puede leer y modificar.
getElementByIdBusca un elemento de la página por su id, como Me.txtNombre en VBA.
addEventListenerConecta un evento (como un clic) con una función.
findIndexDevuelve la posición del primer elemento que cumple una condición, o -1.
pushAgrega un elemento al final de un arreglo.
spliceQuita elementos de un arreglo desde una posición.
=== (igualdad estricta)Compara valor y tipo: "5" === 5 es falso.
?? (coalescencia nula)Usa un valor de respaldo cuando el primero es null o undefined.
Office ScriptsScripts en TypeScript que automatizan Excel para la web.

🎬 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: