La aplicación clásica para aprender JavaScript de verdad: añadir, marcar, borrar y filtrar tareas, con los datos guardados en el navegador para que sigan ahí mañana.
5 pasos
1 hora y media aproximadamente
205 líneas de código
Editor en vivo
Lo que vas a construir
La lista de tareas es el «hola, mundo» de las aplicaciones: parece sencilla, pero obliga a resolver lo que tiene cualquier app real. Vas a guardar los datos en un array, dibujar la página a partir de ellos cada vez que cambian y conservarlos en el navegador con localStorage.
Lo que vas a practicar
Formularios y preventDefault
Crear elementos sin innerHTML
Estado en un array de objetos
Delegación de eventos
Guardar con localStorage y JSON
Resultado final
Antes de empezar, conviene haber hecho estas lecciones
El HTML tiene un formulario con un campo y un botón, una lista vacía donde aparecerán las tareas y un párrafo para el resumen. La etiqueta del campo está oculta a la vista pero la leen los lectores de pantalla.
Para centrarte en JavaScript, los estilos ya vienen completos, incluidos los de las partes que añadirás en los siguientes pasos.
Al enviar el formulario se ejecuta el evento submit. event.preventDefault() evita que la página se recargue, se lee el texto y se crea un <li> nuevo con document.createElement.
El texto se pone con textContent, que lo trata siempre como texto. Con innerHTML, alguien podría escribir HTML o un script y se ejecutaría en la página.
app.js
const formulario = document.querySelector("#formulario");
const campo = document.querySelector("#texto");
const lista = document.querySelector("#lista");
formulario.addEventListener("submit", (event) => {
event.preventDefault(); // que la página no se recargue
const texto = campo.value.trim();
if (!texto) return;
const elemento = document.createElement("li");
elemento.className = "tarea";
elemento.textContent = texto; // textContent, nunca innerHTML con lo que escribe la persona
lista.append(elemento);
campo.value = "";
campo.focus();
});
3
Paso 3 de 5
Marcar y borrar: el estado manda
Para marcar y borrar hace falta recordar las tareas. En lugar de leerlas del HTML, se guardan en un array de objetos (tareas) y una función pintar() vuelve a dibujar la lista entera a partir de él cada vez que algo cambia. Es la misma idea en la que se basan React o Vue.
Cada tarea tiene un id (la hora en milisegundos) que se guarda en data-id. Con un solo escuchador en la lista se sabe qué tarea se marcó o se borró (delegación de eventos).
app.jsLo nuevo, resaltado
const formulario = document.querySelector("#formulario");
const campo = document.querySelector("#texto");
const lista = document.querySelector("#lista");
const resumen = document.querySelector("#resumen");
// El estado: una lista de objetos. La página siempre se dibuja a partir de él.
let tareas = [];
function pintar() {
lista.replaceChildren();
for (const tarea of tareas) {
const elemento = document.createElement("li");
elemento.className = tarea.hecha ? "tarea hecha" : "tarea";
elemento.dataset.id = tarea.id;
const etiqueta = document.createElement("label");
const casilla = document.createElement("input");
casilla.type = "checkbox";
casilla.checked = tarea.hecha;
const texto = document.createElement("span");
texto.textContent = tarea.texto;
etiqueta.append(casilla, texto);
const borrar = document.createElement("button");
borrar.type = "button";
borrar.className = "borrar";
borrar.textContent = "Borrar";
borrar.setAttribute("aria-label", `Borrar «${tarea.texto}»`);
elemento.append(etiqueta, borrar);
lista.append(elemento);
}
const pendientes = tareas.filter((t) => !t.hecha).length;
resumen.textContent = `${pendientes} pendientes de ${tareas.length}`;
}
formulario.addEventListener("submit", (event) => {
event.preventDefault();
const texto = campo.value.trim();
if (!texto) return;
tareas.push({ id: Date.now(), texto, hecha: false });
campo.value = "";
campo.focus();
pintar();
});
// Un solo escuchador para todas las tareas (delegación de eventos)
lista.addEventListener("change", (event) => {
const id = Number(event.target.closest(".tarea").dataset.id);
const tarea = tareas.find((t) => t.id === id);
tarea.hecha = event.target.checked;
pintar();
});
lista.addEventListener("click", (event) => {
if (!event.target.matches(".borrar")) return;
const id = Number(event.target.closest(".tarea").dataset.id);
tareas = tareas.filter((t) => t.id !== id);
pintar();
});
pintar();
4
Paso 4 de 5
Que se guarde al cerrar
localStorage guarda texto en el navegador de forma permanente. Como solo admite texto, el array se convierte con JSON.stringify al guardar y con JSON.parse al cargar. Si no hay nada guardado, ?? [] empieza con la lista vacía.
Las dos operaciones van dentro de try…catch porque pueden fallar (modo privado, almacenamiento bloqueado). En la vista previa del editor no se guarda nada, porque está aislada del resto del navegador: descarga los archivos y ábrelos para probarlo de verdad.
app.jsLo nuevo, resaltado
const formulario = document.querySelector("#formulario");
const campo = document.querySelector("#texto");
const lista = document.querySelector("#lista");
const resumen = document.querySelector("#resumen");
const CLAVE = "mis-tareas";
// El estado: una lista de objetos. La página siempre se dibuja a partir de él.
let tareas = cargar();
function cargar() {
try {
return JSON.parse(localStorage.getItem(CLAVE)) ?? [];
} catch {
return []; // almacenamiento bloqueado o datos dañados: se empieza de cero
}
}
function guardar() {
try {
localStorage.setItem(CLAVE, JSON.stringify(tareas));
} catch {
// modo privado o almacenamiento lleno: la lista funciona igual, pero no se guarda
}
}
function pintar() {
lista.replaceChildren();
for (const tarea of tareas) {
const elemento = document.createElement("li");
elemento.className = tarea.hecha ? "tarea hecha" : "tarea";
elemento.dataset.id = tarea.id;
const etiqueta = document.createElement("label");
const casilla = document.createElement("input");
casilla.type = "checkbox";
casilla.checked = tarea.hecha;
const texto = document.createElement("span");
texto.textContent = tarea.texto;
etiqueta.append(casilla, texto);
const borrar = document.createElement("button");
borrar.type = "button";
borrar.className = "borrar";
borrar.textContent = "Borrar";
borrar.setAttribute("aria-label", `Borrar «${tarea.texto}»`);
elemento.append(etiqueta, borrar);
lista.append(elemento);
}
const pendientes = tareas.filter((t) => !t.hecha).length;
resumen.textContent = `${pendientes} pendientes de ${tareas.length}`;
guardar(); // cada vez que se dibuja, se guarda
}
formulario.addEventListener("submit", (event) => {
event.preventDefault();
const texto = campo.value.trim();
if (!texto) return;
tareas.push({ id: Date.now(), texto, hecha: false });
campo.value = "";
campo.focus();
pintar();
});
// Un solo escuchador para todas las tareas (delegación de eventos)
lista.addEventListener("change", (event) => {
const id = Number(event.target.closest(".tarea").dataset.id);
const tarea = tareas.find((t) => t.id === id);
tarea.hecha = event.target.checked;
pintar();
});
lista.addEventListener("click", (event) => {
if (!event.target.matches(".borrar")) return;
const id = Number(event.target.closest(".tarea").dataset.id);
tareas = tareas.filter((t) => t.id !== id);
pintar();
});
pintar();
5
Paso 5 de 5
Filtros y limpieza
Tres botones cambian qué tareas se ven. La variable filtro guarda el elegido y el objeto FILTROS tiene una función por filtro, que se pasa directamente a tareas.filter(...). aria-pressed indica cuál está activo.
El botón «Borrar las hechas» solo aparece cuando hay alguna, y elimina de una vez las completadas.
Con tu permiso usamos Google Analytics, que guarda cookies, para saber cuántas personas visitan la web y qué páginas les resultan útiles. No usamos cookies de publicidad y tus archivos nunca salen de tu navegador. Puedes cambiar de opinión cuando quieras en «Configurar cookies», al pie de la página. Más información.