Hasta ahora has usado JavaScript como lenguaje. Lo que lo hace útil en el navegador es poder leer y cambiar la página. Para eso, el navegador no te da el HTML como texto: lo convierte en un árbol de objetos llamado DOM (Document Object Model), y ese árbol es lo que JavaScript manipula y lo que realmente se pinta en pantalla.
1 // Del HTML al árbol
Cuando el navegador recibe un documento HTML, lo analiza y crea un objeto por cada etiqueta, cada fragmento de texto y cada comentario. Esos objetos se llaman nodos y se organizan en un árbol que refleja cómo están anidadas las etiquetas:
<body>
<header>
<h1>Tareas</h1>
</header>
<ul id="lista">
<li class="hecha">Comprar</li>
<li>Estudiar</li>
</ul>
</body>Hay varios tipos de nodos, pero dos son los que vas a manejar a diario:
- Nodos elemento: uno por etiqueta (
<ul>,<li>,<button>). Tienen atributos, clases y pueden tener hijos. - Nodos de texto: el contenido textual. No tienen hijos y, como detalle curioso, los espacios y saltos de línea entre etiquetas también generan nodos de texto.
La raíz del árbol es el objeto global document. Desde él se llega a todo lo demás: document.documentElement es <html>, document.head y document.body son lo que su nombre indica.
El HTML es sólo la receta inicial. Una vez construido el árbol, la página es el DOM: si JavaScript cambia el árbol, cambia lo que ves, aunque el fichero HTML siga igual. Por eso la pestaña Elementos de las herramientas de desarrollo muestra el DOM vivo, no el código fuente.
2 // Seleccionar elementos
Antes de cambiar nada hay que encontrar el nodo. La forma moderna es usar selectores CSS, los mismos que ya escribes en las hojas de estilo:
const lista = document.querySelector('#lista'); // por id
const primera = document.querySelector('.hecha'); // primera coincidencia
const tareas = document.querySelectorAll('#lista li'); // todas las coincidencias
const campo = document.querySelector('input[name="email"]'); // por atributo| Método | Devuelve | Si no encuentra nada |
|---|---|---|
querySelector(sel) |
El primer elemento que coincide | null |
querySelectorAll(sel) |
Una NodeList con todos |
NodeList vacía |
getElementById(id) |
El elemento con ese id | null |
querySelector y querySelectorAll también existen sobre cualquier elemento, no sólo sobre document. Así la búsqueda se limita a sus descendientes: lista.querySelectorAll('li') sólo busca dentro de la lista.
La NodeList que devuelve querySelectorAll se parece a un array: tiene length, se puede recorrer con for...of y tiene forEach. Pero no tiene map ni filter. Si los necesitas, conviértela primero:
const textos = Array.from(tareas).map(li => li.textContent);
// o bien: [...tareas].map(li => li.textContent)Cualquier cosa que hagas sobre el resultado de querySelector fallará si el elemento no existe, con el ya conocido Cannot read properties of null. Si un elemento puede no estar, compruébalo antes (if (boton) { ... }) o usa el encadenamiento opcional: boton?.focus().
3 // Moverse por el árbol
A veces no buscas desde arriba, sino a partir de un elemento que ya tienes: el botón que se ha pulsado, la fila de una tabla, un campo de un formulario. Cada nodo conoce a su familia:
| Propiedad | Qué devuelve |
|---|---|
el.parentElement |
El elemento padre |
el.children |
Los elementos hijos (sin nodos de texto) |
el.firstElementChild / el.lastElementChild |
El primer / último hijo elemento |
el.nextElementSibling / el.previousElementSibling |
El hermano siguiente / anterior |
el.closest(sel) |
El antepasado más cercano (o él mismo) que cumple el selector |
Fíjate en que todas llevan la palabra Element. Existen versiones sin ella (childNodes, firstChild, nextSibling) que incluyen los nodos de texto, así que te devolverían los saltos de línea entre etiquetas. Salvo que trabajes con texto a propósito, usa las versiones con Element.
De todas, closest es la más útil en la práctica, porque no depende de cuántos niveles haya en medio: sube desde el elemento hasta encontrar el que buscas.
Una página muestra tarjetas de productos. Cada tarjeta tiene un botón «Añadir», y al pulsarlo necesitas el nombre del producto de esa tarjeta:
<article class="tarjeta" data-id="42">
<h2 class="nombre">Teclado mecánico</h2>
<div class="acciones">
<button class="anadir">Añadir</button>
</div>
</article>- Punto de partida: tienes el
buttonque se pulsó, que está dos niveles por debajo de la tarjeta. - Opción frágil:
boton.parentElement.parentElementfunciona hoy, pero se rompe si alguien añade otrodivalrededor del botón. - Subir hasta la tarjeta:
const tarjeta = boton.closest('.tarjeta');devuelve elarticlesin importar la profundidad. - Bajar hasta el dato:
tarjeta.querySelector('.nombre').textContentbusca sólo dentro de esa tarjeta.
closest para subir y querySelector para bajar obtienes 'Teclado mecánico', y el código sigue funcionando aunque cambie el marcado intermedio. Esta combinación es la base de la delegación de eventos que verás más adelante en este módulo.4 // Para practicar
Dos páginas para practicar la selección y el recorrido del árbol, sin modificar nada todavía. Cada ejercicio trae el enunciado en los comentarios del código.
Selectores sobre un blogtraining-daw-sv/modulo-01/01-el-arbol-dom/01_domContar y filtrar enlacestraining-daw-sv/modulo-01/01-el-arbol-dom/02_count_linksAutoevaluación
Cambias un texto con JavaScript, pulsas «Ver código fuente» y el texto antiguo sigue ahí. En la pestaña Elementos de las herramientas de desarrollo, en cambio, ves el nuevo. ¿Por qué?
¿Qué diferencia hay entre lo que devuelven querySelector('.tarea') y querySelectorAll('.tarea') cuando no hay ninguna coincidencia?
querySelector devuelve null; querySelectorAll, una lista vacía. Por eso llamar a un método sobre el resultado de querySelector puede lanzar un error, mientras que recorrer una NodeList vacía simplemente no hace nada.Tienes el botón «Borrar» de una tarjeta y quieres llegar a la tarjeta completa (.tarjeta) que lo contiene, sin saber cuántos niveles hay entre medias. ¿Qué método usarías y cómo funciona?
boton.closest('.tarjeta'). Sube por el árbol desde el propio botón, comprobando el selector en cada antepasado, y devuelve el primero que coincide. No depende de la estructura intermedia, así que sigue funcionando aunque se añadan envoltorios.