7. jQuery Avanzado

7.1. Efectos

jQuery facilita el uso de animaciones y efectos consiguiendo grandes resultados con apenas un par de líneas, como por ejemplo:

  • Mostrar y ocultar elementos

  • Efectos de aparecer y desvanecer elementos

  • Mover elementos a través de la pantalla

Podemos consultar todos los efectos disponibles en http://api.jquery.com/category/effects/

7.1.1. Mostrar y ocultar

Tanto mostrar como ocultar elementos son acciones comunes y sencillas que podemos realizar de manera inmediata o durante un período de tiempo.

Método Propósito

show()

Muestra cada elemento del conjunto de resultados, si estaban ocultos

show(velocidad[,callback])

Muestra todos los elementos del conjunto del resultados mediante una animación, y opcionalmente lanza un callback tras completar la animación

hide()

Oculta cada elemento del conjunto de resultados, si estaban visibles

hide(velocidad[,callback])

Oculta todos los elementos del conjunto del resultados mediante una animación, y opcionalmente lanza un callback tras completar la animación

toggle()

Cambia la visualización (visible u oculto, de manera contraria a su estado) para cada elemento del conjunto de resultados

toggle(switch)

Cambia la visualización para cada elemento del conjunto de resultados dependiendo del switch (verdadero muestra todos los elementos, falso para ocultarlos)

toggle(velocidad[,callback])

Cambia la visualización de todos los elementos del conjunto del resultados mediante una animación, y opcionalmente lanza un callback tras completar la animación

En aquellos métodos que aceptan una velocidad como parámetro, la podemos indicar de manera númerica, especificando la cantidad de milisegundos de duración del efecto, o mediante una cadena con los posibles valores a slow (lento - 600ms), normal (400ms) o fast (rápido - 200ms).

Por ejemplo, supongamos que tenemos un cuadrado con varios botones para cambiar su visualización:

<!DOCTYPE html>
<html lang="es">
<head>
	<title>Mostrar y Ocultar</title>
	<meta charset="UTF-8">
	<style type="text/css">
	div#laCapa {
		width: 250px; height: 180px; margin: 10px; padding: 20px;
		background: blue; border: 2px solid black; cursor: pointer;
	}
	p, span {
		font-size: 16pt;
	}
	button {
		margin: 5px;
	}
	</style>
</head>
<body>
	<p>Mostrando y Ocultando un elemento</p>
	<div id="laCapa"></div>
	<button id="mostrar">Mostrar</button>
	<button id="ocultar">Ocultar</button>
	<button id="cambiar">Cambiar (Toggle)</button>
</body>
</html>

Y el código para cambiar el estado de visualización con una pequeña animación:

$(function() {
	$("#mostrar").click(function() {
		$("#laCapa").show("normal");
	});
	$("#ocultar").click(function() {
		$("#laCapa").hide(2000);	// ms
	});
	$("#cambiar").click(function() {
		$("#laCapa").toggle("slow");
	});
});

7.1.2. Aparecer y desvanecer

Los efectos más comunes se basan en la aparición progresiva y desvanecimiento del contenido de manera completa o hasta una determinada opacidad. jQuery ofrece un conjunto de métodos para estos efectos:

Método Propósito

fadeIn(velocidad[,callback])

El contenido aparece a la velocidad indicada para cada elemento del conjunto de resultados, y opcionalmente lanza un callback tras completar la animación

fadeOut(velocidad[,callback])

El contenido se desvanece a la velocidad indicada para cada elemento del conjunto de resultados, y opcionalmente lanza un callback tras completar la animación

fadeTo(velocidad,opacidad[,callback])

El contenido cambia a la opacidad y velocidad indicadas para cada elemento del conjunto de resultados, y opcionalmente lanza un callback tras completar la animación

Ya hemos visto que si queremos que se ejecute una función cuando termine la ejecución, la pasaremos como callback:

$(this).fadeOut(1000, function() {
	console.log("He acabado");
});

Por ejemplo, para simular un efecto toggle:

$(this).fadeOut(500, function() {
	$(this).delay(2000).fadeIn(500);
});

Si hubiésemos puesto la función fuera del parámetro del fadeOut, se hubiese ejecutado justamente después de iniciarse la animación, y no al finalizar la misma.

$(this).fadeOut(500).css("margin","50 px");

De manera similar al ejemplo anterior, vamos a dibujar un cuadrado con 4 botones para mostrar estos efectos:

<!DOCTYPE html>
<html lang="es">
<head>
	<title>Aparecer y Desvanecer</title>
	<meta charset="UTF-8">
	<style type="text/css">
		div#laCapa {
			width: 250px; height: 180px; margin: 10px; padding: 20px;
			background: blue; border: 2px solid black; cursor: pointer;
		}
		p, span {
			font-size: 16pt;
		}
		button {
			margin: 5px;
		}
	</style>
</head>
<body>
	<p>Aparecer y Desvanecer un elemento</p>
	<div id="laCapa"></div>
	<button id="aparecer">Aparecer</button>
	<button id="desvanecer">Desvanecer</button>
	<button id="fade03">Opacidad hasta .3</button>
	<button id="fade10">Opacidad hasta 1.0</button>
</body>
</html>

Y el código para cambiar el estado de visualización con una pequeña animación:

$(function() {
	$("#aparecer").click(function() {
		$("#laCapa").fadeIn(300);
	});
	$("#desvanecer").click(function() {
		$("#laCapa").fadeOut("normal");
	});
	$("#fade03").click(function() {
		$("#laCapa").fadeTo("slow", 0.3);
	});
	$("#fade10").click(function() {
		$("#laCapa").fadeTo("slow", 1.0);
	});
});

7.1.3. Enrollar y desenrollar

jQuery ofrece un conjunto de métodos para enrollar y desenrolar los elementos a modo de persiana:

Método Propósito

slideDown(velocidad[,callback])

El contenido se desenrolla a la velocidad indicada modificando la altura de cada elemento del conjunto de resultados , y opcionalmente lanza un callback tras completar la animación

slideUp(velocidad[,callback])

El contenido se enrolla a la velocidad indicada modificando la altura de cada elemento del conjunto de resultados , y opcionalmente lanza un callback tras completar la animación

slideToggle(velocidad[,callback])

Cambia la visualización del contenido enrollando o desenrollando el contenido a la velocidad indicada modificando la altura de cada elemento del conjunto de resultados, y opcionalmente lanza un callback tras completar la animación

De manera similar al ejemplo anterior, vamos a dibujar un cuadrado con 3 botones para mostrar estos efectos:

Ejemplo Enrollar y Desenrollar - http://jsbin.com/hunoq/1/edit?html,css,js,output
<!DOCTYPE html>
<html lang="es">
<head>
	<title>Enrollar y Desenrollar</title>
	<meta charset="UTF-8">
	<style type="text/css">
		div#laCapa {
			width: 250px; height: 180px; margin: 10px; padding: 20px;
			background: blue; border: 2px solid black; cursor: pointer;
		}
		p, span {
			font-size: 16pt;
		}
		button {
			margin: 5px;
		}
	</style>
</head>
<body>
	<p>Enrollando y Desenrollando un elemento</p>
	<div id="laCapa"></div>
	<button id="enrollar">Enrollar</button>
	<button id="desenrollar">Desenrollar</button>
	<button id="cambiar">Cambiar (Toggle)</button>
</body>
</html>

Y el código para enrollar y desenrollar con una pequeña animación:

$(function() {
	$("#enrollar").click(function() {
		$("#laCapa").slideUp("normal");
	});
	$("#desenrollar").click(function() {
		$("#laCapa").slideDown(2000);
	});
	$("#cambiar").click(function() {
		$("#laCapa").slideToggle("slow");
	});
});

7.1.4. Creando animaciones

Para crear animaciones personalizadas sobre las propiedades de los elementos llamaremos a la función animate(), y para detenerlas a stop().

Método Propósito

animate(parámetros, duración, easing, callback)

Crea una animación personalizada donde parámetros indica un objeto CSS con las propiedades a animar, con una duración y easing (linear o swing) determinados y lanza un callback tras completar la animación

animate(parametros, opciones)

Crea una animación personalizada donde parametros indica las propiedades a animar y las opciones de las animación (complete, step, queue)

stop()

Detiene todas las animaciones en marcha para todos los elementos

De manera similar al ejemplo anterior, vamos a dibujar un cuadrado con 4 botones para cambiar el tamaño de un elemento, el tamaño del texto, mover el elemento y hacerlo todo a la vez:

<!DOCTYPE html>
<html lang="es">
<head>
	<title>Animaciones</title>
	<meta charset="UTF-8">
	<style type="text/css">
		div#laCapa {
			position: relative; width: 250px; height: 180px; margin: 10px; padding: 20px;
			background: blue; border: 2px solid black; cursor: pointer;
		}
		p, span {
			font-size: 16pt;
		}
		button {
			margin: 5px;
		}
	</style>
</head>
<body>
	<p>Animaciones</p>
	<div id="laCapa">Anímame un poco!</div>
	<button id="derecha">Crecer a la derecha</button>
	<button id="texto">Texto grande</button>
	<button id="mover">Mover la capa</button>
	<button id="todo">Todo</button>
</body>
</html>

Y el código para cambiar animar cada uno de los botones:

$(function() {
	$("#derecha").click(function() {
	  $("#laCapa").animate({ width: "500px" }, 1000);
	});
	$("#texto").click(function() {
	  $("#laCapa").animate({ fontSize: "24pt" }, 1000);
	});
	$("#mover").click(function() {
	  $("#laCapa").animate({ left: "500" }, 1000, "swing");
	});
	$("#todo").click(function() {
		$("#laCapa").animate({ width: "500px", fontSize: "24pt", left: "500" }, 1000, "swing");
	});
});

7.1.5. Ejemplo carrusel

Para poner estos efectos en practica y por comparar funcionalidad realizada en JavaScript respecto a la misma con jQuery, vamos a realizar un carrusel de imagenes de manera similar a la realizada en sesiones anteriores.

Para ello, vamos a definir una capa para cada una de las imágenes

<!DOCTYPE html>
<html lang="es">
<head>
	<title>Carrusel jQuery</title>
	<meta charset="UTF-8">
	<style type="text/css">
		#carrusel {
			height:400px; width:400px;
		}
		#carrusel div {
			position:absolute; z-index: 0;
		}
		#carrusel div.anterior {
			z-index: 1;
		}
		#carrusel div.actual {
			z-index: 2;
		}
	</style>
</head>
<body>
<h1>Carrusel jQuery</h1>
<div id="carrusel">
	<div class="actual"><img src="imagenes/hierba.jpg" width="400" height="400" class="galeria" /></div>
	<div><img src="imagenes/hoja.jpg"  width="400" height="400" class="galeria" /></div>
	<div><img src="imagenes/primavera.jpg" width="400" height="400" class="galeria" /></div>
	<div><img src="imagenes/agua.jpg" width="400" height="400" class="galeria" /></div>
</div>
</body>
</html>

A continuación, añadimos el código jQuery para que cada 2 segundos cambie de imagen, teniendo en cuenta que al llegar a la última, vuelva a mostrar la primera:

$(function () {
	setInterval("carruselImagenes()", 2000);
});

function carruselImagenes() {
	var fotoActual = $('#carrusel div.actual');
	var fotoSig = fotoActual.next();
	if (fotoSig.length == 0) {
		fotoSig = $('#carrusel div:first');	(1)
	}
	fotoActual.removeClass('actual').addClass('anterior');	(2)
	fotoSig.css({ opacity: 0.0 }).addClass('actual')	(3)
		.animate({ opacity: 1.0 }, 1000, (4)
			function () { fotoActual.removeClass('anterior'); });	(5)
}
1 Si la siguiente foto es la última, seleccionamos la primera foto
2 Cambiamos la clase CSS de la foto actual para que se posicione detrás y se oculte
3 La siguiente foto la hacemos transparente, la marcamos como actual
4 Le añadimos una animación de 1 segundo en la que pasa de transparente a visible
5 Al terminar la animación, le quitamos la clase de anterior para que pase al frente

7.1.6. Deshabilitando los efectos

Si el sistema donde corre nuestra aplicación es poco potente, podemos deshabilitar todos los efectos y animaciones haciendo uso de la propiedad booleana jQuery.fx.off:

$.fx.off = true;
// Volvemos a activar los efectos
$.fx.off = false;

Al estar deshabilitadas, los elementos aparecerán y desaparecer sin ningún tipo de animación.

7.2. AJAX

Todas las peticiones AJAX realizadas con jQuery se realizan con el método $.ajax() (http://api.jquery.com/category/ajax/).

Para simplificar el trabajo, jQuery ofrece varios métodos que realmente son sobrecargas sobre el método $.ajax(). Si comprobamos el código fuente de este función podremos ver como es mucho más complejo que lo que estudiamos en la sesión de JavaScript.

Los métodos que ofrece jQuery son:

Método Propósito

selector.load(url)

Incrustra el contenido crudo de la url sobre el selector

$.get(url, callback, tipoDatos)

Realiza una petición GET a la url. Una vez recuperada la respuesta, se invocará el callback el cual recuperará los datos del tipoDatos

$.getJSON(url, callback)

Similar a $.get() pero recuperando datos en formato JSON

$.getScript(url, callback)

Carga un archivo JavaScript de la url mediante un petición GET, y lo ejecuta.

$.post(url, datos, callback)

Realiza una petición POST a la url, enviando los datos como parámetros de la petición

Todos estos métodos devuelven un objeto jqXHR el cual abstrae el mecanismo de conexión, ya sea un objeto HTMLHttpRequest, un objeto XMLHTTP o una etiqueta <script>.

7.2.1. $.ajax()

Ya hemos comentado que el método $.ajax() es el que centraliza todas las llamadas AJAX. Pese a que normalmente no lo vamos a emplear directamente, conviene conocer todas las posibilidades que ofrece. Para ello, recibe como parámetro un objeto con las siguientes propiedades:

Propiedad Propósito

url

URL a la que se realiza la petición

type

Tipo de petición (GET o POST)

dataType

Tipo de datos que devuelve la petición, ya sea texto o binario.

success

Callback que se invoca cuando la petición ha sido exitosa y que recibe los datos de respuesta como parámetro

error

Callback que se invoca cuando la petición ha fallado

complete

Callback que se invoca cuando la petición ha finalizado

Por ejemplo, si quisieramos recuperar el archivo fichero.txt, realizaríamos una llamada del siguiente modo:

$.ajax({
	url: "fichero.txt",
	type: "GET",
	dataType: "text",
	success: todoOK,
	error: fallo,
	complete: function(xhr, estado) {
		console.log("Peticion finalizada " + estado);
	}
});

function todoOK(datos) {
	console.log("Todo ha ido bien " + datos);
}

function fallo(xhr, estado, msjErr) {
	console.log("Algo ha fallado " + msjErr);
}

Si quisiéramos incrustar el contenido recibido por la petición, en vez de sacarlo por consola, lo podríamos añadir a una capa o un párrafo:

function todoOK(datos) {
	$("#resultado").append(datos);
}

7.2.2. load()

Si queremos incrustrar contenido proveniente de una URL, con la misma funcionalidad que un include estático en JSP, usaremos el método load(url). Por ejemplo:

$("body").load("contacto.html");

De este modo incluiríamos hasta la cabecera del documento html. Para indicarle que estamos interesados en una parte del documento, podemos indicar que cargue el elemento cuya clase CSS sea contenido.

$("body").load("contacto.html .contenido");

Mediante este método vamos a poder incluir contenido de manera dinámica al lanzarse un evento. Supongamos que tenemos un enlace a contacto.html. Vamos a modificarlo para que en vez de redirigir a la página, incruste el contenido:

<a href="contacto.html">Contacto</a>
<div id="contenedor"></div>

<script>
$('a').on('click', function(evt) {
	var href = $(this).attr('href');	(1)
	$('#contenedor').load(href + ' .contenido');	(2)
	evt.preventDefault();	(3)
});
</script>
1 Obtenemos el documento que queremos incluir
2 Incrustamos el contenido del documento dentro de la capa con id contenedor
3 Evitamos que cargue el enlace

7.2.3. Recibiendo información del servidor

Mediante $.get(url [,datos], callBack(datosRespuesta) [,tipoDatosRespuesta]) se envía una petición GET con datos como parámetro. Tras responder el servidor, se ejecutará el callBack con los datos recibidos cuyo tipo son del tipoDatosRespuesta.

Así pues, el mismo ejemplo visto anteriormente puede quedar reducido al siguiente fragmento:

$.get("fichero.txt", function(datos) {
	console.log(datos);
});

En el caso de XML, seguiremos usando el mismo método pero hemos de tener en cuenta el formato del documento. Supongamos que tenemos el siguiente documento heroes.xml:

<heroe>
	<nombre>Batman</nombre>
	<email>batman@heroes.com</email>
</heroe>

Para poder recuperar el contenido hemos de tener el cuenta que trabajaremos con las funciones DOM que ya conocemos:

$.get("heroes.xml", function(datos) {
	var nombre = datos.getElementsByTagName("nombre")[0];
	var email = datos.getElementsByTagName("email")[0];
	var val = nombre.firstChild.nodeValue + " " + email.firstChild.nodeValue;
	$("#resultado").append(val);
},"xml");

Si la información a recuperar es de tipo JSON, jQuery ofrece el método $.getJSON(url [,datos], callBack(datosRespuesta)).

Por ejemplo, supongamos que queremos acceder a Flickr para obtener las imágenes que tienen cierta etiqueta:

var flickrAPI = "http://api.flickr.com/services/feeds/photos_public.gne?jsoncallback=?";
$.getJSON( flickrAPI, {
	tags: "proyecto víbora ii",
	tagmode: "any",
	format: "json"
}, formateaImagenes);

function formateaImagenes(datos) {
	$.each(datos.items, function(i, elemento) {	(1)
		$("<img>").attr("src", elemento.media.m).appendTo("#contenido");	(2)
		if (i === 4) {	(3)
			return false;
		}
	});
}
1 la función $.each(colección, callback(índice, elemento)) recorre el array y realiza una llamada al callback para cada uno de los elementos
2 Por cada imagen, la anexa al id contenido construyendo una etiqueta img
3 Limita el número de imágenes a mostrar en 5. Cuando el callback de $.each() devuelve false, detiene la iteración sobre el array
$.each()

La utilidad $.each() se trata de un método auxiliar que ofrece jQuery para iterar sobre una colección, ya sea:

  • un array mediante $.each(colección, callback(índice, elemento))

  • un conjunto de selectores con $(selector).each(callback(índice, elemento))

  • o las propiedades de un objeto mediante $.each(objeto, callback(clave, valor))

Se emplea mucho para tratar la respuesta de las peticiones AJAX. Más información en http://api.jquery.com/jquery.each/

Finalmente, en ocasiones necesitamos inyectar código adicional al vuelo. Para ello, podemos recuperar un archivo JavaScript y que lo ejecute a continuación mediante el método $.getScript(urlScript, callback). Una vez finalizada la ejecución del script, se invocará al callback.

Supongamos que tenemos el siguiente código en script.js:

console.log("Ejecutado dentro del script");
$("#resultado").html("<strong>getScript</strong>");

Y el código jQuery que ejecuta el script:

$.getScript("script.js", function(datos, statusTxt) {
	console.log(statusTxt);
})

7.2.4. Enviando información al servidor

La función $.post(url, datos, callback(datosRespuesta)) permite enviar datos mediante una petición POST.

Una singularidad es la manera de adjuntar los datos en la petición, ya sea:

  • Creando un objeto cuyos valores obtenemos mediante val().

  • Serializando el formulario mediante el método serialize(), el cual codifica los elementos del formulario mediante una cadena de texto

Vamos a crear un ejemplo con un formulario sencillo para realizar el envío mediante AJAX:

Ejemplo AJAX POST
<form name="formCliente" id="frmClnt" action="#">
<fieldset id="infoPersonal">
	<legend>Datos Personales</legend>
	<p><label for="nombre">Nombre</label>
		<input type="text" name="nombre" id="idNombre" /></p>
	<p><label for="correo">Email</label>
		<input type="email" name="correo" id="idEmail" /></p>
</fieldset>
<p><button type="submit">Guardar</button></p>
</form>

Y el código que se comunica con el servidor:

Ejemplo $.post()
$('form').on('submit', function(evt) {	(1)
	evt.preventDefault();		(2)
	// var nom = $(this).find('#inputName').val();	(3)
	var datos = $(this).serialize();	// nombre=asdf&email=asdf
	$.post("/GuardaFormServlet", datos, function (respuestaServidor) {	(4)
		console.log("Completado " + respuestaServidor);
	});
});
1 Escuchamos el evento de submit
2 Desactivar el envío por defecto del formulario
3 Obtener el contenido de los campos, lo cual podemos hacerlo campo por campo como en la línea 3, u obtener una representación de los datos como parámetros de una URL mediante el método serialize() como en la línea 4.
4 Enviar el contenido a un script del servidor y recuperamos la respuesta. Mediante $.post() le pasaremos el destino del envío, los datos a envíar y una función callback que se llamará cuando el servidor finalice la petición.

7.2.5. Tipos de datos

Ya hemos visto que podemos trabajar con cuatro tipos de datos. A continuación vamos a estudiarlos para averiguar cuando conviene usar uno u otro:

  • Fragmentos HTML necesitan poco para funcionar, ya que mediante load() podemos cargarlos sin necesidad de ejecutar ningún callback. Como inconveniente, los datos puede que no tengan ni la estructura ni el formato que necesitemos, con lo que estamos acoplando nuestro contenido con el externo.

  • Archivos JSON, que permiten estructurar la información para su reutilización. Compactos y fáciles de usar, donde la información es auto-explicativa y se puede manejar mediante objetos mediante JSON.parse() y JSON.stringify(). Hay que tener cuidado con errores en el contenido de los archivos ya que pueden provocar efectos colaterales.

  • Archivos JavaScript, ofrecen flexibilidad pero no son realmente un mecanismo de almacenamiento, ya que no podemos usarlos desde sistemas heterogéneos. La posibilidad de cargar scripts JavaScripts en caliente permite refactorizar el código en archivos externos, reduciendo el tamaño del código hasta que sea necesario.

  • Archivos XML, han perdido mercado en favor de JSON, pero se sigue utilizando para permitir que sistemas de terceros sin importar la tecnología de acceso puedan conectarse a nuestros sistemas.

A día de hoy, JSON tiene todas las de ganar, tanto por rendimiento en las comunicaciones como por el tamaño de la información a transmitir.

7.2.6. Manejadores de eventos AJAX

jQuery ofrece un conjunto de métodos globales para interactuar con los eventos que se lanzan al realizar una petición AJAX. Estos métodos no los llamamos dentro de la aplicación, sino que es el navegador el que realiza las llamadas.

Método Propósito

ajaxComplete()

Registra un manejador que se invocará cuando la petición AJAX se complete

ajaxError()

Registra un manejador que se invocará cuando la petición AJAX se complete con un error

ajaxStart()

Registra un manejador que se invocará cuando la primera petición AJAX comience

ajaxStop()

Registra un manejador que se invocará cuando todas las peticiones AJAX hayan finalizado

ajaxSend()

Adjunta una función que se invocará antes de enviar la petición AJAX

ajaxSuccess()

Adjunta una función que se invocará cuando una petición AJAX finalice correctamente

Recordad que estos métodos son globales y se ejecutan para todas las peticiones AJAX de nuestra aplicación, de ahí que se sólo se adjunten al objeto document.

Por ejemplo, si antes de recuperar el archivo de texto registramos todos estos manejadores:

Ejemplo Manejadores AJAX - http://jsbin.com/loqace/edit?js,console
$(document).ready(function() {
  $(document).ajaxStart(function () {
    console.log("AJAX comenzando");
  });
  $(document).ajaxStop(function () {
    console.log("AJAX petición finalizada");
  });
  $(document).ajaxSend(function () {
    console.log("Antes de enviar la información...");
  });
  $(document).ajaxComplete(function () {
    console.log("Todo ha finalizado!");
  });
  $(document).ajaxError(function (evt, jqXHR, settings, err) {
    console.error("Houston, tenemos un problema: " + evt + " - jq:" + jqXHR + " - settings :" + settings + " err:" + err);
  });
  $(document).ajaxSuccess(function () {
    console.log("Parece que ha funcionado todo!");
  });

  getDatos();
});

function getDatos() {
	$.getJSON("http://www.omdbapi.com/?s=batman&callback=?", todoOk);
}

function todoOk(datos) {
	console.log("Datos recibidos y adjuntándolos a resultado");
	$("#resultado").append(JSON.stringify(datos));
}

Tras ejecutar el código, por la consola aparecerán los siguientes mensajes en el orden en el que se ejecutan:

Eventos AJAX
Figure 1. Eventos globales AJAX
Autoevaluación

Si en el código renombramos la URL por la de un fichero que no encuentra, ¿Qué evento se lanza ahora y cual deja de lanzarse? [1] ¿Qué saldrá por consola? [2]

7.3. Utilidades

A continuación veremos un conjunto de utilidades que ofrece jQuery.

7.3.1. Comprobación de tipos

El siguiente conjunto de funciones de comprobación de tipos, también conocidas como de introspección de objetos, nos van a permitir:

  • Determinar el tipo de un objeto

  • Gestionar el uso de parámetros opcionales

  • Validar parámetros

Función Propósito

$.isArray(array)

Determina si array es un Array. Si es un objeto array-like devolverá falso

$.isFunction(función)

Determina si función es una Función

$.isEmptyObject(objeto)

Determina si objeto esta vacío

$.isPlainObject(objeto)

Determina si objeto es un objeto sencillo, creado como un objeto literal (mediante las llaves) o mediante new objeto.

$.isXmlDoc(documento)

Determina si el documento es un documento XML o un nodo XML

$.isNumeric(objeto)

Determina si objeto es un valor numérico escalar.

$.isWindow(objeto)

Determina si objeto representa una ventana de navegador

$.type(objeto)

Obtiene la clase Javascript del objeto. Los posibles valores son boolean, number, string, function, array, date, regexp, object, undefined o null

Para ver estas utilidades en funcionamiento, vamos a crear un ejemplo sobre un fragmento de código que realiza una llamada a una función:

function llamaOtraFuncion(veces, retraso, funcion) {
	var i = 0;
	( function bucle() {
		i++;
		funcion();
		if (i < veces) {
			setTimeout(bucle, retraso);
		}
	})();
}

function saluda() {
	$("#resultado").append("Saludando desde la función <br />");
}

$(function() {
	llamaOtraFuncion(3, 500, saluda);
});

A continuación, vamos a modificarlo para asignar valores por defecto:

function llamaOtraFuncion(arg1, arg2, arg3) {
	var veces = $.isNumeric(arg1) ? arg1 : 5;
	var retraso = $.isNumeric(arg2) ? arg2 : 1000;
	var funcion = $.isFunction(arg1) ? arg1 : $.isFunction(arg2) ? arg2 : arg3;

	var i = 0;
	// resto de código...

De modo que ahora podemos realizar diferentes llamadas sobrecargando los parámetros:

llamaOtraFuncion(3, 500, saluda);	// 3 veces con retardo de 0,5 seg
llamaOtraFuncion(saluda);	// 5 veces con retardo de 1 seg
llamaOtraFuncion(7, saluda);	// 7 veces con retardo de 1 seg

7.3.2. Manipulación de colecciones

El siguiente conjunto de funciones de manipulación de objetos nos permiten trabajar con arrays y objetos simplificando ciertas tareas:

Función Propósito

$.makeArray(objeto)

Convierte el objeto en un array. Se utiliza cuando necesitamos llamar a funciones que sólo soportan los arrays, como join o reverse, o cuando necesitamos pasar un parametro a una función como array

$.inArray(valor, array)

Determina si el array contiene el valor. Devuelve -1 o el índice que ocupa el valor. Un tercer parámetro opcional permite indicar el índice por el cual comienza la búsqueda.

$.unique(array)

Elimina cualquier elemento duplicado que se encuentre en el array

$.merge(array1, array2)

Combina los contenidos de array1 y array2, similar a la función concat().

$.map(array, callback)

Construye un nuevo array cuyo contenido es el resultado de llamar al callback para cada elemento, similar a la funcion map().

$.grep(array, callback [,invertido])

Filtra el array mediante el callback, de modo que añadirá los elementos que pasen la función, la cual recibe un objeto DOM como parámetro, y devuelve un array JavaScript, similar a la función filter()

A continuación tenemos un fragmento de código con ejemplos de uso de estos métodos:

var miArray = [1, 2, 3, 3, 4, 4, 5];
var miArray2 = [6, 7, 8];

if ($.inArray(4, miArray) != -1) {
	console.log("4 esta en el array");
}

$.unique(miArray);
console.log(miArray); // [1, 2, 3, 4, 5]

$.merge(miArray, miArray2);
console.log(miArray);

var miArrayDoble = $.map(miArray, function(elem, indice) {
	return indice * 2;
});
console.log(miArrayDoble);

var miArrayFiltrado = $.grep(miArray, function(elem) {
	return elem % 2 == 0;
});
console.log(miArrayFiltrado);
Autoevaluación

¿Qué saldrá por la consola tras ejecutar el método $.merge()? [3]

7.3.3. Copiando objetos

Si tenemos uno o varios objetos de los cuales queremos copiar sus propiedades en uno final, podemos hacer uso del método $.extend(destino, origen);

var animal = {
	comer: function() {
		console.log("Comiendo");
	}
}
var perro = {
	ladrar: function() {
		console.log("Ladrando");
	}
}

$.extend(perro, animal);
perro.comer();	// Comiendo

Es decir, permite copiar miembros de un objeto fuente en uno destino, sin realizar herencia, sólo clonando las propiedades. Si hay un conflicto, se sobreescribirán con las propiedades del objeto fuente, y si tenemos múltiples objetos fuentes, de izquierda a derecha.

Si los objetos que vamos a clonar contienen objetos anidados, necesitamos indicarle a jQuery que el clonado debe ser recursivo, mediante un booleano a true como primer parámetro:

var animal = {
	acciones: {
		comer: function() {
			console.log("Comiendo");
		},
		sentar: function() {
			console.log("Sentando");
		}
	}
};
var perro = {
	acciones: {
		ladrar: function() {
			console.log("Ladrando");
		},
		cavar: function() {
			console.log("Cavando");
		}
	}
};
var perroCopia = {};

$.extend(perroCopia, perro);	(1)
perroCopia.acciones.ladrar();	// Ladrando

$.extend(true, perroCopia, animal);	(2)
perroCopia.acciones.comer();	// Comiendo
perroCopia.acciones.ladrar();	// Ladrando

$.extend(perro, animal);		(3)
perro.acciones.comer();		// Comiendo
perro.acciones.ladrar();	// error
1 Copiamos los atributos de perro en perroCopia, con lo que podemos acceder a las propiedades
2 Al hacer una copia recursiva, en vez de sustituir la propiedad acciones de perroCopia por la de animal, las fusiona
3 En cambio, si no hacemos la copia recursiva, podemos acceder a las propiedades de animal pero no a las de perro

7.4. Plugins

Aunque el núcleo de jQuery ya ofrece multitud de funcionalidad y utilidades, también soporta una arquitectura de plugins para extender la funcionalidad de la librería.

El website de jQuery ofrece un enorme repositorio de plugins en http://plugins.jquery.com/, donde se listan con demos, código de ejemplo y tutoriales para facilitar su uso.

En la siguiente sesión estudiaremos el plugin jQueryUI como un ejemplo de complemento que extiende la librería.

Aunque jQuery ofrece múltitud de plugins que extienden el código, en ocasiones necesitamos ir un poco más allá y nos toca escribir nuestro propio código que podemos empaquetar como un nuevo plugin.

El archivo fuente que contenga nuestro plugin debería cumplir la siguiente convención de nombrado:

jquery.nombrePlugin.js
jquery.nombrePlugin-1.0.js

Además, se recomienda añadir un comentario en la cabecera donde se visualice la versión del mismo.

7.4.1. Creando un plugin

A la hora de crear un plugin, lo primero que asumimos es que jQuery ha cargado. Lo que no podemos asumir es que el alias $ esté disponible. Por ello, dentro de nuestros plugins usaremos el nombre completo jQuery o definiremos el $ por nosotros mismos.

Conviene recordar que podemos hacer uso de una IIFE para poder usar el $ dentro de nuestro plugin:

(function($) {
	// código del plugin
})(jQuery);

7.4.2. Funciones globales

Del mismo modo que jQuery ofrece la función $.ajax(), la cual no necesita ningún objeto para funcionar, nosotros podemos extender el abanico de funciones de utilidades que ofrece jQuery.

Para añadir una función al espacio de nombre de jQuery únicamente hemos de asignar la función como una propiedad del objeto jQuery:

Añadir función global en jQuery - http://jsbin.com/zohugu/1/edit?js,console
(function($) {
	$.suma = function(array) {	(1)
		var total = 0;
		$.each(array, function (indice, valor) {
			valor = $.trim(valor);
			valor = parseFloat(valor) || 0;

			total += valor;
		});
		return total;
	};
})(jQuery);
1 Le asociamos la función a la propiedad de jQuery

De este modo, vamos a poder llamar a esta función mediante:

var resultado = $.suma([1,2,3,4]);	// 10

También podríamos crear un nuevo espacio de nombres para las funciones globales que queramos añadir, y así evitar conflictos que puedan aparecer con otros plugins. Para ello, sólo hemos de asociar a nuestra función global un objeto el cual contenga como propiedades las funciones que queramos añadir como plugin:

(function($) {
	$.MathUtils = {
		suma : function(array) {
			// código de la función
		},
		media: function(array) {
			// código de la función
		}
	};
})(jQuery);

De este modo, invocaremos a las funciones así:

var resultado = $.MathUtils.suma([1,2,3,4]);
var resultado = $.MathUtils.media([1,2,3,4]);

7.4.3. Métodos de objeto

Si queremos extender las funciones de jQuery, mediante prototipos podemos crear métodos nuevos que se apliquen al objeto jQuery activo. jQuery utiliza el alias fn en vez prototype:

(function($) {
	$.fn.nombreNuevaFuncion = function() {
		// código nuevo
	};
})(jQuery);

Normalmente no sabemos si la función trabajará sobre un sólo objeto o sobre una colección, ya que un selector de jQuery puede devolver cero, uno o múltiples elementos. De modo que una buena práctica es plantear un escenario donde recibimos un array de datos.

La manera más facil de garantizar este comportamiento es iterar sobre la colección mediante el método each().

$.fn.nombreNuevaFuncion = function() {
	this.each(function() {
		// Hacemos algo con cada elemento
	});
};

Así pues, si quisiéramos extender jQuery y ofrecer un nuevo método que le cambiase la clase CSS a un nodo haríamos:

(function($) {
	$.fn.cambiarClase = function(clase1, clase2) {
		this.each(function() {	(1)
			var elem = $(this);	(2)
			if (elem.hasClass(clase1)) {
				elem.removeClass(clase1).addClass(clase2);
			} else if (elem.hasClass(clase2)) {
				elem.removeClass(clase2).addClass(clase1);
			}
		});
	};
})(jQuery);
1 Recorremos la colección de objetos que nos devuelve el selector. En este punto this referencia al objeto devuelto por jQuery
2 Cacheamos la referencia al elemento en concreto sobre el que iteramos

Esto permitirá que posteriormente realicemos una llamada al nuevo método mediante cualquier selector:

$("div").cambiarClase("principal","secundaria");
Encadenar funciones

Al crear una función prototipo, hemos de tener en cuenta que probablemente, después de llamar a nuestra función, es posible que el desarrollador quiera seguir encadenando llamadas.

Es por ello, que es muy importante que la función devuelva un objeto jQuery para permitir que continúe el encadenamiento. Este objeto normalmente es el mismo que this.

En nuestro caso, podemos modificar el plugin para devolver el objeto que iteramos:

(function($) {
	$.fn.cambiarClase = function(clase1, clase2) {
		return this.each(function() {	(1)
			var elem = $(this);
			if (elem.hasClass(clase1)) {
				elem.removeClass(clase1).addClass(clase2);
			} else if (elem.hasClass(clase2)) {
				elem.removeClass(clase2).addClass(clase1);
			}
		});
	};
})(jQuery);
1 Al devolver cada objeto que iteramos, permitimos el encadenamiento
Función como parámetro

Al encadenar funciones, si queremos que nuestro plugin reciba como párametro una función, por ejemplo para dar soporte a callbacks, para evitar un mal funcionamiento cuando no se pase ninguna función, hemos de comprobar si es una función e invocarla en dicho caso:

$.fn.pluginQueRecibeFuncion = function(funcionParam) {
	if ($.isFunction(funcionParam)) {
		funcionParam.call(this);
	}
	return this;
}
Opciones

Conforme los plugins crecen, es una buena práctica permitir que el plugin reciba un objeto con las opciones de configuración del mismo.

$.fn.pluginConConf = function(opciones) {
	var confFabrica = {prop: "valorPorDefecto"};
	var conf = $.extend(confFabrica, opciones);	(1)

	return this.each(function() {
		// código que trabaja con conf.prop (2)
	});
};
1 Sobreescribe los valores de fabrica del objeto de configuración con el recibido como parámetro
2 Dentro de la iteración, usamos los valores que contiene el objeto de configuración

Además, es conveniente que permitamos modificar los valores de fabrica para permitir mayor flexibilidad. Para ello, extraemos los valores de fábrica del plugin a una propiedad del método:

$.fn.pluginConConf = function(opciones) {

	var conf = $.extend({}, $.fn.pluginConConf.confFabrica, opciones);	(1)

	return this.each(function() {
		// código que trabaja con conf.prop
	});
};

$.fn.pluginConConf.confFabrica = {prop: "valorPorDefecto"};
1 Creamos un nuevo objeto de configuración

De este modo, vamos a poder inicializar los valores de fábrica, incluso desde fuera de un bloque de ready mediante:

$.fn.pluginConConf.confFabrica.prop = "nuevoValor";

7.5. Rendimiento

A la hora de escribir código jQuery es útil conocer la manera de que tenga el mejor rendimiento sin penalizar la comprensión ni mantenibilidad del código. Sin embargo, es importante tener en mente que la optimización prematura suele ser la raíz de todos los males.

7.5.1. Consejos de rendimiento

Una vez localizado el problema mediante el profiling del código que vimos en la sesión de JavaScript, llega el momento de optimizar el código. Para ello, deberemos:

  1. Utilizar la última versión de jQuery, ya que siempre contienen mejoras de rendimiento que repercutirán en nuestra aplicación.

  2. Cachear los selectores, para evitar búsquedas innecesarias. Por ejemplo, el siguiente fragmento realiza 1000 búsquedas:

    console.time("Sin cachear");
    for (var i=0; i < 1000; i++) {
    	var s = $("div");
    }
    console.timeEnd("Sin cachear");

    Mientras que así sólo realizamos una:

    console.time("Cacheando");
    var miCapa = $("div");
    for (var i=0; i < 1000; i++) {
    	var s = miCapa;
    }
    console.timeEnd("Cacheando");

    Y por la consola tendremos que el fragmento que no cachea tarda 8.990ms mientras que el que cachea sólo 0.045ms, es decir, 200 veces menos.

  3. Cachear otros elementos, como llamadas a métodos o acceso a propiedades dentro de bucles. Por ejemplo, el siguiente fragmento recorre un conjunto de 1000 capas:

    console.time("Sin cachear");
    var miCapa = $("div");
    var s = 0;
    for (var i=0; i < miCapa.length; i++) {
    	s += i;
    }
    console.timeEnd("Sin cachear");

    Mientras que sí extraemos la llamada al método fuera del bucle, accederemos a la propiedad de longitud una sola vez:

    console.time("Cacheando");
    var miCapa = $("div");
    var longitud = miCapa.length;
    var s = 0;
    for (var i=0; i < longitud; i++) {
    	s += i;
    }
    console.timeEnd("Cacheando");

    También podemos cachear llamadas AJAX del siguiente modo:

    Ejemplo cacheo llamadas AJAX - http://jsbin.com/kabuju/edit?js,console,output
    function getDatos(id) {
      if (!api[id]) {
    		var url = "http://www.omdbapi.com/?s=" + busqueda + "&callback=?";
    		console.log("Petición a " + url);
    		api[id] = $.getJSON(url);
      }
    
      api[id].done(todoOk).fail(function() {
        $("#resultado").html("Error");
      });
    }
    
    function todoOk(datos) {
    	console.log("Datos recibidos y adjuntándolos a resultado");
    	$("#resultado").append(JSON.stringify(datos));
    }
  4. Cuando sea posible, utilizar propiedades de elementos. En vez de utilizar un método jQuery, en ocasiones, podemos obtener la misma información a partir de una propiedad. Por ejemplo, en vez de obtener el atributo id mediante el método attr hacerlo mediante su propiedad:

    var lento = miCapa.attr("id");
    var rapido = miCapa[0].id;
    Hay que tener cuidado que normalmente al acceder mediante una propiedad el resultado deja de ser un objeto jQuery, con lo que no vamos a poder encadenar el resultado en una llamada posterior.

Estos y más consejos detallados los promueve jQuery en http://learn.jquery.com/performance/

7.6. Ejercicios

7.6.1. (0.4 ptos) Ejercicio 71. Apuntes 2.0

A partir del ejercicio 62 realizado en la sesión anterior donde modificábamos la página de apuntes, vamos a mejorarlo de manera que:

  • Al cargar la página, todos los capítulos estén ocultos, incluidas las notas al pie.

  • Al pulsar sobre un enlace de la tabla de contenidos, se muestre el contenido de dicho capítulo. Si había un capítulo mostrado, debe ocultarse. Es decir, sólo puede visualizar en pantalla un único capítulo.

  • Tanto la aparición como la desaparición del contenido se debe realizar mediante una animación.

Al cargar la página la apariencia será similar a la siguiente imagen:

Apuntes 2.0
Figure 2. Apuntes 2.0

Todo el código estará incluido en el archivo ej71.js.

7.6.2. (0.5 ptos) Ejercicio 72. Star Wars jQuery

A partir del ejercicio 51 sobre Star Wars API, vamos a basarnos en el mismo API pero escribiendo el código mediante jQuery y su API para AJAX.

Ahora vamos a mostrar un listado con todos los personajes. Al elegir uno de los disponibles se mostrará su információn básica, así como el título de las películas en las que aparece. Cuando se cambie el personaje, mediante una animación se ocultará la información del antiguo personaje (borrando sus datos) antes de mostrar sólo la información del nuevo.

Para ello, usaremos la siguiente plantilla:

<!DOCTYPE html>
<html>
<head lang="es">
	<meta charset="UTF-8">
	<title>Ejercicio 72</title>
</head>
<body>
<h1>Star Wars API</h1>
<h2>Personajes - <span id='total'></span></h2>
Elige entre los principales personajes: <select id='personajes'></select>
<br />
<br />
<div id='personaje' style='display:none; background:ghostwhite'>
	Nombre: <span id='nombre'></span><br />
	Sexo: <span id='sexo'></span><br />
	Especie: <span id='especie'></span><br />
	Películas: <ul id='peliculas'></ul><br />
</div>

<script src="jquery-1.11.2.js"></script>
<script src="ej72.js"></script>
</body>
</html>

Al cargar a Obi-Wan la apariencia será similar a la siguiente imagen:

Star Wars jQuery
Figure 3. Star Wars jQuery

Todo el código estará incluido en el archivo ej72.js.

7.6.3. (0.3 ptos) Ejercicio 73. Plugin Canificador

A partir del ejercicio 41 donde creamos el módulo Canificador, crear un plugin de jQuery que integre la misma funcionalidad, de modo que podamos utilizarlo tanto de manera global como si fuese un método de objeto:

var cadenaCaniada = $.toCani("Mi cadena");
$("#contenidoCani").toCani();

El plugin debe permitir modificar la configuración base para que el final de la cadena sea diferente a HHH.

Todo el código estará incluido en el archivo jquery.canificador.js.


1. Antes se lanzaba ajaxSuccess y ahora se lanza ajaxError
2. AJAX Comenzando, Antes de enviar la información…​, GET url Not found, Houston tenemos un problema: Not found, Todo ha finalizado!, AJAX petición finalizada
3. Un array con los valores: 5, 4, 3, 2, 1, 6, 7, 8