En este proyecto vamos a integrar los conceptos principales de la ruta de JavaScript construyendo una aplicación de tareas.
La aplicación permitirá agregar, completar, eliminar y filtrar tareas. Además guardará los datos en localStorage para conservarlos al recargar la página.
Qué vamos a practicar
- variables y constantes;
- funciones;
- arrays y objetos;
- métodos de array;
- DOM;
- eventos;
- formularios;
- delegación de eventos;
- JSON;
localStorage.
Estructura del proyecto
tareas-js/ |-- index.html |-- styles.css |-- app.js
HTML
<!DOCTYPE html> <html lang="es"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Mis tareas</title> <link rel="stylesheet" href="styles.css"> <script src="app.js" defer></script> </head> <body> <main class="app"> <h1>Mis tareas</h1> <form id="form-tarea"> <label for="titulo">Nueva tarea</label> <input id="titulo" name="titulo" type="text" maxlength="80" required > <button type="submit">Agregar</button> </form> <div class="filtros"> <button data-filtro="todas">Todas</button> <button data-filtro="pendientes">Pendientes</button> <button data-filtro="completadas">Completadas</button> </div> <p id="resumen"></p> <ul id="lista-tareas"></ul> </main> </body> </html>
CSS mínimo
body {
font-family: Arial, sans-serif;
margin: 0;
background: #f4f6f8;
}
.app {
width: 90%;
max-width: 700px;
margin: 3rem auto;
}
form,
.filtros {
display: flex;
gap: 0.75rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
li {
display: flex;
justify-content: space-between;
gap: 1rem;
padding: 1rem;
margin-bottom: 0.75rem;
background: white;
border-radius: 8px;
}
.completada {
text-decoration: line-through;
opacity: 0.6;
}
Estado inicial
Primero guardamos referencias a los elementos del DOM y definimos el estado de la aplicación: el array de tareas y el filtro activo.
const formulario = document.querySelector("#form-tarea"); const inputTitulo = document.querySelector("#titulo"); const lista = document.querySelector("#lista-tareas"); const resumen = document.querySelector("#resumen"); const filtros = document.querySelector(".filtros"); let tareas = cargarTareas(); let filtroActual = "todas";
Cargar desde localStorage
Al iniciar la aplicación intentamos recuperar las tareas guardadas. Si no existen o el contenido no es válido, comenzamos con un array vacío.
function cargarTareas() { const guardadas = localStorage.getItem("tareas"); if (!guardadas) { return []; } try { return JSON.parse(guardadas); } catch (error) { console.error("Datos inválidos en localStorage", error); return []; } }
Guardar tareas
Cada vez que cambia el array de tareas lo convertimos a JSON y lo guardamos. De esta manera los datos sobreviven a una recarga de la página.
function guardarTareas() { localStorage.setItem("tareas", JSON.stringify(tareas)); }
localStorage guarda strings. Por eso convertimos el array a JSON antes de almacenarlo y lo reconstruimos con JSON.parse().
Crear una tarea
Centralizamos la creación en una función para que todas las tareas tengan siempre la misma estructura: identificador, título y estado de completada.
function crearTarea(titulo) { return { id: Date.now(), titulo, completada: false }; }
Filtrar tareas
La función no modifica el array original: usa filter() para devolver únicamente las tareas que corresponden al filtro seleccionado.
function obtenerTareasVisibles() { if (filtroActual === "pendientes") { return tareas.filter(tarea => !tarea.completada); } if (filtroActual === "completadas") { return tareas.filter(tarea => tarea.completada); } return tareas; }
Renderizar
Renderizar significa tomar el estado actual y reflejarlo en la interfaz. Primero limpiamos la lista y después volvemos a crear los elementos visibles.
function renderizar() { lista.innerHTML = ""; const visibles = obtenerTareasVisibles(); visibles.forEach(tarea => { const item = document.createElement("li"); if (tarea.completada) { item.classList.add("completada"); } item.dataset.id = tarea.id; const texto = document.createElement("span"); texto.textContent = tarea.titulo; const acciones = document.createElement("div"); const botonCompletar = document.createElement("button"); botonCompletar.type = "button"; botonCompletar.dataset.accion = "completar"; botonCompletar.textContent = tarea.completada ? "Desmarcar" : "Completar"; const botonEliminar = document.createElement("button"); botonEliminar.type = "button"; botonEliminar.dataset.accion = "eliminar"; botonEliminar.textContent = "Eliminar"; acciones.append(botonCompletar, botonEliminar); item.append(texto, acciones); lista.append(item); }); const pendientes = tareas.filter( tarea => !tarea.completada ).length; resumen.textContent = `${pendientes} tarea(s) pendiente(s)`; }
Agregar desde el formulario
Interceptamos el envío, validamos el texto, agregamos una nueva tarea al array, guardamos y volvemos a renderizar.
formulario.addEventListener("submit", event => { event.preventDefault(); const titulo = inputTitulo.value.trim(); if (titulo === "") { return; } tareas.push(crearTarea(titulo)); guardarTareas(); renderizar(); formulario.reset(); inputTitulo.focus(); });
Completar y eliminar con delegación
En lugar de crear un evento para cada botón, escuchamos un solo click en la lista. Según el atributo data-accion decidimos qué operación ejecutar.
lista.addEventListener("click", event => { const boton = event.target.closest("button"); if (!boton) { return; } const item = boton.closest("li"); const id = Number(item.dataset.id); const accion = boton.dataset.accion; if (accion === "completar") { tareas = tareas.map(tarea => { if (tarea.id === id) { return { ...tarea, completada: !tarea.completada }; } return tarea; }); } if (accion === "eliminar") { tareas = tareas.filter(tarea => tarea.id !== id); } guardarTareas(); renderizar(); });
Cambiar filtros
Los botones sólo cambian filtroActual. Después llamamos nuevamente a renderizar() para que la interfaz refleje ese nuevo estado.
filtros.addEventListener("click", event => { const boton = event.target.closest("[data-filtro]"); if (!boton) { return; } filtroActual = boton.dataset.filtro; renderizar(); });
Inicializar
La primera llamada a renderizar() muestra inmediatamente las tareas recuperadas de localStorage.
renderizar();
app.js completo
const formulario = document.querySelector("#form-tarea"); const inputTitulo = document.querySelector("#titulo"); const lista = document.querySelector("#lista-tareas"); const resumen = document.querySelector("#resumen"); const filtros = document.querySelector(".filtros"); let tareas = cargarTareas(); let filtroActual = "todas"; function cargarTareas() { const guardadas = localStorage.getItem("tareas"); if (!guardadas) { return []; } try { return JSON.parse(guardadas); } catch (error) { console.error("Datos inválidos en localStorage", error); return []; } } function guardarTareas() { localStorage.setItem("tareas", JSON.stringify(tareas)); } function crearTarea(titulo) { return { id: Date.now(), titulo, completada: false }; } function obtenerTareasVisibles() { if (filtroActual === "pendientes") { return tareas.filter(tarea => !tarea.completada); } if (filtroActual === "completadas") { return tareas.filter(tarea => tarea.completada); } return tareas; } function renderizar() { lista.innerHTML = ""; const visibles = obtenerTareasVisibles(); visibles.forEach(tarea => { const item = document.createElement("li"); if (tarea.completada) { item.classList.add("completada"); } item.dataset.id = tarea.id; const texto = document.createElement("span"); texto.textContent = tarea.titulo; const acciones = document.createElement("div"); const completar = document.createElement("button"); completar.type = "button"; completar.dataset.accion = "completar"; completar.textContent = tarea.completada ? "Desmarcar" : "Completar"; const eliminar = document.createElement("button"); eliminar.type = "button"; eliminar.dataset.accion = "eliminar"; eliminar.textContent = "Eliminar"; acciones.append(completar, eliminar); item.append(texto, acciones); lista.append(item); }); const pendientes = tareas.filter( tarea => !tarea.completada ).length; resumen.textContent = `${pendientes} tarea(s) pendiente(s)`; } formulario.addEventListener("submit", event => { event.preventDefault(); const titulo = inputTitulo.value.trim(); if (titulo === "") { return; } tareas.push(crearTarea(titulo)); guardarTareas(); renderizar(); formulario.reset(); inputTitulo.focus(); }); lista.addEventListener("click", event => { const boton = event.target.closest("button"); if (!boton) { return; } const item = boton.closest("li"); const id = Number(item.dataset.id); if (boton.dataset.accion === "completar") { tareas = tareas.map(tarea => tarea.id === id ? { ...tarea, completada: !tarea.completada } : tarea ); } if (boton.dataset.accion === "eliminar") { tareas = tareas.filter(tarea => tarea.id !== id); } guardarTareas(); renderizar(); }); filtros.addEventListener("click", event => { const boton = event.target.closest("[data-filtro]"); if (!boton) { return; } filtroActual = boton.dataset.filtro; renderizar(); }); renderizar();
Qué aprendimos con el proyecto
- mantener un estado en un array;
- modelar cada tarea con un objeto;
- renderizar el estado en el DOM;
- procesar formularios;
- usar delegación de eventos;
- transformar arrays con
mapyfilter; - guardar y recuperar JSON con
localStorage.
Mejoras para seguir practicando
- editar tareas;
- agregar prioridades;
- incorporar fechas límite;
- ordenar pendientes y completadas;
- agregar contador por filtro;
- confirmar antes de eliminar;
- reemplazar localStorage por una API.
Después de esta ruta
Con estos fundamentos ya podés profundizar en módulos, clases, tooling, testing, frameworks y desarrollo frontend moderno. Lo importante es que DOM, eventos, arrays, objetos, asincronía y HTTP estén claros antes de depender de una librería.
¿Te sirvió esta guía? ☕
Si este contenido te ayudó a aprender JavaScript, podés apoyar a Club Programador. Cada aporte ayuda a mantener el sitio online y a seguir publicando material gratuito.
🇦🇷 Apoyar desde Argentina
☕ Invitame un café con Mercado Pago
🌎 Apoyar desde otro país
Seguí aprendiendo JavaScript
← JavaScript asíncrono: Promises, async/await, fetch y APIs
Con este proyecto completás la ruta inicial de JavaScript.
Descubre más desde Club Programador
Suscríbete y recibe las últimas entradas en tu correo electrónico.
Un comentario en “Proyecto final JavaScript: aplicación de tareas con DOM, eventos y localStorage”