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);