El DOM y los eventos

Los tres lenguajes de una página

Una página web la escriben tres lenguajes, cada uno con su encargo:

JavaScript es un lenguaje de programación completo: calcula, valida formularios, guarda datos, pide información a un servidor. En estos apuntes se emplea solo para una parte de lo que sabe hacer: modificar la página una vez cargada.

Dónde vive la página: navigator, window y document

Antes de tocar la página con JavaScript conviene saber qué partes del navegador están a su alcance. Son tres objetos, uno dentro de otro:

navigator
El navegador en sí: su idioma, si hay conexión, la cámara. navigator.language devuelve "es-ES". Es el mismo para todas las pestañas abiertas.
window
La pestaña en la que se ha cargado la página. window.innerWidth devuelve el ancho del viewport en píxeles: es la medida que consultan las media queries.
document
La página cargada en esa pestaña. Es con el que se trabaja casi siempre, y es el que da acceso al DOM.
Los tres objetos son uno de dentro de otro. La segunda pestaña, en azul, es otro window con su propio document: el navigator es el mismo para las dos.

Cada pestaña tiene su propio window y su propio document, y lo que se escribe en una no afecta a la otra. Por eso, al escribir document.getElementById(…) no hace falta decir de qué página se habla: siempre es la que contiene el script.

Qué es el DOM

Al abrir una página, el navegador lee el archivo HTML y construye con él un árbol de objetos en memoria. Ese árbol es el DOM, y es lo que el navegador dibuja en pantalla.

El archivo y el árbol no son lo mismo. El archivo se lee una vez y se acabó; el árbol está vivo mientras la página esté abierta, y puede cambiar sin que el archivo cambie.

En la asignatura ya hemos trabajado sobre el DOM en dos ocasiones:

El vocabulario del árbol

Este documento HTML

<html>
  <head>
    <title></title>
    <meta>
    <style></style>
  </head>
  <body>
    <header></header>
    <main></main>
    <footer></footer>
  </body>
</html>

produce este árbol:

                        html
                          │
             ┌────────────┴────────────┐
            head                      body
             │                         │
    ┌────────┼────────┐       ┌────────┼────────┐
  title    meta    style   header    main    footer

Sobre él se definen los términos:

Nodo
Cada punto del árbol. Son nodos los elementos, y también el texto contenido en ellos.
Elemento
El nodo que corresponde a una etiqueta. Los diez del dibujo son elementos.
Raíz
El nodo del que desciende el resto: html.
Padre e hijo
Un nodo es padre de los que cuelgan directamente de él. head es padre de title, meta y style; esos tres son sus hijos.
Hermanos
Los nodos que comparten padre. head y body son hermanos, y también lo son header, main y footer. En cambio title y header no lo son: están al mismo nivel pero tienen padres distintos.
Descendiente
Cualquier nodo que cuelgue de otro, directamente o a través de otros. footer es hijo de body y descendiente de body y de html.

Un selector descendiente de CSS es un recorrido por ese árbol. nav ul li a designa los nodos a que descienden de un li, que desciende de un ul, que desciende de un nav. El selector .con-submenu:hover .submenu parte de un nodo y desciende hasta otro.

Cada relación del árbol tiene su selector:

Relación En el árbol de arriba Selector
Descendiente footer desciende de html html footer
Hijo header es hijo de body body > header
Hermano siguiente main viene detrás de header header + main
Raíz html :root

Qué añade JavaScript

El CSS lee el árbol y le aplica estilos. JavaScript puede además modificarlo y reaccionar a lo que ocurre. Son tres capacidades:

  1. Seleccionar un nodo del árbol.
  2. Cambiarlo: su texto, sus clases, sus atributos.
  3. Escuchar: pedir que se avise cuando pase algo.

Seleccionar

Tres vías, que corresponden a las tres formas de identificar un elemento en HTML: por id, por etiqueta y por clase.

getElementById("menu")
Devuelve el elemento cuyo id es ese, o null si no existe. El id es único en el documento, así que solo puede devolver uno. No lleva almohadilla.
getElementsByTagName("p")
Devuelve todos los elementos de esa etiqueta, en una colección.
getElementsByClassName("foto")
Devuelve todos los que llevan esa clase. Sin punto.

Las dos últimas llevan plural —getElementsBy…— y devuelven siempre una colección, aunque solo encuentren un elemento.

document.getElementById("menu")           // id, sin #
document.getElementsByTagName("p")        // todos los párrafos
document.getElementsByClassName("foto")   // clase, sin punto

Las tres parten de document, que representa el documento entero, pero también pueden partir de un elemento ya seleccionado. En ese caso la búsqueda se limita a sus descendientes:

const galeria = document.getElementById("galeria");
const fotos = galeria.getElementsByTagName("img");   // solo las de dentro de la galería

Un elemento o una colección

getElementById devuelve un elemento, con el que se puede trabajar directamente. Los otros dos devuelven una colección, y una colección no es un elemento: no se le puede cambiar el texto ni la clase. Hay que entrar en ella por el número de orden, que empieza en cero, o recorrerla entera.

const fotos = document.getElementsByClassName("foto");

fotos[0].classList.add("vista");           // solo la primera

for (let i = 0; i < fotos.length; i++) {    // todas, una por una
  fotos[i].classList.add("vista");
}

fotos.length dice cuántas hay. Y la colección está viva: si más tarde se añade al documento otro elemento con esa clase, aparece en ella sin volver a buscar, y length cambia solo.

Recorrer el árbol desde un nodo

Teniendo un elemento se puede llegar a sus parientes, con los nombres del vocabulario de antes:

caja.parentElement        // su padre
caja.children             // sus hijos, en una colección
caja.firstElementChild    // el primer hijo
caja.lastElementChild     // el último
caja.nextElementSibling   // el hermano siguiente
caja.previousElementSibling   // el hermano anterior
caja.closest(".card")  // el antepasado más cercano que cumpla ese selector

closest sube por el árbol, que es justo lo que los selectores de CSS no pueden hacer.

Cambiar

Sobre un elemento seleccionado se puede modificar su contenido, sus clases y sus atributos.

El contenido:

caja.textContent = "Otro texto";            // el texto, sin interpretar etiquetas
caja.innerHTML = "<strong>Otro</strong>";   // interpretando el HTML

Entre los dos, textContent es el seguro. innerHTML interpreta lo que se le pase como HTML, de modo que si ese texto procede de lo que alguien ha escrito en un formulario, se estaría ejecutando código ajeno dentro de la página.

Las clases:

caja.classList.add("activa");               // añadir
caja.classList.remove("activa");            // quitar
caja.classList.toggle("activa");            // alternar
caja.classList.contains("activa")           // consultar: true o false

Los atributos:

enlace.getAttribute("href")                 // leer
enlace.setAttribute("href", "work.html");   // escribir
boton.removeAttribute("disabled");          // eliminar

imagen.src = "IMG/otra.jpg";                // los más usados tienen atajo
imagen.alt = "Otra foto";
campo.value = "";

También se puede escribir directamente en el estilo del elemento:

caja.style.backgroundColor = "red";         // se desaconseja

Se desaconseja porque equivale a un atributo style escrito a mano: gana a todo lo de la hoja de estilos, dispersa la apariencia entre dos ficheros y obliga a desandarlo para quitarlo. Lo que corresponde es cambiar la clase y dejar la apariencia en el CSS, como se explica más abajo.

Los eventos

Un evento es un suceso que el navegador detecta: un clic, una pulsación de tecla, el final de una animación. addEventListener registra una función que se ejecutará cada vez que ese suceso se produzca en ese elemento.

boton.addEventListener("click", function () {
  // lo que hay que hacer cuando ocurra
});

La función no se ejecuta al declararla: queda registrada y el navegador la invoca cuando el suceso ocurre.

En los ejemplos que siguen, el código lleva dos colores:

El resto —document, addEventListener, function, los nombres de etiqueta— son palabras del lenguaje o del navegador, y van en negro.

un clic que cambia el texto

<div class="row">
  <button id="boton">Púlsame</button>
  <div class="caja" id="aviso">Todavía no me han pulsado</div>
</div>

Familias de eventos

Hay muchos; estos son los que se encuentran siempre.

De ratón
click, dblclick, mouseenter, mouseleave, mousemove.
De teclado
keydown, keyup.
De formulario
submit al enviarlo, input mientras se escribe, change al terminar de cambiar un campo, focus y blur al entrar y salir.
De la página
DOMContentLoaded cuando el árbol está construido, load cuando además han llegado las imágenes, scroll, resize.
De animación
transitionend y animationend, para encadenar algo justo cuando termina el movimiento.

Los atributos onclick que aparecen en los manuales

En W3Schools y en mucho código antiguo se ve esto:

<button onclick="alert('hola')">Púlsame</button>

Funciona, pero es la forma antigua y tiene dos problemas: mezcla el comportamiento con la estructura, que es justo lo que se lleva separando todo el curso, y solo admite una reacción por evento, porque la segunda pisa a la primera. Con addEventListener se pueden apuntar las que haga falta.

Los tres avisos del navegador

El navegador trae tres ventanas de aviso hechas. No hay que construirlas ni darles estilo: se invocan y aparecen.

alert("texto")
Muestra un mensaje con un solo botón. No devuelve nada.
confirm("pregunta")
Muestra una pregunta con Aceptar y Cancelar. Devuelve true o false.
prompt("pregunta")
Pide un texto. Devuelve lo escrito, o null si se cancela.

Las tres detienen la página mientras están abiertas: no se puede hacer nada más hasta cerrarlas.

El aviso de bienvenida

alert: un mensaje y un botón

<div class="row">
  <button id="oferta">Ver la oferta de hoy</button>
</div>

El aviso de las cookies

Aquí no basta con informar: hay que saber qué ha contestado. Para eso sirve confirm, cuyo valor de retorno se guarda en una variable y se consulta con un if.

confirm: la respuesta se guarda y se consulta

<div class="row">
  <button id="cookies">Preguntar por las cookies</button>
  <div class="caja" id="resultado">Sin respuesta todavía</div>
</div>

Por qué los sitios reales no usan estas ventanas

Las tres tienen tres inconvenientes: no admiten estilo alguno, detienen la página, y el navegador permite silenciarlas. Un aviso de cookies hecho así sería ilegible en un móvil y el usuario podría bloquearlo.

Lo que se hace es escribir el aviso como un elemento más del documento, con su CSS, y quitarlo del árbol o taparlo con una clase cuando se responde. El aspecto del aviso está en la hoja de estilos; JavaScript solo le cambia la clase.

el mismo aviso, hecho como elemento del documento

<div class="cookie-banner" id="aviso">
  <p>Respetamos tu privacidad. Acepta las cookies y continúa navegando
    la web sin límites.</p>
  <div class="row">
    <button id="aceptar">Aceptar</button>
    <button id="rechazar">Rechazar</button>
  </div>
</div>

<p class="caja" id="estado">Sin respuesta todavía</p>

Las dos funciones del final llaman a una tercera, responder, en vez de repetir las dos líneas cada una. Lo que cambia entre ambas es un texto, y eso es exactamente lo que se le pasa como argumento.

Cómo interviene JavaScript en el aspecto

El aspecto lo define el CSS. Para cambiarlo con la página ya abierta, JavaScript opera sobre el DOM: selecciona un elemento y le añade, le quita o le modifica una clase o un atributo. El significado de esa clase está escrito en la hoja de estilos, y es la hoja de estilos la que produce el cambio.

Así se trabajará en estos apuntes. JavaScript puede además escribir valores de estilo directamente sobre un elemento —y en algún ejemplo hará falta—, pero entonces el aspecto queda repartido entre dos archivos y hay que buscarlo en dos sitios.

JavaScript pone la clase; el movimiento lo hace el CSS

<div class="row">
  <button id="interruptor">Pulsa el botón para aceptar</button>
</div>

<div class="panel" id="panel">
  Muchas gracias por aceptar
</div>

El JavaScript del ejemplo no contiene ningún color, medida ni duración. Modificar la forma en que el panel aparece exige cambiar el CSS y nada más.

Qué permite que no permitía el CSS solo

Los disparadores de CSS tienen dos limitaciones. :hover no existe en una pantalla táctil, y :checked exige que el elemento disparador preceda en el HTML al elemento afectado, porque los selectores no ascienden ni retroceden.

JavaScript no está sujeto a esa restricción: alcanza cualquier nodo del árbol con independencia de su posición. De ahí que el menú de tipo hamburguesa, en el que un botón situado en la cabecera despliega una lista que puede estar en cualquier punto del documento, se resuelva por esta vía.

un menú que se abre con un botón, no con el ratón

<button id="hamburguesa" aria-expanded="false">Menú</button>

<nav class="dropdown" id="menu">
  <ul>
    <li><a href="#">Inicio</a></li>
    <li><a href="#">Work</a></li>
    <li><a href="#">About me</a></li>
    <li><a href="#">Contact</a></li>
  </ul>
</nav>

aria-expanded no altera la presentación: es la información que recibe quien navega con un lector de pantalla. Un control que despliega contenido debe declarar su estado.

Cambiar de estado

Hasta aquí JavaScript ha cambiado texto y clases. También puede cambiar atributos, y el caso más frecuente es el src de una imagen: sustituir un archivo por otro sin recargar la página.

Hacen falta dos cosas: una variable que recuerde en qué estado está, y un if que decida qué archivo poner. La variable es necesaria porque el navegador no guarda nada entre un clic y el siguiente: cada vez que la función se ejecuta, empieza sin memoria de lo que hizo la vez anterior.

Los dos archivos de este ejemplo (bombilla-on.svg y bombilla-off.svg) están en la carpeta IMG.

cambiar el src de una imagen

<div class="row">
  <img src="IMG/bombilla-off.svg" alt="Bombilla apagada" id="bombilla" class="bombilla">
  <button id="interruptor-luz">Encender</button>
</div>

El alt se cambia junto con el src. Si no, quien no ve la imagen seguiría leyendo que la bombilla está apagada.

let, y no const: el valor de encendida cambia en cada clic, y lo que se declara con const no se puede volver a asignar. Las referencias a los elementos sí van con const, porque se buscan una vez y no se reasignan nunca.

Más de dos: la colección y el índice

Con dos archivos basta un if. Con cinco imágenes haría falta un if de cinco ramas, y con veinte sería inviable. El planteamiento que escala es distinto: se ponen todas las imágenes en el documento y se oculta todas menos una.

Eso exige dos piezas:

  1. Una colección con las imágenes, que es lo que devuelve getElementsByClassName. Sus elementos se numeran desde cero: la primera es la 0, la segunda la 1, y fotos.length dice cuántas hay.
  2. Un índice: una variable con el número de la que toca mostrar.

Y una función que, dado el índice, recorra la colección y deje visible solo esa. El recorrido se hace con un bucle for que va de 0 a fotos.length, tomando en cada vuelta la imagen que ocupa el lugar i.

dos imágenes, un índice y display: none

<div class="slideshow">
  <img class="slide" src="IMG/naturaleza-abetos.jpg" alt="Arroyo entre abetos bajo un cielo nublado">
  <img class="slide" src="IMG/naturaleza-valle.jpg" alt="Valle con niebla al amanecer">
</div>

<div class="row">
  <button id="pasar">Pasar a la siguiente</button>
  <div class="caja" id="contador">1 de 2</div>
</div>

Tres detalles que conviene retener, porque la clase siguiente se construye sobre ellos.

Las imágenes se ocultan desde JavaScript, no desde el CSS. Si el estado inicial se pusiera en la hoja de estilos habría que escribir una regla distinta para cada imagen. La llamada a mostrar(indice) antes de registrar el evento deja la primera visible y las demás ocultas, sea cual sea su número.

El i === n devuelve true o false, y el operador ? : elige entre dos valores según cuál sea. Equivale a un if de dos ramas escrito en una línea.

Este ejemplo usa style.display, que es la excepción al criterio de dejar la apariencia en el CSS. Se admite porque mostrar y ocultar no es apariencia, sino estado, y porque no hay clase que sirva igual para un número indeterminado de imágenes. Si hiciera falta una transición de entrada, sí habría que pasar a una clase, porque un display no se puede transicionar.

Las flechas

Un slideshow se gobierna con dos flechas. No hace falta ninguna imagen para dibujarlas: son caracteres, y se escriben de tres maneras.

Con el carácter directamente

<button>←</button>  <button>→</button>
<button>‹</button>  <button>›</button>

Funciona si el archivo está en UTF-8, como corresponde. Es lo más legible al leer el HTML.

Con la referencia numérica

Se escribe &#, el número del carácter y un punto y coma. Es lo que se encuentra en los manuales, porque no depende de poder teclear el carácter:

&#8592;  ←      flecha a la izquierda
&#8594;  →      flecha a la derecha
&#10094; ❮      ángulo izquierdo (el de los pases de imágenes)
&#10095; ❯      ángulo derecho

Con un triángulo de CSS

Un elemento sin contenido, de ancho y alto cero, al que se le dan tres bordes transparentes y uno con color: lo que queda a la vista es un triángulo. Es un recurso antiguo y hoy no se justifica, porque un carácter hace lo mismo y se puede leer.

Lo que importa: que la flecha diga hacia dónde

Las dos flechas hacen lo mismo con un dato distinto. Lo que corresponde entonces no es escribir dos funciones, sino una función con un argumento, llamada dos veces con valores distintos. El argumento es el sentido.

dos flechas que llaman a la misma función

<div class="row">
  <button class="arrow" id="izquierda" aria-label="Anterior">&#10094;</button>
  <button class="arrow" id="derecha" aria-label="Siguiente">&#10095;</button>
  <div class="caja" id="aviso-flecha">Todavía no se ha pulsado ninguna flecha</div>
</div>

El aria-label es necesario: el contenido del botón es un signo de puntuación, y un lector de pantalla leería «ángulo izquierdo» en vez de «anterior».

La versión que se usará en el slideshow

Repetir el addEventListener por cada flecha funciona con dos, no con muchas. La alternativa es escribir el dato en el propio HTML, con un atributo data-, y registrar el evento una sola vez recorriendo la colección:

<button class="arrow" data-sentido="izquierda">&#10094;</button>
<button class="arrow" data-sentido="derecha">&#10095;</button>
const flechas = document.getElementsByClassName("arrow");

for (let i = 0; i < flechas.length; i++) {
  flechas[i].addEventListener("click", function () {
    mover(flechas[i].dataset.sentido);
  });
}

Los atributos que empiezan por data- son de uso libre: el navegador no les da ningún significado y se leen desde JavaScript con dataset, quitando el data- del nombre. Así el HTML declara qué hace cada botón y el JavaScript no contiene ninguna lista de identificadores.

Dónde se pone el script

Si el navegador encuentra el <script> antes que los elementos, el árbol aún no los contiene y getElementById devuelve null. Hay dos soluciones equivalentes:

<!-- al final del body, cuando ya está todo -->
    <script src="main.js"></script>
  </body>

<!-- o en el head, pidiendo que espere -->
  <script src="main.js" defer></script>

Resumen

  1. El DOM es el árbol que el navegador tiene en memoria, no el archivo.
  2. La selección emplea los selectores de CSS ya conocidos.
  3. Un evento es un suceso detectado por el navegador; addEventListener registra la función que lo atiende.
  4. El aspecto lo define el CSS; JavaScript lo cambia operando sobre el DOM: añade, quita o modifica clases y atributos.
  5. El script, al final del body o con defer.
  6. alert, confirm y prompt son avisos del navegador: no admiten estilo y detienen la página. Un aviso de verdad se escribe como elemento del documento.
  7. Un estado que debe sobrevivir de un clic al siguiente se guarda en una variable let declarada fuera de la función.
  8. Con dos opciones basta un if; con muchas, una colección y un índice.
  9. Dos controles que hacen lo mismo con un dato distinto son una función con un argumento, no dos funciones.

Reconocimientos

Las fotos son de Pexels, con licencia libre: se pueden usar y modificar sin pagar y sin citar la fuente. Citarla no es obligatorio, pero es lo correcto, y aquí sirve además de ejemplo de cómo se hace.