JavaScript de Cero a Experto / Eventos y listeners
Reto: Mi Bolsillo interactivo
Este es el hito del supercurso hasta ahora: tu primera APLICACIÓN completa. Formulario que valida y agrega, borrado por delegación, y un total que se recalcula solo gracias a un evento propio. Módulos 5, 6, 7 y 8 trabajando juntos en una sola página — exactamente como en el mundo real.
Reto
Mi Bolsillo interactivo
Qué debes construir
La app funciona así (pruébala — esta vista previa es LA META, completamente funcional):
<h1>Mi Bolsillo</h1>
<form id="registro">
<input id="nombre" placeholder="Gasto..." />
<input id="valor" type="number" placeholder="Valor" />
<button>Agregar</button>
</form>
<p id="error"></p>
<ul id="lista"></ul>
<p class="resumen">Total: <strong id="total"></strong></p>body { font-family: system-ui; padding: 24px; background: #0b1120; color: #f1f5f9; }
h1 { font-size: 24px; margin: 0 0 14px; }
form { display: flex; gap: 8px; }
input { padding: 10px 14px; border: 1px solid #2a3752; border-radius: 8px; background: #151e31; color: #f1f5f9; width: 130px; }
input:focus { outline: 2px solid #7c3aed; }
form button { padding: 10px 18px; border: 0; border-radius: 8px; background: #6d28d9; color: white; font-weight: 600; cursor: pointer; }
#error { color: #ef4444; font-size: 14px; min-height: 20px; margin: 8px 0; }
li { display: flex; align-items: center; gap: 10px; margin: 6px 0; color: #cbd5e1; }
.borrar { background: #151e31; color: #ef4444; border: 1px solid #2a3752; border-radius: 8px; padding: 2px 10px; cursor: pointer; font-weight: 600; }
.resumen { color: #94a3b8; }
.resumen strong { color: #f1f5f9; }const gastosIniciales = [
{ nombre: "Almuerzo", valor: 18000 },
{ nombre: "Bus", valor: 5800 },
{ nombre: "Café", valor: 4500 },
];
const cop = new Intl.NumberFormat("es-CO", {
style: "currency",
currency: "COP",
maximumFractionDigits: 0,
});
function crearItem(nombre, valor) {
const item = document.createElement("li");
item.dataset.valor = valor;
item.textContent = `${nombre} — ${cop.format(valor)} `;
const boton = document.createElement("button");
boton.className = "borrar";
boton.textContent = "✕";
item.append(boton);
return item;
}
function avisarCambio() {
document.dispatchEvent(new CustomEvent("bolsillo:cambio"));
}
document.addEventListener("bolsillo:cambio", () => {
const items = [...document.querySelectorAll("#lista li")];
const total = items.reduce((acum, li) => acum + Number(li.dataset.valor), 0);
document.querySelector("#total").textContent = cop.format(total);
});
const lista = document.querySelector("#lista");
lista.append(...gastosIniciales.map((g) => crearItem(g.nombre, g.valor)));
avisarCambio();
document.querySelector("#registro").addEventListener("submit", (event) => {
event.preventDefault();
const error = document.querySelector("#error");
const nombre = document.querySelector("#nombre").value.trim();
const valor = Number(document.querySelector("#valor").value);
if (nombre === "" || Number.isNaN(valor) || valor <= 0) {
error.textContent = "Escribe un nombre y un valor mayor que cero.";
return;
}
error.textContent = "";
lista.append(crearItem(nombre, valor));
avisarCambio();
event.target.reset();
document.querySelector("#nombre").focus();
});
lista.addEventListener("click", (event) => {
const boton = event.target.closest(".borrar");
if (boton === null) return;
boton.closest("li").remove();
avisarCambio();
});El HTML de partida trae el formulario, #error, la #lista vacía y el
#total (mismo markup de la vista previa). Requisitos:
- Render inicial: los 3 gastos del array
gastosIniciales(Almuerzo 18000, Bus 5800, Café 4500) se pintan al cargar con una funcióncrearItem—createElement, ceroinnerHTML— y UNA sola inserción. - Cada
liguarda su valor crudo endataset.valory trae un botón✕con claseborrar. - Agregar:
submit+preventDefault; nombre sin espacios sobrantes (trim) no vacío y valor numérico mayor que 0. Si falla: mensaje en#errory no se agrega nada. Si pasa: error limpio, item nuevo,form.reset()yfocus()al campo nombre. - Borrar por delegación: UN solo listener en la
ul(conclosesty su chequeo denull) — nada de listeners por botón; debe funcionar en items agregados después. - Total reactivo con CustomEvent: agregar y borrar NO calculan nada —
solo disparan
bolsillo:cambioendocument. El ÚNICO lugar que calcula es el listener de ese evento: lee loslidel DOM, suma susdataset.valorconreducey pinta el total. - Formato: un
Intl.NumberFormatde pesos (es-CO, COP, sin decimales) creado una sola vez, usado en items y total.
Reglas
- Cero
innerHTML: el nombre lo escribe el usuario (lección 3 del módulo 7 — eso es sagrado). - El total SIEMPRE sale de leer el DOM en el listener del evento — nada de variables acumuladoras regadas por el código.
- Patrón
iniciarpara esperar el DOM. - Todo el CSS ya está dado: este reto es 100% JavaScript.
40:00
Cuando estés listo, arranca el reloj.
Qué sigue
Dos módulos y "Mi Bolsillo" pasó de imprimir en consola a ser una app que responde al usuario. Pero todos tus datos siguen viviendo en la página: al recargar, se esfuman — y traerlos de un servidor toma tiempo. El módulo 9 abre la puerta grande: ASINCRONÍA — callbacks, promesas y el arte de esperar sin congelar la página. El JavaScript que separa a los junior de los que pasan la entrevista.