Introducción a AJAX

10.1. El framework Prototype

Prototype es un framework que facilita el desarrollo de aplicaciones web con JavaScript y AJAX. Su autor original es Sam Stephenson, aunque las últimas versiones incorporan código e ideas de muchos otros programadores. A pesar de que incluye decenas de utilidades, la librería es compacta y está programada de forma muy eficiente.

Prototype se ha convertido en poco tiempo en una referencia básica de AJAX y es la base de muchos otros frameworks y librerías relacionadas como script.aculo.us. Las primeras versiones de Prototype no incluían ningún tipo de documentación, lo que dificultaba su uso y provocaba que la mayoría de usuarios desconocieran su verdadero potencial.

Afortunadamente, las versiones más recientes del framework disponen de una completa documentación de todas las funciones y métodos que componen su API. La documentación incluye la definición completa de cada método, sus atributos y varios ejemplos de uso: http://www.prototypejs.org/api

10.1.1. Funciones y métodos básicos

La primera función que se estudia cuando se está aprendiendo Prototype es tan útil como impronunciable: $(). La "función dólar" es un atajo mejorado de la función document.getElementById().

Si se le pasa una cadena de texto con el identificador de un elemento, obtiene ese elemento. La función admite uno o más parámetros: si se le pasa un parámetro, devuelve un objeto; si se le pasan varios parámetros, devuelve un array simple con todos los objetos.

// Con JavaScript
var elemento = document.getElementById('primero');

// Con Prototype
var elemento = $('primero');

// Con JavaScript
var elemento1 = document.getElementById('primero');
var elemento2 = document.getElementById('segundo');

// Con Prototype
var elementos = $('primero', 'segundo');

Otra de las funciones más útiles de Prototype es $F(), que es similar a la anterior función, pero se utiliza para obtener directamente el valor de los campos de formulario:

<input id="municipio" />
// Con JavaScript
document.getElementById("municipio").value

// Con Prototype
$F("municipio")
<select id="municipio">
  <option>...</option>
</select>
// Con JavaScript
document.getElementById("municipio").options[document.getElementById("municipio").selectedIndex].value

// Con Prototype
$F("municipio")

Una de las funciones más espectaculares de Prototype y que no tiene equivalente en JavaScript es $$(), que permite seleccionar elementos de la página utilizando selectores de CSS.

<div id="principal">
  <p>Primer párrafo</p>
  <p>Segundo párrafo</p>
</div>
<p>Tercer párrafo</p>
var todosParrafos = $$('p');
var parrafosInteriores = $$('#principal p');

Prototype incluye una función muy útil llamada $A(), para convertir en array "cualquier cosa que se parezca a un array". Algunas funciones de JavaScript, como por ejemplo getElementsByTagName() devuelven objetos de tipo NodeList o HTMLCollection, que no son arrays, aunque pueden recorrerse como tales.

<select id="lista">
  <option value="1">Primer valor</option>
  <option value="2">Segundo valor</option>
  <option value="3">Tercer valor</option>
</select>
// 'lista_nodos' es una variable de tipo NodeList
var lista_nodos = $('lista').getElementsByTagName('option');

// 'nodos' es una variable de tipo array
var nodos = $A(lista_nodos);
// nodos = [objeto_html_opcion1, objeto_html_opcion2, objeto_html_opcion3]

Una función similar a $A() es $H(), que crea arrays asociativos (también llamados "hash") a partir del argumento que se le pasa:

var usuarios = { usuario1: "password1",
                 usuario2: "password2",
                 usuario3: "password3" };

var hash_usuarios = $H(usuarios);

var logins = hash_usuarios.keys();
// logins = ["usuario1", "usuario2", "usuario3"]

var passwords = hash_usuarios.values();
// passwords = ["password1", "password2", "password3"]

var queryString = hash_usuarios.toQueryString();
// queryString = "usuario1=password1&usuario2=password2&usuario3=password3"

var debug = hash_usuarios.inspect();
// #<Hash:{'usuario1':'password1', 'usuario2':'password2’,'usuario3':'password3'}>

Por último, Prototype incluye la función $R() para crear rangos de valores. El rango de valores se crea desde el valor del primer argumento hasta el valor del segundo argumento. El tercer argumento de la función indica si se excluye o no el último valor (por defecto, el tercer argumento vale false, que indica que sí se incluye el último valor).

var rango = $R(0, 100, false);
// rango = [0, 1, 2, 3, ..., 100]

var rango = $R(0, 100);
// rango = [0, 1, 2, 3, ..., 100]

var rango = $R(0, 100, true);
// rango = [0, 1, 2, 3, ..., 99]

var rango2 = $R(100, 0);
// rango2 = [100]

var rango = $R(0, 100);
var incluido = rango.include(4);
// incluido = true

var rango = $R(0, 100);
var incluido = rango.include(400);
// incluido = false

Los rangos que se pueden crear van mucho más allá de simples sucesiones numéricas. La "inteligencia" de la función $R() permite crear rangos tan avanzados como los siguientes:

var rango = $R('a', 'k');
// rango = ['a', 'b', 'c', ..., 'k']

var rango = $R('aa', 'ak');
// rango = ['aa', 'ab', 'ac', ..., 'ak']

var rango = $R('a_a', 'a_k');
// rango = ['a_a', 'a_b', 'a_c', ..., 'a_k']

Por último, una función muy útil que se puede utilizar con cadenas de texto, objetos y arrays de cualquier tipo es inspect(). Esta función devuelve una cadena de texto que es una representación de los contenidos del objeto. Se trata de una utilidad imprescindible cuando se están depurando las aplicaciones, ya que permite visualizar el contenido de variables complejas.

10.1.2. Funciones para cadenas de texto

El framework Prototype extiende las cadenas de texto de JavaScript añadiéndoles una serie de funciones que pueden resultar muy útiles:

stripTags(): Elimina todas las etiquetas HTML y XML de la cadena de texto

stripScripts(): Elimina todos los bloques de tipo <script></script> de la cadena de texto

escapeHTML(): transforma todos los caracteres problemáticos en HTML a su respectiva entidad HTML (< se transforma en &lt;, & se transforma en &amp;, etc.)

var cadena = "<p>Prueba de texto & caracteres HTML</p>".escapeHTML();
// cadena = "&lt;p&gt;Prueba de texto &amp; caracteres HTML&lt;/p&gt;"

unescapeHTML(): función inversa de escapeHTML()

var cadena = "<p>Prueba de texto & caracteres HTML</p>".unescapeHTML();
// cadena = "Prueba de texto & caracteres HTML"

var cadena = "<p>&ntilde; &aacute; &iquest; &amp;</p>".unescapeHTML();
// cadena = "ñ á ¿ &"

extractScripts(): devuelve un array con todos los bloques <script></script> de la cadena de texto

evalScripts(): ejecuta cada uno de los bloques <script></script> de la cadena de texto

toQueryParams(): convierte una cadena de texto de tipo query string en un array asociativo hash) de pares parámetro/valor

var cadena = "parametro1=valor1&parametro2=valor2&parametro3=valor3";

var parametros = cadena.toQueryParams();
// $H(parametros).inspect() = #<Hash:{'parametro1':'valor1', 'parametro2':'valor2',' parametro3':'valor3'}>

toArray(): convierte la cadena de texto en un array que contiene sus letras

camelize(): convierte una cadena de texto separada por guiones en una cadena con notación de tipo CamelCase

var cadena = "el-nombre-de-la-variable".camelize();
// cadena = "elNombreDeLaVariable"

underscore(): función inversa de camelize(), ya que convierte una cadena de texto escrita con notación CamelCase en una cadena de texto con las palabras separadas por guiones bajos

var cadena = "elNombreDeLaVariable".underscore();
// cadena = "el_nombre_de_la_variable"

dasherize(): modifica los guiones bajos (_) de una cadena de texto por guiones medios (-)

var cadena = "el_nombre_de_la_variable".dasherize();
// cadena = "el-nombre-de-la-variable"

Combinando camelize(), underscore() y dasherize(), se puede obtener el nombre DOM de cada propiedad CSS y viceversa:

var cadena = 'borderTopStyle'.underscore().dasherize();
// cadena = 'border-top-style'

var cadena = 'border-top-style'.camelize();
// cadena = 'borderTopStyle'

10.1.3. Funciones para elementos

Prototype define funciones muy útiles para manipular los elementos incluidos en las páginas HTML. Cualquier elemento obtenido mediante la función $() puede hacer uso de las siguientes funciones:

Element.visible(): devuelve true/false si el elemento es visible/oculto (devuelve true para los campos tipo hidden)

Element.show() y Element.hide(): muestra y oculta el elemento indicado

Element.toggle(): si el elemento es visible, lo oculta. Si es elemento está oculto, lo muestra

Element.scrollTo(): baja o sube el scroll de la página hasta la posición del elemento indicado

Element.getStyle() y Element.setStyle(): obtiene/establece el valor del estilo CSS del elemento (el estilo completo, no la propiedad className)

Element.classNames(), Element.hasClassName(), Element.addClassName(), Element.removeClassName(): obtiene los class del elemento, devuelve true/false si incluye un determinado class, añade un class al elemento y elimina el class al elemento respectivamente

Todas las funciones anteriores se pueden invocar de dos formas diferentes:

// Las dos instrucciones son equivalentes
Element.toggle('principal');
$('principal').toggle()

10.1.4. Funciones para formularios

Prototype incluye muchas utilidades relacionadas con los formularios y sus elementos. A continuación se muestran las más útiles para los campos de un formulario:

Field.clear(): borra el valor de cada campo que se le pasa (admite uno o más parámetros)

Field.present(): devuelve true si los campos que se le indican han sido rellenados por parte del usuario, es decir, si contienen valores no vacíos (admite uno o más parámetros)

Field.focus(): establece el foco del formulario en el campo que se le indica

Field.select(): selecciona el valor del campo (solo para los campos en los que se pueda seleccionar su texto)

Field.activate(): combina en una única función los métodos focus() y select()

A las funciones anteriores se les debe pasar como parámetro una cadena de texto con el identificador del elemento o el propio elemento (obtenido por ejemplo con $()). Las funciones mostradas se pueden invocar de tres formas diferentes:

// Las 3 instrucciones son equivalentes
Form.Element.focus('id_elemento');
Field.focus('id_elemento')
$('id_elemento').focus()

Además de las funciones específicas para los campos de los formularios, Prototype también define utilidades para los propios formularios completos. Todas las funciones requieren un solo parámetro: el identificador o el objeto del formulario.

Form.serialize(): devuelve una cadena de texto de tipo "query string" con el valor de todos los campos del formulario ("campo1=valor1&campo2=valor2&campo3=valor3")

Form.findFirstElement(): devuelve el primer campo activo del formulario

Form.getElements(): devuelve un array con todos los campos del formulario (incluyendo los elementos ocultos)

Form.getInputs(): devuelve un array con todos los elementos de tipo <input> del formulario. Admite otros dos parámetros para filtrar los resultados. El segundo parámetro indica el tipo de <input> que se quiere obtener y el tercer parámetro indica el nombre del elemento <input>.

Form.disable(): deshabilita todo el formulario deshabilitando todos sus campos

Form.enable(): habilita el formulario completo habilitando todos sus campos

Form.focusFirstElement(): pone el foco del formulario en el primer campo que sea visible y esté habilitado

Form.reset(): resetea el formulario completo, ya que es equivalente al método reset() de JavaScript

10.1.5. Funciones para arrays

Las utilidades añadidas a los arrays de JavaScript es otro de los puntos fuertes de Prototype:

clear(): vacía de contenidos el array y lo devuelve

compact(): devuelve el array sin elementos null o undefined

first(): devuelve el primer elemento del array

flatten(): convierte cualquier array que se le pase en un array unidimensional. Se realiza un proceso recursivo que va "aplanando" el array:

var array_original = ["1", "2", 3,
                       ["a", "b", "c",
                         ["A", "B", "C"]
                       ]
                     ];

var array_plano = array_original.flatten();
// array_plano = ["1", "2", 3, "a", "b", "c", "A", "B", "C"]

indexOf(value): devuelve el valor de la posición del elemento en el array o -1 si no lo encuentra

var array = ["1", "2", 3, ["a", "b", "c", ["A", "B", "C"] ] ];
array.indexOf(3);   // 2
array.indexOf("C"); // -1

last(): devuelve el último elemento del array

reverse(): devuelve el array original en sentido inverso:

var array = ["1", "2", 3, ["a", "b", "c", ["A", "B", "C"] ] ];
array.reverse();
// array = [["a", "b", "c", ["A", "B", "C"]], 3, "2", "1"]

shift(): devuelve el primer elemento del array y lo extrae del array (el array se modifica y su longitud disminuye en 1 elemento)

without(): devuelve el array del que se han eliminado todos los elementos que coinciden con los argumentos que se pasan a la función. Permite filtrar los contenidos de un array

var array = [12, 15, 16, 3, 40].without(16, 12)
// array = [15, 3, 40]

10.1.6. Funciones para objetos enumerables

Algunos tipos de objetos en JavaScript se comportan como colecciones de valores, también llamadas "enumeraciones" de valores. Prototype define varias utilidades para este tipo de objetos a través de Enumerable, que es uno de los pilares básicos del framework y una de las formas más sencillas de mejorar la productividad cuando se desarrollan aplicaciones JavaScript.

Algunos de los objetos obtenidos mediante las funciones de Prototype, ya incorporan todos los métodos de Enumerable. Sin embargo, si se quieren añadir estos métodos a un objeto propio, se pueden utilizar las utilidades de Prototype para crear objetos y extenderlos:

var miObjeto = Class.create();
Object.extend(miObjeto.prototype, Enumerable);

Gracias a Enumerable, se pueden recorrer los arrays de forma mucho más eficiente:

// Array original
var vocales = ["a", "e", "i", "o", "u"];

// Recorrer el array con JavaScript
for(var i=0; i<vocales.length; i++) {
  alert("Vocal " + vocales[i] + " está en la posición " + i);
}

// Recorrer el array con Prototype:
vocales.each(function(elemento, indice) {
  alert("Vocal " + elemento + " está en la posición " + indice);
});

El método select(), que es un alias del método findAll(), permite filtrar los contenidos de un array:

var numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
resultado = numeros.findAll(function(elemento) { return elemento > 5; });
// resultado = [6, 7, 8, 9, 10]

Otro método útil es pluck(), que permite obtener el valor de una misma propiedad para todos los elementos de la colección:

var numLetras = ['hola', 'mundo', 'que', 'bien', 'funciona', 'Prototype'].pluck('length');
// numLetras = [4, 5, 3, 4, 8, 9]

Enumerable incluye decenas de utilidades y métodos, algunos tan curiosos como partition() que permite dividir una colección en dos grupos: el de los elementos de tipo true y el de los elementos de tipo false (valores como null, undefined, etc.)

var valores = ['nombreElemento', 12, null, 2, true, , false].partition();
// valores = [['nombreElemento', 12, 2, true], [null, undefined, false]]

El método partition() permite asignar una función propia para decidir si un elemento se considera true o false. En el siguiente ejemplo, se divide un array con letras en dos grupos, el de las vocales y el de las consonantes:

var letras = $R('a', 'k').partition(function(n) {
  return ['a', 'e', 'i', 'o', 'u'].include(n);
})
// letras = [['a', 'e', 'i'], ['b', 'c', 'd', 'f', 'g', 'h', 'j', 'k']]

El método invoke() permite ejecutar una función para todos los elementos de la colección:

var palabras = ['hola', 'mundo', 'con', 'Prototype'].invoke('toUpperCase');
// palabras = ['HOLA', 'MUNDO', 'CON', 'PROTOTYPE']

La documentación de Enumerable incluye la definición y ejemplos de muchos otros métodos útiles como inGroupsOf() (agrupa elementos en subconjuntos del mismo tamaño), sortBy() (permite definir la ordenación de los elementos mediante una función propia), zip() (asocia uno a uno los elementos de dos colecciones), collect() (permite transformar los elementos de la colección con una función propia), etc.

10.1.7. Otras funciones útiles

Try.these(): permite probar varias funciones de forma consecutiva hasta que una de ellas funcione. Es muy útil para las aplicaciones que deben funcionar correctamente en varios navegadores diferentes.

El propio código fuente de Prototype utiliza Try.these() para obtener el objeto encargado de realizar las peticiones AJAX:

var Ajax = {
  getTransport: function() {
    return Try.these(
      function() {return new XMLHttpRequest()},
      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
    ) || false;
  },

  activeRequestCount: 0
}

Class.create(): permite crear clases de una forma elegante y sencilla:

MiClase = Class.create();
MiClase.prototype = {
  initialize: function(a, b) {
    this.a = a;
    this.b = b;
  }
}

var miClase = new MiClase("primer_valor", "segundo_valor");

Object.extend(): se emplea para añadir o sobrescribir las propiedades de un objeto en otro objeto. Se puede considerar como una forma primitiva y muy básica de herencia entre clases. En la llamada a la función, el primer objeto es el destino en el que se copian las propiedades del segundo objeto pasado como parámetro:

Object.extend(objetoDestino, objetoOrigen);

Esta función es muy útil para que las aplicaciones definan una serie de opciones por defecto y puedan tener en cuenta las opciones establecidas por cada usuario:

// El array "opciones" guarda las opciones por defecto de la aplicación
var opciones = {campo: "usuario", orden: "ASC"};

// El usuario establece sus propias opciones
var opciones_usuario = {orden: "DESC", tipoBusqueda: "libre"};

// Se mezclan los dos arrays de opciones, dando prioridad
// a las opciones establecidas por los usuarios
Object.extend(opciones, opciones_usuario);

// Ahora, opciones.orden = "DESC"

El código fuente de Prototype utiliza Object.extend() continuamente para añadir propiedades y métodos útiles a los objetos de JavaScript. El código que se muestra a continuación añade cinco métodos al objeto Number original de JavaScript:

Object.extend(Number.prototype, {
  toColorPart: function() {
    return this.toPaddedString(2, 16);
  },

  succ: function() {
    return this + 1;
  },

  times: function(iterator) {
    $R(0, this, true).each(iterator);
    return this;
  },

  toPaddedString: function(length, radix) {
    var string = this.toString(radix || 10);
    return '0'.times(length - string.length) + string;
  },

  toJSON: function() {
    return isFinite(this) ? this.toString() : 'null';
  }
});

10.1.8. Funciones para AJAX

Además de todas las funciones y utilidades para la programación tradicional de JavaScript, Prototype incluye numerosas funciones relacionadas con el desarrollo de aplicaciones AJAX. Los métodos que componen este módulo son Ajax.Request(), Ajax.Updater(), Ajax.PeriodicalUpdater() y Ajax.Responders().

Ajax.Request() es el método principal de este módulo. Se utiliza para realizar peticiones AJAX y procesar sus resultados. Su sintaxis es:

new Ajax.Request(url, opciones);

El primer parámetro (url) es la URL que solicita la petición AJAX y el segundo parámetro (opciones) es opcional y se emplea para especificar valores diferentes a las opciones por defecto. Las opciones se indican en forma de array asociativo:

new Ajax.Request('/ruta/hasta/pagina.php', {
  method: 'post',
  asynchronous: true,
  postBody: 'parametro1=valor1&parametro2=valor2',
  onSuccess: procesaRespuesta,
  onFailure: muestraError
});

Como es habitual, para establecer la función que procesa la respuesta del servidor, se indica el nombre de la función sin paréntesis. Las funciones externas asignadas para procesar la respuesta, reciben como primer parámetro el objeto que representa la respuesta del servidor. Haciendo uso de este objeto, las funciones pueden acceder a todas las propiedades habituales:

function procesaRespuesta(respuesta) {
  alert(respuesta.responseText);
}

A continuación se incluye una tabla con todas las opciones que se pueden definir para el método Ajax.Request():

Opción Descripción
method El método de la petición HTTP. Por defecto es POST
parameters Lista de valores que se envían junto con la petición. Deben estar formateados como una query string: parametro1=valor1&parametro2=valor2
encoding Indica la codificación de los datos enviados en la petición. Su valor por defecto es UTF-8
asynchronous Controla el tipo de petición que se realiza. Por defecto es true, lo que indica que la petición realizada al servidor es asíncrona, el tipo de petición habitual en las aplicaciones AJAX
postBody Contenido que se envía en el cuerpo de la petición de tipo POST
contentType Indica el valor de la cabecera Content-Type utilizada para realizar la petición. Su valor por defecto es application/x-www-form-urlencoded
requestHeaders Array con todas las cabeceras propias que se quieren enviar junto con la petición
onComplete onLoaded on404 on500 Permiten asignar funciones para el manejo de las distintas fases de la petición. Se pueden indicar funciones para todos los códigos de estado válidos de HTTP
onSuccess Permite indicar la función que se encarga de procesar las respuestas correctas de servidor
onFailure Se emplea para indicar la función que se ejecuta cuando la respuesta ha sido incorrecta
onException Permite indicar la función encargada de manejar las peticiones erróneas en las que la respuesta del servidor no es válida, los argumentos que se incluyen en la petición no son válidos, etc.

La función Ajax.Updater() es una versión especial de Ajax.Request() que se emplea para actualizar el contenido HTML de un elemento de la página con la respuesta del servidor.

<div id="info"></div>
new Ajax.Updater('info', '/ruta/hasta/pagina.php');

Si la respuesta del servidor es

<ul>
  <li>Lorem ipsum dolor sit amet</li>
  <li>Consectetuer adipiscing elit</li>
  <li>Curabitur risus magna, lobortis</li>
</ul>

Después de realizar la petición de tipo Ajax.Updater(), el contenido HTML de la respuesta del servidor se muestra dentro del <div>:

<div id="info">
  <ul>
    <li>Lorem ipsum dolor sit amet</li>
    <li>Consectetuer adipiscing elit</li>
    <li>Curabitur risus magna, lobortis</li>
  </ul>
</div>

La sintaxis de Ajax.Updater() se muestra a continuación:

new Ajax.Updater(elemento, url, opciones);

Además de todas las opciones de Ajax.Request(), la función Ajax.Updater() permite establecer las siguientes opciones:

Opción Descripción
insertion Indica cómo se inserta el contenido HTML en el elemento indicado. Puede ser Insertion.Before, Insertion.Top, Insertion.Bottom o Insertion.After
evalScripts Si la respuesta del servidor incluye scripts en su contenido, esta opción permite indicar si se ejecutan o no. Su valor por defecto es false, por lo que no se ejecuta ningún script

La función Ajax.PeriodicalUpdater() es una versión especializada de Ajax.Updater(), que se emplea cuando se quiere ejecutar de forma repetitiva una llamada a Ajax.Updater(). Esta función puede ser útil para ofercer información en tiempo real como noticias:

<div id="titulares"></div>
new Ajax.PeriodicalUpdater('titulares', '/ruta/hasta/pagina.php', { frequency:30 });

El código anterior actualiza, cada 30 segundos, el contenido del <div> con la respuesta recibida desde el servidor.

Además de todas las opciones anteriores, Ajax.PeriodicalUpdater() dispone de las siguientes opciones propias:

Opción Descripción
frequency Número de segundos que se espera entre las peticiones. El valor por defecto es de 2 segundos
decay Indica el factor que se aplica a la frecuencia de actualización cuando la última respuesta del servidor es igual que la anterior. Ejemplo: si la frecuencia es 10 segundos y el decay vale 3, cuando una respuesta del servidor sea igual a la anterior, la siguiente petición se hará 3 * 10 = 30 segundos después de la última petición

Por último, Ajax.Responders permite asignar de forma global las funciones que se encargan de responder a los eventos AJAX. Una de las principales utilidades de Ajax.Responders es la de indicar al usuario en todo momento si se está realizando alguna petición AJAX.

Los dos métodos principales de Ajax.Responders son register() y unregister() a los que se pasa como argumento un objeto de tipo array asociativo que incluye las funciones que responden a cada evento:

Ajax.Responders.register({
  onCreate: function() {
    if($('info') && Ajax.activeRequestCount> 0) {
      $('info').innerHTML = Ajax.activeRequestCount + "peticiones pendientes";
    }
  },
  onComplete: function() {
    if($('info') && Ajax.activeRequestCount> 0) {
      $('info').innerHTML = Ajax.activeRequestCount + "peticiones pendientes";
    }
  }
});

10.1.9. Funciones para eventos

El módulo de eventos de Prototype es uno de los menos desarrollados, por lo que va a ser completamente rediseñado en las próximas versiones del framework. Aún así, Prototype ofrece una solución sencilla y compatible con todos los navegadores para manejar los eventos de la aplicación. Event.observe() registra los eventos, Event almacena el objeto con la información del evento producido y Event.stopObserving() permite eliminar los eventos registrados.

<div id="pinchable">Si se pulsa en este DIV, se muestra un mensaje</div>
// Registrar el evento
Event.observe('pinchable', 'click', procesaEvento, false);

// Eliminar el evento registrado
// Event.stopObserving('pinchable', 'click', procesaEvento, false);

function procesaEvento(e) {
  // Obtener el elemento que ha originado el evento (el DIV)
  var elemento = Event.element(e);

  // Determinar la posicion del puntero del ratón
  var coordenadas = [Event.pointerX(e), Event.pointerY(e)];

  // Mostrar mensaje con los datos obtenidos
  alert("Has pinchado el DIV '"+elemento.id+"' con el raton en la posicion ("+coordenadas[0]+","+coordenadas[1]+")");

  // Evitar la propagacion del evento
  Event.stop(e);
}

La sintaxis completa del método Event.observe() se muestra a continuación:

Event.observe(elemento, nombreEvento, funcionManejadora, [usarCapture]);

El primer argumento (elemento) indica el identificador del elemento o el propio elemento que puede originar el evento. El segundo argumento (nombreEvento) indica el nombre del evento que se quiere manejar, sin incluir el prefijo on (load, click, mouseover, etc.). El tercer argumento (funcionManejadora) es el nombre de la función que procesa el evento cuando se produce. El último parámetro (usarCapture) no se suele emplear, pero indica si se debe utilizar la fase de capture o la fase de bubbling.

El objeto Event incluye la información disponible sobre el evento producido. A continuación se muestra una tabla con sus métodos y propiedades principales:

Método/Propiedad Descripción
element() Devuelve el elemento que ha originado el evento (un div, un botón, etc.)
isLeftClick() Indica si se ha pulsado el botón izquierdo del ratón
pointerX() pointerY() Posición x e y del puntero del ratón
stop() Detiene la propagación del evento
observers() Devuelve un array con todos los eventos registrados en la página

Además, Event define una serie de constantes para referirse a las teclas más habituales que se manejan en las aplicaciones (tabulador, ENTER, flechas de dirección, etc.) Las constantes definidas son KEY_BACKSPACE, KEY_TAB, KEY_RETURN, KEY_ESC, KEY_LEFT, KEY_UP, KEY_RIGHT, KEY_DOWN, KEY_DELETE.

Prototype también incluye otros métodos útiles para la gestión de eventos con formularios:

Form.Observer(formulario, frecuencia, funcionManejadora);

Form.Observer() permite monitorizar el formulario indicado cada cierto tiempo (el tiempo se indica en segundos mediante el parámetro frecuencia). Si se produce cualquier cambio en el formulario, se ejecuta la función cuyo nombre se indica en el parámetro funcionManejadora. Form.Observer() se emplea para los formularios que contienen elementos sin eventos registrados que procesen sus cambios.

Otra función similar es Form.EventObserver() cuya definición formal es:

Form.EventObserver(formulario, funcionManejadora);

La principal diferencia de Form.EventObserver() respecto a Form.Observer() es que, en este caso, se utilizan los eventos registrados por los elementos del formulario para detectar si se ha producido algún cambio en el formulario.

10.1.10. Métodos para funciones

Cuando se pasan referencias a funciones en el código de JavaScript, es posible que se pierda el contexto original de la función. El contexto de las funciones es fundamental para el correcto funcionamiento de la palabra reservada this.

Prototype incluye la posibilidad de asegurar que una función se va a ejecutar en un determinado contexto. Para ello, extiende la clase Function() para añadir el método bind(). Considerando el siguiente ejemplo:

nombre = 'Estoy fuera';
var objeto = {
  nombre: 'Estoy dentro',
  funcion: function() {
    alert(this.nombre);
  }
};

function ejecutaFuncion(f) {
  f();
}

var funcion2 = objeto.funcion.bind(objeto);

ejecutaFuncion(objeto.funcion);
ejecutaFuncion(funcion2);

El código anterior define en primer lugar la variable global nombre y le asigna el valor Estoy fuera. A continuación, se define un objeto con un atributo llamado también nombre y con un método sencillo que muestra el valor del atributo utilizando la palabra reservada this.

Si se ejecuta la función del objeto a través de una referencia suya (mediante la función ejecutaFuncion()), la palabra reservada this se resuelve en el objeto window y por tanto el mensaje que se muestra es Estoy fuera. Sin embargo, si se utiliza el método bind(objeto) sobre la función, siempre se ejecuta considerando su contexto igual al objeto que se pasa como parámetro al método bind().

Prototype incluye además el método bindAsEventListener() que es equivalente a bind() pero que se puede emplear para evitar algunos de los problemas comunes de los eventos que se producen en algunos navegadores como Internet Explorer.

10.1.11. Rehaciendo ejemplos con Prototype

Las aplicaciones realizadas con el framework Prototype suelen ser muy concisas en comparación con las aplicaciones JavaScript tradicionales, pero siguen manteniendo una gran facilidad para leer su código y entenderlo.

Por ejemplo, el ejercicio que mostraba y ocultaba diferentes secciones de contenidos se realizó de la siguiente manera:

function muestraOculta() {
  // Obtener el ID del elemento
  var id = this.id;
  id = id.split('_');
  id = id[1];

  var elemento = document.getElementById('contenidos_'+id);
  var enlace = document.getElementById('enlace_'+id);

  if(elemento.style.display == "" || elemento.style.display == "block") {
    elemento.style.display = "none";
    enlace.innerHTML = 'Mostrar contenidos';
  }
  else {
    elemento.style.display = "block";
    enlace.innerHTML = 'Ocultar contenidos';
  }
}

window.onload = function() {
  document.getElementById('enlace_1').onclick = muestraOculta;
  document.getElementById('enlace_2').onclick = muestraOculta;
  document.getElementById('enlace_3').onclick = muestraOculta;
}

Con Prototype, su código se puede reducir a las siguientes instrucciones:

function muestraOculta() {
  var id = (this.id).split('_')[1];

  $('contenidos_'+id).toggle();
  $('enlace_'+id).innerHTML = (!$('contenidos_'+id).visible()) ? 'Ocultar contenidos' : 'Mostrar contenidos';
}

window.onload = function() {
  $R(1, 3).each(function(n) {
    Event.observe('enlace_'+n, 'click', muestraOculta);
  });
}

Los métodos $R(), toggle() y visible() permiten simplificar el código original a una mínima parte de su longitud, pero conservando el mismo funcionamiento, además de ser un código sencillo de entender.

Otro de los ejercicios anteriores realizaba peticiones AJAX al servidor para comprobar si un determinado nombre de usuario estaba libre. El código original de JavaScript era:

var READY_STATE_COMPLETE = 4;
var peticion_http = null;

function inicializa_xhr() {
  if(window.XMLHttpRequest) {
    return new XMLHttpRequest();
  }
  else if(window.ActiveXObject) {
    return new ActiveXObject("Microsoft.XMLHTTP");
  }
}

function comprobar() {
  var login = document.getElementById("login").value;
  peticion_http = inicializa_xhr();
  if(peticion_http) {
    peticion_http.onreadystatechange = procesaRespuesta;
    peticion_http.open("POST", "http://localhost/compruebaDisponibilidad.php", true);

    peticion_http.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    peticion_http.send("login="+login+"&nocache="+Math.random());
  }
}

function procesaRespuesta() {
  if(peticion_http.readyState == READY_STATE_COMPLETE) {
    if(peticion_http.status == 200) {
      var login = document.getElementById("login").value;
      if(peticion_http.responseText == "si") {
        document.getElementById("disponibilidad").innerHTML = "El nombre elegido ["+login+"] está disponible";
      }
      else {
        document.getElementById("disponibilidad").innerHTML = "NO está disponible el nombre elegido ["+login+"]";
      }
    }
  }
}

window.onload = function() {
  document.getElementById("comprobar").onclick = comprobar;
}

Con Prototype se puede conseguir el mismo comportamiento con tres veces menos de líneas de código:

function comprobar() {
  var login = $F('login');
  var url = 'http://localhost/compruebaDisponibilidad.php?nocache=' + Math.random();
  var peticion = new Ajax.Request(url, {
    method: 'post',
    postBody: 'login='+login,
    onSuccess: function(respuesta) {
      $('disponibilidad').innerHTML = (respuesta.responseText == 'si') ?
        'El nombre elegido ['+login+'] está disponible' : 'NO está disponible el nombre elegido ['+login+']';
    },
    onFailure: function() { alert('Se ha producido un error'); }
  });
}

window.onload = function() {
  Event.observe('comprobar', 'click', comprobar);
}