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:
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:
Los principales métodos para acceder a los diferentes nodos son:
let nodo = document.getElementById('main'); // nodo contendrá el nodo cuya id es _main_
let nodos = document.getElementsByClassName('error'); // nodos contendrá todos los nodos cuya clase es _error_
NOTA: las colecciones son similares a arrays (se accede a sus elementos con [indice]) pero no se les pueden aplicar sus métodos filter, map, … a menos que se conviertan a arrays con Array.from()
let nodos = document.getElementsByTagName('p'); // nodos contendrá todos los nodos de tipo _<p>_
name
con el valor indicado. Ej.:
let radiosSexo = document.getElementsByName('sexo'); // radiosSexo contendrá todos los nodos con ese atributo (seguramente radiobuttons con name="sexo")
let nodo = document.querySelector('p.error'); // nodo contendrá el primer párrafo de clase _error_
let nodos = document.querySelectorAll('p.error'); // nodos contendrá todos los párrafos de clase _error_
NOTA: al aplicar estos métodos sobre document se seleccionará sobre la página pero podrían también aplicarse a cualquier nodo y en ese caso la búsqueda se realizaría sólo entre los descendientes de dicho nodo.
También tenemos ‘atajos’ para obtener algunos elementos comunes:
document.documentElement
: devuelve el nodo del elemento <html>document.head
: devuelve el nodo del elemento <head>document.body
: devuelve el nodo del elemento <body>document.title
: devuelve el nodo del elemento <title>document.link
: devuelve una colección con todos los hiperenlaces del documentodocument.anchor
: devuelve una colección con todas las anclas del documentodocument.forms
: devuelve una colección con todos los formularios del documentodocument.images
: devuelve una colección con todas las imágenes del documentodocument.scripts
: devuelve una colección con todos los scripts del documentoEJERCICIO: 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:
- El elemento con id ‘input2’
- La colección de párrafos
- Lo mismo pero sólo de los párrafos que hay dentro del div ‘lipsum’
- El formulario (ojo, no la colección con el formulario sino sólo el formulario)
- Todos los inputs
- Sólo los inputs con nombre ‘sexo’
- Los items de lista de la clase ‘important’ (sólo los LI)
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:
elemento.parentElement
: devuelve el elemento padre de elementoelemento.children
: devuelve la colección con todos los elementos hijo de elemento (sólo elementos HTML, no comentarios ni nodos de tipo texto)elemento.childNodes
: devuelve la colección con todos los hijos de elemento, incluyendo comentarios y nodos de tipo texto por lo que no suele utilizarseelemento.firstElementChild
: devuelve el elemento HTML que es el primer hijo de elementoelemento.firstChild
: devuelve el nodo que es el primer hijo de elemento (incluyendo nodos de tipo texto o comentarios)elemento.lastElementChild
, elemento.lastChild
: igual pero con el último hijoelemento.nextElementSibling
: devuelve el elemento HTML que es el siguiente hermano de elementoelemento.nextSibling
: devuelve el nodo que es el siguiente hermano de elemento (incluyendo nodos de tipo texto o comentarios)elemento.previousElementSibling
, elemento.previousSibling
: igual pero con el hermano anteriorelemento.hasChildNodes
: indica si elemento tiene o no nodos hijoselemento.childElementCount
: devuelve el nº de nodos hijo de elementoelemento.closest(selector)
: devuelve el ancestro más cercano que coincide con el selector. Por ejemplo si el elemento es un <td> de una tabla entonces elemento.closest('table')
devolverá la tabla a la que perteneceIMPORTANTE: a menos que me interesen comentarios, saltos de página, etc siempre debo usar los métodos que sólo devuelven elementos HTML, no todos los nodos.
EJERCICIO: Siguiento con la página de ejemplo obtén desde la consola, al menos de 2 formas diferentes:
- El primér párrafo que hay dentro del div ‘lipsum’
- El segundo párrafo de ‘lipsum’
- El último item de la lista
- La label de ‘Escoge sexo’
Las principales propiedades de un nodo son:
elemento.innerHTML
: todo lo que hay entre la etiqueta que abre elemento y la que lo cierra, incluyendo otras etiquetas HTML. Por ejemplo si elemento es el nodo <p>Esta página es <strong>muy simple</strong></p>
let contenido = elemento.innerHTML; // contenido='Esta página es <strong>muy simple</strong>'
elemento.textContent
: todo lo que hay entre la etiqueta que abre elemento y la que lo cierra, pero ignorando otras etiquetas HTML. Siguiendo con el ejemplo anterior:
let contenido = elemento.textContent; // contenido='Esta página es muy simple'
elemento.value
: devuelve la propiedad ‘value’ de un <input> (en el caso de un <input> de tipo text devuelve lo que hay escrito en él). Como los <inputs> no tienen etiqueta de cierre (</input>) no podemos usar .innerHTML ni .textContent. Por ejemplo si elem1 es el nodo <input name="nombre">
y elem2 es el nodo <input tipe="radio" value="H">Hombre
let cont1 = elem1.value; // cont1 valdría lo que haya escrito en el <input> en ese momento
let cont2 = elem2.value; // cont2="H"
Otras propiedades:
elemento.innerText
: igual que textContentelemento.focus
: da el foco a elemento (para inputs, etc). Para quitarle el foco elemento.blur
elemento.clientHeight
/ elemento.clientWidth
: devuelve el alto / ancho visible del elementoelemento.offsetHeight
/ elemento.offsetWidth
: devuelve el alto / ancho total del elementoelemento.clientLeft
/ elemento.clientTop
: devuelve la distancia de elemento al borde izquierdo / superiorelemento.offsetLeft
/ elemento.offsetTop
: devuelve los píxels que hemos desplazado elemento a la izquierda / abajoEJERCICIO: Obtén desde la consola, al menos de 2 formas:
- El innerHTML de la etiqueta de ‘Escoge sexo’
- El textContent de esa etiqueta
- El valor del primer input de sexo
- El valor del sexo que esté seleccionado (difícil, búscalo por Internet)
Vamos a ver qué métodos nos permiten cambiar el árbol DOM, y por tanto modificar la página:
document.createElement('etiqueta')
: crea un nuevo elemento HTML con la etiqueta indicada, pero aún no se añade a la página. Ej.:
let nuevoLi = document.createElement('li');
elemento.append(elementos o texto)
: añade al DOM los parámetros pasados como últimos hijos de elemento. Se le puede pasar tanto un nodo DOM como una cadena de texto (para la que se creará su nodo de texto correspondiente) y que se le pueden pasar varios parámetros para crear varios nodos. Ej.:
nuevoLi.append('Nuevo elemento de lista'); // añade el texto pasado al elemento LI creado
let miPrimeraLista = document.getElementsByTagName('ul')[0]; // selecciona el 1º UL de la página
miPrimeraLista.append(nuevoLi); // añade LI como último hijo de UL, es decir al final de la lista
elemento.prepend(elementos o texto)
: como el anterior pero en lugar de añadirlos como últimos hijos los añade antes del primer hijo.
const primerLi = document.createElement('li');
primerLi.append('Primer elemento de lista');
let miPrimeraLista = document.getElementsByTagName('ul')[0];
miPrimeraLista.prepend(nuevoLi);
elemento.after(elementos o texto)
: como append pero en lugar de añadirlos como últimos hijos los añade como los siguientes hermanos de elemento.
const otroLi = document.createElement('li');
otroLi.append('Segundo elemento de lista');
primerLi.after(otroLi);
elemento.before(elementos o texto)
: como el anterior pero los añade como los anteriores hermanos de elemento.elemento.remove()
: borra el nodo elemento del documento.elemento.replaceWith(nuevoNodo)
: reemplaza el nodo elemento con el nuevoNodo pasado
let primerElementoDeLista = document.getElementsByTagName('ul')[0].firstChild; // selecciona el 1º LI de miPrimeraLista
primerElementoDeLista.replaceChild(nuevoLi); // reemplaza el 1º elemento de la lista con nuevoLi
elementoAClonar.cloneNode(boolean)
: devuelve un clon de elementoAClonar o de elementoAClonar con todos sus descendientes según le pasemos como parámetro false o true. Luego podremos insertarlo donde queramos.Otros métodos menos usados son:
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 append y el resto de métodos anteriores ya lo crean automáticamente. Ej.:
let textoLi = document.createTextNode('Nuevo elemento de lista');
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. Ejemplo:
nuevoLi.appendChild(textoLi); // añade el texto creado al elemento LI creado
let miPrimeraLista = document.getElementsByTagName('ul')[0]; // selecciona el 1º UL de la página
miPrimeraLista.appendChild(nuevoLi); // añade LI como último hijo de UL, es decir al final de la lista
elemento.insertBefore(nuevoNodo, nodo)
: añade nuevoNodo como hijo de elemento antes del hijo nodo. Ej.:
let miPrimeraLista = document.getElementsByTagName('ul')[0]; // selecciona el 1º UL de la página
let primerElementoDeLista = miPrimeraLista.getElementsByTagName('li')[0]; // selecciona el 1º LI de miPrimeraLista
miPrimeraLista.insertBefore(nuevoLi, primerElementoDeLista); // añade LI al principio de la lista
elemento.removeChild(nodo)
: borra nodo de elemento y por tanto se elimina de la página. Ej.:
let miPrimeraLista = document.getElementsByTagName('ul')[0]; // selecciona el 1º UL de la página
let primerElementoDeLista = miPrimeraLista.getElementsByTagName('li')[0]; // selecciona el 1º LI de miPrimeraLista
miPrimeraLista.removeChild(primerElementoDeLista); // borra el primer elemento de la lista
// También podríamos haberlo borrado sin tener el padre con:
primerElementoDeLista.parentElement.removeChild(primerElementoDeLista);
elemento.replaceChild(nuevoNodo, viejoNodo)
: reemplaza viejoNodo con nuevoNodo como hijo de elemento. Ej.:
let miPrimeraLista = document.getElementsByTagName('ul')[0]; // selecciona el 1º UL de la página
let primerElementoDeLista = miPrimeraLista.getElementsByTagName('li')[0]; // selecciona el 1º LI de miPrimeraLista
miPrimeraLista.replaceChild(nuevoLi, primerElementoDeLista); // reemplaza el 1º elemento de la lista con nuevoLi
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.
Supongamos que tenemos un DIV cuya id es myDiv al que queremos añadir al final dos párrafos, el último de ellos con un texto en negrita. El código podría ser:
let miDiv = document.getElementById('myDiv');
let nuevoParrafo = document.createElement('p');
nuevoParrafo.textContent = 'Párrafo añadido al final';
let ultimoParrafo = document.createElement('p');
const textoNegrita = document.createElement('strong');
textoNegrita.textContent = 'con texto en negrita';
ultimoParrafo.append('Último párrafo ', textoNegrita);
miDiv.append(nuevoParrafo, ultimoParrafo);
Si utilizamos la propiedad innerHTML el código a usar es mucho más simple:
let miDiv = document.getElementById('myDiv');
miDiv.innerHTML += '<p>Párrafo añadido al final</p><p>Último párrafo <strong>con texto en negrita</strong></p>';
OJO: La forma de añadir el último párrafo (línea #3: miDiv.innerHTML+='<p>Párrafo añadido al final</p>';
) aunque es válida no es muy eficiente ya que obliga al navegador a volver a pintar TODO el contenido de miDIV.
Podemos ver más ejemplos de creación y eliminación de nodos en W3Schools.
EJERCICIO: Añade a la página:
- Un nuevo párrafo al final del DIV ‘lipsum’ con el texto “Nuevo párrafo añadido por javascript” (fíjate que una palabra está en negrita)
- Un nuevo elemento al formulario tras el ‘Dato 1’ con la etiqueta ‘Dato 1 bis’ y el INPUT con id ‘input1bis’ que al cargar la página tendrá escrito “Hola”
Podemos ver y modificar los valores de los atributos de cada elemento HTML y también añadir o eliminar atributos:
elemento.attributes
: devuelve un array con todos los atributos de elementoelemento.hasAttribute('nombreAtributo')
: indica si elemento tiene o no definido el atributo nombreAtributoelemento.getAttribute('nombreAtributo')
: devuelve el valor del atributo nombreAtributo de elemento. Para muchos elementos este valor puede directamente con elemento.atributo
.elemento.setAttribute('nombreAtributo', 'valor')
: establece valor como nuevo valor del atributo nombreAtributo de elemento. También puede cambiarse el valor directamente con elemento.atributo=valor
.elemento.removeAttribute('nombreAtributo')
: elimina el atributo nombreAtributo de elementoA algunos atributos comunes como id
, title
o className
(para el atributo class) se puede acceder y cambiar como si fueran una propiedad del elemento (elemento.atributo
). Ejemplos:
elemento.id = 'primera-lista';
// es equivalente ha hacer:
elemento.setAttribute('id', 'primera-lista');
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.
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('className', '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:
elemento.classList.add('primero'); // ahora elemento será <p class="destacado direccion primero">...
elemento.classList.remove('direccion'); // ahora elemento será <p class="destacado primero">...
elemento.classList.toogle('destacado'); // ahora elemento será <p class="primero">...
elemento.classList.toogle('direccion'); // ahora elemento será <p class="primero direccion">...
elemento.classList.contains('direccion'); // devuelve true
elemento.classList.replace('primero', 'ultimo'); // ahora elemento será <p class="ultimo direccion">...
Tened en cuenta que NO todos los navegadores soportan classList por lo que si queremos añadir o quitar clases en navegadores que no lo soportan debemos hacerlo con los métodos estándar, por ejemplo para añadir la clase ‘rojo’:
let clases = elemento.className.split(" ");
if (clases.indexOf('rojo') == -1) {
elemento.className += ' ' + 'rojo';
}
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