Resumen de la clase de Ajax

Introducción

  • Ajax significa solamente asinchronous Javascript and XML.
  • Que nos permite agregarle a nuestro código Javascript la posibilidad de ir a buscar dinámicamente información al servidor. De esta forma podemos actualizar dinámicamente "un pedacito" de nuestra página HTML.
  • Esta capacidad de actualización parcial nos permite alejarnos del paradigma de request-response, propio de la programación web tradicional.
  • La información que podemos obtener dinámicamente del servidor puede ser brindada por un JSP o un servlet o cualquier otra herramienta que conteste pedidos HTTP.
  • Hay muchos mecanismos por los cuales se puede lograr este objetivo, generalmente cuando decimos ajax nos estamos refiriendo a un objeto particular que permite esa funcionalidad: XMLHttpRequest.
  • Mediante este objeto, hay básicamente dos formatos de información que podemos manejar:
    • HTML, llamando a una página que devuelve la porción de HTML que necesitamos.
    • XML, llamando a una página que devuelve información sin formato (es decir XML) y luego en el cliente convertimos ese XML a HTML.

Ejemplo básico

Tomando como base el ejemplo de búsqueda de libros que está en ejemplos-basicos/rest, vamos a agregarle un poco de comportamiento Ajax. Si prefieren ver directamente el resultado pueden consultar: ejemplos-basicos/js-ajax-base.

  • En lugar de invocar a una página nueva cuando se pide el detalle del libro, vamos a cargarlo en la misma página que estamos. Para eso cambiamos el link:
    • <a href="javascript:cambiarLibro(${libro.id});">${libro.titulo}</a>
    • Y agregamos un div que va a ser el lugar donde mostraremos los libros:
    • <div id="libro" />
  • Antes de poder implementar la función cambiarLibro, debemos crear el objeto ajaxRequest, esto es distinto en Firefox y Explorer, entonces hay que hacer algo así de desagradable:
    • function createAjaxRequest() {
    • if (typeof XMLHttpRequest != "undefined") {
    • return new XMLHttpRequest();
    • }
    • else if (window.ActiveXObject) {
    • return new ActiveXObject("Microsoft.XMLHTTP");
    • }
    • }
    • El ajaxRequest debemos guardarlo en una variable global:
    • var ajaxRequest = createAjaxRequest();
  • Ahora estamos en condiciones de implemementar cambiarLibro, que tomará un id y enviará un pedido ajax:
    • function cambiarLibro(nuevoId) {
    • // Se guarda el id para referencia futura
    • id = nuevoId;
    • // Construye la URL
    • var url = "detalleLibro?id=" + id;
    • // Envía el pedido
    • ajaxRequest.open("GET", url, true);
    • ajaxRequest.onreadystatechange = updateLibro;
    • ajaxRequest.send(null);
    • }
  • Como estamos trabajando con pedidos asincrónicos, esto no nos dará un resultado. En cambio, le indicamos a qué función invocar cuando se reciba el resultado, eso se hace con la línea:
    • ajaxRequest.onreadystatechange = updateLibro;
  • Entonces debemos programar la función updateLibro:
    • function updateLibro() {
    • if (ajaxRequest.readyState == 4) {
    • if (ajaxRequest.status == 200) {
    • document.getElementById("libro").innerHTML = ajaxRequest.responseText;
    • }
    • else {
    • alert("No se pudieron obtener los detalles del libro " + id + "\n" +
    • "Por un error: " + ajaxRequest.status + ajaxRequest.statusText);
    • }
    • }
    • }
    • Hay tres variables del ajaxRequest que nos indica el estado:
      • readyState nos permite saber si se recibió una respuesta (4 es el número mágico, no pregunten por qué).
      • status nos permite conocer si fue una respuesta correcta o un error (són códigos de error HTTP, 200 significa OK).
      • statusText nos da (en caso de error) un mensaje representativo del error.
    • Otras dos variables nos permiten obtener el contenido del ajaxRequest:
      • responseText nos permite obtener el resultado en formato plano (puede ser texto, HTML o cualquier cosa que deseemos, sin procesar).
      • responseXML nos pemite obtener el resultado XML como un objeto, que va a ser más fácil de manipular en caso de desear ese formato.
    • Como antes, el innerHTML nos permite asignar el contenido de un div:
    • document.getElementById("libro").innerHTML = ajaxRequest.responseText;

Un poco de diseño: MVC

Para implementar esto tomamos como base el paso final del ejemplo anterior ejemplos-basicos/js-ajax-base. El resultado final de esto lo pueden consultar en ejemplos-basicos/js-ajax

  • Si bien la versión anterior tiene la ventaja de no requerir estado en el servidor, es limitado para implementar cosas como los links anterior-siguiente (que requieren de tener la lista entera de resultados a mano. Eso requiere de un modelo.
    • Ese modelo puede implementarse de dos maneras:
      • La más sencilla es poner ese código en el servidor, con la consecuencia de que volvemos a tener un sistema con estado.
      • Otra variante es modelarlo en Javascript, absteniéndonos de utilizar el estado en sesión pero obligándonos a manipular mucho más HTML/DOM en el cliente.
    • Ambas soluciones pueden ser válidas dependiendo del tipo de sistema que uno esté construyendo. En este ejemplo vamos a usar la primera variante.
  • Construimos entonces la clase BuscadorLibros, que reflejará el estado de nuestra pantalla en todo momento. Para eso este objeto debería tener:
      • Una propiedad que permita guardar el texto ingresado para buscar (asociada al campo de texto en el formulario).
      • Un método buscar (asociado al botón "buscar").
      • Una propiedad resultados (asociada a la grilla de resultados).
      • Una propiedad libroActual (asociada al libro del que estamos mostrando el detalle).
      • Métodos para elegir un libro para mostrar en el detalle (las acciones anterior y siguiente podrán ser acciones específicas o pueden simularse en base a la anterior, vamos a elegir lo primero para mostrar esa variante).
  • Entonces ahora tanto en index.jsp como en detalle.jsp, vamos a mirar siempre a nuestro modelo, el buscador, que lo dejaremos permanentemente en sesión.
    • Para ello en index.jsp debemos modiificar tres cosas:
      • Al recorrer los libros usamos la propiedad resultados del buscador:
        • <c:forEach items="${buscador.resultados}" var="libro">
        • ...
        • </c:forEach>
      • Para fijarnos si mostrar resultados o no, podemos ver si el buscador está presente:
        • <c:if test="${buscador != null}" >
        • ...
        • </c:if>
      • Para volver a mostrar el último texto ingresado para buscar, también podemos almacenarlo en el mismo buscador:
        • <input type="text" name="titulo" id="titulo" value="${buscador.textoBusqueda}" />
  • Para elegir el libro actual, seguimos usando la función cambiar libro, con un pequeño cambio (en lugar de usar el id del libro vamos a trabajar con la posición.
    • var url = "detalleLibro?posicion=" + id;
    • Eso nos obliga a modificar el link que nos lleva al detalle:
    • <a href="javascript:cambiarLibro(${status.index});">${libro.titulo}</a>
  • En detalle.jsp en lugar de buscar una variable libro, podemos obtenerla directamente del Buscador:
    • <td>${buscador.libroActual.titulo}</td>
    • También hay que cambiar la forma de acceder al anterior y al siguiente. Para saber si hay anterior o siguiente podemos consultar al buscador, mientras que para armar los links podemos también usar la función cambiarLibro.
    • <c:if test="${buscador.puedeAnterior}">
    • <a href="javascript:cambiarLibro(${buscador.posicionLibroActual - 1});">Anterior</a>
    • </c:if>
    • <c:if test="${buscador.puedeSiguiente}">
    • <a href="javascript:cambiarLibro(${buscador.posicionLibroActual + 1});">Siguiente</a>
    • </c:if>
  • En el servlet, debemos manipular el buscador, buscarlo en la sesión, si no está lo creamos. Como eso se va a usar desde SearchServlet y desde DetalleServlet, lo ponemos en un método en una superclase (BaseServlet).
    • protected BuscadorLibros getBuscador(HttpServletRequest request) {
    • BuscadorLibros buscador = (BuscadorLibros) request.getSession().getAttribute("buscador");
    • if (buscador == null) {
    • buscador = new BuscadorLibros();
    • request.getSession().setAttribute("buscador", buscador);
    • }
    • return buscador;
    • }
    • Con eso, el SearchServlet sólo debe obtener el parámetro y delegar en el Buscador:
    • String titulo = request.getParameter("titulo");
    • this.getBuscador(request).buscar(titulo);
    • En el DetalleServlet pasa algo parecido:
    • int posicion = Integer.parseInt(request.getParameter("posicion"));
    • this.getBuscador(request).elegirLibro(posicion);