3. JavaScript y DOM

JavaScript funciona de la mano de las páginas web, y éstas se basan en el DOM y trabajan sobre un navegador que interactúa con el BOM.

3.1. BOM

El BOM (Browser Object Model) define una serie de objetos que nos permiten interactuar con el navegador, como son window, navigator y location.

BOM
Figure 1. BOM

3.1.1. Objeto window

El objeto window es el objeto raíz (global) del navegador.

Mediante el objeto window podemos:

  • abrir una nueva ventana del navegador mediante window.open()

    En los navegadores actuales, si intentamos abrir más de una ventana mediante window.open el propio navegador bloqueará su apertura, tal como sucede con las ventanas emergentes de publicidad.

    Tras abrir una ventana mediante window.open() el método devolverá un nuevo objeto window, el cual será el objeto global para el script que corre sobre dicha ventana, conteniendo todas las propiedades comunes a los objetos como el constructor Object o el objeto Math. Pero si intentamos mirar sus propiedades, la mayoría de los navegadores no lo permitirán, ya que funciona a modo de sandbox.

    Este modo de trabajo implica que el navegador sólo nos mostrará la información relativa al mismo dominio, y si abrimos una página de un dominio diferente al nuestro no tendremos control sobre las propiedades privadas del objeto window. Este caso lo estudiaremos en la siguiente unidad y aprenderemos como solucionarlo.

  • cerrar una ventana mediante window.close()

  • mostrar mensajes de alerta, confirmación y consulta mediante window.alert(mensaje), window.confirm(mensaje) y window.prompt(mensaje [,valorPorDefecto]).

    Por ejemplo, si ejecutamos el siguiente fragmento:

    alert("uno");
    confirm("dos");
    var resp = prompt("tres");

    Podemos observar las diferencias entre los tres diálogos:

    Mensajes del objeto window
    Figure 2. Mensajes del objeto window

3.1.2. Objeto navigator

Mediante el objeto navigator podemos acceder a propiedades de información del navegador, tales como su nombre y versión. Para ello, podemos acceder a las propiedades navigator.appName o navigator.appVersion.

Objeto navigator
Figure 3. Objeto navigator - http://jsbin.com/luhaye/1/edit?js

3.1.3. Objeto document

Cada objeto window contiene la propiedad document, el cual contiene un objeto que representa el documento mostrado. Este objeto, a su vez, contiene la propiedad location que nos permite obtener información sobre la URL con las propiedades:

  • href: cadena que representa la URL completa

  • protocol: protocolo de la URL

  • host: nombre del host

  • pathname: trayectoria del recurso

  • search: parte que contiene los parámetros, incluido el símbolo ?

console.log("href:" + location.href);  // http://localhost:63342/Pruebas/bom/location.html?alfa=beta&gama=delta
console.log("protocol:" + location.protocol);  // http:
console.log("host:" + location.host);  // localhost:63342
console.log("pathname:" + location.pathname);  // /Pruebas/bom/location.html
console.log("search:" + location.search);  // ?alfa=beta&gama=delta

Si a location.href le asignamos una nueva URL, el navegador realizará una petición a dicha URL y el navegador cargará el nuevo documento.

Una operación a destacar dentro del objeto document es document.write(texto), que permite escribir contenido HTML en el documento. Si le pasamos como texto un documento HTML entero, sustituirá por completo el documento existente por el recibido por el método write. Así pues, mediante este método podemos añadir contenido dinámico al documento.

Por ejemplo, si queremos generar la hora actual, podemos usar el objeto Date y mediante document.write() escribirla:

<html>
<head><title>La hora</title></head>
<body>
    <p>Son las
      <script type="text/javascript">
        var time = new Date();
        document.write(time.getHours() + ":" + time.getMinutes());
      </script>
    </p>
  </body>
</html>

3.2. Trabajando con el DOM

El 99% de las aplicaciones JavaScript están orientadas a la web, y de ahí su integración con el DOM de las páginas web.

El API DOM permite interactuar con el documento HTML y cambiar el contenido y la estructura del mismo, los estilos CSS y gestionar los eventos mediante listeners.

Se trata de un modelo que representa un documento mediante una jerarquía de objetos en árbol y que facilita su programación mediante métodos y propiedades.

DOM ha ido evolucionando en diferentes versiones:

  • DOM Level 0 (Legacy DOM): define las colecciones forms, links e images.

  • DOM Level 1 (1998): introduce el objeto Node y a partir de él, los nodos Document, Element, Attr y Text. Además, las operaciones getElemensByTagName, getAttribute, removeAtribute y setAttribute

  • DOM Level 2: facilita el trabajo con XHTML y añade los métodos getElementById, hasAttributes y hasAttribute

  • DOM Level 3: añade atributos al modelo, entre ellos textContent y el método isEqualNode.

  • DOM Level 4 (2004): supone el abandono de HTML por XML e introduce los métodos getElementsByClassName, prepend, append, before, after, replace y remove.

3.2.1. El objeto window

Tal como vimos en la primera sesión, al declarar una variable tiene un alcance global. Realmente, todas las variable globales forman parte del objeto window, ya que éste objeto es el padre de la jerarquía de objetos del navegador (DOM, Document Object Model).

var batman = "Bruce Wayne";
console.log(window.batman);

Por lo general, casi nunca vamos a referenciar directamente al objeto window. Una de las excepciones es cuando desde una función declaramos una variable local a la función (con alcance anidado) que tiene el mismo nombre que una variable global y queremos referenciar a la global de manera únivoca, utilizaremos el objeto window.

var superheroe = "Batman";
var mejorSuperheroe = function () {
  var superheroe = "Superman";

  if (window.superheroe != superheroe) {
    superheroe = window.superheroe;
  }
}

Queda claro que sería más conveniente utilizar otro nombre para la variable local, pero ahí queda un ejemplo de su uso.

3.2.2. El objeto document

El objeto document nos da acceso al DOM (Modelo de objetos de documento) de una página web, y a partir de él, acceder a los elementos que forman la página mediante una estructura jerárquica.

Podemos cargar los scripts JS al final de un documento HTML para acelerar la carga de una página web y asegurarnos que el DOM ya está cargado.

Al objeto que hace de raíz del árbol, el nodo html, se puede acceder mediante la propiedad document.documentElement. Sin embargo, la mayoría de ocasiones necesitaremos acceder al elemento body más que a la raíz, con lo que usaremos document.body.

3.2.3. Elementos del DOM

Cada elemento exceptuando el elemento <html> forma parte de otro elemento, que se le conoce como padre (parent). Un elemento a su vez puede contener elementos hijos (child) y/o hermanos (sibling).

Supongamos el siguiente fragmento de código HTML:

<p><strong>Hello</strong> how are you doing?</p>

Cada porción de este fragmento se convierte en un nodo DOM con punteros a otros nodos que apuntan sus nodos relativos (padres, hijos, hermanos, …​), del siguiente modo:

Relaciones entre nodos DOM
Figure 4. Relaciones entre nodos DOM

Vamos a estudiar estos enlaces en detalle.

Enlaces de nodos

Podemos acceder a los enlaces existentes entre los nodos mediante las propiedades que poseen los nodos. Cada objeto DOM contiene un conjunto de propiedades para acceder a los nodos con lo que mantiene alguna relación. Por ejemplo, cada nodo tiene una propiedad parentNode que referencia a su padre (si tiene). Estos padres, a su vez, contienen enlaces que devuelven la referencia a sus hijos, pero como puede tener más de un hijo, se almacenan en un pseudoarray denominado childNodes.

Plantilla Ejemplo DOM

Para poder realizar algunos ejemplos de manejo de DOM, vamos a crear un sencillo documento HTML:

Plantilla HTML con la que vamos a trabajar el DOM - http://jsbin.com/bopije/1/edit?html
<!DOCTYPE html>
<html lang="es">
<head>
<title>Ejemplo DOM</title>
<meta charset="utf-8" />
</head>
<body>
<h1>Encabezado uno</h1>
<p>Primer párrafo</p>
<p>Segundo párrafo</p>
<div><p id="tres">Tercer párrafo dentro de un div</p></div>
<script src="dom.js" charset="utf-8"></script>
</body>
</html>

En el documento anterior, document.body.childNodes contiene 5 elementos: el encabezado h1, los dos párrafos, la capa y la referencia al script.

Si el documento HTML contiene saltos de línea, tabuladores, etc…​ DOM interpreta estos como nodos de texto, con lo que el número de hijos de un nodo puede no ser el que nosotros esperemos.

Los otros enlaces que ofrece un nodo son:

  • firstChild: primer hijo de un nodo, o null si no tiene hijos.

  • lastChild: último hijo de un nodo, o null si no tiene hijos.

  • nextSibling: siguiente hermano de un nodo, o null si no tiene un hermano a continuación (es el último).

  • previousSibling: anterior hermano de un nodo, o null si no tiene un hermano previo (es el primero).

Por ejemplo:

var encabezado = document.body.firstChild;
var scriptJS = document.body.lastChild;
var parrafo1 = encabezado.nextSibling;
var capa = scriptJS.previousSibling;

En resumen, los punteros que ofrece un nodo DOM son:

Puntero de un nodo DOM
Figure 5. Puntero de un nodo DOM
Tipos de nodos

Cada uno de los elementos se conoce como nodo, que pueden ser de los siguientes tipos:

  • Element: nodo que contiene una etiqueta HTML

  • Attr: nodo que forma parte de un elemento HTML

  • Text: nodo que contiene texto y que no puede tener hijos

Si nos centramos en el código de la capa:

<div> (1)
  <p id="tres"> (2)
    Tercer párrafo dentro de un div (3)
  </p>
</div>
1 La capa es un nodo de tipo elemento
2 El atributo id es un nodo de tipo atributo
3 El contenido del párrafo es un nodo de tipo texto

Si queremos averiguar si un nodo representa un texto o un elemento, usaremos la propiedad nodeType, que devolverá un número, por ejemplo: 1 si es un elemento (nodo HTML), 3 si es de texto. Estos números están asociados a constantes del objeto document: document.ELEMENT_NODE, document.TEXT_NODE, etc…​

Así pues, si quisiéramos crear una función que averiguase si es un nodo de texto:

function esNodoTexto(nodo) {
  return nodo.nodeType == document.TEXT_NODE; // 3
}
esNodoTexto(document.body); // false
esNodoTexto(document.body.firstChild.firstChild); // true
Realmente existen 12 tipos, pero estos tres son los más importantes. Por ejemplo, el objeto document es el tipo 9.

Los elementos contienen la propiedad nodeName que indica el tipo de etiqueta HTML que representa (siempre en mayúsculas). Los nodos de texto, en cambio, contienen nodeValue que obtiene el texto contenido.

document.body.firstChild.nodeName; // H1
document.body.firstChild.firstChild.nodeValue; // Encabezado uno

3.2.4. Recorriendo el DOM

Si queremos recorrer todos los nodos de un árbol, lo mejor es realizar un recorrido recursivo. Por ejemplo, si queremos crear una función que indique si un nodo (o sus hijos) contiene una determinada cadena:

function buscarTexto(nodo, cadena) {
  if (nodo.nodeType == document.ELEMENT_NODE) {
    for (var i=0, len=nodo.childNodes.length; i<len; i++) {
      if (buscarTexto(nodo.childNodes[i], cadena)) {
        return true;
      }
    }
    return false;
  } else if (nodo.nodeType == document.TEXT_NODE) {
    return nodo.nodeValue.indexOf(cadena) > -1;
  }
}

3.2.5. Seleccionando elementos

Mediante el DOM, podemos usar dos métodos para seleccionar un determinado elemento.

Si queremos seleccionar un conjunto de elementos, por ejemplo, todos los párrafos del documento, necesitamos utilizar el método document.getElementsByTagName(nombreDeTag). En cambio, si queremos acceder a un elemento por su id (que debería ser único), usaremos el método document.getElementById(nombreDeId).

(function() {
  var pElements = document.getElementsByTagName("p"); // NodeList
  console.log(pElements.length);  // 3
  console.log(pElements[0]);  // Primer párrafo

  var divpElement = document.getElementById("tres");
  console.log(divpElement); // "<p id="tres">Tercer párrafo dentro de un div</p>"
}());
En vez de usarlos sobre el objeto document, podemos utilizarlos sobre un elemento en concreto para refinar la búsqueda

Destacar que si estamos interesados en un único elemento, lo haremos mediante su id, mientras que si queremos más de un elemento, lo haremos mediante tu tag, ya que obtendremos un NodeList, el cual es similar a un array, y es una representación viva de los elementos, de modo que si se produce algún cambio en el DOM se verá trasladado al navegador.

querySelector

El método de getElementsByTagName es antiguo y no se suele utilizar. En el año 2013 se definió el Selector API, que define los métodos querySelectorAll y querySelector, los cuales permiten obtener elementos mediantes consultas CSS, las cuales ofrecen mayor flexibilidad:

var pElements = document.querySelectorAll("p");
var divpElement = document.querySelector("div p");
var tresElement = document.querySelector("#tres");

Esta manera de acceder a los elementos es la misma que usa jQuery, por lo que la estudiaremos en la sesión correspondiente.

Conviene citar que getElementById es casi 5 veces más rápido que querySelector. Más información en: http://jsperf.com/getelementbyid-vs-queryselector

3.2.6. Añadiendo contenido al DOM

Si queremos añadir un párrafo al documento que teníamos previamente, primero tenemos que crear el contenido y luego decidir donde colocarlo. Para ello, usaremos el método createElement para crear el elemento y posteriormente, decidir donde colocarlo y añadir el contenido (por ejemplo, mediante appendChild).

(function() {
  var elem = document.createElement("p"),
    texto = "<strong>Nuevo párrafo creado dinámicamente</strong>",
    contenido = document.createTextNode(texto);

  elem.appendChild(contenido);
  elem.id = "conAppendChild";

  document.body.appendChild(elem);
  // lo añade como el último nodo detrás de script
}());

Si probamos el código veremos que las etiquetas <strong> se han parseado y en vez de mostrar el texto en negrita se muestra el código de la etiqueta. Además, si intentamos ver el código fuente de la página no veremos el contenido creado dinámicamente, y necesitaremos utilizar las herramientas de desarrollador que ofrecen los navegadores web.

Añadiendo un nodo

Los métodos que podemos utilizar para añadir contenidos son:

  • appendChild(nuevoElemento): el nuevo nodo se incluye inmediatamente después de los hijos ya existentes (si hay alguno) y el nodo padre cuenta con una nueva rama.

  • insertBefore(nuevoElemento, elementoExistente): permiten elegir un nodo existente del documento e incluir otro antes que él.

insertAfter

Aunque parezca mentira, no existe ningún método para añadir después, pero podríamos crearlo fácilmente de la siguiente manera:

function insertAfter(e, i){
  if (e.nextSibling){
    e.parentNode.insertBefore(i, e.nextSibling);
  } else {
    e.parentNode.appendChild(i);
  }
}
  • replaceChild(nuevoElemento, elementoExistente): reemplazar un nodo por otro

  • removeChild(nodoABorrar): elimina un nodo

  • cloneNode(): permite clonar un nodo, permitiendo tanto el elemento como el elemento con su contenido

Así pues, si queremos añadir el párrafo dentro de la capa que tenemos definida en vez de al final del documento, podemos obtener el nodo que contiene el párrafo de la capa, y añadir el nuevo nodo a su padre en la posición que deseemos (al final, antes del nodo o sustituirlo).

(function() {
  var doc = document,   (1)
    elem = doc.createElement("p"),
    contenido = doc.createTextNode("<strong>Nuevo párrafo creado dinámicamente</strong>"),
    pTres = doc.getElementById("tres"); (2)

  elem.appendChild(contenido);
  elem.id = "conAppendChild";

  pTres.parentNode.appendChild(elem); (3)
}());
1 Guardamos en una variable la referencia a document para evitar tener que salir del alcance y subir al alcance global con cada referencia. Se trata de una pequeño mejora que aumenta la eficiencia del código.
2 Obtenemos una referencia al Node que contiene el párrafo de dentro del div
3 Insertamos un hijo al padre de #tres, lo que lo convierte en su hermano. Si hubiesemos querido que se hubiese colocado delante, tendríamos que haber utilizado pTres.parentNode.insertBefore(elem, pTres). En cambio, para sustituir un párrafo por el nuevo necesitaríamos hacer pTres.parentNode.replaceChild(elem, pTres);

Otra forma de añadir contenido es mediante la propiedad innerHTML, el cual sí que va a parsear el código incluido. Para ello, en vez de crear un elemento y añadirle contenido, el contenido se lo podemos añadir como una propiedad del elemento.

(function() {
  var
    doc = document,
    elem = doc.createElement("p"),
    pTres = doc.getElementById("tres");

  elem.innerHTML = "<strong>Nuevo párrafo reemplazado dinámicamente</strong>";
  elem.id = "conInner";

  pTres.parentNode.replaceChild(elem, pTres);
}());
Sustituyendo un nodo con parseo HTML
innerHTML vs nodeValue

Mientras que innerHTML interpreta la cadena como HTML, nodeValue la interpreta como texto plano, con lo que los símbolos de < y > no aportan significado al contenido

document.write()

Pese a que el método document.write(txt) permite añadir contenido a un documento, hemos de tener mucho cuidado porque, pese a funcionar si lo incluimos al cargar una página, al ejecutarlo dentro de una función una vez el DOM ya ha cargado, el texto que queramos escribir sustituirá todo el contenido que había previamente. Por lo tanto, si queremos añadir contenido, es mejor hacerlo añadiendo un nodo o mediante innerHtml

getElementsBy vs querySelector

Una diferencia importante es que las referencias con getElementsBy* están vivas y siempre contienen el estado actual del documento, mientras que con querySelector* obtenemos las referencias existentes en el momento de ejecución, sin que cambios posteriores en el DOM afecten a las referencias obtenidas.

getElementsBy vs querySelector - http://jsbin.com/luzexi/2/
(function() {
  var
    getElements = document.getElementsByTagName("p"),
    queryElements = document.querySelectorAll("p");

  console.log("Antes con getElements:" + getElements.length);  // 3
  console.log("Antes con querySelector:" + queryElements.length);  // 3

  var elem = document.createElement("p");
  elem.innerHTML = "getElements vs querySelector";
  document.body.appendChild(elem);

  console.log("Después con getElements:" + getElements.length); // 4
  console.log("Después con querySelector:" + queryElements.length); // 3
}());

Si ejecutamos la página y visualizamos el contenido de la consola tendremos:

getElement vs querySelector
Gestionando atributos

Una vez hemos recuperado un nodo, podemos acceder a sus atributos mediante el método getAttribute(nombreAtributo) y modificarlo mediante setAttribute(nombreAtributo, valorAtributo).

(function() {
  var pTres = document.getElementById("tres");
  pTres.setAttribute("align","right");
}());

También podemos acceder a los atributos como propiedades de los elementos, con lo que podemos hacer lo mismo del siguiente modo:

(function() {
  var pTres = document.getElementById("tres");
  pTres.align = "right";
}());

La gestión de los atributos están en desuso en favor de las hojas de estilo para dotar a las páginas de un comportamiento predefinido y desacoplado en su archivo correspondiente.

Cuando veamos el impacto de HTML 5 en JavaScript estudiaremos el uso de los atributos data y el conjunto dataset.

3.3. Trabajando con CSS

En este apartado vamos a ver como modificar el estilos de los elementos, cambiando sus propiedades CSS mediante el uso de Javascript.

Se supone que el alumno ya tiene unos conocimientos básicos de CSS

Para ello, vamos a basarnos en el siguiente código HTML.

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="utf-8" />
  <title></title>
  <style>
    #batman { }
    .css-class {
      color: blue;
      border : 1px solid black;
    }
  </style>
</head>
<body>
  <div style="font-size:xx-large" id="batman">Batman siempre gana.</div>
  <script src="css.js"></script>
</body>
</html>

La propiedad style de los elementos nos permiten obtener/modificar los estilos. Por ejemplo, si queremos cambiar mediante JavaScript el color del texto del párrafo a azul y añadirle un borde negro, haríamos lo siguiente:

(function() {
  var divBatman = document.getElementById("batman");
  divBatman.style.color = "blue";
  divBatman.style.border = "1px solid black";
}());
Si la propiedad CSS contiene un guión, para usarla mediante JavaScript, se usa la notación camelCase. Así pues, background-color pasará a usarse como backgroundColor.

3.3.1. Trabajando con clases

Si vamos a modificar más de un estilo, es conveniente usar una clase CSS para evitar los múltiples accesos, a la cual accederemos mediante mediante JavaScript con la propiedad className.

No se utiliza class por que se trata de una palabra reservada.
(function() {
  var divBatman = document.getElementById("batman");
  divBatman.className = "css-class";
  // divBatman.className = ""; -> elimina la clase CSS
}());

Si queremos añadir más de una clase, podemos separarlas con espacios o utilizar la propiedad classList (en navegadores actuales - http://caniuse.com/classlist ) que permite añadir clases mediante el método add.

(function() {
  var divBatman = document.getElementById("batman");
  divBatman.classList.add("css-class");
  divBatman.classList.add("css-class2");
}());

Otros métodos útiles de classList son remove para eliminar una clase, toggle para cambiar una clase por otra, length para averiguar la longitud de la lista de clases y contains para averiguar si una clase existe dentro de la lista.

Si en algún momento queremos averiguar el estilo de una determinada propiedad, podemos acceder a la propiedad de window.getComputedStyle(elem, null).getPropertyValue(cssProperty). En el caso de que el navegador no lo soporte (sólo IE antiguos), hay que usar el array currentStyle.

(function() {
  var divBatman = document.getElementById("batman");
  var color = window.getComputedStyle(divBatman, null).getPropertyValue("color");
  var colorIE = divBatman.currentStyle["color"];
}());

3.3.2. Mostrando y ocultando contenido

Una acción muy común es mostrar y ocultar contenido de manera dinámica. Esto se puede conseguir ocultando un nodo, y posteriormente volverlo a mostrar.

Para ello, tenemos la propiedad style.display. Cuando toma el valor de "none", no se mostrará el elemento, aunque el mismo exista. Más adelante, para volver a mostrarlo, le asignaremos una cadena vacía a la propiedad style.display, y el elemento reaparecerá.

(function() {
  var divBatman = document.getElementById("batman");
  divBatman.style.display = "none"; // oculta
  divBatman.style.display = ""; // visible
}());

3.4. Animaciones

Uno de las acciones más usadas en las web actual es el movimiento y la manipulación de contenidos, de modo que hay texto que aparece/desaparece o cambia de lugar de manera dinámica.

Por ejemplo, si lo que queremos es crear una animación, haremos llamadas sucesivas a una función, pero con un límite de ejecuciones mediante el uso de [Timers].

(function() {
  var velocidad = 2000,
    i = 0;
    miFuncion = function() {
      console.log("Batman vuelve " + i);
      i = i + 1;
      if (i < 10) {
        setTimeout(miFuncion, velocidad);  (2)
      }
    };
  setTimeout(miFuncion, velocidad); (1)
}());
1 Hacemos una llamada a miFuncion
2 Con los valores de i de 0 a 9 se ejecutará miFuncion con una separación de velocidad milisegundos

Otra manera de hacerlo es utilizar la función setInterval(funcion, intervalo):

(function() {
  var velocidad = 2000,
    i = 0;
    miFuncion = function() {
      console.log("Batman vuelve " + i);
      i = i + 1;
      if (i > 9) {
        clearInterval(timer);
      }
    };
  var timer = setTimeout(miFuncion, velocidad);
}());

Para ver un ejemplo de animación en movimiento, a partir de una capa con un cuadrado azul, vamos a moverla horizontalmente de manera ininterrumpida.

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="utf-8" />
  <title>Caja</title>
  <style>
      #caja {
        position: absolute;
        left: 50px;
        top: 50px;
        background-color: blue;
        height: 100px;
        width: 100px;
      }
  </style>
</head>
<body>
  <div id="caja"></div>
  <script src="caja.js"></script>
</body>
</html>

Y en script, de forma similar al ejemplo anterior:

(function() {
  var velocidad = 10,
    mueveCaja = function(pasos) {
      var el = document.getElementById("caja"),
        izq = el.offsetLeft; (1)

      if ((pasos > 0 && izq > 399) || (pasos < 0 && izq < 51)) {
        clearTimeout(timer); (2)
        timer = setInterval(function() {
          mueveCaja(pasos * -1);  (3)
        }, velocidad);
      }

      el.style.left = izq + pasos + "px"; (4)
    };

  var timer = setInterval(function () {
    mueveCaja(3);
  }, velocidad);
}());
1 Obtenemos su posición respecto al margen izquierdo
2 Si llegamos a uno de los límites horizontales (de 0 a 400, paramos la función)
3 Y volvemos a llamarla pero en sentido contrario
4 Fija la posición del cuadrado con la que tenía más el número de pasos definidos (de 3 en 3, o de -3 en -3)

3.5. Eventos

Los eventos nos van a permitir asociar funciones a acciones que ocurren en el navegador tras la interacción con el usuario, ya sea mediante el teclado o el ratón.

3.5.1. Gestionando eventos

Para comenzar con un ejemplo muy sencillo, vamos a basarnos en la página web que dibujaba un cuadrado. A partir del evento de hacer click sobre el mismo, cambiaremos su color a rojo. Para ello:

(function() {
  var el = document.getElementById("caja");

  el.onclick = function() {  (1)
    this.style.backgroundColor = "red";
  };  (2)

}());
1 Definimos una función que se ejecutará cada vez que se haga click sobre el elemento
2 Un error muy común es olvidar el ; tras la definición de la función

Podemos observar como hemos asociado una función a la propiedad onclick de un elemento, mediante la técnica de elemento.evento. De este modo, cada vez que hagamos click sobre el elemento caja se ejecutará dicha función anónima. Otros eventos que podemos usar son onload, onmouseover, onblur, onfocus, etc…​

Otra formar de asociar una acción al evento, aunque no se recomienda, es incrustar el código JavaScript como parte de un atributo de etiqueta HTML, con el nombre del evento:

<button onclick="this.style.backgroundColor='red';">Incrustado</button>

Y la tercera forma es utilizar la función addEventListener(evento, función, flujoEvento) que veremos más adelante.

Ejemplo botones y click

Vamos a hacer un ejemplo un poco más completo. Para ello, vamos a definir una página de web que va a cambiar el estilo con los colores de fondo/frente conforme pulsemos un botón u otro.

<!DOCTYPE html>
<html lang="es">
  <head>
    <meta charset="utf-8" />
    <title>Hola Eventos</title>
    <style>
      .normal {
        background-color: white;
        color: black;
      }
      .contrario {
        background-color: black;
        color: white;
      }
    </style>
  </head>
  <body class="normal">
      <h1>Hola Eventos</h1>
      <p><a href="http://es.wikipedia.org/wiki/Batman">Batman</a> Forever</p>

      <button>Normal</button>
      <button>Contrario</button>

      <script src="eventos.js"></script>
  </body>
</html>

Y el código JavaScript sería el siguiente:

(function() {
  var botones = document.getElementsByTagName("button");

  for (var i=0, len=botones.length; i<len; i=i+1) {
    botones[i].onclick = function() {
      var className = this.innerHTML.toLowerCase();
      document.body.className = className;
    };
    // botones[i].onclick = function() {};
  }
}());

Para cada uno de los botones, le añadimos una función anónima al evento onclick para que le asigne al body la clase que coincide con el nombre del botón (en minúsculas).

Mucho cuidado con añadir una segunda función al mismo evento tal cual está en la línea 9, ya que realmente estaremos sobreescribiendo (y borrando) la función anterior.

Ante el mismo evento, un objeto puede tener solo un manejador, pero varios listeners.

3.5.2. Flujo de eventos

A la hora de propagarse un evento, existen dos posibilidades al definir el flujo de eventos para saber cual es el elemento que va a responder al mismo:

  • Con la captura de eventos, al pulsar sobre un elemento, se produce una evento de arriba a abajo, desde el elemento window, pasando por <body> hasta llegar al elemento que lo captura.

  • En cambio, mediante el burbujeo de eventos (event bubbling), el evento se produce en el elemento de más abajo y va subiendo hasta llegar al window.

Por ejemplo, si tuviéramos el siguiente fragmento HTML:

Ejemplo de Flujo de Evento
<html onclick="procesaEvento()">
  <head><title>Ejemplo de flujo de eventos</title></head>
  <body onclick="procesaEvento()">
    <div onclick="procesaEvento()">Pincha aqui</div>
  </body>
</html>

Cuando se pulsa sobre el texto "Pincha aquí" que se encuentra dentro del <div>, si el navegador sigue el modelo de event bubbling, se ejecutan los siguientes eventos en el orden que muestra el siguiente esquema:

Burbuja de evento
Figure 6. Burbuja de Eventos

En cambio mediante la captura de eventos tendríamos:

Captura de eventos
Figure 7. Captura de Eventos

El flujo de eventos definido en la especificación DOM soporta tanto el burbujeo como la captura, pero la captura de eventos se ejecuta en primer lugar. Los dos flujos de eventos recorren todos los objetos DOM desde el objeto document hasta el elemento más específico o viceversa.

3.5.3. El modelo estándar

También conocido como eventos de DOM nivel 0, consisten en asignar una función al evento de un elemento.

Este modelo se puede hacer utilizando el atributo on* de las etiquetas HTML, o de manera programativa mediante el uso del método addEventListener(evento, función, flujoEvento).

Vamos a obviar a IE8 y versiones anteriores que no gestionan los eventos de esta manera. Si estás interesado en dicho navegador, emplean la función attachEvent con el mismo propósito. La gestión de código cross-browser la solucionaremos mediante el uso de jQuery.

El parámetro flujoEvento puede tomar los valores true para aplicar el modelo de captura de eventos, o false para event bubbling (recomendado).

(function() {
  var botonClick = function() {  (1)
    var className = this.innerHTML.toLowerCase();
    document.body.className = className;
  }

  var botones = document.getElementsByTagName("button");
  for (var i=0, len=botones.length; i<len; i=i+1) {
    botones[i].addEventListener("click", botonClick, false);  (2)
    // botones[i].removeEventListener("click", botonClick, false);
  }
}());
1 Función que cambia la clase del body al valor del elemento con el que se invoque (en nuestro caso, el nombre del botón)
2 A cada botón, se le registra al evento click el manejador de eventos de la función del paso 1, y con flujo de event bubbling.
Al añadir eventos mediante addEventListener sí que podemos añadir más de una función con el mismo evento a un mismo elemento.

Para borrar un evento podemos usar su antónimo removeEventListener(evento, función, flujoEvento). Cabe destacar que la función a eliminar debe ser la misma que la utilizada al añadir el evento, por lo que no podemos usar funciones anónimas.

Si en algún momento necesitamos obtener información sobre el evento, podemos recoger en la función manejadora del evento un parámetro que se asociará de manera automática al evento. Este parámetro contiene las propiedad type que índica el evento del que proviene (en el ejemplo, sería click), y target con el elemento sobre el cual esta registrado (en el ejemplo sería HTMLButtonElement).

var botonClick = function(evt) {
  var className = this.innerHTML.toLowerCase();
  document.body.className = className;
  console.log(evt.type + " - " + evt.target); // click - HTMLButtonElement
}

Por último, sobre el evento podemos llamar al método preventDefault() para cancelar el comportamiento por defecto que tenía asociado el elemento sobre el cual se ha registrado el evento. Por ejemplo, si quisiéramos evitar que al pulsar sobre el enlace nos llevase a su destino:

var enlaceClick = function(evt) {
  evt.preventDefault();
}

3.5.4. Delegación de eventos

Se basa en el flujo de eventos ofrecidos por event bubbling para delegar un evento desde un elemento inferior en el DOM que va a subir como una burbuja hasta el exterior.

(function() {
  document.addEventListener("click", function(evt) {
    var tag = evt.target.tagName;
      console.log("Click en " + tag);

    if ("A" == tag) {
      evt.preventDefault();
    }
  }, false);
})();

3.5.5. Tipos de eventos

Podemos dividir, a grosso modo, los eventos en los siguientes tipos:

  • Evento de carga.

  • Eventos de foco.

  • Eventos de ratón.

  • Eventos de teclado.

Evento de carga

Para poder asignar un listener a un elemento del DOM, éste debe haberse cargado, y de ahí la conveniencia de incluir el código JavaScript al final de la página HTML, justo antes de cerrar el body.

Para asegurarnos que el documento ha cargado completamente, podemos usar el evento window.onload. De este modo podríamos enlazar el código JavaScript al inicio de la página:

function preparandoManejadores() {
  var miLogo = document.getElementById("logo");
  miLogo.onclick() {
    alert("Has venido al sitio adecuado.");
  }
}

window.onload = function() {
  preparandoManejadores();
}

Un fallo recurrente es incluir más de un manejador para el mismo evento onload. Si incluimos diferentes archivos .js, únicamente se cargará el último manejador, lo que puede provocar comportamientos no deseados.

Una característica a destacar es que el evento se lanzará una vez todo el contenido se ha descargado, lo que incluye las imágenes de la página.

Eventos de foco

Cuando trabajamos con formularios, al clickar o acceder a un campo mediante el uso del tabulador, éste obtiene el foco, lo que lanza el evento onfocus. Al cambiar a otro campo, el elemento que tiene el foco lo pierde, y se lanza el evento onblur.

Supongamos el siguiente fragmento de un formulario:

<form name="miForm">
  Nombre: <input type="text" name="nombre" id="nom" tabindex="10" />
  Apellidos: <input type="text" name="apellidos" id="ape" tabindex="20" />
</form>

Vamos a simular el comportamiento del atributo placeHolder de HTML5, que escribe un texto de ayuda al campo que desaparece al tener el foco. Tal como veremos más adelante, para acceder al valor de un campo de texto hemos de utilizar la propiedad value:

var campoNombre = document.getElementById("nom");
campoNombre.value = "Escribe tu nombre";

campoNombre.onfocus = function() {
  if ( campoNombre.value == "Escribe tu nombre") {
    campoNombre.value = "";
  }
};

campoNombre.onblur = function() {
  if ( campoNombre.value == "") {
    campoNombre.value = "Escribe tu nombre";
  }
};
Eventos de ratón

Cuando el usuario hace click con el ratón, se generan tres eventos. Primero se genera mousedown en el momento que se presiona el botón. Luego, mouseup cuando se suelta el botón. Finalmente, se genera click para indicar que se ha clickado sobre un elemento. Cuando esto sucede dos veces de manera consecutiva, se genera un evento dblclick (doble click).

Cuando asociamos un manejador de eventos a un botón, lo normal es que sólo nos interese si ha hecho click. En cambio, si asociamos el manejador a un nodo que tiene hijos, al hacer click sobre los hijos el evento "burbujea" hacia arriba, por lo que nos interesará averiguar que hijo ha sido el responsable, utilizando la propiedad target del evento.

Si nos interesa las coordenadas exactas del click, el objeto evento contiene la propiedades clientX y clientY con los valores en pixeles del cursor en la pantalla. Como los documentos pueden usar el scroll, en ocasiones, estas coordenadas no nos indica en que parte del documento se encuentra el ratón. Algunos navegadores ofrecen las propiedades pageX y pageY para este propósito, aunque otros no. Por suerte, la información respecto a la cantidad de píxeles que el documento ha sido scrollado puede encontrarse en document.body.scrollLeft y document.body.scrollTop.

Aunque también es posible averiguar que botón del ratón hemos pulsado mediante las propiedades which y button del evento, es muy dependiente del navegador, por lo que dejaremos su gestión a jQuery.

Si además de los clicks nos interesa el movimiento del ratón, el evento mousemove salta cada vez que el ratón se mueve sobre un elemento. Además, los evento mouseover y mouseout se lanzan al entrar o salir del elemento. Para estos dos eventos, la propiedad target referencia el nodo que ha lanzado el evento, mientras que relatedTarget nos indica el nodo de donde viene el ratón (para mouseover) o adonde va (para mouseout)

Si el nodo tiene hijos, los eventos mouseover y mouseout pueden dar lugar a malinterpretaciones. Los evento lanzado desde los hijos nodos burbujean hasta el elemento padre, con lo que tendremos un evento mouseover cuando el ratón entre en uno de los hijos. Para detectar (e ignorar) estos eventos, podemos usar la propiedad target:

miParrafo.addEventListener("mouseover", function(event) {
  if (event.target == miParrafo)
    console.log("El ratón ha entrado en mi párrafo");
}, false);
Eventos de teclado

Si queremos que nuestra aplicación reaccione a la pulsación de las teclas, tenemos de nuevo 3 eventos:

  • keydown: al pulsar una tecla; también se lanza si se mantiene pulsada

  • keyup: al soltar una tecla

  • keypress: tras soltar la tecla, pero sin las teclas de modificación; también se lanza si se mantiene pulsada

Cuando se pulsa una tecla correspondiente a un carácter alfanumérico, se produce la siguiente secuencia de eventos: keydown, keypress, keyup. Cuando se pulsa otro tipo de tecla, se produce la siguiente secuencia de eventos: keydown, keyup. Si se mantiene pulsada la tecla, en el primer caso se repiten de forma continua los eventos keydown y keypress y en el segundo caso, se repite el evento keydown de forma continua.

Normalmente, usaremos keydown y keyup para averiguar que tecla se ha pulsado, por ejemplo los cursores. Si estamos interesado en el carácter pulsado, entonces usaremos keypress.

Para probar estos eventos, nos vamos a basar en el siguiente código:

<!DOCTYPE html>
<html lang="es">
<head>
  <title>Eventos Teclado</title>
  <meta charset="utf-8" />
</head>
<body>
    <input type="text" name="cajaTexto" id="cajaTexto" />
    <script src="eventos.js"></script>
</body>
</html>

Por ejemplo, si nos basamos en el evento keypress, si queremos que sólo se permitan escribir letras en mayúsculas, mediante evt.charCode podemos obtener el código ASCII de la tecla pulsada:

(function() {
  var caja = document.getElementById("cajaTexto");
  document.addEventListener("keypress", function(evt) {

    var ascii = evt.charCode;

    if (ascii >= 65 && ascii <=90) {  (1)
      // solo dejamos mayúsculas
      // las minúsculas van del 97 al 122
    } else {
      evt.preventDefault();
    }
  }, false);
}());
1 Letras en Mayúsculas. Mediante String.fromCharCode podemos obtener una cadena de una letra con la representación del caracter.

En cambio, si usamos los eventos keydown y keyup, podemos consultar a partir del evento las propiedades:

  • keyCode: obtiene el código ASCII del elemento pulsado

  • altKey: devuelve true/false si ha pulsado la tecla ALT

  • ctrlKey: devuelve true/false si ha pulsado la tecla CTRL

  • shiftKey: devuelve true/false si ha pulsado la tecla SHIFT

Si usamos un sistema OSX (Apple) al tener un combinación de teclas diferente, la tecla de opción se asocia a la propiedad altKey y la tecla command tienen su propia propiedad metaKey.

Por ejemplo, si queremos capturar cuando el usuario pulsa CTRL + B dentro de la caja de texto, haremos:

document.addEventListener("keydown", function(evt) {
  var code = evt.keyCode;
  var ctrlKey = evt.ctrlKey;

  if (ctrlKey && code === 66) {
    console.log("Ctrl+B");
  }

}, false);
Cuidado con confundir charCode para un evento keypress con keyCode en un evento keydown/keyup

3.5.6. Eventos y closures

Una manera de asociar una función a un evento que responda a determinados elementos es mediante Closures. Vamos a crear un ejemplo muy sencillo para demostrar su uso.

Supongamos que queremos añadir algunos botones a una página para ajustar el tamaño del texto. Una manera de hacer esto es especificar el tamaño de fuente del elemento body en píxeles y, a continuación, ajustar el tamaño de los demás elementos de la página (como los encabezados) utilizando la unidad relativa em:

body {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 12px;
}
h1 {
  font-size: 1.5em;
}
h2 {
  font-size: 1.2em;
}

Nuestros botones interactivos de tamaño de texto pueden cambiar la propiedad font-size del elemento body, y los ajustes serán aplicados por los otros elementos de la página gracias a las unidades relativas.

Aquí está el código JavaScript:

function cambiarTamanyo(tamanyo) {  (1)
  return function() {
    document.body.style.fontSize = tamanyo + 'px';
  };
}

var tam12 = cambiarTamanyo(12);  (2)
var tam14 = cambiarTamanyo(14);
var tam16 = cambiarTamanyo(16);
1 Closure que cambia el tamaño de texto del body
2 tam12, tam14 y tam16 ahora son funciones que cambian el tamaño del texto de body a 12, 14 y 16 píxeles respectivamente

Suponiendo que tenemos tres botones con los diferentes tamaños, tal que así:

<button id="tam-12">12</button>
<button id="tam-14">14</button>
<button id="tam-16">16</button>

Podemos conectar los closures a los botones de la siguiente forma:

document.getElementById('tam-12').onclick = tam12;
document.getElementById('tam-14').onclick = tam14;
document.getElementById('tam-16').onclick = tam16;

3.6. Trabajando con formularios

Previo a HTML5, gran parte del código JavaScript se centraba en la validación de formularios.

Para poder interactuar con un formulario y poder capturar cuando se envía la información, lo mejor es crear un id al formulario y acceder al formulario mediante dicho id tal como hemos visto anteriormente con getElementById.

Si el formulario no tiene atributo id pero si name, podremos acceder mediante la propiedad document.forms.nombreDelFormulario.

Del mismo modo, para acceder a un campo del formulario, bien lo haremos a través de su id o mediante la propiedad document.forms.nombreDelFormulario.nombreDelCampo.

Supongamos que tenemos el siguiente formulario:

<form name="formCliente" id="frmClnt">
<fieldset id="infoPersonal">
  <legend>Datos Personales</legend>
  <p><label for="nom">Nombre</label>
    <input type="text" name="nombre" id="nom" /></p>
  <p><label for="email">Email</label>
    <input type="email" name="correo" id="email" /></p>
</fieldset>

<!-- ...Dirección ... -->

</fieldset>
</form>

Para acceder al formulario y al nombre del cliente usaríamos:

// Mediante el atributo name
var formulario = document.forms.formCliente;
var correo = formulario.correo;
// Mediante el atributo id
var formuId = document.getElementById("frmClnt");
var correoId = document.getElementById("email");

Al enviarse un formulario se lanza el evento onsubmit. Si queremos interrumpir el envío, sólo tenemos que devolver false desde el manejador de eventos:

function preparandoManejadores() {
  document.getElementById("frmClnt").onsubmit = function() {
    var ok = false;
    // validamos el formulario
    if (ok) {
      return true;  // se realiza el envío
    } else {
      return false;
    }
  };
}

window.onload =  function() {
  preparandoManejadores();
};

3.6.1. Campos de texto

Si nos centramos en los campos de texto (type="text") (también válido para los campos de tipo password o hidden), para obtener el valor del campo usaremos la propiedad value, tal como vimos en el apartado de Eventos de foco. Cabe recordar que los eventos que puede lanzar un campo de texto son: focus, blur, change, keypress, keydown y keyup.

var correoId = document.getElementById("email");
if (correoId.value == "") {
  alert("Por favor, introduce el correo");
}

3.6.2. Desplegables

Para un desplegable creado con select, mediante la propiedad type podemos averiguar si se trata de una lista de selección única (select-one) o selección múltiple (select-multiple).

Al seleccionar un elemento, se lanza el evento change. En el caso de tener una selección única, para acceder al elemento seleccionado, usaremos la propiedad selectedIndex (de 0 a n-1). En el caso de selección múltiple, tendremos que recorrer el array de options y consultar la propiedad selected, es decir, options[i].selected.

Cada uno de los elementos de la lista es un objeto Option. Si queremos obtener el valor de una opción usaremos la propiedad value, y para su texto text.

Supongamos el siguiente código con una lista:

<form name="formCliente" id="frmClnt">
<!-- ... Datos Personales ... -->

<fielset id="direccion">
  <legend>Dirección</legend>
  <p><label for="tipoVia">Tipo de Via</label>
    <select name="tipoVia" id="tipoViaId">
      <option value="calle">Calle</option>
      <option value="avda">Avenida</option>
      <option value="pza">Plaza</option>
    </select>
  </p>
  <p><label for="domicilio">Domicilio</label>
    <input type="text" name="domicilio" id="domi" /></p>
</fieldset>
</form>

Para obtener que tipo de vía ha seleccionado el usuario haríamos:

var tipoViaId = document.getElementById("tipoViaId");

tipoViaId.onchange = function() {
  var indice = tipoViaId.selectedIndex;   // 1
  var valor = tipoViaId.options[indice].value;  // avda
  var texto = tipoViaId.options[indice].text;  // Avenida
};

Si en algún momento queremos añadir o eliminar un elemento a la lista de manera dinámica, usaremos los métodos add(option, lugarAntesDe) (si no se indica el lugar se insertará al final de la lista) o remove(indice) respectivamente:

var op = new Option("Camino Rural", "rural");
tipoViaId.add(op, tipoViaId.options[3]);

3.6.3. Opciones

Tanto los radio como los checkboxes tienen la propiedad checked que nos dice si hay algún elemento seleccionado (true o false). Para averiguar cual es el elemento marcado, tenemos que recorrer el array de elementos que lo forman.

color.checked = true;

var colorElegido = "";

for (var i = 0, l = color.length; i < l; i = i + 1) {
  if (color[i].checked) {
    colorElegido = color[i].value;
  }
}

Respecto a los eventos, se lanzan tanto el evento click como el change.

3.7. Ejercicios

3.7.1. (0.4 ptos) Ejercicio 31. Contenido bloqueado

Crear una función que recorra el DOM desde la etiqueta <body> y si encuentra la palabra "sexo", elimine el elemento y lo sustituya por "Contenido Bloqueado", con el texto en negrita.

La función se almacenará en una archivo denominado ej31.js, y tendrá la siguiente definición:

function bloquearContenido() {}

3.7.2. (0.3 ptos) Ejercicio 32. Temporizador DOM

Siguiendo la misma idea que el ejercicio 12 del Temporizador, crear dos elementos de formulario que indiquen los minutos y segundos de un temporizador, de modo que al darle a comenzar, se muestre el temporizador por pantalla.

El código para mostrar el temporizador será el siguiente:

<h2>Temporizador</h2>
<form id="formTemporizador">
    Min: <input type="number" name="min" id="formMin"> <br />
    Seg: <input type="number" name="seg" id="formSeg"> <br />
    <input type="submit">
</form>

<br />

<div id="temporizador">
<span id="min">_</span>:<span id="seg">_</span>
</div>

La apariencia será similar a la siguiente:

Ejercicio temporizador
Figure 8. Ejercicio Temporizador

El contenido HTML y el código JavaScript se almacenarán en una archivo denominado ej32.html.

3.7.3. (0.5 ptos) Ejercicio 33. Tabla dinámica

A partir de una tabla con la siguiente estructura:

<input type="text" id="texto" />
<button onclick="anyadirFila()">Añadir fila</button>

<br />

<table>
<thead>
<tr><th>Contenido</th><th>Operación</th></tr>
</thead>
<tbody id="bodyTabla">
<tr>
  <td id="fila1">Ejemplo de contenido</td>
  <td><button onclick="toCani('fila1')">Caniar</button></td>
</tr>
</tbody>
</tr>
</table>

Añade el código necesarios para que la pulsar sobre los botones realice las siguientes acciones:

  • Añadir fila: Añade el contenido del campo como última fila de la tabla

  • Caniar: Transformar el texto de la celda mediante la función toCani de la primera sesión

  • Al pasar el ratón por encima de una celda, cambiará el color de fondo de la misma.

El contenido HTML y el código JavaScript se almacenarán en una archivo denominado ej33.html.

3.7.4. (0.3 ptos) Ejercicio 34. Carrusel

A partir de un array de imágenes:

var imagenes = ["img/agua.jpg", "img/hieba.jpg", "img/hoja.jpg", "img/primavera.jpg"];

Escribir el código necesario para que tras dos segundos, se muestre la siguiente imagen. Una vez mostrada la última imagen, el carrusel volverá a comenzar por la primera.

El contenido HTML y el código JavaScript se almacenarán en una archivo denominado ej34.html.