materials

Document Object Model (DOM)

Introducción

La mayoría de las veces que programamos con Javascript es para que se ejecute en una página web mostrada por el navegador. En este contexto tenemos acceso a ciertos objetos que nos permiten interactuar con la página (DOM) y con el navegador (Browser Object Model, BOM).

El DOM es una estructura en árbol que representa todos los elementos HTML de la página y sus atributos. Todo lo que contiene la página se representa como nodos del árbol y mediante el DOM podemos acceder a cada nodo, modificarlo, eliminarlo o añadir nuevos nodos de forma que cambiamos dinámicamente la página mostrada al usuario.

La raíz del árbol DOM es document y de este nodo cuelgan el resto de elementos HTML. Cada uno constituye su propio nodo y tiene subnodos con sus atributos, estilos y elementos HTML que contiene.

Por ejemplo, la página HTML:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Página simple</title>
</head>
<body>
  <p>Esta página es <strong>muy simple</strong></p>
</body>
</html>

se convierte en el siguiente árbol DOM:

Árbol DOM

Cada etiqueta HTML suele originar 2 nodos:

Cada nodo es un objeto con sus propiedades y métodos.

El ejemplo anterior está simplificado porque sólo aparecen los nodos de tipo elemento pero en realidad también generan nodos los saltos de línea, tabuladores, espacios, comentarios, etc. En el siguiente ejemplo podemos ver TODOS los nodos que realmente se generan. La página:

<!DOCTYPE html>
<html>
<head>
  <title>My Document</title>
</head>
<body>
  <h1>Header</h1>
  <p>
    Paragraph
  </p>
</body>
</html>

se convierte en el siguiente árbol DOM:

Dom tree

Acceso a los nodos

Los principales métodos para acceder a los diferentes nodos son:

Una diferencia importante entre los getElementsBy... y los querySelector... es que los primeros devuelven una colección en vivo (si se añaden o eliminan nodos que cumplan la condición la colección se actualiza automáticamente) mientras que los segundos devuelven una colección estática (no se actualiza aunque se añadan o eliminen nodos). Los querySelector... tienen el método forEach para recorrer la colección mientras que los getElementsBy..., aunque todos puedes recorrerse con un for...of.

También tenemos ‘atajos’ para obtener algunos elementos comunes:

EJERCICIO: Para hacer los ejercicios de este tema descárgate esta página de ejemplo y ábrela en tu navegador. Obtén por consola, al menos de 2 formas diferentes:

Acceso a nodos a partir de otros

En muchas ocasiones queremos acceder a cierto nodo a partir de uno dado. Para ello tenemos los siguientes métodos que se aplican sobre un elemento del árbol DOM:

IMPORTANTE: a menos que me interesen comentarios, saltos de línea, etc siempre debo usar los métodos que sólo devuelven elementos HTML, no todos los nodos.

Recorrer el árbol DOM

EJERCICIO: Siguiendo con la página de ejemplo obtén desde la consola, al menos de 2 formas diferentes:

Propiedades de un nodo

Las principales propiedades de un nodo son:

Otras propiedades:

EJERCICIO: Obtén desde la consola, al menos de 2 formas:

Manipular el árbol DOM

Vamos a ver qué métodos nos permiten cambiar el árbol DOM, y por tanto modificar la página:

miLista.replaceChildren(); // vacía la lista (equivale a miLista.innerHTML = ‘’ o miLista.textContent = ‘’)

const li1 = document.createElement(‘li’); li1.textContent = ‘El Quijote’; const li2 = document.createElement(‘li’); li2.textContent = ‘La Regenta’; miLista.replaceChildren(li1, li2); // la lista ahora sólo contiene estos 2 elementos


Otros métodos menos usados son:
* `elemento.appendChild(nuevoNodo)`: añade _nuevoNodo_ como último hijo de _elemento_ y lo devuelve. Se diferencia con _append_ en que sólo permite un parámetro y éste debe ser un nodo, no puede ser texto. Por eso no suele usarse.
* `document.createTextNode('texto')`: crea un nuevo nodo de texto con el texto indicado, que luego tendremos que añadir a un nodo HTML. Normalmente no se usa porque con _append_ y el resto de métodos anteriores se hace más fácilmente. Por ejemplo, el equivalente a `nuevoLi.append('Nuevo elemento de lista')` que hicimos antes sería:
```javascript
let textoLi = document.createTextNode('Nuevo elemento de lista');
nuevoLi.appendChild(textoLi);     // añade el texto creado al elemento LI creado

OJO: Si añado con el método append o appendChild un nodo que estaba en otro sitio se elimina de donde estaba para añadirse a su nueva posición. Si quiero que esté en los 2 sitios deberé clonar el nodo y luego añadir el clon y no el nodo original.

Añadir nuevos nodos con innerHTML

Supongamos que tenemos un DIV cuya id es myDiv al que queremos añadir al final un párrafo con un texto en negrita. El código podría ser:

const miDiv = document.getElementById('myDiv');
const nuevoParrafo = document.createElement('p');
const textoNegrita = document.createElement('strong');
textoNegrita.textContent = 'con texto en negrita';
nuevoParrafo.append('Último párrafo ', textoNegrita);
miDiv.append(nuevoParrafo);

Si utilizamos la propiedad innerHTML el código a usar es mucho más simple:

let miDiv = document.getElementById('myDiv');
miDiv.innerHTML += '<p>Último párrafo <strong>con texto en negrita</strong></p>';

Pero esta forma de hacerlo (miDiv.innerHTML+='<p>...';) aunque es válida no es muy eficiente ya que obliga al navegador a volver a renderizar TODO el contenido de miDIV, y si algún párrafo tuviera algún escuchador de eventos se eliminaría.

La forma ideal sería algo intermedio: creamos el nuevo nodo con createElement y luego le asignamos su contenido con innerHTML:

let miDiv = document.getElementById('myDiv');
let nuevoParrafo = document.createElement('p');
nuevoParrafo.innerHTML = 'Último párrafo <strong>con texto en negrita</strong>';
miDiv.append(nuevoParrafo);

También podemos usar insertAdjacentHTML para añadir un nuevo nodo sin tener que crear el nodo y luego añadirlo y sin que se renderice de nuevo lo que ya estaba. Siguiendo con el ejemplo anterior:

let miDiv = document.getElementById('myDiv');
miDiv.insertAdjacentHTML('beforeend', '<p>Último párrafo <strong>con texto en negrita</strong></p>');

El primer parámetro indica dónde queremos añadir el nuevo nodo:

NOTA IMPORTANTE sobre seguridad: Cuando se inserte texto HTML en una página usando innerHTML o insertAdjacentHTML() debe tener cuidado de no usar cadenas de texto que no contengan las secuencias de escape pertinentes según sea el caso para evitar ataques de inyección de código. Si queremos insertar texto plano (por ejemplo, lo que escriba un usuario en un formulario) debemos escapar los caracteres especiales de HTML como <, >, &, ", ' y /. Por ejemplo, si el usuario escribe <img src="x" onerror="alert('Hola')"> y lo insertamos en la página sin escapar los caracteres especiales, se ejecutará el código JavaScript y se mostrará la alerta. Para evitar esto, podemos usar textContent o insertAdjacentText() para insertar el texto plano sin interpretar el HTML.

Podemos ver más ejemplos de creación y eliminación de nodos en W3Schools.

EJERCICIO: Añade a la página:

Atributos de los nodos

Podemos ver y modificar los valores de los atributos de cada elemento HTML y también añadir o eliminar atributos:

Los elementos tienen propiedades para algunos atributos comunes como id, title o className (para el atributo class) a las que se puede acceder y cambiar su valor directamente. Ejemplos:

elemento.id = 'primera-lista';
// es equivalente a hacer:
elemento.setAttribute('id', 'primera-lista');

Estilos de los nodos

Los estilos están accesibles como el atributo style. Cualquier estilo es una propiedad de dicho atributo pero con la sintaxis camelCase en vez de kebab-case. Por ejemplo para cambiar el color de fondo (propiedad background-color) y ponerle el color rojo al elemento miPrimeraLista haremos:

miPrimeraLista.style.backgroundColor = 'red';

De todas formas normalmente NO CAMBIAREMOS ESTILOS a los elementos sino que les pondremos o quitaremos clases que harán que se le apliquen o no los estilos definidos para ellas en el CSS.

Atributos de clase

Ya sabemos que el aspecto de la página debe configurarse en el CSS por lo que no debemos aplicar atributos style al HTML. En lugar de ello les ponemos clases a los elementos que harán que se les aplique el estilo definido para dicha clase.

Como es algo muy común en lugar de utilizar las instrucciones de elemento.setAttribute('class', 'destacado') o directamente elemento.className='destacado' podemos usar la propiedad classList que devuelve la colección de todas las clases que tiene el elemento. Por ejemplo si elemento es <p class="destacado direccion">...:

let clases=elemento.classList;   // clases=['destacado', 'direccion'], OJO es una colección, no un Array

Además dispone de los métodos:

Atributos de datos

HTML5 permite agregar atributos personalizados no visuales a las etiquetas utilizando data-*. Estos atributos pueden ser accesibles a través de JavaScript usando dataset.

<article
    id="electriccars"
    data-columns="3"
    data-index-number="12314"
    data-parent="cars">
    ...
</article>
let article = document.getElementById('electriccars');
console.log(article.dataset.columns); // "3"
console.log(article.dataset.indexNumber); // "12314"

Fuente: Curso DWEC de José Castillo

Reaccionar a las acciones del usuario

Para poder hacer cosas cuando el usuario interactúa con la página (pulsa un botón, envía un formulario, …) tenemos que escuchar los eventos que se producen. Los veremos en detalle en el tema de eventos pero de momento nos basta con saber que con el método addEventListener sobre un elemento indicamos qué función queremos que se ejecute cuando se produzca un evento sobre dicho elemento. Recibe 2 parámetros:

const boton = document.getElementById('borrar');
boton.addEventListener('click', (event) => {
  // este código se ejecuta cada vez que se pulse el botón
  boton.closest('li').remove();
});

En el caso de un formulario no escucharemos el click del botón de enviar sino el evento submit del formulario, que se produce tanto al pulsar el botón como al pulsar Enter en un campo. Además, por defecto el navegador envía el formulario y recarga la página, así que lo primero que haremos será evitarlo con event.preventDefault():

const form = document.getElementById('form-libro');
form.addEventListener('submit', (event) => {
  event.preventDefault();      // evitamos que se recargue la página
  // Cojemos los datos del formulario
  const titulo = document.getElementById('titulo').value;
  // ... y los usamos
});

OJO: un escuchador se pone a un elemento concreto. Si ese elemento se borra o se vuelve a crear (por ejemplo, con innerHTML += sobre su padre) el escuchador desaparece.