{{quote {author: "Friedrich Nietzsche", title: "Más allá del bien y del mal", chapter: true}
¡Tanto peor! ¡Otra vez la vieja historia! Cuando uno ha acabado de construir su casa advierte que, mientras la construía, ha aprendido, sin darse cuenta, algo que tendría que haber sabido absolutamente antes de comenzar a construir.
quote}}
{{figure {url: "img/chapter_picture_14.jpg", alt: "Foto de un árbol con letras y scripts colgando de sus ramas", chapter: "framed"}}}
{{index drawing, parsing}}
Cuando abres una página web en tu navegador, el navegador obtiene el texto de la página ((HTML)) y lo analiza, de una manera bastante similar a la manera en que nuestro analizador del Capítulo ? analizaba los programas. El navegador construye un modelo de la ((estructura)) del documento y utiliza este modelo para dibujar la página en la pantalla.
{{index "live data structure"}}
Esta representación del ((documento)) es uno de los juguetes que un programa de JavaScript tiene disponible en su ((caja de arena)). Es una ((estructura de datos)) que puedes leer o modificar. Y actúa como una estructura en tiempo real: cuando se modifica, la página en la pantalla es actualizada para reflejar los cambios.
{{index [HTML, structure]}}
Te puedes imaginar a un documento HTML como un conjunto anidado de
((caja))s. Las etiquetas como <body>
y </body>
encierran otras
((etiqueta))s, que a su vez, contienen otras etiquetas o ((texto)).
Este es el documento de ejemplo del
capítulo anterior:
<!doctype html>
<html>
<head>
<title>Mi página de inicio</title>
</head>
<body>
<h1>Mi página de inicio</h1>
<p>Hola, mi nombre es Marijn y esta es mi página de inicio.</p>
<p>También escribí un libro! Léelo
<a href="http://eloquentjavascript.net">aquí</a>.</p>
</body>
</html>
Esta página tiene la siguiente estructura:
{{figure {url: "img/html-boxes.svg", alt: "HTML document as nested boxes", width: "7cm"}}}
{{indexsee "Document Object Model", DOM}}
La estructura de datos que el navegador utiliza para representar el documento sigue esta figura. Para cada caja, hay un objeto, con el que podemos interactuar para descubrir cosas como que etiqueta de HTML representa y que cajas y texto contiene. Esta representación es llamada Modelo de Objeto del Documento o ((DOM)) (por sus siglas en inglés "Document Object Model").
{{index "documentElement property", "head property", "body property", "html (HTML tag)", "body (HTML tag)", "head (HTML tag)"}}
El objeto de enlace global document
nos da acceso a esos objetos. Su
propiedad documentElement
hace referencia al objeto que representa
a la etiqueta <html>
. Dado que cada documento HTML tiene una cabecera
y un cuerpo, también tiene propiedades head
y body
que apuntan a
esos elementos.
{{index [nesting, "of objects"]}}
Pensemos en los ((árbol))es ((sintáctico))s del Capítulo ? por un momento. Sus estructuras son sorprendentemente similares a la estructura de un documento del navegador. Cada ((nodo)) puede referirse a otros nodos hijos que, a su vez, pueden tener hijos propios. Esta forma es típica de las estructuras anidadas donde los elementos pueden contener sub elementos que son similares a ellos mismos.
{{index "documentElement property", [DOM, tree]}}
Le damos el nombre de ((árbol)) a una estructura de datos cuando
tiene una estructura de ramificación, no tiene ((ciclo))s (un nodo
no puede contenerse a sí mismo, directa o indirectamente) y tiene
una única ((raíz)) bien definida. En el caso del DOM,
document.documentElement
hace la función de raíz.
{{index sorting, ["data structure", "tree"], "syntax tree"}}
Los árboles aparecen constantemente en las ciencias de la computación (computer sience). Además de representar estructuras recursivas como los documentos HTML o programas, también son comúnmente usados para mantener ((conjunto))s ordenados de datos debido a que los elementos generalmente pueden ser encontrados o agregados más eficientemente en un árbol que en un arreglo plano.
{{index "leaf node", "Egg language"}}
Un árbol típico tiene diferentes tipos de ((nodo))s. El árbol sintáctico del lenguaje Egg tenía identificadores, valores y nodos de aplicación. Los nodos de aplicación pueden tener hijos, mientras que los identificadores y valores son hojas, o nodos sin hijos.
{{index "body property", [HTML, structure]}}
Lo mismo sucede para el DOM, los nodos para los ((elemento))s,
los cuales representan etiquetas HTML, determinan la estructura del
documento. Estos pueden tener ((nodos hijos)). Un ejemplo de estos
nodos es document.body
. Algunos de estos hijos pueden ser
((nodos hoja)), como los fragmentos de ((texto)) o los nodos
((comentario)).
{{index "text node", element, "ELEMENT_NODE code", "COMMENT_NODE code", "TEXT_NODE code", "nodeType property"}}
Cada objeto nodo DOM tiene una propiedad nodeType
, la cual
contiene un código (numérico) que identifica el tipo de nodo. Los
Elementos tienen el código 1, que también es definido por la
propiedad constante Node.ELEMENT_NODE
. Los nodos de texto, representando
una sección de texto en el documento, obtienen el código 3
(Node.TEXT_NODE
). Los comentarios obtienen el código 8
(Node.COMMENT_NODE
).
Otra forma de visualizar nuestro ((árbol)) de documento es la siguiente:
{{figure {url: "img/html-tree.svg", alt: "HTML document as a tree",width: "8cm"}}}
Las hojas son nodos de texto, y las flechas nos indican las relaciones padre-hijo entre los nodos.
{{id standard}}
{{index "programming language", [interface, design], [DOM, interface]}}
Usar códigos numéricos crípticos para representar a los tipos de nodos no es algo que se parezca al estilo de JavaScript para hacer las cosas. Más adelante en este capítulo, veremos cómo otras partes de la interfaz DOM también se sienten engorrosas y alienígenas. La razón de esto es que DOM no fue diseñado solamente para JavaScript. Más bien, intenta ser una interfaz independiente del lenguaje que puede ser usada en otros sistemas, no solamente para HTML pero también para ((XML)), que es un ((formato de datos)) genérico con una sintaxis similar a la de HTML.
{{index consistency, integration}}
Esto es desafortunado. Usualmente los estándares son bastante útiles. Pero en este caso, la ventaja (consistencia entre lenguajes) no es tan conveniente. Tener una interfaz que está propiamente integrada con el lenguaje que estás utilizando te ahorrará más tiempo que tener una interfaz familiar en distintos lenguajes.
{{index "array-like object", "NodeList type"}}
A manera de ejemplo de esta pobre integración, considera la propiedad
childNodes
que los nodos elemento en el DOM tienen. Esta propiedad
almacena un objeto parecido a un arreglo, con una propiedad length
y propiedades etiquetadas por números para acceder a los nodos hijos.
Pero es una instancia de tipo NodeList
, no un arreglo real, por
lo que no tiene métodos como slice
o map
.
{{index [interface, design], [DOM, construction], "side effect"}}
Luego, hay problemas que son simplemente un pobre diseño. Por ejemplo, no hay una manera de crear un nuevo nodo e inmediatamente agregar hijos o ((attributo))s. En vez de eso, tienes que crearlo primero y luego agregar los hijos y atributos uno por uno, usando efectos secundarios. El código que interactúa mucho con el DOM tiende a ser largo, repetitivo y feo.
{{index library}}
Pero estos defectos no son fatales. Dado que JavaScript nos permite crear nuestra propias ((abstraccion))es, es posible diseñar formas mejoradas para expresar las operaciones que estás realizando. Muchas bibliotecas destinadas a la programación del navegador vienen con esas herramientas.
{{index pointer}}
Los nodos del DOM contienen una amplia cantidad de ((enlace))s a otros nodos cercanos. El siguiente diagrama los ilustra:
{{figure {url: "img/html-links.svg", alt: "Links between DOM nodes",width: "6cm"}}}
{{index "child node", "parentNode property", "childNodes property"}}
A pesar de que el diagrama muestra un solo enlace por cada tipo, cada
nodo tiene una propiedad parentNode
que apunta al nodo al que
pertenece, si es que hay alguno. Igualmente, cada nodo elemento
(nodo tipo 1) tiene una propiedad childNodes
que apunta a un objeto
similar a un arreglo que almacena a sus hijos.
{{index "firstChild property", "lastChild property", "previousSibling property", "nextSibling property"}}
En teoría, te deberías poder mover donde quieras en el árbol
utilizando únicamente estos enlaces entre padre e hijo. Pero JavaScript
también te otorga acceso a un número de enlaces adicionales convenientes.
Las propiedades firstChild
y lastChild
apuntan al primer y último
elementos hijo, o tiene el valor null
para nodos sin hijos. De
manera similar, las propiedades previousSibling
y nextSibling
apuntan a los nodos adyacentes, los cuales, son nodos con el mismo
padre que aparecen inmediatamente antes o después del nodo. Para el
primer hijo previousSibling
será null
y para el último hijo,
nextSibling
será null
.
{{index "children property", "text node", element}}
También existe una propiedad children
, que es parecida a childNodes
pero contiene únicamente hijos de tipo elemento (tipo 1), excluyendo
otros tipos de nodos. Esto puede ser útil cuando no estás interesando
en nodos de tipo texto.
{{index "talksAbout function", recursion, [nesting, "of objects"]}}
Cuando estás tratando con estructuras de datos anidadas como esta,
las funciones recursivas son generalmente útiles. La siguiente función
escanea un documento por ((nodos de texto)) que contengan una cadena
dada y regresan true
en caso de que encuentren una:
{{id talksAbout}}
function hablaSobre(nodo, cadena) {
if (nodo.nodeType == Node.ELEMENT_NODE) {
for (let i = 0; i < nodo.childNodes.length; i++) {
if (hablaSobre(nodo.childNodes[i], cadena)) {
return true;
}
}
return false;
} else if (nodo.nodeType == Node.TEXT_NODE) {
return nodo.nodeValue.indexOf(cadena) > -1;
}
}
console.log(hablaSobre(document.body, "libro"));
// → true
{{index "childNodes property", "array-like object", "Array.from function"}}
Debido a que childNodes
no es un arreglo real, no podemos iterar
sobre el usando for
/of
por lo que tenemos que iterar sobre el rango
del índice usando un for
regular o usando Array.from
.
{{index "nodeValue property"}}
La propiedad nodeValue
de un nodo de texto almacena la cadena de
texto que representa.
{{index [DOM, querying], "body property", "hard-coding", [whitespace, "in HTML"]}}
Navegar por estos ((enlace))s entre padres, hijos y hermanos suele
ser útil. Pero si queremos encontrar un nodo específico en el documento,
alcanzarlo comenzando en document.body
y siguiendo un camino fijo de
propiedades es una mala idea. Hacerlo genera suposiciones en nuestro
programa sobre la estructura precisa del documento que tal vez quieras
cambiar después. Otro factor complicado es que los nodos de texto son
creados incluso para los espacios en blanco entre nodos. La etiqueta
<body>
en el documento de ejemplo no tiene solamente tres hijos
(<h1>
y dos elementos <p>
), en realidad tiene siete: esos tres,
más los espacios posteriores y anteriores entre ellos.
{{index "search problem", "href attribute", "getElementsByTagName method"}}
Por lo que si queremos obtener el atributo href
del enlace en ese
documento, no queremos decir algo como "Obten el segundo hijo del
sexto hijo del elemento body del documento". Sería mejor si pudiéramos
decir "Obten el primer enlace en el documento". Y de hecho podemos.
let link = document.body.getElementsByTagName("a")[0];
console.log(link.href);
{{index "child node"}}
Todos los nodos elemento tienen un método getElementsByTagName
, el
cual recolecta a todos los elementos con un nombre de etiqueta dado que
son descendientes (hijos directos o indirectos) de ese nodo y los
regresa como un objeto parecido a un arreglo.
{{index "id attribute", "getElementById method"}}
Para encontrar un único nodo en específico, puedes otorgarle un
atributo id
y usar document.getElementById
.
<p>Mi avestruz Gertrudiz:</p>
<p><img id="gertrudiz" src="img/ostrich.png"></p>
<script>
let avestruz = document.getElementById("gertrudiz");
console.log(avestruz.src);
</script>
{{index "getElementsByClassName method", "class attribute"}}
Un tercer método similar es getElementsByClassName
, el cual, de
manera similar a getElementsByTagName
busca a través de los
contenidos de un nodo elemento y obtiene todos los elementos que
tienen una cadena dada en su attributo class
.
{{index "side effect", "removeChild method", "appendChild method", "insertBefore method", [DOM, construction], [DOM, modification]}}
Prácticamente todo sobre la estructura de datos DOM puede ser cambiado.
La forma del árbol de documento puede ser modificada cambiando las
relaciones padre-hijo. Los nodos tienen un método remove
para ser
removidos de su nodo padre actual. Para agregar un nodo hijo a un
nodo elemento, podemos usar appendChild
, que lo pondrá al final de
la lista de hijos, o insertBefore
, que insertará el nodo en el
primer argumento antes del nodo en el segundo argumento.
<p>Uno</p>
<p>Dos</p>
<p>Tres</p>
<script>
let parrafos = document.body.getElementsByTagName("p");
document.body.insertBefore(parrafos[2], parrafos[0]);
</script>
Un nodo puede existir en el documento solamente en un lugar. En consecuencia, insertar el párrafo Tres enfrente del párrafo Uno primero lo removerá del final del documento y luego lo insertará en la parte delantera, resultando en Tres/Uno/Dos. Todas las operaciones que insertan un nodo en alguna parte causarán, a modo de ((efecto secundario)), que el nodo sea removido de su posición actual (si es que tiene una).
{{index "insertBefore method", "replaceChild method"}}
El método replaceChild
es usado para reemplazar a un nodo hijo con
otro. Toma dos nodos como argumentos: un nuevo nodo y el nodo que será
reemplazado. El nodo reemplazado debe ser un nodo hijo del elemento
desde donde se está llamando el método. Nótese que tanto replaceChild
como insertBefore
esperan que el nuevo nodo sea el primer argumento.
{{index "alt attribute", "img (HTML tag)"}}
Digamos que queremos escribir un script que reemplace todas las
((imagen))es (etiquetas <img>
) en el documento con el texto contenido
en sus atributos alt
, los cuales especifican una representación
textual alternativa de la imagen.
{{index "createTextNode method"}}
Esto no solamente involucra remover las imágenes, si no que también
involucra agregar un nuevo nodo texto que las reemplace. Los nodos texto
son creados con el método document.createTextNode
.
<p>El <img src="img/cat.png" alt="Gato"> en el
<img src="img/hat.png" alt="Sombrero">.</p>
<p><button onclick="sustituirImagenes()">Sustituir</button></p>
<script>
function sustituirImagenes() {
let imagenes = document.body.getElementsByTagName("img");
for (let i = imagenes.length - 1; i >= 0; i--) {
let imagen = imagenes[i];
if (imagen.alt) {
let texto = document.createTextNode(imagen.alt);
imagen.parentNode.replaceChild(texto, imagen);
}
}
}
</script>
{{index "text node"}}
Dada una cadena, createTextNode
nos da un nodo texto que podemos
insertar en el documento para hacer que aparezca en la pantalla.
{{index "live data structure", "getElementsByTagName method", "childNodes property"}}
El ciclo que recorre las imágenes empieza al final de la lista. Esto es
necesario dado que la lista de nodos regresada por un método como
getElementsByTagName
(o una propiedad como childNodes
) se actualiza
en tiempo real. Esto es, que se actualiza conforme el documento cambia.
Si empezáramos desde el frente, remover la primer imagen causaría que
la lista perdiera su primer elemento de tal manera que la segunda
ocasión que el ciclo se repitiera, donde i
es 1, se detendría dado que
la longitud de la colección ahora es también 1.
{{index "slice method"}}
Si quieres una colección de nodos sólida, a diferencia de una en
tiempo real, puedes convertir la colección a un arreglo real llamando
Array.from
.
let casi_arreglo = {0: "uno", 1: "dos", length: 2};
let arreglo = Array.from(casi_arreglo);
console.log(arreglo.map(s => s.toUpperCase()));
// → ["UNO", "DOS"]
{{index "createElement method"}}
Para crear nodos ((elemento)), puedes utilizar el método
document.createElement
. Este método toma un nombre de etiqueta y
regresa un nuevo nodo vacío del tipo dado.
{{index "Popper, Karl", [DOM, construction], "elt function"}}
{{id elt}}
El siguiente ejemplo define una utilidad elt
, la cual crea un
elemento nodo y trata el resto de sus argumentos como hijos de ese
nodo. Luego, esta función es utilizada para agregar una atribución
a una cita.
<blockquote id="cita">
Ningún libro puede terminarse jamás. Mientras se trabaja en
él aprendemos solo lo suficiente para encontrar inmaduro
el momento en el que nos alejamos de él.
</blockquote>
<script>
function elt(tipo, ...hijos) {
let nodo = document.createElement(tipo);
for (let hijo of hijos) {
if (typeof hijo != "string") nodo.appendChild(hijo);
else nodo.appendChild(document.createTextNode(hijo));
}
return nodo;
}
document.getElementById("cita").appendChild(
elt("footer", "—",
elt("strong", "Karl Popper"),
", prefacio de la segunda edición de ",
elt("em", "La sociedad abierta y sus enemigos"),
", 1950"));
</script>
{{if book
Así es como se ve el documento resultante:
{{figure {url: "img/blockquote.png", alt: "A blockquote with attribution",width: "8cm"}}}
if}}
{{index "href attribute", [DOM, attributes]}}
Los ((atributo))s de algunos elementos, como href
para los enlaces,
pueden ser accedidos a través de una propiedad con el mismo nombre en
el objeto ((DOM)) del elemento. Este es el caso para los atributos
estándar más comúnmente utilizados.
{{index "data attribute", "getAttribute method", "setAttribute method", attribute}}
Pero HTML te permite establecer cualquier atributo que quieras en los
nodos. Esto puede ser útil debido a que te permite almacenar información
extra en un documento. Sin embargo, si creas tus propios nombres de
atributo, dichos atributos no estarán presentes como propiedades en el
nodo del elemento. En vez de eso, tendrás que utilizar los métodos
getAttribute
y setAttribute
para poder trabajar con ellos.
<p data-classified="secreto">El código de lanzamiento es: 00000000.</p>
<p data-classified="no-classificado">Yo tengo dos pies.</p>
<script>
let parrafos = document.body.getElementsByTagName("p");
for (let parrafo of Array.from(parrafos)) {
if (parrafo.getAttribute("data-classified") == "secreto") {
parrafo.remove();
}
}
</script>
Se recomienda anteponer los nombres de dichos atributos inventados con
data-
para asegurarse de que no conflictúan con ningún otro atributo.
{{index "getAttribute method", "setAttribute method", "className property", "class attribute"}}
Existe un atributo comúnmente usado, class
, que es una ((palabra clave))
en el lenguaje JavaScript. Por motivos históricos, algunas
implementaciones antiguas de JavaScript podrían no manejar nombres de
propiedades que coincidan con las palabras clave, la propiedad utilizada
para acceder a este atributo tiene por nombre className
. También
puedes acceder a ella bajo su nombre real, "class"
, utilizando los
métodos getAttribute
y setAttribute
.
{{index layout, "block element", "inline element", "p (HTML tag)", "h1 (HTML tag)", "a (HTML tag)", "strong (HTML tag)"}}
Tal vez hayas notado que diferentes tipos de elementos se exponen
de manera distinta. Algunos, como en el caso de los párrafos
(<p>
) o encabezados (<h1>
), ocupan todo el ancho del documento
y se renderizan en líneas separadas. A estos se les conoce como
elementos block (o bloque). Otros, como los enlaces (<a>
) o el
elemento <strong>
, se renderizan en la misma línea con su texto
circundante. Dichos elementos se les conoce como elementos
inline (o en línea).
{{index drawing}}
Para cualquier documento dado, los navegadores son capaces de calcular una estructura (layout), que le da a cada elemento un tamaño y una posición basada en el tipo y el contenido. Luego, esta estructura se utiliza para trazar el documento.
{{index "border (CSS)", "offsetWidth property", "offsetHeight property", "clientWidth property", "clientHeight property", dimensions}}
Se puede acceder al tamaño y la posición de un elemento desde
JavaScript. Las propiedades offsetWidth
y offsetHeight
te dan el
espacio que el elemento utiliza en ((pixel))es. Un píxel es la unidad
básica de las medidas del navegador. Tradicionalmente correspondía al
punto más pequeño que la pantalla podía trazar, pero en los monitores
modernos, que pueden trazar puntos muy pequeños, este puede no ser
más el caso, por lo que un píxel del navegador puede abarcar varios
puntos en la pantalla.
De manera similar, clientWidth
y clientHeight
te dan el tamaño
del espacio dentro del elemento, ignorando la anchura del borde.
<p style="border: 3px solid red">
Estoy dentro de una caja
</p>
<script>
let parrafo = document.body.getElementsByTagName("p")[0];
console.log("clientHeight:", parrafo.clientHeight);
console.log("offsetHeight:", parrafo.offsetHeight);
</script>
{{if book
Darle un borde a un párrafo hace que se dibuje un rectángulo a su alrededor.
{{figure {url: "img/boxed-in.png", alt: "A paragraph with a border",width: "8cm"}}}
if}}
{{index "getBoundingClientRect method", position, "pageXOffset property", "pageYOffset property"}}
{{id boundingRect}}
La manera más efectiva de encontrar la posición precisa de un elemento
en la pantalla es el método getBoundingClientRect
. Este devuelve un
objeto con las propiedades top
, bottom
, left
, y right
, indicando
las posiciones de pixeles de los lados el elemento en relación con la
parte superior izquierda de la pantalla. Si los quieres en relación a
todo el documento, deberás agregar la posición actual del scroll, la
cual puedes obtener en los bindings pageXOffset
y pageYOffset
.
{{index "offsetHeight property", "getBoundingClientRect method", drawing, laziness, performance, efficiency}}
Estructurar un documento puede requerir mucho trabajo. En los
intereses de velocidad, los motores de los navegadores no
reestructuran inmediatamente un documento cada vez que lo cambias,
en cambio, se espera lo más que se pueda. Cuando un programa de
JavaScript que modifica el documento termina de ejecutarse, el
navegador tendrá que calcular una nueva estructura para trazar el
documento actualizado en la pantalla. Cuando un programa solicita
la posición o el tamaño de algo, leyendo propiedades como offsetHeight
o llamando a getBoundingClientRect
, proveer la información correcta
también requiere que se calcule una nueva estructura.
{{index "side effect", optimization, benchmark}}
A un programa que alterna repetidamente entre leer la información de la estructura DOM y cambiar el DOM, fuerza a que haya bastantes cálculos de estructura, y por consecuencia se ejecutará lentamente. El siguiente código es un ejemplo de esto. Contiene dos programas diferentes que construyen una línea de X caracteres con 2,000 pixeles de ancho y que mide el tiempo que toma cada uno.
<p><span id="uno"></span></p>
<p><span id="dos"></span></p>
<script>
function tiempo(nombre, accion) {
let inicio = Date.now(); // Tiempo actual en milisegundos
accion();
console.log(nombre, "utilizo", Date.now() - inicio, "ms");
}
tiempo("inocente", () => {
let objetivo = document.getElementById("uno");
while (objetivo.offsetWidth < 2000) {
objetivo.appendChild(document.createTextNode("X"));
}
});
// → inocente utilizo 32 ms
tiempo("ingenioso", function() {
let objetivo = document.getElementById("dos");
objetivo.appendChild(document.createTextNode("XXXXX"));
let total = Math.ceil(2000 / (objetivo.offsetWidth / 5));
objetivo.firstChild.nodeValue = "X".repeat(total);
});
// → ingenioso utilizo 1 ms
</script>
{{index "block element", "inline element", style, "strong (HTML tag)", "a (HTML tag)", underline}}
Hemos visto que diferentes elementos HTML se trazan de manera diferente.
Algunos son desplegados como bloques, otros en línea. Algunos agregan
estilos, por ejemplo <strong>
hace que su contenido esté en
((negritas)) y <a>
lo hace azul y lo subraya.
{{index "img (HTML tag)", "default behavior", "style attribute"}}
La forma en la que una etiqueta <img>
muestra una imagen o una
etiqueta <a>
hace que un enlace sea seguido cuando se hace click en
el, está fuertemente atado al tipo del elemento. Pero podemos cambiar los
estilos asociados a un elemento, tales como el color o si está subrayado.
Este es un ejemplo que utiliza la propiedad style
:
<p><a href=".">Enlace normal</a></p>
<p><a href="." style="color: green">Enlace verde</a></p>
{{if book
El segundo enlace será verde en vez del color por defecto.
{{figure {url: "img/colored-links.png", alt: "A normal and a green link",width: "2.2cm"}}}
if}}
{{index "border (CSS)", "color (CSS)", CSS, "colon character"}}
Un atributo style puede llegar a contener una o más declaraciones, que
consisten en una propiedad (como color
) seguido del símbolo de dos puntos
y un valor (como green
). Cuando hay más de una declaración, estas deben
ser separadas por ((punto y coma)), como en "color: red; border: none"
.
{{index "display (CSS)", layout}}
Muchos de los aspectos del documento pueden ser influenciados por la
estilización. Por ejemplo, la propiedad display
controla si un
elemento es desplegado como un bloque o como un elemento en línea.
El texto es desplegado <strong>en línea</strong>,
<strong style="display: block">como bloque</strong>, y
<strong style="display: none">no se despliega</strong>.
{{index "hidden element"}}
La etiqueta block
terminará en su propia línea dado que los elementos
((bloque)) no son desplegados en línea con el texto que los rodea.
La última etiqueta no se despliega—display: none
previene que el
elemento sea mostrado en la pantalla. Esta es una manera de ocultar
elementos. A menudo esto es preferido sobre removerlos completamente
del documento debido a que hace más fácil mostrarlos nuevamente en el
futuro.
{{if book
{{figure {url: "img/display.png", alt: "Different display styles",width: "4cm"}}}
if}}
{{index "color (CSS)", "style attribute"}}
El código de JavaScript puede manipular directamente el estilo de un
elemento a través de la propiedad style
del elemento. Esta propiedad
almacena un objeto que tiene propiedades por todas las posibles
propiedades de estilo. Los valores de esas propiedades son cadenas, que
podemos escribir para cambiar un aspecto en particular del estilo del
elemento.
<p id="parrafo" style="color: purple">
Texto mejorado
</p>
<script>
let parrafo = document.getElementById("parrafo");
console.log(parrafo.style.color);
parrafo.style.color = "magenta";
</script>
{{index "camel case", capitalization, "hyphen character", "font-family (CSS)"}}
Algunos nombres de propiedades pueden contener guiones, como es el caso
de font-family
. Dado que estos nombres de propiedades son incómodos
para trabajar con ellos en JavaScript (tendrías que decir
style["font-family"]
), los nombres de propiedades en el objeto style
para tales propiedades no tendrán guiones y las letra después del guion
estará en mayúsculas (style.fontFamily
).
{{index "rule (CSS)", "style (HTML tag)"}}
{{indexsee "Cascading Style Sheets", CSS}} {{indexsee "style sheet", CSS}}
El sistema de estilos para HTML es llamado ((CSS)) por sus siglas en
ingles Cascading Style Sheets (Hojas de estilo en cascada). Una
hoja de estilos es un conjunto de reglas sobre cómo estilizar
a los elementos en un documento. Puede estar declarado dentro de
una etiqueta <style>
.
<style>
strong {
font-style: italic;
color: gray;
}
</style>
<p>Ahora <strong>el texto en negritas</strong> esta en italicas y es gris.</p>
{{index "rule (CSS)", "font-weight (CSS)", overlay}}
La sección cascada en el nombre se refiere al hecho de que varias
reglas son combinadas para producir el estilo final para un elemento.
En el ejemplo, el estilo por defecto para las etiquetas <strong>
, que
les da font-weight: bold
, es superpuesto por la regla en la etiqueta
<style>
, que le agrega font-style
y color
.
{{index "style (HTML tag)", "style attribute"}}
Cuando varias reglas definen un valor para una misma propiedad, la
regla leída más recientemente obtiene una mayor ((precedencia))
y gana. Por lo que si la regla en la etiqueta <style>
incluyera
font-weight: normal
, contradiciendo la regla por defecto de
font-weight
, el texto se vería normal, no en negritas. Los estilos
en un atributo style
aplicados directamente al nodo tienen la
mayor precedencia y siempre ganan.
{{index uniqueness, "class attribute", "id attribute"}}
Es posible apuntar a otras cosas que no sean nombres de ((etiqueta))
en las reglas CSS. Una regla para .abc
aplica a todos los elementos
con "abc"
en su atributo class
. Una regla para #xyz
aplica a
todos los elementos con un atributo id
con valor "xyz"
(que debería
ser único en el documento).
.sutil {
color: gray;
font-size: 80%;
}
#cabecera {
background: blue;
color: white;
}
/* Elementos p con un id principal y clases a y b */
p#principal.a.b {
margin-bottom: 20px;
}
{{index "rule (CSS)"}}
La regla de precedencia que favorece a las reglas más recientemente
definidas aplican solamente cuando las reglas tienen la misma
((especificidad)). La especificidad de una regla es una medida de que
tan precisamente describe a los elementos que coinciden con la regla,
determinado por el número y tipo (etiqueta, clase o ID) de los
aspectos del elemento que requiera. Por ejemplo, una regla que apunta
a p.a
es más específica que las reglas que apuntan a p
o solamente
a .a
y tendrá precedencia sobre ellas.
{{index "direct child node"}}
La notación p > a {…}
aplica los estilos dados a todas las etiquetas
<a>
que son hijas directas de las etiquetas <p>
. De manera similar,
p a {…}
aplica a todas las etiquetas <a>
dentro de etiquetas <p>
,
sin importar que sean hijas directas o indirectas.
{{index complexity, CSS}}
No utilizaremos mucho las hojas de estilo en este libro. Entenderlas es útil cuando se programa en el navegador, pero son lo suficientemente complicadas para justificar un libro por separado.
{{index "domain-specific language", [DOM, querying]}}
La principal razón por la que introduje la sintaxis de ((selección))—la notación usada en las hojas de estilo para determinar a cuales elementos aplicar un conjunto de estilos—es que podemos utilizar el mismo mini-lenguaje como una manera efectiva de encontrar elementos DOM.
{{index "querySelectorAll method", "NodeList type"}}
El método querySelectorAll
, que se encuentra definido tanto en el
objeto document
como en los nodos elemento, toma la cadena de un
selector y regresa una NodeList
que contiene todos los elementos
que coinciden con la consulta.
<p>And if you go chasing
<span class="animal">rabbits</span></p>
<p>And you know you're going to fall</p>
<p>Tell 'em a <span class="caracter">hookah smoking
<span class="animal">caterpillar</span></span></p>
<p>Has given you the call</p>
<script>
function contar(selector) {
return document.querySelectorAll(selector).length;
}
console.log(contar("p")); // Todos los elementos <p>
// → 4
console.log(contar(".animal")); // Clase animal
// → 2
console.log(contar("p .animal")); // Animales dentro de <p>
// → 2
console.log(contar("p > .animal")); // Hijos directos de <p>
// → 1
</script>
{{index "live data structure"}}
A diferencia de métodos como getElementsByTagName
, el objeto que
regresa querySelectorAll
no es un objeto en tiempo real. No cambiará
cuando cambies el documento. Sin embargo, sigue sin ser un arreglo real,
por lo que aún necesitas llamar a Array.from
si lo quieres tratar como
uno real.
{{index "querySelector method"}}
El método querySelector
(sin la parte de All
) trabaja de una manera
similar. Este es útil si quieres un único elemento en específico. Regresará
únicamente el primer elemento que coincida o null en el caso que
ningún elemento coincida.
{{id animation}}
{{index "position (CSS)", "relative positioning", "top (CSS)", "left (CSS)", "absolute positioning"}}
La propiedad de estilo position
influye de un manera poderosa sobre
la estructura. Por defecto, tiene el valor de static
, eso significa
que el elemento se coloca en su lugar normal en el documento. Cuando
se establece como relative
, el elemento sigue utilizando espacio en el
documento pero ahora las propiedades top
y left
pueden ser
utilizadas para moverlo relativamente a ese espacio normal. Cuando
position
se establece como absolute
, el elemento es removido del
flujo normal del documento—esto es, deja de tomar espacio y puede
encimarse con otros elementos. Además, sus propiedades top
y left
pueden ser utilizadas para posicionarlo absolutamente con relación a
la esquina superior izquierda del elemento envolvente más cercano cuya
propiedad position
no sea static
, o con relación al documento si dicho
elemento envolvente no existe.
{{index [animation, "spinning cat"]}}
Podemos utilizar esto para crear una animación. El siguiente documento despliega una imagen de un gato que se mueve alrededor de una ((elipse)):
<p style="text-align: center">
<img src="img/cat.png" style="position: relative">
</p>
<script>
let gato = document.querySelector("img");
let angulo = Math.PI / 2;
function animar(tiempo, ultimoTiempo) {
if (ultimoTiempo != null) {
angulo += (tiempo - ultimoTiempo) * 0.001;
}
gato.style.top = (Math.sin(angulo) * 20) + "px";
gato.style.left = (Math.cos(angulo) * 200) + "px";
requestAnimationFrame(nuevoTiempo => animar(nuevoTiempo, tiempo));
}
requestAnimationFrame(animar);
</script>
{{if book
La flecha gris indica el camino por el que se mueve la imagen.
{{figure {url: "img/cat-animation.png", alt: "A moving cat head",width: "8cm"}}}
if}}
{{index "top (CSS)", "left (CSS)", centering, "relative positioning"}}
Nuestra imagen se centra en la página y se le da un valor para position
de relative
. Actualizaremos repetidamente los estilos top
y left
de la imagen para moverla.
{{index "requestAnimationFrame function", drawing, animation}}
{{id animationFrame}}
El script utiliza requestAnimationFrame
para programar la función
animar
para ejecutarse en el momento en el que el navegador está
listo para volver a pintar la pantalla. La misma función animar
llama a requestAnimationFrame
otra vez para programar la siguiente
actualización. Cuando la ventana del navegador (o pestaña) está activa,
esto causará que sucedan actualizaciones a un rango de aproximadamente
60 actualizaciones por segundo, lo que tiende a producir una animación
agradable a la vista.
{{index timeline, blocking}}
Si únicamente actualizáramos el DOM en un ciclo, la página se
congelaría, y no se mostraría nada en la pantalla. Los navegadores
no actualizan la pantalla si un programa de JavaScript se encuentra
en ejecución, tampoco permiten ninguna interacción con la página. Es
por esto que necesitamos a requestAnimationFrame
—le permite al navegador
saber que hemos terminado por el momento, y que puede empezar a hacer las
cosas que le navegador hace, cómo actualizar la pantalla y responder a las
acciones del usuario.
{{index "smooth animation"}}
A la función animar
se le pasa el ((tiempo)) actual como un
argumento. Para asegurarse de que el movimiento del gato por milisegundo
es estable, basa la velocidad a la que cambia el ángulo en la
diferencia entre el tiempo actual y la última vez que la función
se ejecutó. Si solamente movieramos el ángulo una cierta cantidad
por paso, la animación tartamudearía si, por ejemplo, otra tarea
pesada se encontrara ejecutándose en la misma computadora que
pudiera prevenir que la función se ejecutará por una fracción de segundo.
{{index "Math.cos function", "Math.sin function", cosine, sine, trigonometry}}
{{id sin_cos}}
Moverse en círculos se logra a través de las funciones Math.cos
y
Math.sin
. Para aquellos que no estén familiarizados con estas,
las introduciré brevemente dado que las usaremos ocasionalmente en
este libro.
{{index coordinates, pi}}
Las funciones Math.cos
y Math.sin
son útiles para encontrar puntos
que recaen en un círculo alrededor del punto (0,0) con un radio de
uno. Ambas funciones interpretan sus argumentos como las posiciones
en el círculo, con cero denotando el punto en la parte más alejada
del lado derecho del círculo, moviéndose en el sentido de las manecillas del
reloj hasta que 2π (cerca de 6.28) nos halla tomado alrededor de todo el
círculo. Math.cos
indica la coordenada x del punto que corresponde
con la posición dada, y Math.sin
indica la coordenada y. Las
posiciones (o ángulos) mayores que 2π o menores que 0 son válidas—la
rotación se repite por lo que a+2π se refiere al mismo ((ángulo))
que a.
{{index "PI constant"}}
Esta unidad para medir ángulos se conoce como ((radian))es-un círculo
completo corresponde a 2π radianes, de manera similar a 360 grados
cuando se utilizan grados. La constante π está disponible como Math.PI
en JavaScript.
{{figure {url: "img/cos_sin.svg", alt: "Using cosine and sine to compute coordinates",width: "6cm"}}}
{{index "counter variable", "Math.sin function", "top (CSS)", "Math.cos function", "left (CSS)", ellipse}}
El código de animación del gato mantiene un contador, angulo
,
para el ángulo actual de la animación y lo incrementa cada vez que la
función animar
es llamada. Luego, se puede utilizar este ángulo para
calcular la posición actual del elemento imagen. El estilo top
es
calculado con Math.sin
y multiplicado por 20, que es el radio
vertical de nuestra elipse. El estilo left
se basa en Math.cos
multiplicado por 200 por lo que la elipse es mucho más ancha que su
altura.
{{index "unit (CSS)"}}
Nótese que los estilos usualmente necesitan unidades. En este caso,
tuvimos que agregar "px"
al número para informarle al navegador que
estábamos contando en ((pixel))es (al contrario de centímetros, "ems",
u otras unidades). Esto es sencillo de olvidar. Usar números sin
unidades resultará en estilos que son ignorados—a menos que el número
sea 0, que siempre indica la misma cosa, independientemente de su
unidad.
Los programas de JavaScript pueden inspeccionar e interferir con el documento que el navegador está desplegando a través de una estructura de datos llamada el DOM. Esta estructura de datos representa el modelo del navegador del documento, y un programa de JavaScript puede modificarlo para cambiar el documento visible.
El DOM está organizado como un árbol, en el cual los elementos están
ordenados jerárquicamente de acuerdo a la estructura del documento.
Estos objetos que representan a los elementos tienen propiedades como
parentNode
y childNodes
, que pueden ser usadas para navegar
a través de este árbol.
La forma en que un documento es desplegado puede ser influenciada por
la estilización, tanto agregando estilos directamente a los nodos
cómo definiendo reglas que coincidan con ciertos nodos. Hay muchas
propiedades de estilo diferentes, tales como color
o display
. El
código de JavaScript puede manipular el estilo de un elemento
directamente a través de su propiedad de style
.
{{id exercise_table}}
{{index "table (HTML tag)"}}
Una tabla HTML se construye con la siguiente estructura de etiquetas:
<table>
<tr>
<th>nombre</th>
<th>altura</th>
<th>ubicacion</th>
</tr>
<tr>
<td>Kilimanjaro</td>
<td>5895</td>
<td>Tanzania</td>
</tr>
</table>
{{index "tr (HTML tag)", "th (HTML tag)", "td (HTML tag)"}}
Para cada ((fila)), la etiqueta <table>
contiene una etiqueta <tr>
.
Dentro de estas etiquetas <tr>
, podemos poner ciertos elementos:
ya sean celdas cabecera (<th>
) o celdas regulares (<td>
).
Dado un conjunto de datos de montañas, un arreglo de objetos con
propiedades nombre
, altura
y lugar
, genera la estructura DOM
para una tabla que enlista esos objetos. Deberá tener una columna
por llave y una fila por objeto, además de una fila cabecera con elementos
<th>
en la parte superior, listando los nombres de las columnas.
Escribe esto de manera que las columnas se deriven automáticamente de los objetos, tomando los nombres de propiedad del primer objeto en los datos.
Agrega la tabla resultante al elemento con el atributo id
de "montañas"
de manera que se vuelva visible en el documento.
{{index "right-aligning", "text-align (CSS)"}}
Una vez que lo tengas funcionando, alinea a la derecha las celdas que
contienen valores numéricos, estableciendo su propiedad style.textAlign
cómo "right"
.
{{if interactive
<h1>Montañas</h1>
<div id="montañas"></div>
<script>
const MONTAÑAS = [
{nombre: "Kilimanjaro", altura: 5895, ubicacion: "Tanzania"},
{nombre: "Everest", altura: 8848, ubicacion: "Nepal"},
{nombre: "Monte Fuji", altura: 3776, ubicacion: "Japón"},
{nombre: "Vaalserberg", altura: 323, ubicacion: "Países Bajos"},
{nombre: "Denali", altura: 6168, ubicacion: "Estados Unidos"},
{nombre: "Popocatepetl", altura: 5465, ubicacion: "México"},
{nombre: "Mont Blanc", altura: 4808, ubicacion: "Italia/Francia"}
];
// Tu codigo va aqui
</script>
if}}
{{hint
{{index "createElement method", "table example", "appendChild method"}}
Puedes utilizar document.createElement
para crear nuevos nodos elemento,
document.createTextNode
para crear nuevos nodos de texto, y el método
appendChild
para poner nodos dentro de otros nodos.
{{index "Object.keys function"}}
Querrás recorrer los nombres de las llaves una vez para llenar la fila
superior y luego nuevamente para cada objeto en el arreglo para
construir las filas de datos. Para obtener un arreglo con los nombres
de las llaves proveniente del primer objeto, la función Object.keys
será de utilidad.
{{index "getElementById method", "querySelector method"}}
Para agregar la tabla al nodo padre correcto, puedes utilizar
document.getElementById
o document.querySelector
para encontrar el
nodo con el atributo id
adecuado.
hint}}
{{index "getElementsByTagName method", recursion}}
El método document.getElementsByTagName
regresa todos los elementos
hijo con un nombre de etiqueta dado. Implementa tu propia versión de
esto como una función que toma un nodo y una cadena (el nombre de la
etiqueta) como argumentos y regresa un arreglo que contiene todos los
nodos elemento descendientes con el nombre del tag dado.
{{index "nodeName property", capitalization, "toLowerCase method", "toUpperCase method"}}
Para encontrar el nombre del tag de un elemento, utiliza su propiedad
nodeName
. Pero considera que esto regresará el nombre de la etiqueta
todo en mayúsculas. Utiliza las funciones de las cadenas (string
),
toLowerCase
o toUpperCase
para compensar esta situación.
{{if interactive
<h1>Encabezado con un elemento <span>span</span>.</h1>
<p>Un parráfo con <span>uno</span>, <span>dos</span>
spans.</p>
<script>
function byTagName(nodo, etiqueta) {
// Tu código va aquí.
}
console.log(byTagName(document.body, "h1").length);
// → 1
console.log(byTagName(document.body, "span").length);
// → 3
let parrafo = document.querySelector("p");
console.log(byTagName(parrafo, "span").length);
// → 2
</script>
if}}
{{hint
{{index "getElementsByTagName method", recursion}}
La solución es expresada de manera más sencilla con una función
recursiva, similar a la función hablaSobre
definida
anteriormente en este capítulo.
{{index concatenation, "concat method", closure}}
Puedes llamar a byTagname
recursivamente, concatenando los arreglos
resultantes para producir la salida. O puedes crear una función
interna que se llama a sí misma recursivamente y que tiene acceso a
un arreglo definido en la función exterior, al cual se
puede agregar los elementos coincidentes que encuentre. No
olvides llamar a la ((función interior)) una vez desde la función
exterior para iniciar el proceso.
{{index "nodeType property", "ELEMENT_NODE code"}}
La función recursiva debe revisar el tipo de nodo. En este caso
solamente estamos interesados por los nodos de tipo 1
(Node.ELEMENT_NODE
). Para tales nodos, debemos de iterar sobre
sus hijos y, para cada hijo, observar si los hijos coinciden con la
consulta mientras también se realiza una llamada recursiva en él
para inspeccionar a sus propios hijos.
hint}}
{{index "cat's hat (exercise)", [animation, "spinning cat"]}}
Extiende la animación del gato definida anteriormente
de manera de que tanto el gato como su sombrero (<img src="img/hat.png">
)
orbiten en lados opuestos de la elipse.
O haz que el sombrero circule alrededor del gato. O altera la animación en alguna otra forma interesante.
{{index "absolute positioning", "top (CSS)", "left (CSS)", "position (CSS)"}}
Para hacer el posicionamiento de múltiples objetos más sencillo,
probablemente sea buena idea intercambiar a un posicionamiento absoluto.
Esto significa que top
y left
serán contados con relación a la
parte superior izquierda del documento. Para evitar usar coordenadas
negativas, que pueden causar que la imagen se mueva fuera de la página
visible, puedes agregar un número fijo de pixeles a los valores de las
posiciones.
{{if interactive
<style>body { min-height: 200px }</style>
<img src="img/cat.png" id="cat" style="position: absolute">
<img src="img/hat.png" id="hat" style="position: absolute">
<script>
let gato = document.querySelector("#cat");
let sombrero = document.querySelector("#hat");
let angulo = 0;
let ultimoTiempo = null;
function animar(tiempo) {
if (ultimoTiempo != null) angulo += (tiempo - ultimoTiempo) * 0.001;
ultimoTiempo = tiempo;
gato.style.top = (Math.sin(angulo) * 40 + 40) + "px";
gato.style.left = (Math.cos(angulo) * 200 + 230) + "px";
// Tus extensiones van aquí.
requestAnimationFrame(animar);
}
requestAnimationFrame(animar);
</script>
if}}
{{hint
Las funciones Math.cos
y Math.sin
miden los ángulos en radianes,
donde un círculo completo es 2π. Para un ángulo dado, puedes obtener
el ángulo inverso agregando la mitad de esto, que es Math.PI
. Esto
puede ser útil para colocar el sombrero en el lado opuesto de la órbita.
hint}}