Ir al contenido
wannacode

El DOM: la página desde JavaScript

En resumen: El DOM es el árbol de objetos con el que JavaScript lee y cambia la página: querySelector encuentra un elemento, textContent cambia su texto, y createElement y append añaden otros nuevos.

Problema

El precio se queda en «—»

El precio y el stock del producto vienen de JavaScript. Un compañero puso el script justo al principio «para que arranque antes». La página muestra «—» en lugar de un precio.

HTML, CSS, JS

La consola dice dos cosas. console.log(price) imprimió null, y la línea siguiente falla con TypeError: Cannot set properties of null. El elemento <strong id="price"> está ahí mismo, en el HTML. ¿Por qué JavaScript no lo encuentra?

La idea

El documento como objeto

En el módulo de HTML viste que el navegador construye un árbol a partir del texto. JavaScript trabaja con el mismo árbol: para él la página es un objeto llamado document. Cada elemento del árbol es un objeto con campos y métodos. Por eso el árbol se llama DOM: Document Object Model.

Un elemento se busca con un selector, igual que en CSS:

const price = document.querySelector('#price'); // por id
const title = document.querySelector('h1'); // por etiqueta
const first = document.querySelector('.product'); // por clase: el primero
const links = document.querySelectorAll('nav a'); // todos los enlaces dentro de nav
  • querySelector recorre el árbol de arriba abajo y devuelve el primer elemento que coincide. Si no hay ninguno, devuelve null.
  • querySelectorAll los devuelve todos como una NodeList: una lista con length y forEach, pero sin map ni filter. Para usarlos, conviértela en un array: [...links]. Los arrays y forEach son del curso de JavaScript: arrays y lambdas.
  • La NodeList es una instantánea: un elemento añadido después no está en ella.

null significa «no hay tal elemento», no «un elemento vacío». El error llega después: null no tiene textContent, y el código falla en la línea siguiente.

La idea

Cuándo se ejecuta el script

El parser lee el HTML de arriba abajo. Cuando llega a <script>, se detiene, ejecuta el script y solo entonces sigue. En el problema el script está antes de <article>. Cuando se ejecuta, el árbol solo tiene <body>, y el precio todavía no está. Por eso querySelector devuelve null.

La solución es hacer que el script espere al árbol:

<head>
<meta charset="utf-8">
<script src="script.js" defer></script>
</head>
  • defer dice: descarga el script ya, pero ejecútalo cuando todo el HTML esté leído. Para entonces cada elemento está en el árbol.
  • La forma antigua es un <script> al final del todo, justo antes de </body>. Funciona por la misma razón.
  • defer solo funciona en un script con src. En un script con código entre las etiquetas no hace nada.

Pruébalo

Arregla el precio

Arregla la página para que muestre el precio y el stock. No toques script.js: el problema es cuándo se ejecuta, no lo que hace.

HTML, CSS, JS

La idea

El texto: textContent e innerHTML

El elemento que encontraste lo cambias a través de sus campos. Para el texto hay dos:

price.textContent = '1,80 €'; // el texto tal cual
note.innerHTML = 'Precio: <strong>1,80 €</strong>'; // el texto se lee como HTML
  • textContent pone texto. Si contiene <strong>, se ve como caracteres, no como una etiqueta.
  • innerHTML le pasa el texto al parser, que construye elementos nuevos con él. Los hijos anteriores del elemento desaparecen.

innerHTML con datos de un usuario (un nombre, una reseña, una búsqueda) es peligroso. Si contiene HTML con código, ese código se ejecuta en el navegador de cualquiera que abra la página: es un ataque XSS, del que trata la lección «Entrada no confiable». Por eso el texto se pone con textContent, y los elementos nuevos se crean con createElement (más abajo).

Después del cambio el navegador vuelve a dibujar por sí solo. No toda la página, solo lo que cambió.

Míralo

Mira cómo cambia la página

A la izquierda está el árbol, a la derecha la página que el navegador dibuja a partir de él. El código encuentra el carrito, cambia su texto y su clase y añade una línea nueva a la lista.

const cart = document.querySelector('#cart');
cart.textContent = 'Carrito: 1';
cart.classList.add('full');
const li = document.createElement('li');
li.textContent = 'Leche';
document.querySelector('ul').append(li);
Paso a paso, en palabras
  1. El parser ya construyó el árbol a partir del HTML. A la derecha está la página que el navegador dibuja con él. JavaScript ve el mismo árbol a través de document.
  2. querySelector('#cart') recorre el árbol desde arriba y compara cada elemento con el selector. Devuelve el primero que coincide: cart ya apunta a ese nodo. Si no hay ninguno, devuelve null.
  3. textContent reemplaza el texto del nodo. El navegador nota el cambio y vuelve a dibujar solo esa parte de la página. Lo demás se queda como estaba.
  4. classList.add pone una clase en el elemento. Una regla CSS para .full le da un color nuevo, y otra vez solo se vuelve a dibujar el carrito.
  5. createElement crea un <li> nuevo, y textContent le da texto. Todavía no está en el árbol, así que la página no cambia.
  6. append lo pone como último hijo de <ul>. Ahora está en el árbol y la lista de la página crece una línea.
  7. JavaScript nunca dibuja la página por sí mismo. Cambia el árbol, y el navegador dibuja lo que cambió. La página no se vuelve a cargar.

JavaScript no dibuja nada por sí mismo. Cambia el árbol, y el navegador dibuja el cambio. La página no se vuelve a cargar, así que el texto de los campos y el punto donde estabas con el scroll se conservan.

Pruébalo

Quedan 3 unidades

Muestra el stock en #stock: «Quedan 3 uds.». Cuando queden menos de 5, añade al elemento la clase low (el CSS la pone en rojo). El texto en negrita de la clase stock tiene que seguir.

HTML, CSS, JS

La idea

Atributos, propiedades y clases

En HTML un elemento tiene atributos: value="1", disabled, data-id="7". En JavaScript tiene propiedades: campos del objeto. La mayoría de los atributos tienen una propiedad con el mismo nombre, pero no son lo mismo.

  • input.value es lo que hay ahora en el campo. El atributo value es el valor inicial y no cambia mientras la compradora escribe. El atributo se lee con getAttribute('value').
  • button.disabled = true apaga el botón. La propiedad es true o false.
  • Los atributos data-… sirven para tus propios datos. Los lees con dataset: data-stock es dataset.stock, y data-product-id es dataset.productId. El valor es siempre una cadena.
  • Con las clases trabajas a través de classList: add, remove, toggle (añade la clase si falta y la quita si está) y contains.
HTML, CSS, JS

La consola muestra que la propiedad es 3, mientras que el atributo sigue siendo 1. dataset.stock es "12", una cadena: conviértela en número con Number(...) antes de calcular con ella.

Pruébalo

Encuentra el error

La oferta debería poner todos los precios en rojo, pero la consola dice Cannot read properties of undefined (reading 'add').

Elige la línea con el error.

La idea

Elementos nuevos

Un elemento nuevo se hace en tres pasos: crearlo, darle contenido y ponerlo en el árbol.

const li = document.createElement('li'); // un <li> nuevo, todavía no está en la página
li.textContent = 'Leche';
li.dataset.id = '2';
document.querySelector('ul').append(li); // ahora es el último hijo de <ul>

Mientras no lo pongas con append, el elemento no está en la página. append también acepta varios elementos a la vez: li.append(title, button). prepend lo pone al principio, y remove() saca el elemento del árbol.

Más precisamente, para curiosos

Más formas de buscar. getElementById('price') equivale a querySelector('#price'). getElementsByClassName y getElementsByTagName devuelven una lista «viva»: cambia cuando cambia el árbol. querySelectorAll devuelve una instantánea. Después de un append de un .price nuevo, la lista viva tiene un elemento más y la instantánea no.

Buscar dentro de un elemento. Todos los elementos tienen querySelector, no solo document: card.querySelector('.price') busca únicamente dentro de la tarjeta.

setAttribute('disabled', 'false') apaga el botón. En disabled solo importa que el atributo esté presente, no su valor. Por eso se escribe button.disabled = false o removeAttribute('disabled').

innerHTML y <script>. Un script puesto con innerHTML no se ejecuta. Un atributo como onerror en una imagen, sí. Por eso innerHTML con datos ajenos es peligroso incluso sin <script>.

className frente a classList. className es todo el atributo class como una sola cadena. Si le asignas algo, reemplazas todas las clases de golpe.

Ejercicio

La tarjeta del producto

script.js tiene un objeto product. Rellena la tarjeta con él: el nombre es el encabezado (#name), el precio es «8,90 €» (dos decimales, con coma) y el stock es «Agotado» cuando stock es 0. Entonces la tarjeta recibe la clase sold-out y el botón se apaga. Pon en la tarjeta un data-id con el id del producto. Por cada etiqueta de badges, añade un <li> a #badges.

HTML, CSS, JS

Test

Ponte a prueba

1. ¿Qué devuelve document.querySelector('#total') cuando la página no tiene ningún elemento con id="total"?
2. El script está en <head> sin defer y busca un botón del <body>. ¿Qué encuentra?
3. El nombre de un producto lo escribe un usuario. ¿Cómo lo pones en la página?
4. La compradora escribió 3 en <input value="1">. ¿Qué dan input.value y input.getAttribute('value')?
5. ¿Cuál añade la clase sale a todos los elementos .price?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·