FullStackJS Camp
Módulo 4·proyecto·8h
Objetivos de aprendizaje
  • Seleccionar y modificar elementos del DOM de forma segura.
  • Crear y eliminar nodos dinámicamente.
  • Manejar eventos con addEventListener y delegación.
  • Entender el event loop, callbacks y Promises.
  • Usar async/await con try/catch para operaciones asíncronas.
  • Construir un Gestor de Tareas completo con DOM nativo.

DOM, Eventos y Async

El Document Object Model (DOM) es la representación en memoria del HTML de la página. Con JavaScript puedes leerlo, modificarlo y reaccionar a las acciones del usuario en tiempo real.


Selección de elementos

javascript
// Un solo elemento
const titulo    = document.querySelector("h1");
const boton     = document.querySelector("#btn-guardar");
const primerLi  = document.querySelector("ul li");

// Múltiples elementos → NodeList (iterable)
const items     = document.querySelectorAll(".item");
const botones   = document.querySelectorAll("button[data-accion]");

// Iterar
items.forEach(item => console.log(item.textContent));

// Convertir a array para usar .map, .filter, etc.
const textos = [...items].map(el => el.textContent.trim());

Leer y modificar contenido

javascript
const p = document.querySelector("#descripcion");

// Leer
console.log(p.textContent); // texto plano (seguro)
console.log(p.innerHTML);   // HTML interno (cuidado con XSS)

// Escribir — siempre preferir textContent sobre innerHTML
p.textContent = "Nuevo texto";

// Atributos
const img = document.querySelector("img");
img.setAttribute("alt", "Logo de la empresa");
img.src = "nuevo-logo.png"; // acceso directo como propiedad

// Dataset (data-*)
const card = document.querySelector(".card");
card.dataset.id     = "42";
card.dataset.estado = "activo";
console.log(card.dataset.id); // "42"

Crear y eliminar nodos

javascript
function crearItemTarea(texto, id) {
  const li = document.createElement("li");
  li.className = "tarea-item";
  li.dataset.id = id;

  const span = document.createElement("span");
  span.textContent = texto;             // seguro: no interpreta HTML

  const btnEliminar = document.createElement("button");
  btnEliminar.textContent = "×";
  btnEliminar.className   = "btn-eliminar";
  btnEliminar.setAttribute("aria-label", `Eliminar tarea: ${texto}`);

  li.append(span, btnEliminar);
  return li;
}

const lista = document.querySelector("#lista-tareas");
lista.appendChild(crearItemTarea("Aprender DOM", 1));

// Eliminar un elemento
function eliminarPorId(id) {
  const el = document.querySelector(`[data-id="${id}"]`);
  el?.remove();
}

Eventos y delegación

addEventListener

javascript
const btn = document.querySelector("#btn-agregar");

btn.addEventListener("click", (event) => {
  console.log("Click en botón", event.target);
});

// Prevenir comportamiento por defecto (ej: envío de formulario)
const form = document.querySelector("#form-tarea");
form.addEventListener("submit", (event) => {
  event.preventDefault();                  // no recarga la página
  const valor = event.target.texto.value.trim();
  if (valor) agregarTarea(valor);
});

Event Bubbling y Delegación

Los eventos suben del elemento hijo hacia el padre. La delegación aprovecha esto para escuchar en un contenedor en lugar de asignar un listener a cada hijo:

javascript
// ❌ Ineficiente: un listener por cada botón
document.querySelectorAll(".btn-eliminar").forEach(btn => {
  btn.addEventListener("click", () => { /* ... */ });
});

// ✅ Delegación: un solo listener en el contenedor
const lista = document.querySelector("#lista-tareas");
lista.addEventListener("click", (event) => {
  const btn = event.target.closest(".btn-eliminar");
  if (!btn) return; // click en otro lugar — ignorar

  const li = btn.closest("li");
  const id = li?.dataset.id;
  if (id) eliminarTarea(id);
});

Asincronía — el Event Loop

JavaScript es single-threaded: ejecuta una sola operación a la vez. Las operaciones lentas (fetch, timers, I/O) se delegan al navegador y su resultado se procesa en la cola de microtareas o macrotareas.

javascript
console.log("1 — síncrono");

setTimeout(() => {
  console.log("3 — macrotarea (timer)");
}, 0);

Promise.resolve().then(() => {
  console.log("2 — microtarea (promise)");
});

console.log("4 — síncrono");

// Orden de salida:
// "1 — síncrono"
// "4 — síncrono"
// "2 — microtarea (promise)"   ← microtareas antes que macrotareas
// "3 — macrotarea (timer)"

Promises

Una Promise representa un valor que aún no está disponible:

javascript
function esperarMs(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function obtenerUsuario(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id <= 0) {
        reject(new Error("ID inválido"));
        return;
      }
      resolve({ id, nombre: "Ana", rol: "admin" });
    }, 300);
  });
}

// Encadenamiento con .then / .catch
obtenerUsuario(1)
  .then(usuario => {
    console.log("Usuario:", usuario.nombre);
    return usuario.rol;
  })
  .then(rol => console.log("Rol:", rol))
  .catch(err => console.error("Error:", err.message))
  .finally(() => console.log("Fin"));

async / await

La misma lógica con sintaxis lineal y más legible:

javascript
async function cargarDashboard(usuarioId) {
  try {
    const usuario = await obtenerUsuario(usuarioId);
    console.log(`Bienvenido, ${usuario.nombre}`);

    // Operaciones paralelas con Promise.all
    const [perfil, notificaciones] = await Promise.all([
      fetch(`/api/perfil/${usuarioId}`).then(r => r.json()),
      fetch(`/api/notificaciones/${usuarioId}`).then(r => r.json()),
    ]);

    return { usuario, perfil, notificaciones };
  } catch (error) {
    console.error("Error cargando dashboard:", error.message);
    throw error; // re-lanzar si el caller también debe manejarlo
  } finally {
    console.log("Carga finalizada");
  }
}

Proyecto: Gestor de Tareas

Integra DOM + Eventos + Asincronía en un mini sistema completo:

javascript
// fakeApi.js — simula latencia de red
const fakeApi = {
  async guardar(tarea) {
    await new Promise(r => setTimeout(r, 400)); // simula 400ms de latencia
    if (Math.random() < 0.1) throw new Error("Error de red"); // falla 10%
    return { ...tarea, id: Date.now() };
  },

  async cargar() {
    await new Promise(r => setTimeout(r, 600));
    return [
      { id: 1, texto: "Revisar PR de Ana", completada: false },
      { id: 2, texto: "Preparar demo del viernes", completada: true },
    ];
  },
};

// app.js
const estado = {
  tareas: [],
  cargando: false,
};

function renderizar() {
  const lista = document.querySelector("#lista-tareas");
  const conteo = document.querySelector("#conteo");

  lista.innerHTML = "";  // limpiar (OK aquí: no viene de usuario)

  estado.tareas.forEach(tarea => {
    const li = document.createElement("li");
    li.dataset.id = tarea.id;
    li.className  = tarea.completada ? "tarea tarea--completada" : "tarea";

    const chk = document.createElement("input");
    chk.type    = "checkbox";
    chk.checked = tarea.completada;

    const span = document.createElement("span");
    span.textContent = tarea.texto;

    const btnDel = document.createElement("button");
    btnDel.textContent       = "×";
    btnDel.className         = "btn-eliminar";
    btnDel.dataset.idTarea   = tarea.id;

    li.append(chk, span, btnDel);
    lista.appendChild(li);
  });

  conteo.textContent = `${estado.tareas.filter(t => !t.completada).length} pendientes`;
}

async function agregarTarea(texto) {
  const ui = document.querySelector("#btn-agregar");
  ui.disabled = true;

  try {
    const nueva = await fakeApi.guardar({ texto, completada: false });
    estado.tareas.unshift(nueva);
    renderizar();
  } catch (err) {
    alert(`No se pudo guardar: ${err.message}`);
  } finally {
    ui.disabled = false;
  }
}

async function iniciar() {
  document.querySelector("#status").textContent = "Cargando…";
  try {
    estado.tareas = await fakeApi.cargar();
    renderizar();
    document.querySelector("#status").textContent = "";
  } catch {
    document.querySelector("#status").textContent = "Error al cargar tareas";
  }
}

// Eventos (delegación)
document.querySelector("#lista-tareas").addEventListener("click", e => {
  const btnDel = e.target.closest(".btn-eliminar");
  if (btnDel) {
    const id = Number(btnDel.dataset.idTarea);
    estado.tareas = estado.tareas.filter(t => t.id !== id);
    renderizar();
  }
});

document.querySelector("#form-nueva").addEventListener("submit", e => {
  e.preventDefault();
  const input = e.target.querySelector("input[name='texto']");
  const texto = input.value.trim();
  if (texto) {
    agregarTarea(texto);
    input.value = "";
  }
});

iniciar();

Práctica

Práctica interactiva · JavaScript