Proyecto: lista de tareas
💡 🎯 Al terminar esta lección podrás…
- Aplicar todo el curso en una aplicación completa.
- Separar los datos de la interfaz y pintar a partir del estado.
- Persistir datos con
localStoragede forma segura. - Revisar la calidad, seguridad y accesibilidad de una app.
1.Qué vas a construir
Vas a crear una aplicación de tareas completa, la «lista de pendientes» que casi todo programador ha hecho alguna vez. Reúne todo lo que has aprendido: variables, decisiones, bucles, funciones, arrays, objetos, el DOM, eventos, formularios, JSON y localStorage.
| Función de la app | Lo que aplicas | Lección |
|---|---|---|
| Guardar las tareas en una lista de objetos | Arrays y objetos | 6 y 7 |
| Añadir con un formulario y validar | Eventos, submit, preventDefault | 9 |
| Marcar como hechas y borrar | Delegación de eventos, closest() | 9 |
| Filtrar: todas, pendientes, hechas | filter, atributos aria-pressed | 6 y 8 |
| Contador de pendientes | Funciones, plantillas de texto | 2 y 5 |
| Pintar la lista a partir de los datos | createElement, textContent | 8 |
| Guardar y recuperar al recargar | localStorage, JSON, try/catch | 7 y 11 |
💡 Consejo · Una idea que lo cambia todo
La clave de este proyecto es separar los datos (el array tareas) de lo que se ve (el DOM). No cambies la página a mano: cambia los datos y vuelve a pintar. Así la pantalla siempre refleja la realidad y es fácil guardar. Es la idea en la que se basan frameworks como React y Vue.
2.Plan: los datos y las funciones
El modelo de datos
{ id: 1712345678, texto: "Estudiar JavaScript", hecha: false }
// Toda la aplicación vive en este array:
let tareas = [
{ id: 1, texto: "Estudiar JavaScript", hecha: false },
{ id: 2, texto: "Hacer ejercicio", hecha: true },
];
// Y en una variable para el filtro actual:
let filtro = "todas"; // "todas" | "pendientes" | "hechas"
Las funciones que necesitas
| Función | Responsabilidad |
|---|---|
cargar() | Lee el array guardado en localStorage (o []) |
guardar() | Guarda el array actual como JSON |
visibles() | Devuelve las tareas que pasan el filtro actual |
crearElemento(tarea) | Construye el <li> de una tarea |
pintar() | Vuelve a dibujar la lista, el contador y los filtros |
cambiar() | guardar() y pintar(): la usarás tras cada cambio |
iniciar() | Carga los datos y pinta; se llama al arrancar |
Además, cuatro manejadores de eventos: enviar el formulario, pulsar dentro de la lista (marcar y borrar, con delegación), pulsar un filtro y pulsar «Borrar completadas».
3.La estructura del <li>
Para que tu código y las pruebas del ejercicio se entiendan, cada tarea debe pintarse con esta estructura:
<li data-id="1712345678" class="hecha"> <!-- la clase "hecha" solo si está completada -->
<input type="checkbox" class="check" checked aria-label="Marcar como hecha">
<span class="texto">Estudiar JavaScript</span>
<button type="button" class="borrar" aria-label="Borrar tarea">✕</button>
</li>
- El
idde la tarea se guarda endata-idpara saber cuál es. - El texto va en un
<span class="texto">y se inserta contextContent. - La casilla y el botón llevan
aria-label, porque no tienen texto visible. - Los botones de filtro ya están en el HTML con
data-filtroyaria-pressed.
⚠️ Importante · textContent, siempre
El texto de cada tarea lo escribe el usuario. Si lo insertaras con innerHTML, alguien podría escribir <img src=x onerror=alert(1)> y ejecutar código en tu página. Con textContent el texto es siempre texto. Las pruebas del ejercicio lo comprueban.
4.Paso a paso
Paso 1 · Datos y almacenamiento
const CLAVE = "tareas";
let tareas = [];
let filtro = "todas";
function cargar() {
try { return JSON.parse(localStorage.getItem(CLAVE)) ?? []; }
catch { return []; }
}
function guardar() {
localStorage.setItem(CLAVE, JSON.stringify(tareas));
}
Paso 2 · Pintar
pintar() recorre visibles(), crea un <li> por tarea con crearElemento() y los coloca en la lista con lista.replaceChildren(...). Después actualiza el contador ("1 pendiente" o "3 pendientes") y marca con aria-pressed="true" el botón del filtro activo.
Paso 3 · Añadir tareas
Escucha submit en el formulario: preventDefault(), recorta el texto, si está vacío muestra el error en #error; si no, añade { id, texto, hecha: false } al array, vacía el campo y llama a cambiar().
Paso 4 · Marcar y borrar con delegación
Un solo manejador de click en #lista: busca el li con e.target.closest("li"), localiza la tarea por su data-id y, según el elemento pulsado (.check o .borrar), cambia hecha o elimina la tarea. Termina con cambiar().
Paso 5 · Filtros y limpiar
Los botones [data-filtro] cambian la variable filtro y llaman a pintar(). «Borrar completadas» deja en el array solo las pendientes (filter) y llama a cambiar().
Paso 6 · Arrancar
Escribe iniciar() (carga los datos, restablece el filtro y pinta) y llámala al final del script. Prueba: añade tareas, recarga con F5 en tu propia página y comprueba que siguen ahí.
📌 Nota · Un identificador por tarea
No uses la posición en el array como identificador: cambia al borrar o filtrar. Un id propio (por ejemplo Date.now()) no cambia nunca, y es lo que permite localizar la tarea correcta.
5.Revisión de calidad
| Revisión | Cómo comprobarlo |
|---|---|
| Funciona | Añadir, marcar, borrar, filtrar y limpiar, sin errores en la consola |
| Persiste | Recarga la página: las tareas siguen ahí (revisa en F12 → Aplicación → Almacenamiento local) |
| Es seguro | Añade una tarea con <b>hola</b> o <script>: debe verse como texto, no ejecutarse |
| Teclado | Todo se puede hacer con Tab, Enter y Espacio |
| Lector de pantalla | El contador y el error se anuncian (role="status", role="alert") |
| Casos límite | Texto vacío, solo espacios, un texto larguísimo, borrar la última tarea |
| Código | Funciones pequeñas, nombres claros, sin repetición, sin variables globales innecesarias |
💡 Consejo · Estilo con CSS
Esta app está pidiendo a gritos el curso de CSS: una tarjeta centrada con Grid o Flexbox, la clase .hecha con texto tachado y atenuado, transiciones al marcar, un tema oscuro con variables y que funcione perfecto en el móvil. Haz esa parte y tendrás una pieza lista para tu portafolio.
6.Retos para ir más lejos
- Editar una tarea con doble clic.
- Añadir una fecha límite y ordenar por ella.
- Un botón «Marcar todas» y otro de deshacer el último borrado.
- Guardar el filtro elegido en
localStorage. - Dividir el código en módulos (
almacenamiento.js,vista.js,app.js). - Cargar tareas de ejemplo desde una API con
fetch. - Arrastrar y soltar para reordenar (API *Drag and Drop*).
- Reescribirla con un framework (React, Vue, Svelte) y comparar.
7.Qué sigue en tu camino
Con este curso ya sabes programar en JavaScript y construir interfaces interactivas. El camino hacia el desarrollo full stack tiene estas paradas:
| Etapa | Qué aprenderás | Dónde empezar |
|---|---|---|
| Herramientas modernas | npm, Vite, módulos, depuración avanzada | Vite |
| Un framework de interfaz | React (Vue y Svelte son alternativas): componentes y estado | Curso de React de este sitio |
| Backend con Node.js | Servidores, rutas y APIs propias | nodejs.org |
| Bases de datos | SQL (PostgreSQL) o NoSQL (MongoDB) | MDN: servidores |
| TypeScript | JavaScript con tipos para proyectos grandes | typescriptlang.org |
| Despliegue | Publicar apps con backend (Render, Railway, Vercel) | Documentación de cada plataforma |
Y el consejo de siempre: construye cosas. Una calculadora, un juego del ahorcado, un clon de tu app favorita. Cada proyecto te enseña algo que ningún curso puede enseñarte.
8.Errores frecuentes en el proyecto
- Modificar el DOM a mano en lugar de cambiar los datos y volver a pintar: la pantalla y el array se desincronizan.
- Olvidar
guardar()tras un cambio: al recargar se pierde. - Usar el índice del array como id.
innerHTMLcon el texto de la tarea.- Un manejador por tarea (en lugar de delegación) que se duplica al repintar.
- Comparar ids de tipo texto con ids numéricos (
li.dataset.ides texto: conviértelo conNumber()). - Leer
localStoragesin valor por defecto nitry/catch. - No volver a pintar después de cambiar el filtro.
📒 Apuntes para llevar
- Separa los datos (el array
tareas) de lo que se ve (el DOM): cambia los datos y vuelve a pintar. - Cada tarea es un objeto
{ id, texto, hecha }con unidpropio estable. - Un conjunto de funciones pequeñas (
cargar,guardar,visibles,pintar,cambiar,iniciar) hace el programa fácil de seguir. - La delegación en la lista atiende marcar y borrar, incluso en tareas nuevas.
textContent(nuncainnerHTML) inserta texto del usuario de forma segura.localStorage+ JSON persiste los datos; lee con?? []ytry/catch.- La accesibilidad también es JavaScript:
aria-label,aria-pressed,role="status"yrole="alert". - El siguiente paso: un framework, Node.js y bases de datos, rumbo a full stack.
9.Glosario
| Término | Significado |
|---|---|
| Estado | Los datos que describen cómo está la aplicación en un momento. |
| Renderizar / pintar | Dibujar la interfaz a partir del estado. |
| Delegación de eventos | Un manejador en el contenedor que atiende a sus hijos. |
| Persistencia | Conservar los datos aunque se cierre o recargue la página. |
| Identificador (id) | Valor único que distingue a cada elemento. |
| Caso límite | Situación extrema o poco habitual que hay que probar. |
| XSS | Ataque que inyecta código en una página mediante datos de usuario. |
| Full stack | Desarrollo tanto del lado del navegador como del servidor. |
🧠 Comprueba lo aprendido
Elige una respuesta en cada pregunta. Verás al instante si acertaste y por qué.
¿Cuál es la idea central de este proyecto?
Separar datos y vista mantiene todo sincronizado y facilita guardar.
¿Por qué no usar la posición del array como id de una tarea?
Un id propio no cambia cuando la lista se reordena o se filtra.
Un usuario escribe <b>hola</b> como tarea. ¿Cómo lo muestras de forma segura?
textContent lo muestra como texto literal.
Recargas la página y las tareas desaparecen. ¿Qué falta casi seguro?
Sin guardar en localStorage, los datos solo viven en memoria.
¿Para qué sirve la delegación de eventos en la lista de tareas?
Un manejador en la lista y closest("li") basta.
Tu progreso se guarda en este navegador y se refleja en el índice del curso.
💡 🚀 ¡Ahora te toca!
La práctica fija lo aprendido. Pasa al ejercicio de esta lección.