Skip to content

7. Implementación de Ajax en una aplicación Spring MVC

7.1. El papel de AJAX en una aplicación web

Por el momento, los ejemplos de aprendizaje estudiados tenían la siguiente arquitectura:

Para pasar de una vista [Vue1] a una vista [Vue2], el navegador:

  • envía una solicitud a la aplicación web;
  • recibe la vista [Vue2] y la muestra en lugar de la vista [Vue1].

Este es el esquema clásico:

  • solicitud del navegador;
  • el servidor web genera una vista en respuesta al cliente;
  • el navegador muestra esta nueva vista.

Desde hace algunos años existe otra forma de interacción entre el navegador y el servidor web: AJAX (Asynchronous JavaScript and XML). De hecho, se trata de interacciones entre la vista que muestra el navegador y el servidor web. El navegador sigue haciendo lo que sabe hacer, es decir, mostrar una vista HTML, pero ahora es controlado por JavaScript integrado en la vista HTML que se muestra. El esquema es el siguiente:

  • en [1], ocurre un evento en la página que se muestra en el navegador (clic en un botón, cambio de texto, etc.). Este evento es interceptado por JavaScript (jS) integrado en la página;
  • en [2], el código de JavaScript realiza una solicitud HTTP tal como lo habría hecho el navegador. La solicitud es asíncrona: el usuario puede seguir interactuando con la página sin que se bloquee mientras espera la respuesta a la solicitud HTTP. La solicitud sigue el proceso clásico de procesamiento. Nada (o casi nada) la distingue de una solicitud convencional;
  • en [3], se envía una respuesta al cliente jS. En lugar de una vista HTML completa, se envía más bien una vista HTML parcial, un flujo XML o jSON (notación de objetos JavaScript) lo que se envía;
  • en [4], el JavaScript recupera esta respuesta y la utiliza para actualizar una sección de la página HTML que se muestra.

Para el usuario, se produce un cambio en la vista porque lo que ve ha cambiado. Sin embargo, no hay una recarga total de la página, sino simplemente una modificación parcial de la página que se muestra. Esto contribuye a dar fluidez e interactividad a la página: como no hay una recarga total de la página, podemos gestionar eventos que antes no se podían manejar. Por ejemplo, ofrecerle al usuario una lista de opciones a medida que va ingresando caracteres en un campo de entrada. Con cada nuevo carácter que se escribe, se envía una solicitud AJAX al servidor, el cual devuelve otras sugerencias. Sin Ajax, este tipo de ayuda para la entrada de datos era imposible anteriormente. No se podía recargar una nueva página con cada carácter escrito.

7.2. Actualización de una página con un flujo HTML

7.2.1. Las vistas

Nos proponemos analizar la siguiente aplicación:

  • en [1], la hora de carga de la página;
  • en [2], se realizan las cuatro operaciones aritméticas con dos números reales A y B;
  • en [3], la respuesta del servidor se inserta en una sección de la página;
  • en [4], la hora del cálculo. Esta es diferente de la hora de carga de la página [5]. Esta última es igual a [1], lo que indica que la sección [6] no se ha recargado. Además, la sección URL y [7] de la página no han cambiado.

7.2.2. La acción [/ajax-01]

  

El controlador [Ajax.java] define la siguiente acción [/ajax-01]:


    @RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax01(Locale locale, Model modèle, HttpSession session, String tempo) {
        // ¿Tiempo válido?
        if (tempo != null) {
            boolean valide = false;
            int valueTempo = 0;
            try {
                valueTempo = Integer.parseInt(tempo);
                valide = valueTempo >= 0;
            } catch (NumberFormatException e) {

            }
            if (valide) {
                session.setAttribute("tempo", new Integer(valueTempo));
            }
        }
        // se prepara la plantilla de la vista [vue-01]
        ...
}
  • línea 2: la acción [/ajax-01] solo acepta un parámetro, [tempo]. Se trata del tiempo, en milisegundos, que el servidor deberá esperar antes de enviar los resultados de las operaciones aritméticas;
  • línea 4: el parámetro [tempo] es opcional;
  • líneas 5-12: se verifica que el valor del parámetro [tempo] sea válido;
  • líneas 13-15: si es así, el valor del tiempo de espera se guarda en la sesión. Esto significa que estará vigente mientras no se modifique;

El código de la acción [/ajax-01] continúa así:


    @RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax01(Locale locale, Model modèle, HttpSession session, String tempo) {
        // ¿El tiempo es válido?
...
        // Se está preparando la plantilla de la vista [vue-01]
        modèle.addAttribute("actionModel01", new ActionModel01());
...
        // vista
        return "vue-01";
}

La clase [ActionModel01] sirve principalmente para encapsular los valores enviados por la acción [/ajax-01]. En este caso, no se envía nada. Se crea una clase vacía que se incluye en el modelo porque la vista [vue-01.xml] la utiliza. La clase [ActionModel01] es la siguiente:


package istia.st.springmvc.models;

import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotNull;

public class ActionModel01 {

    // datos enviados
    @NotNull
    @DecimalMin(value = "0.0")
    private Double a;

    @NotNull
    @DecimalMin(value = "0.0")
    private Double b;

    // getters y setters
    ...
}
  • líneas 11 y 15: dos valores reales [a,b] que se enviarán mediante un formulario;

Volvamos al código de la acción:


    @RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax01(Locale locale, Model modèle, HttpSession session, String tempo) {
...
        // se prepara el modelo de la vista [vue-01]
        modèle.addAttribute("actionModel01", new ActionModel01());
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
...
        // vista
        return "vue-01";
}
  • líneas 6-7: se inserta una instancia de tipo [Resultats] en la plantilla;

El tipo [Resultats] que se incluye en el modelo es el siguiente:

  

package istia.st.springmvc.models;

public class Resultats {

    // datos
    private String aplusb;
    private String amoinsb;
    private String amultiplieparb;
    private String adiviseparb;
    private String heureGet;
    private String heurePost;
    private String erreur;
    private String vue;
    private String culture;

    // getters y setters
    ...
}
  • líneas 6-9: el resultado de las cuatro operaciones aritméticas con los números [a,b];
  • línea 10: la hora de la carga inicial de la página;
  • línea 11: la hora de ejecución de las cuatro operaciones aritméticas;
  • línea 12: un posible mensaje de error;
  • línea 13: la vista que se debe mostrar, si corresponde;
  • línea 14: la configuración de la vista, [fr-FR] o [en-US];

El código de la acción [/ajax-01] continúa así:


    @RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax01(ActionModel01 formulaire, Locale locale, Model modèle, HttpSession session) {
        ...
        // configuración regional
        setLocale(locale, modèle, résultats);
...
}
  • línea 5: el método [setLocale] sirve para insertar en la plantilla de la vista la configuración regional que se debe utilizar, [fr-FR] o [en-US]. Esta configuración regional está destinada al JavaScript integrado en la vista;

El método [setLocale] es el siguiente:


    private void setLocale(Locale locale, Model modèle, Resultats résultats) {
        // solo se manejan las configuraciones regionales fr-FR y en-US
        String language = locale.getLanguage();
        String country = null;
        switch (language) {
        case "fr":
            country = "FR";
            break;
        default:
            language = "en";
            country = "US";
            break;
        }
        // cultura
        résultats.setCulture(String.format("%s-%s", language, country));
}

En el modelo, la cadena [${resultats.culture}] será igual a 'fr-FR' o 'en-US'.

Volvamos a la acción [/ajax-01]:


@RequestMapping(value = "/ajax-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax01(ActionModel01 formulaire, Locale locale, Model modèle, HttpSession session) {
...
        // configuración regional
        setLocale(locale, modèle, résultats);
        // hora
        résultats.setHeureGet(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        // vista
        return "vue-01";
    }
  • línea 7: se inserta la hora de GET en la plantilla;
  • línea 9: se muestra la vista [vue-01.xml]:

7.2.3. La vista [vue-01.xml]

La vista [vue-01.xml] es la siguiente:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>Ajax-01</title>
        <link rel="stylesheet" href="/css/ajax01.css" />
        <script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="/js/jquery/jquery.validate.min.js"></script>
        <script type="text/javascript" src="/js/jquery/jquery.validate.unobtrusive.min.js"></script>
        <script type="text/javascript" src="/js/jquery/globalize/globalize.js"></script>
        <script type="text/javascript" src="/js/jquery/globalize/cultures/globalize.culture.fr-FR.js"></script>
        <script type="text/javascript" src="/js/jquery/globalize/cultures/globalize.culture.en-US.js"></script>
        <script type="text/javascript" src="/js/jquery/jquery.unobtrusive-ajax.js"></script>
        <script type="text/javascript" src="/js/json3.js"></script>
        <script type="text/javascript" src="/js/client-validation.js"></script>
        <script type="text/javascript" src="/js/local1.js"></script>
        <script th:inline="javascript">
            /*<![CDATA[*/
                    var culture = [[${resultats.culture}]];
                    Globalize.culture(culture);
                    /*]]>*/
        </script>
    </head>
    <body>
        <h2>Ajax - 01</h2>
        <p>
            <strong th:text="#{labelHeureGetCulture(${resultats.heureGet},${resultats.culture})}">
                Heure de chargement :
            </strong>
        </p>
        <h4>
            <p th:text="#{titre.part1}">
                Opérations arithmétiques sur deux nombres réels A et B positifs ou nuls
            </p>
        </h4>
        <form id="formulaire" name="formulaire" ... ">
...
        </form>
        <hr />
        <div id="resultats" />
    </body>
</html>
  • líneas 7-12: las bibliotecas jQuery de validación e internacionalización (culturas);
  • línea 15: la biblioteca [client-validation] creada en el párrafo 6.3;
  • línea 14: la biblioteca jSON utilizada por la biblioteca [client-validation]. Es opcional si se han desactivado los registros de validación;
  • línea 13: la biblioteca [Unobtrusive Ajax] de Microsoft. Esta biblioteca a veces permite evitar tener que escribir JavaScript;
  • línea 16: un archivo jS para nuestras propias necesidades;
  • líneas 17-22: para gestionar del lado del cliente las configuraciones de formato [fr-FR] y [en-US]. Ya hemos visto este código;
  • línea 27: un mensaje configurado. Los hemos analizado en el párrafo 5.18;
  • líneas 36-38: el formulario al que volveremos más adelante;
  • línea 40: el área del documento en la que el JavaScript colocará la respuesta del servidor;

7.2.4. El formulario

 

En la vista [vue-01.xml], el formulario es el siguiente:


<form id="formulaire" name="formulaire" th:action="@{/ajax-02.html}" method="post" th:object="${actionModel01}" th:attr="data-ajax='true',data-ajax-loading='#loading',data-ajax-loading-duration='0',data-ajax-method='post',data-ajax-mode='replace',data-ajax-update='#resultats', data-ajax-begin='beforeSend',data-ajax-complete='afterComplete' ">
    <table>
        <thead>
            <tr>
                <th>
                    <span th:text="#{valeur.a}"></span>
                </th>
                <th>
                    <span th:text="#{valeur.b}"></span>
                </th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>
                    <input type="text" th:field="*{a}" th:value="*{a}" data-val="true"
                        th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-min=#{actionModel01.a.min},data-val-min-value=#{actionModel01.a.min.value}" />
                </td>
                <td>
                    <input type="text" th:field="*{b}" th:value="*{b}" data-val="true"
                        th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-min=#{actionModel01.b.min},data-val-min-value=#{actionModel01.b.min.value}" />
                </td>
            </tr>
            <tr>
                <td>
                    <span class="field-validation-valid" data-valmsg-for="a" data-valmsg-replace="true"></span>
                    <span th:if="${#fields.hasErrors('a')}" th:errors="*{a}" class="error">Donnée
                        erronée
                    </span>
                </td>
                <td>
                    <span class="field-validation-valid" data-valmsg-for="b" data-valmsg-replace="true"></span>
                    <span th:if="${#fields.hasErrors('b')}" th:errors="*{b}" class="error">Donnée
                        erronée
                    </span>
                </td>
            </tr>
        </tbody>
    </table>
    <p>
        <input type="submit" th:value="#{action.calculer}" value="Calculer"></input>
        <img id="loading" style="display: none" src="/images/loading.gif" />
        <a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
    </p>
</form>

que genera el siguiente HTML:


<form id="formulaire" name="formulaire" method="post" data-ajax-update="#resultats" data-ajax-complete="afterComplete"     data-ajax-begin="beforeSend" data-ajax-loading-duration="0" data-ajax-mode="replace" data-ajax="true" data-ajax-method="post" data-ajax-loading="#loading" action="/ajax-02.html">
    <table>
        <thead>
            <tr>
                <th>
                    <span>valeur de A</span>
                </th>
                <th>
                    <span>valeur de B</span>
                </th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>
                    <input type="text" data-val="true" data-val-min="Le nombre doit être supérieur ou égal à 0" data-val-number="Format invalide" data-val-min-value="0" data-val-required="Le champ est obligatoire" value="" id="a" name="a" />
                </td>
                <td>
                    <input type="text" data-val="true" data-val-min="Le nombre doit être supérieur ou égal à 0" data-val-number="Format invalide" data-val-min-value="0" data-val-required="Le champ est obligatoire" value="" id="b" name="b" />
                </td>
            </tr>
            <tr>
                <td>
                    <span class="field-validation-valid" data-valmsg-for="a" data-valmsg-replace="true"></span>

                </td>
                <td>
                    <span class="field-validation-valid" data-valmsg-for="b" data-valmsg-replace="true"></span>

                </td>
            </tr>
        </tbody>
    </table>
    <p>
        <input type="submit" value="Calculer" />
        <img id="loading" style="display: none" src="/images/loading.gif" />
        <a href="javascript:postForm()">Calculer</a>
    </p>
</form>
  • línea 16: al campo [a] se le asocian los validadores [required], [number] y [min];
  • línea 19: lo mismo ocurre con el campo [b];

Los distintos mensajes se encuentran en los archivos [messages.properties] del proyecto:

  

[messages_fr.properties]


NotNull=Le champ est obligatoire
typeMismatch=Format invalide
actionModel01.a.min=Le nombre doit être supérieur ou égal à 0
DecimalMin.actionModel01.a=Le nombre doit être supérieur ou égal à 0
DecimalMax.actionModel01.b=Le nombre doit être supérieur ou égal à 0
actionModel01.b.min=Le nombre doit être supérieur ou égal à 0
valeur.a=valeur de A
valeur.b=valeur de B
actionModel01.a.min.value=0
actionModel01.b.min.value=0
labelHeureCalcul=Heure de calcul : 
LabelErreur=Une erreur s''est produite : [{0}]
labelAplusB=A+B=
labelAmoinsB=A-B=
labelAfoisB=A*B=
labelAdivB=A/B=
titre.part1=Opérations arithmétiques sur deux nombres réels A et B positifs ou nuls
labelHeureGetCulture=Heure de chargement : [{0}], culture : [{1}]
action.calculer=Calculer
erreur.aleatoire=erreur aléatoire
resultats=Résultats
resultats.erreur=Une erreur s''est produite : [{0}]
resultats.titre=Résultats
message.zone=Nombre d'accès : 

[messages_en.properties]


NotNull=Required field
typeMismatch=Invalid format
actionModel01.a.min=The number must be greater or equal to 0
DecimalMin.actionModel01.a=The number must be greater or equal to 0
DecimalMax.actionModel01.b=The number must be greater or equal to 0
actionModel01.b.min=The number must be greater or equal to 0
valeur.a=A value
valeur.b=B value
actionModel01.a.min.value=0
actionModel01.b.min.value=0
labelHeureCalcul=Computing hour: 
LabelErreur=There was an error: [{0}]
labelAplusB=A+B=
labelAmoinsB=A-B=
labelAfoisB=A*B=
labelAdivB=A/B=
titre.part1=Arithmetic operations on two positive or equal to zero real numbers
labelHeureGetCulture=Loading hour: [{0}], culture: [{1}]
action.calculer=Calculate
erreur.aleatoire=randomly generated error
resultats=Results
resultats.erreur=Some error occurred : [{0}]
resultats.titre=Results
message.zone=Number of hits:

Ahora, analicemos los atributos de la etiqueta [form]:


<form id="formulaire" name="formulaire" method="post" data-ajax-update="#resultats" data-ajax-complete="afterComplete" data-ajax-begin="beforeSend" data-ajax-loading-duration="0" data-ajax-mode="replace" data-ajax="true" data-ajax-method="post" data-ajax-loading="#loading" action="/ajax-02.html">

Se reconocen los atributos clásicos de la etiqueta [form]:


<form id="formulaire" name="formulaire" method="post" action="/ajax-02.html">

Se puede observar de inmediato que, si en el navegador que muestra la página el JavaScript está desactivado, el formulario se enviará a URL [/ajax-02.html]. Ahora, analicemos los demás atributos:


<form ... data-ajax-update="#resultats" data-ajax-complete="afterComplete" data-ajax-begin="beforeSend" data-ajax-loading-duration="0" data-ajax-mode="replace" data-ajax="true" data-ajax-method="post" data-ajax-loading="#loading">

Los atributos [data-ajax-xxx] son administrados por la biblioteca jS [unobtrusive-ajax], que fue importada por la vista [vue-01.xml]:


<script type="text/javascript" src="/js/jquery/jquery.unobtrusive-ajax.js"></script>

Cuando están presentes los atributos [data-ajax-xxx], el [submit] del formulario se ejecutará mediante una llamada Ajax a la biblioteca [unobtrusive-ajax]. El significado de los parámetros es el siguiente:

  • [data-ajax="true"]: es la presencia de este atributo lo que hace que el [submit] del formulario se ejecute mediante Ajax;
  • [data-ajax-method="post"]: el método del [submit]. El URL del POST será el del atributo [action="/ajax-02.html"];
  • [data-ajax-loading="#loading"]: el identificador de un campo que se mostrará mientras se espera la respuesta del servidor. El campo identificado por [loading] en la vista [vue-01.xml] es el siguiente:

<img id="loading" style="display: none" src="/images/loading.gif" />

Se trata de una imagen animada de espera que se mostrará mientras no se reciba la respuesta del servidor;

  • [data-ajax-loading-duration="0"]: el tiempo de espera en milisegundos antes de que se muestre el área [data-ajax-loading="#loading"]. En este caso, se mostrará tan pronto como comience la espera;
  • [data-ajax-begin="beforeSend"]: la función jS que se debe ejecutar antes de ejecutar la función [submit];
  • [data-ajax-complete="afterComplete"]: la función jS que se ejecutará cuando se haya recibido la respuesta;
  • [data-ajax-update="#resultats"]: el identificador del campo donde se colocará el resultado enviado por el servidor. La vista [vue-01.xml] cuenta con el siguiente campo:

<div id="resultats" />
  • [data-ajax-mode="replace"]: el modo de inserción del resultado en el campo anterior. El modo [replace] hará que el resultado «sobrescriba» lo que había antes en el campo con el identificador [resultats];

Cabe señalar que el código JavaScript [submit] solo se ejecutará si los validadores han declarado válidos los valores probados.

La biblioteca jS [unobtrusive-ajax] tiene dos objetivos:

  • asegurar que el formulario se adapte correctamente a las dos posibilidades: que JavaScript esté activado o no en el navegador;
  • evitar escribir código JavaScript. Veremos que, en este caso, no se pudo evitar.

7.2.5. La acción [/ajax-02]

Hemos visto que los valores enviados se remitían a la acción [/ajax-02]. Esta es la siguiente:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(ActionModel01 formulaire, Locale locale, Model modèle, HttpSession session) throws InterruptedException {
        // ¿ritmo?
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
        // se prepara la plantilla de la siguiente vista
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
        // se establece la configuración regional
        setLocale(locale, modèle, résultats);
        // hora
        résultats.setHeurePost(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        ...
}
  • En primer lugar, vamos a simplificar: suponemos que la acción POST que se lleva a cabo fue realizada por el JavaScript de la vista [vue-01.xml]. Volveremos sobre esta suposición un poco más adelante;
  • línea 2: los valores [a,b] enviados se colocan en el modelo [ActionModel01];
  • líneas 4-7: si el usuario había establecido un tiempo de espera en un GET anterior, este se recupera de la sesión y se aplica el tiempo de espera (línea 6). El objetivo de esto es permitir que el usuario vea el efecto del atributo [data-ajax-loading="#loading"] en el formulario;
  • líneas 9-10: se agrega un atributo [resultats] a la plantilla;
  • línea 12: se agrega la configuración regional [fr-FR] o [en-US] al modelo;
  • línea 14: se incluye la hora del POST en la plantilla;

Recordemos el tipo [Resultats] que se incluyó en la plantilla:


public class Resultats {

    // datos
    private String aplusb;
    private String amoinsb;
    private String amultiplieparb;
    private String adiviseparb;
    private String heureGet;
    private String heurePost;
    private String erreur;
    private String vue;
    private String culture;

    // getters y setters
...
}

El código de la acción [/ajax-02] continúa así:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle,    HttpSession session) throws InterruptedException {
...
        résultats.setHeurePost(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        // se genera un error una de cada dos veces
        int val = new Random().nextInt(2);
        if (val == 0) {
            // se devuelve un mensaje de error
            résultats.setErreur("erreur.aleatoire");
            return "vue-03";
        }
...
    }
  • líneas 6-11: a modo de ejemplo, se muestra cómo devolver una página de error al cliente jS. Una de cada dos veces, se devuelve la siguiente vista [vue-03.xml]:

Cabe destacar, en la línea 9, que lo que se incluye en la plantilla no es un mensaje, sino una clave de mensaje:

[messages_fr.properties]


erreur.aleatoire=erreur aléatoire

[messages_fr.properties]


erreur.aleatoire=randomly generated error

El código de la vista [vue-03.xml] es el siguiente:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h4>Résultats</h4>
        <p>
            <strong>
                <span th:text="#{labelHeureCalcul}">Heure de calcul :</span>
                <span id="heureCalcul" th:text="${resultats.heurePost}"></span>
            </strong>
        </p>
        <p style="color: red;">
            <span th:text="#{LabelErreur(#{${resultats.erreur}})}">Une erreur s'est produite :</span>
            <!-- <span id="error" th:text="${resultats.erreur}"></span> -->
        </p>
    </body>
</html>

  • En la línea 12, se observa un mensaje configurado mediante una clave de mensaje que, a su vez, se calcula. Presentamos este concepto en el párrafo 5.18, página 170.

El código de la acción [/ajax-02] continúa así:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle,    HttpSession session) throws InterruptedException {
...
        // se recuperan los valores enviados
        double a = formulaire.getA();
        double b = formulaire.getB();
        // se construye el modelo
        résultats.setAplusb(String.valueOf(a + b));
        résultats.setAmoinsb(String.valueOf(a - b));
        résultats.setAmultiplieparb(String.valueOf(a * b));
        try {
            résultats.setAdiviseparb(String.valueOf(a / b));
        } catch (RuntimeException e) {
            résultats.setAdiviseparb("NaN");
        }
        // se muestra la vista
        return "vue-02";
    }
  • líneas 5-15: las cuatro operaciones aritméticas se realizan sobre los números [a,b] y se encapsulan en la instancia [Resultats] del modelo;
  • línea 17: se devuelve la siguiente vista [vue-02.xml]:

La vista [vue-02.xml] es la siguiente:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h4>Résultats</h4>
        <p>
            <strong>
                <span th:text="#{labelHeureCalcul}">Heure de calcul :</span>
                <span id="heureCalcul" th:text="${resultats.heurePost}"></span>
            </strong>
        </p>
        <p>
            <span th:text="#{labelAplusB}">A+B=</span>
            <span id="aplusb" th:text="${resultats.aplusb}"></span>
        </p>
        <p>
            <span th:text="#{labelAmoinsB}">A-B=</span>
            <span id="amoinsb" th:text="${resultats.amoinsb}"></span>
        </p>
        <p>
            <span th:text="#{labelAfoisB}">A*B=</span>
            <span id="amultiplieparb" th:text="${resultats.amultiplieparb}"></span>
        </p>
        <p>
            <span th:text="#{labelAdivB}">A/B=</span>
            <span id="adiviseparb" th:text="${resultats.adiviseparb}"></span>
        </p>
    </body>
</html>

Ya sea que el resultado sea la vista [vue-02.xml] o la vista [vue-03.xml], este resultado HTML se coloca en el campo identificado por [resultats] en la vista [vue-01.xml], debido al atributo [data-ajax-update="#resultats"] del formulario.

7.2.6. El POST de los valores ingresados

Aquí tenemos una dificultad con los valores publicados. Trabajamos con dos formatos, [fr-FR] y [en-US], que escriben los números reales de manera diferente. Ya habíamos abordado esta dificultad cuando, en el párrafo 6.3, página 190, tuvimos que publicar números reales en dos formatos diferentes. Aquí vamos a retomar las herramientas que utilizamos en ese momento. Sin embargo, tenemos una dificultad adicional: no tenemos acceso al método que procesa los valores ingresados en POST. Por eso, hemos agregado los siguientes atributos a la etiqueta del formulario:

  • [data-ajax-begin="beforeSend"]: la función jS que se debe ejecutar antes de realizar el [submit];
  • [data-ajax-complete="afterComplete"]: la función jS que se debe ejecutar cuando se haya recibido la respuesta;

No tenemos acceso a la función jS, que enviará los valores ingresados, pero podemos escribir dos funciones jS:

  • [beforeSend]: una función jS que se ejecuta antes de la POST;
  • [afterComplete]: una función jS que se ejecuta al recibir la respuesta a la función POST;

Estas dos funciones se encuentran en un archivo [local1.js]:

  

El archivo [local1.js] inicializa el entorno jS de la vista [vue-01.xml] de la siguiente manera:


// datos globales
var loading;
var formulaire;
var résultats;
var a, b;

// al cargar el documento
$(document).ready(function() {
    // se recuperan las referencias de los distintos componentes de la página
    loading = $("#loading");
    formulaire = $("#formulaire");
    resultats = $('#resultados');
    a = $("#a");
    b = $("#b");
    // se ocultan ciertos elementos
    loading.hide();
    // se analizan los validadores del formulario
    $.validator.unobtrusive.parse(formulaire);
    // se gestionan dos configuraciones regionales [fr_FR, en_US]
    // los valores reales [a,b] son enviados por el servidor en formato anglosajón
    // se les aplica el formato francés si es necesario
    checkCulture(2);
});
  • línea 22: la función [checkCulture] se presenta un poco más adelante;

La función jS [beforeSend] será la siguiente:


function beforeSend(jqXHR, settings) {
    // antes de POST
    // los nombres deben publicarse en formato anglosajón
    var culture = Globalize.culture().name;
    if (culture === 'fr-FR') {
        checkCulture(1);
        settings.data = formulaire.serialize();
    }
}

function afterComplete(jqXHR, settings) {
    ...
}

function checkCulture(mode) {
    if (mode == 1) {
        // se convierten los números [a,b] al formato anglosajón
        var value1 = a.val().replace(",", ".");
        a.val(value1);
        var value2 = b.val().replace(",", ".");
        b.val(value2);
    }
    if (mode == 2) {
...
    }
}
  • líneas 4-6: se verifica si la cultura de la vista es [fr-FR]. En ese caso, hay que modificar los valores enviados. De hecho, si el usuario ha ingresado [1,6], hay que enviar el valor [1.6]; de lo contrario, el valor [1,6] será rechazado por el servidor. Para ello, basta con cambiar la coma de los valores enviados por un punto decimal (líneas 18-21);
  • pero no podemos quedarnos ahí. De hecho, cuando se llama a la función [beforeSend], la cadena de los valores enviados [a=val1&b=valB] ya se ha construido. Por lo tanto, debemos modificarla. Esto se hace mediante el segundo parámetro [settings] de la función;
  • línea 7: [settings.data] (settings es un parámetro de la función) representa la cadena enviada. Recreamos esta cadena con la expresión [formulaire.serialize()]. Esta expresión recorre el formulario en busca de los valores que se deben enviar y construye la cadena de POST. A continuación, tomará los nuevos valores de [a,b] con puntos decimales;

Si no se hace nada más, el servidor enviará su respuesta, la cual se mostrará correctamente. Sin embargo, ahora los valores de [a,b] tienen el punto decimal, mientras que seguimos en la configuración de [fr-FR]. Por lo tanto, si el usuario no se da cuenta y vuelve a hacer clic en [Calculer], los validadores le indicarán que los valores de [a,b] no son válidos. Lo cual es correcto. Ahí es donde interviene la función [afterComplete], que se ejecuta al recibir el resultado:


function beforeSend(jqXHR, settings) {
    // antes de POST
...
}

function afterComplete(jqXHR, settings) {
    // después de POST
    // los números deben volver al formato francés si es necesario
    var culture = Globalize.culture().name;
    if (culture === 'fr-FR') {
        checkCulture(2);
    }
}

function checkCulture(mode) {
    if (mode == 1) {
...
    }
    if (mode == 2) {
        // se ponen los números en formato francés
        var value1 = a.val().replace(".", ",");
        a.val(value1);
        var value2 = b.val().replace(".", ",");
        b.val(value2);
    }
}
  • líneas 9-12: si la configuración regional de la vista es [fr-FR], se vuelven a poner los números [a,b] en formato francés.

7.2.7. Pruebas

A continuación se muestran algunas capturas de pantalla de las pruebas:

  • en [1], la respuesta del servidor;
  • en [2], la respuesta del servidor con un mensaje de error;
  • en [3], se establece un tiempo de espera de 5 segundos. Esto significa que el servidor esperará 5 segundos antes de enviar su respuesta. En la etiqueta [form], hemos utilizado el atributo [data-ajax-loading='#loading']. El parámetro [loading] es el identificador de un área que:
    • se muestra durante todo el tiempo de espera;
    • se oculta tras recibir la respuesta del servidor;

Aquí, [loading] es el identificador de una imagen animada que se ve en [4].

7.2.8. Desactivación de JavaScript con el tema [en-US]

¿Qué sucede si se desactiva JavaScript en el navegador?

La POST de los valores ingresados se realizará según la etiqueta [form], cuyos atributos [data-ajax-attr] no se utilizarán. Todo ocurre como si tuviéramos la siguiente etiqueta [form]:


<form id="formulaire" name="formulaire" method="post" action="/ajax-02.html">

Por lo tanto, los valores ingresados se enviarán a la acción [/ajax-02]. No se habrán verificado del lado del cliente. Por lo tanto, serán los validadores del lado del servidor los que intervendrán. Ya intervenían anteriormente, pero sobre valores que ya habían sido validados del lado del cliente, por lo que eran correctos. Ya no es así.

Modificamos la acción [/ajax-02] de la siguiente manera:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle,    HttpSession session, HttpServletRequest request) throws InterruptedException {
        // ¿Solicitud Ajax?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        ...
    }
  • línea 4: la acción [/ajax-02] ahora puede invocarse mediante un POST Ajax o mediante un POST convencional. Debemos saber diferenciar estos dos casos. Esto se hace mediante los encabezados HTTP enviados por el navegador del cliente;

Al observar el tráfico de red en la consola de desarrollo de Chrome (Ctrl-Mayús-I) con JavaScript activado, se ve que el cliente envía los siguientes encabezados al momento de la llamada a POST:

Se observa arriba que:

  • se envió un encabezado [X-Requested-With] junto con [1];
  • se ha agregado un parámetro [X-Requested-With] a los valores enviados en [2];

Esto no ocurre en el caso de un POST convencional. Por lo tanto, tenemos dos opciones para recuperar la información: recuperarla de los encabezados HTTP o de los valores publicados. La línea 4 de la acción [/ajax-02] ha optado por la primera solución.

Continuemos con el código de esta acción:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle, HttpSession session, HttpServletRequest request) throws InterruptedException {
        // ¿Solicitud Ajax?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        // ¿tiempo?
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
        // Se prepara la plantilla de la siguiente vista
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
        // se establece la configuración regional
        setLocale(locale, modèle, résultats);
        // hora
        String heure = new SimpleDateFormat("hh:mm:ss").format(new Date());
        résultats.setHeurePost(heure);
        résultats.setHeureGet(heure);
        // ¿Solicitud válida?
        if (!isAjax && result.hasErrors()) {
            return "vue-01";
        }
...
  • línea 2: el parámetro [@Valid ActionModel01 formulaire] activa los validadores del lado del servidor;
  • líneas 20-22: si la llamada no es una llamada Ajax y la validación ha fallado, entonces se devuelve la vista [vue-01.xml] con los mensajes de error.

A continuación se muestra un ejemplo:

Continuemos con el análisis de la acción [/ajax-02]:


@RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle,    HttpSession session, HttpServletRequest request) throws InterruptedException {
        // ¿Solicitud Ajax?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
        // ¿Solicitud válida?
        if (!isAjax && result.hasErrors()) {
            return "vue-01";
        }
        // se genera un error una de cada dos veces
        int val = new Random().nextInt(2);
        if (val == 0) {
            // se devuelve un mensaje de error
            résultats.setErreur("erreur.aleatoire");
            if (isAjax) {
                return "vue-03";
            } else {
                résultats.setVue("vue-03");
                return "vue-01";
            }
        }
...
  • línea 14: se genera un error aleatorio;
  • línea 16: en el caso de una llamada Ajax, se devuelve la vista [vue-03.xml], que se colocará en el área identificada por [resultats];
  • línea 18: en caso de una llamada que no sea Ajax, se coloca la vista que se mostrará en el modelo de tipo [Resultats];
  • línea 19: se vuelve a mostrar la vista [vue-01.xml];

La vista [vue-01.xml] se modifica de la siguiente manera:


<div id="resultats" />
<div th:if="${resultats.vue}=='vue-02'" th:include="vue-02" />
<div th:if="${resultats.vue}=='vue-03'" th:include="vue-03" />
  • línea 3: la vista [vue-03.xml] se insertará debajo del área [resultats];

A continuación se muestra un ejemplo:

Cabe señalar que, a partir de ahora, las horas [1] y [2] son idénticas.

Continuemos con el análisis de la acción [/ajax-02]:


    @RequestMapping(value = "/ajax-02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax02(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, Model modèle, HttpSession session, HttpServletRequest request) throws InterruptedException {
        // ¿solicitud Ajax?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
        // se recuperan los valores enviados
        double a = formulaire.getA();
        double b = formulaire.getB();
        // se construye el modelo
        résultats.setAplusb(String.valueOf(a + b));
        résultats.setAmoinsb(String.valueOf(a - b));
        résultats.setAmultiplieparb(String.valueOf(a * b));
        try {
            résultats.setAdiviseparb(String.valueOf(a / b));
        } catch (RuntimeException e) {
            résultats.setAdiviseparb("NaN");
        }
        // se muestra la vista
        if (isAjax) {
            return "vue-02";
        } else {
            résultats.setVue("vue-02");
            return "vue-01";
        }
}
  • líneas 7-17: los resultados de las cuatro operaciones aritméticas se colocan en el modelo;
  • líneas 22-23: se genera la vista [vue-01.xml] (línea 22) insertando en ella la vista [vue-02.xml] (línea 22);

Esta inserción se realiza de la siguiente manera en [vue-01.xml]:


<div id="resultats" />
<div th:if="${resultats.vue}=='vue-02'" th:include="vue-02" />
<div th:if="${resultats.vue}=='vue-03'" th:include="vue-03" />
  • línea 2: la vista [vue-02.xml] se insertará debajo del área [resultats];

A continuación se muestra un ejemplo de ejecución:

 

7.2.9. Desactivación de JavaScript con la configuración [fr-FR]

Con la configuración [fr-FR] se presenta el siguiente problema:

Los valores ingresados en formato francés se han declarado inválidos. De hecho, el servidor espera valores reales en formato anglosajón. La solución es bastante compleja. Vamos a crear un filtro que:

  • intercepte la solicitud;
  • cambiar las comas en los valores enviados [a] y [b] por el punto decimal;
  • y luego pasar la nueva solicitud a la acción que debe procesarla;

En primer lugar, agregamos un campo oculto en la vista [vue-01.xml]:


<form ...>
...
</p>
    <!-- campos ocultos -->
    <input type="hidden" id="culture" name="culture" th:value="${resultats.culture}"></input>
</form>
  • línea 5: el valor de [fr-FR] o [en-US] se asigna al campo de atributo [name=culture]. Como la etiqueta [input] se encuentra en el formulario, su valor se enviará junto con los valores de [a] y [b]. De este modo, se obtendrá una cadena enviada con el siguiente formato:
culture=fr-FR&a=12,7&b=20,78

Es importante entender este punto.

A continuación, incluimos un filtro en la configuración de la aplicación:

  

El archivo [Config] se modifica de la siguiente manera:


@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
...
    @Bean
    public Filter cultureFilter() {
        return new CultureFilter();
    }
}
  • línea 7: el hecho de que el bean [cultureFilter] devuelva un tipo [Filter] lo convierte en un filtro. El bean, por su parte, puede tener cualquier nombre;

El siguiente paso es crear el filtro en sí:

  

package istia.st.springmvc.config;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.filter.OncePerRequestFilter;

public class CultureFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // siguiente controlador
        filterChain.doFilter(new CultureRequestWrapper(request), response);
    }
}
  • línea 12: extendemos la clase [OncePerRequestFilter], que es una clase de Spring, y lo que debemos hacer es redefinir el método [doFilterInternal] de esta clase;
  • línea 15: el método [doFilterInternal] recibe tres datos:
    • [HttpServletRequest request]: la consulta que se va a filtrar. Esta no se puede modificar,
    • [HttpServletResponse response]: la respuesta que se enviará al servidor. El filtro puede decidir generarla por sí mismo,
    • [FilterChain filterChain]: la cadena de filtros. Una vez que el método [doFilterInternal] ha terminado su trabajo, debe pasar la solicitud al siguiente filtro de la cadena de filtros;
  • línea 18: se crea una nueva solicitud a partir de la que se recibió ([new CultureRequestWrapper(request)]) y se le pasa al siguiente filtro. Como no se puede modificar la solicitud inicial ([HttpServletRequest request]), se crea una nueva;

La clase [CultureRequestWrapper] es la siguiente:

  

package istia.st.springmvc.config;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class CultureRequestWrapper extends HttpServletRequestWrapper {

    public CultureRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    @Override
    public String[] getParameterValues(String name) {
        // valores enviados a y b
        if (name != null && (name.equals("a") || name.equals("b"))) {
            String[] values = super.getParameterValues(name);
            String[] newValues = values.clone();
            newValues[0] = newValues[0].replace(",", ".");
            return newValues;
        }
        // otros casos
        return super.getParameterValues(name);
    }

}
  • línea 6: la clase [CultureRequestWrapper] extiende la clase [HttpServletRequestWrapper] y redefinirá algunos de sus métodos;
  • líneas 8-10: el constructor que recibe la solicitud que se va a filtrar y la pasa a la clase padre;
  • hay que entender aquí que la solicitud filtrada terminará por convertirse en un parámetro de entrada de una clase llamada servlet. Con Spring MVC, este servlet es de tipo [DispatcherServlet]. Esta clase cuenta con varios métodos para recuperar los parámetros de la solicitud: [getParameter, getParameterMap, getParameterNames, getParameterValues, ...]. Es necesario redefinir el método utilizado por el servlet. Para ello, habría que leer el código de la clase [DispatcherServlet]. Yo no lo hice y redefiní varios métodos. Finalmente, se redefinió el método [getParameterValues];
  • línea 13: el método [getParameterValues] recibe como parámetro el nombre de uno de los parámetros devueltos por el método [getParameterNames] y debe devolver la matriz de sus valores. De hecho, sabemos que un parámetro puede aparecer varias veces en una solicitud;
  • línea 18: se reemplaza la coma por un punto decimal;

A continuación se muestra un ejemplo de ejecución:

  • en [1], los valores de [a,b] se ingresan en formato francés;
  • en [2], los resultados;
  • en [3], el servidor devolvió una página con números en formato anglosajón.

Este último problema se puede resolver con Thymeleaf de la siguiente manera en la vista [vue-01.xml]


<tr>
    <td>
        <input type="text" id="a" name="a"    th:value="${resultats.culture}=='fr-FR' and ${actionModel01.a}!=null? ${#strings.replace(actionModel01.a,'.',',')} : ${actionModel01.a}" data-val="true" th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-min=#{actionModel01.a.min},data-val-min-value=#{actionModel01.a.min.value}" />
    </td>
    <td>
        <input type="text" id="b" name="b" th:value="${resultats.culture}=='fr-FR' and ${actionModel01.b}!=null? ${#strings.replace(actionModel01.b,'.',',')} : ${actionModel01.b}" data-val="true" th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-min=#{actionModel01.b.min},data-val-min-value=#{actionModel01.b.min.value}" />
    </td>
</tr>

Hay varios cambios que se deben realizar en las líneas 3 y 6. Analicemos la línea 3:

  • habíamos escrito [th:field="*{a}"]. El parámetro [th:field] establece los atributos [id, name, value] de la etiqueta HTML [input] generada. En este caso, queremos gestionar el atributo [value] por nuestra cuenta. Por lo tanto, también establecemos los atributos [id, name] por nuestra cuenta;
  • el atributo [th:value] evalúa una expresión que utiliza el operador ternario ?. Se evalúa la expresión [${resultats.culture}=='fr-FR' and ${actionModel01.b}!=null]. Si es verdadera, se le asigna al atributo [value] el valor de [actionModel01.a], donde el punto decimal se reemplaza por la coma. Si es falsa, se le asigna al atributo [value] el valor de [actionModel01.a] sin modificaciones;
  • línea 6: se repite el mismo proceso para el campo [b];

A continuación se muestra un ejemplo de ejecución:

  • En [1], los números de [a,b] conservaron la notación francesa. Este no es el caso en [2];

Este nuevo problema se resuelve de la misma manera que el anterior. Modificamos la vista [vue-03.xml] de la siguiente manera:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h4 th:text="#{resultats}">Résultats</h4>
        <p>
            <strong>
                <span th:text="#{labelHeureCalcul}">Heure de calcul :</span>
                <span id="heureCalcul" th:text="${resultats.heurePost}"></span>
            </strong>
        </p>
        <p>
            <span th:text="#{labelAplusB}">A+B=</span>
            <span id="aplusb" th:text="${resultats.culture}=='fr-FR' and ${resultats.aplusb}!=null? ${#strings.replace(resultats.aplusb,'.',',')} : ${resultats.aplusb}"></span>
        </p>
        <p>
            <span th:text="#{labelAmoinsB}">A-B=</span>
            <span id="amoinsb"     th:text="${resultats.culture}=='fr-FR' and ${resultats.amoinsb}!=null? ${#strings.replace(resultats.amoinsb,'.',',')} : ${resultats.amoinsb}"></span>
        </p>
        <p>
            <span th:text="#{labelAfoisB}">A*B=</span>
            <span id="amultiplieparb" th:text="${resultats.culture}=='fr-FR' and ${resultats.amultiplieparb}!=null? ${#strings.replace(resultats.amultiplieparb,'.',',')} : ${resultats.amultiplieparb}"></span>
        </p>
        <p>
            <span th:text="#{labelAdivB}">A/B=</span>
            <span id="adiviseparb" th:text="${resultats.culture}=='fr-FR' and ${resultats.adiviseparb}!=null? ${#strings.replace(resultats.adiviseparb,'.',',')} : ${resultats.adiviseparb}"></span>
        </p>
    </body>
</html>

A continuación, un ejemplo:

Ahora contamos con una aplicación que maneja correctamente dos formatos de fecha en un entorno que utilice o no JavaScript. Para lograrlo, fue necesario complicar considerablemente el código del lado del servidor. De ahora en adelante, siempre asumiremos que el JavaScript del navegador está activado. Esto permite hacer cosas que son imposibles en el modo solo servidor.

7.2.10. Manejo del enlace [Calculer]

Analicemos el enlace [Calculer] de la página principal [vue-01.xml]:

El código del enlace [Calculer] en la vista [vue-01.xml] es el siguiente:


<a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>

La función jS [postForm] se define en el archivo [local1.js] de la siguiente manera:


// datos globales
var loading;
var formulaire;
var résultats;
var a, b;

function postForm() {
    // ¿Formulario válido?
    if (!formulaire.validate().form()) {
        // ¿Formulario inválido? - Finalizado
        return;
    }
    // se manejan dos configuraciones regionales [fr_FR, en_US]
    // los datos reales [a,b] deben enviarse en formato anglosajón en todos los casos
    // el filtro [CultureFilter] se encargará de ello

    // se realiza una llamada Ajax manualmente
    $.ajax({
        url : '/ajax-02',
        headers : {
            '«X-Requested-With»: «XMLHttpRequest»
        },
        type : 'POST',
        data : formulaire.serialize(),
        dataType : 'html',
        beforeSend : function() {
            loading.show();
        },
        success : function(data) {
            resultats.html(data);
        },
        complete : function() {
            loading.hide();
        },
        error : function(jqXHR) {
            résultats.html(jqXHR.responseText);
        }
    })
}
  • líneas 2-5: recordemos que estos elementos fueron inicializados por la función [$(document).ready];
  • líneas 9-12: se ejecutan los validadores jS del formulario. Si alguno de los valores no es válido, la expresión [formulaire.validate().form()] devuelve el valor false. En ese caso, se anula el [submit] del formulario;
  • líneas 18-38: se realiza una llamada Ajax manualmente;
  • línea 19: el URL es el destino de la llamada Ajax;
  • líneas 20-22: una matriz de encabezados HTTP que se agregará a los que ya están presentes por defecto en la solicitud HTTP. Aquí se agrega el encabezado HTTP, que le indicará al servidor que se está realizando una llamada Ajax;
  • línea 23: el método HTTP utilizado;
  • línea 24: los datos enviados. [formulaire.serialize] crea la cadena que se enviará ([culture=fr-FR&a=12,7&b=20,89]) del formulario con el identificador [formulaire]. Aquí nos encontramos con el problema analizado anteriormente: los valores [a,b] deben enviarse en formato anglosajón. Sabemos que este problema ya se ha resuelto con la creación del filtro [cultureFilter];
  • línea 25: el tipo de datos que se espera recibir como respuesta. Sabemos que el servidor devolverá un flujo HTML;
  • línea 26: el método que se debe ejecutar al iniciar la solicitud. Aquí se indica que se debe mostrar el componente con id [loading]. Se trata de la imagen animada de espera;
  • línea 29: el método que se debe ejecutar si la solicitud Ajax tiene éxito. El parámetro [data] es la respuesta completa del servidor. Sabemos que se trata de un flujo HTML;
  • línea 30: se actualiza el componente con id [résultats] con el valor HTML del parámetro [data].
  • línea 33: se oculta la señal de espera;
  • línea 35: función que se ejecuta cuando se recibe la respuesta del servidor, independientemente de si es un éxito o un error;
  • líneas 35-37: en caso de error (el servidor devolvió una respuesta HTTP con un estado que indica que hubo un error del lado del servidor), se muestra la respuesta HTML del servidor en el área [resultats];

A continuación se muestra un ejemplo de ejecución:

7.3. Actualización de una página HTML con un flujo jSON

En el ejemplo anterior, el servidor web respondía a la solicitud Ajax HTTP con un flujo HTML. En este flujo, había datos acompañados de formato HTML. Nos proponemos retomar el ejemplo anterior, pero esta vez con respuestas jSON (JavaScript en notación de objeto) que contengan únicamente los datos. La ventaja es que así se transmiten menos bytes. Suponemos que JavaScript está habilitado en el navegador.

7.3.1. La acción [/ajax-04]

La acción [/ajax-04] es idéntica a la acción [/ajax-01], salvo que se muestra la vista [vue-04.xml] en lugar de la vista [vue-01.xml]:


@RequestMapping(value = "/ajax-04", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax04(Locale locale, Model modèle, HttpSession session, String tempo) {
        ...
        // vista
        return "vue-04";
    }

7.3.2. La vista [vue-04.xml]

 

La vista [vue-04.xml] retoma el cuerpo de la vista [vue-01.xml] con las siguientes diferencias:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        ...
        <script type="text/javascript" src="/js/local4.js"></script>
        <script th:inline="javascript">
            /*<![CDATA[*/
                    var culture = [[${resultats.culture}]];
                    Globalize.culture(culture);
                    /*]]>*/
        </script>
    </head>
    <body>
        <h2>Ajax - 04</h2>
    ...
        <form id="formulaire" name="formulaire" th:object="${actionModel01}">
...
            <p>
                <img id="loading" style="display: none" src="/images/loading.gif" />
                <a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
            </p>
            <!-- campos ocultos -->
            <input type="hidden" id="culture" name="culture" th:value="${resultats.culture}"></input>
</form>
        <hr />
        <div id="entete">
            <h4 id="titre">Résultats</h4>
            <p>
                <strong>
                    <span id="labelHeureCalcul">Heure de calcul :</span>
                    <span id="heureCalcul">12:10:87</span>
                </strong>
            </p>
        </div>
        <div id="résultats">
            <p>
                A+B=
                <span id="aplusb">16,7</span>
            </p>
            <p>
                A-B=
                <span id="amoinsb">16,7</span>
            </p>
            <p>
                A*B=
                <span id="afoisb">16,7</span>
            </p>
            <p>
                A/B=
                <span id="adivb">16,7</span>
            </p>
        </div>
        <div id="erreur">
            <p style="color: red;">
                <span id="msgErreur">xx</span>
            </p>
        </div>
    </body>
</html>
  • línea 5: el JavaScript de la vista ahora se encuentra en el archivo [local4.js];
  • línea 16: la etiqueta [form] ya no tiene los parámetros [data-ajax-attr] de la biblioteca [Unobtrusive Ajax]. No la vamos a utilizar aquí. La etiqueta [form] tampoco tiene los atributos [method] y [action], que indican cómo y dónde enviar los valores ingresados en el formulario. Esto se debe a que este será enviado por una función jS (línea 20);
  • líneas 26-57: el campo de identificación [resultats], que antes estaba vacío, ahora contiene el código HTML para mostrar los resultados;
  • líneas 26-34: el encabezado de los resultados, donde se muestra la hora del cálculo;
  • líneas 35-52: los resultados de las cuatro operaciones aritméticas;
  • líneas 53-57: un posible mensaje de error enviado por el servidor;

El código jS, que se ejecuta al cargar la vista [vue-04.xm], se encuentra en el archivo [local4.js]. Es el siguiente:


// datos globales
    var loading;
    var formulaire;
    var résultats;
    var titre;
    var labelHeureCalcul;
    var heureCalcul;
    var aplusb;
    var amoinsb;
    var afoisb;
    var adivb;
    var msgErreur;

// al cargar el documento
$(document).ready(function() {
    // se recuperan las referencias de los distintos componentes de la página
    loading = $("#loading");
    formulaire = $("#formulaire");
    résultats = $('#resultados');
    titre=$("#titre");
    labelHeureCalcul=$("#labelHeureCalcul");
    heureCalcul=$("#heureCalcul");
    aplusb=$("#aplusb");
    amoinsb=$("#amoinsb");
    afoisb=$("#afoisb");
    adivb=$("#adivb");
    msgErreur=$("#msgErreur");
    // se ocultan ciertos elementos
    résultats.hide();
    erreur.hide();
    loading.hide();
});
  • líneas 17-27: se recuperan las referencias jQuery de todos los elementos de la página;
  • línea 29: se oculta el área de resultados;
  • línea 30: así como el área de error;
  • línea 31: así como la imagen animada de espera;
  • líneas 2-12: las referencias recuperadas se definen como globales para que las demás funciones puedan utilizarlas;

7.3.3. La función jS [postForm]

El enlace [Calculer] es el siguiente:


<p>
    <img id="loading" style="display: none" src="/images/loading.gif" />
    <a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
</p>

La función jS [postForm] se define en el archivo [local.js] de la siguiente manera:


function postForm() {
    // ¿Formulario válido?
    if (!formulaire.validate().form()) {
        // ¿Formulario inválido? - Finalizado
        return;
    }
    // se realiza una llamada Ajax manualmente
    $.ajax({
        url : '/ajax-05',
        headers : {
            'Accept: 'application/json'
        },
        type : 'POST',
        data : formulaire.serialize(),
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}

// antes de la llamada Ajax
function onBegin() {
...
}

// al recibir la respuesta del servidor
// en caso de éxito
function onSuccess(data) {
...
}

// al recibir la respuesta del servidor
// en caso de error
function onError(jqXHR) {
...
}

// después de [onSuccess, onError]
function onComplete() {
...
}
  • líneas 3-6: antes de enviar los valores ingresados, se verifican. Si son incorrectos, no se ejecuta la función POST del formulario;
  • línea 9: los valores ingresados se envían a la acción [/ajax-05], que detallaremos más adelante;
  • líneas 10-12: un encabezado HTTP para indicarle al servidor que esperamos una respuesta en formato jSON;
  • línea 13: los valores ingresados se enviarán;
  • línea 14: serialización de los valores ingresados en una cadena lista para ser enviada [a=1,6&b=2,4&culture=fr-FR];
  • línea 15: el tipo de respuesta enviada por el servidor. Será jSON;
  • línea 16: la función que se debe ejecutar antes de POST;
  • línea 17: la función que se debe ejecutar al recibir la respuesta del servidor si esta es exitosa. El «éxito» de una solicitud HTTP se mide en función del estado de la respuesta HTTP del servidor. Una respuesta [HTTP/1.1 200 OK ] es una respuesta exitosa. Una respuesta [HTTP/1.1 500 Internal Server Error] es una respuesta fallida. Lo que se conoce como el estado de una respuesta HTTP es el código [200] o [500]. Algunos de estos códigos están relacionados con el «éxito», mientras que otros están relacionados con el «fracaso»;
  • línea 18: la función que se debe ejecutar al recibir la respuesta del servidor cuando el estado HTTP de dicha respuesta indica un error;
  • línea 18: la función que se debe ejecutar en último lugar, después de las funciones [onSuccess, onError] anteriores;

La función [onBegin] es la siguiente:


// antes de la llamada Ajax
function onBegin() {
    console.log("onBegin");
    // se muestra la imagen animada
    loading.show();
    // se ocultan ciertos elementos de la vista
    entete.hide();
    résultats.hide();
    erreur.hide();
}

Antes de analizar las demás funciones jS de la llamada Ajax, necesitamos conocer la respuesta enviada por la acción [/ajax-05].

7.3.4. La acción [/ajax-05]

La acción [/ajax-05] es la siguiente:


    @RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
    @ResponseBody()
    // procesa el POST de la vista [vue-04]
    public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale,    HttpServletRequest request, HttpSession session) throws InterruptedException {
        if(result.hasErrors()){
            // caso anormal: no se muestra nada
            return null;
        }
        ...
}
  • línea 2: el atributo [ResponseBody] indica que la acción [/ajax-05] devuelve ella misma la respuesta al cliente. Dado que una biblioteca jSON se encuentra entre las dependencias del proyecto, Spring Boot configura automáticamente este tipo de acciones para que devuelvan jSON. Por lo tanto, es la cadena jSON de tipo [JsonResults] (línea 4) la que se enviará al cliente;
  • línea 2: los valores enviados [a, b, culture] se encapsularán en un tipo [ActionModel01], del cual se solicita la validación [@Valid ActionModel01]. Es solo por formalidad. Partimos de la hipótesis de que JavaScript está habilitado en el navegador del cliente y, por lo tanto, cuando llegan, los valores enviados ya han sido verificados del lado del cliente. Sin embargo, podemos prever el caso de un POST no autorizado que no utilizaría nuestro cliente jS. En ese caso, la validación podría fallar;
  • líneas 5-7: en caso de error, se devuelve un flujo jSON vacío;

Continuemos con el análisis de la acción [/ajax-05]:


    @RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
    @ResponseBody()
    // procesa el POST de la vista [vue-04]
    public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale,
            HttpServletRequest request, HttpSession session) throws InterruptedException {
...
        // el contexto de la aplicación Spring
        WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
        // ¿tiempo?
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
    ...
        // se devuelve el resultado
        return résultats;
}
  • línea 8: se recupera el contexto [ctx] de la aplicación Spring. Se necesita para recuperar los mensajes de los archivos [messages.properties] a partir de una clave de mensaje y una configuración regional. Esto se hace con la siguiente sintaxis:

ctx.getMessage(clé_message, tableau_de_paramètres, locale)
    • [clé_message]: la clave del mensaje buscado;
    • [locale]: la configuración regional utilizada. Así, si esta configuración regional es [en_US], se utilizará el archivo [messages_en.properties];
    • [tableau_de_paramètres]: el mensaje obtenido se puede configurar tal como en [clé=message {0} {1}]. En este mensaje hay dos parámetros [{0} {1}]. Se deberá proporcionar como segundo parámetro de [ctx.getMessage] una matriz de dos valores;
  • líneas 10-13: si hay una pausa en la sesión, se detiene el hilo actual durante el tiempo que dure dicha pausa;

La acción [/ajax-05] continúa de la siguiente manera:


        // se prepara el modelo de la siguiente vista
        JsonResults résultats = new JsonResults();
        ...
}
  • línea 2: creación de la plantilla de la cadena jSON enviada al cliente;

La plantilla [JsonResults] es la siguiente:

 

package istia.st.springmvc.models;

public class JsonResults {

    // datos
    private String titre;
    private String labelHeureCalcul;
    private String heureCalcul;
    private String aplusb;
    private String amoinsb;
    private String afoisb;
    private String adivb;
    private String msgErreur;

    // getters y setters
...

}
  • líneas 6-13: cada uno de los campos de la clase [JsonResult] corresponde a un campo del mismo tipo en [id] dentro de la vista [vue-04.xml]:

La acción [/ajax-05] continúa de la siguiente manera:


        // se prepara el modelo de la siguiente vista
        JsonResults résultats = new JsonResults();
        // encabezado
        résultats.setTitre(ctx.getMessage("resultats.titre", null, locale));
        résultats.setLabelHeureCalcul(ctx.getMessage("labelHeureCalcul", null, locale));
        résultats.setHeureCalcul(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        // se genera un error una de cada dos veces
        int val = new Random().nextInt(2);
        if (val == 0) {
            // se devuelve un mensaje de error
            résultats.setMsgErreur(ctx.getMessage("resultats.erreur",
                    new Object[] { ctx.getMessage("erreur.aleatoire", null, locale) }, locale));
            return résultats;
}
  • línea 2: creación de la plantilla de la cadena jSON enviada al cliente;
  • líneas 4-6: se crean los mensajes del encabezado de los resultados;
  • líneas 8-14: en promedio, una de cada dos veces, se genera un mensaje de error. En ese caso, no se continúa y se devuelve la cadena jSON al cliente (línea 13);
  • línea 11: aquí tenemos un ejemplo de mensaje configurado:

erreur.aleatoire=erreur aléatoire
resultats.erreur=Une erreur s''est produite : [{0}]

La acción [/ajax-05] continúa de la siguiente manera:


        // se recuperan los valores enviados
        double a = formulaire.getA();
        double b = formulaire.getB();
        // se construye el modelo
        résultats.setAplusb(String.valueOf(a + b));
        résultats.setAmoinsb(String.valueOf(a - b));
        résultats.setAfoisb(String.valueOf(a * b));
        try {
            résultats.setAdivb(String.valueOf(a / b));
        } catch (RuntimeException e) {
            résultats.setAdivb("NaN");
        }
        // se devuelve el resultado
return résultats;
  • líneas 2-3: se recuperan los valores de [a] y [b];
  • líneas 5-12: se construyen los cuatro resultados;
  • línea 14: se envía la cadena jSON [JsonResults] al cliente;

Veamos cómo queda con el cliente [Advanced Rest Client]:

  • en [1-2], se realiza una solicitud POST a la acción [/ajax-05];
  • en [3], se envían valores incorrectos;
  • en [4], el servidor devolvió un flujo vacío;
  • en [1], se envían valores correctos;
  • en [2], el objeto jSON devuelto por el servidor, en este caso con un mensaje de error;
  • en [1], se envían valores correctos;
  • en [2], el objeto jSON devuelto por el servidor, en este caso con los cuatro resultados;
  • en [1], se envían valores correctos;
  • en [2], se ha logrado provocar una excepción del lado del servidor. Se observa que el servidor vuelve a enviar un objeto jSON. En este mensaje, se ve que el estado HTTP de la respuesta es [500], lo que indica que hubo un error del lado del servidor;

7.3.5. La función jS [postForm] - 2

Ahora que conocemos el objeto jSON devuelto por el servidor, podemos utilizarlo en JavaScript. El método [onSuccess], que se ejecuta cuando el servidor envía una respuesta con el estado HTTP [200], es el siguiente:


// al recibir la respuesta del servidor
// en caso de éxito
function onSuccess(data) {
    console.log("onSuccess");
    // se completa el área de resultados
    titre.text(data.titre);
    labelHeureCalcul.text(data.labelHeureCalcul);
    heureCalcul.text(data.heureCalcul);
    entete.show();
    // resultados sin error
    if (!data.msgErreur) {
        aplusb.text(data.aplusb);
        amoinsb.text(data.amoinsb);
        afoisb.text(data.afoisb);
        adivb.text(data.adivb);
        résultats.show();
        return;
    }
    // resultados con error
    msgErreur.text(data.msgErreur);
    erreur.show();
}
  • línea 3: el parámetro [data] es el objeto jSON devuelto por el servidor:
 

El método [onError], que se ejecuta cuando el estado de la respuesta HTTP es [500], es el siguiente:


// al recibir la respuesta del servidor
// en caso de fallo
function onError(jqXHR) {
    console.log("onError");
    // error del sistema
    msgErreur.text(jqXHR.responseText);
    erreur.show();
}
  • línea 3: el objeto JQuery [jqXHR] tiene, entre sus propiedades, las siguientes:
    • responseText: el texto de la respuesta del servidor,
    • status: el código de error devuelto por el servidor,
    • statusText: el texto asociado a este código de error;
  • línea 6: el objeto [jqXHR.responseText] es el siguiente objeto jSON:
 

7.3.6. Pruebas

Veamos algunas capturas de pantalla de la ejecución de la aplicación web:

 
 
 

7.4. Aplicación web de una sola página

7.4.1. Introducción

La tecnología Ajax permite crear aplicaciones de una sola página:

  • la primera página se obtiene mediante una solicitud convencional del navegador;
  • las páginas siguientes se obtienen mediante llamadas Ajax. Por lo tanto, al final, el navegador nunca cambia de URL y nunca carga una nueva página. A este tipo de aplicación se le denomina «aplicación de página única» (APU) o, en inglés, «Single Page Application» (SPA).

A continuación se muestra un ejemplo básico de este tipo de aplicación. La nueva aplicación tendrá dos vistas:

  • en [1], la acción [/ajax-06] nos permite acceder a la primera página, la página 1;
  • en [2], un enlace nos permite pasar a la página 2 mediante una llamada Ajax;
  • en [3], el URL no ha cambiado. La página que se muestra es la página 2;
  • en [4], un enlace nos permite regresar a la página 1 mediante una llamada Ajax;
  • en [5], el URL no ha cambiado. La página que se muestra es la página 1.

7.4.2. La acción [/ajax-06]

El código de la acción [/ajax-06] es el siguiente:


    @RequestMapping(value = "/ajax-06", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax06() {
        return "vue-06";
}
  • líneas 1-4: la acción [/ajax-06] simplemente muestra la vista [vue-06.xml];

7.4.3. La vista [vue-06.xml]

La vista [vue-06.xml] es la siguiente:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>Ajax-06</title>
        <link rel="stylesheet" href="/css/ajax01.css" />
        <script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="/js/local6.js"></script>
    </head>
    <body>
        <h3>Ajax - 06 - Navigation dans une Application à Page Unique</h3>
        <div id="content" th:include="vue-07" />
    </body>
</html>
  • línea 8: la vista utiliza un script [local6.js];
  • línea 12: se incluye la vista [vue-07.xml] en el campo de ID [content] de la vista [vue-06.xml];

7.4.4. La vista [vue-07.xml]

La vista [vue-07.xml] es la siguiente:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h4>Page 1</h4>
        <p>
            <a href="javascript:gotoPage(2)">Page 2</a>
        </p>
    </body>
</html>

7.4.5. La función jS [gotoPage]

El enlace [Page 2] de la vista [vue-07.xml] utiliza la función jS [gotoPage] definida en el siguiente archivo [local6.js]:


// datos globales
var content;

function gotoPage(num) {
    // se realiza una llamada Ajax manualmente
    $.ajax({
        url : '/ajax-07',
        type : 'POST',
        data : 'num=' + num,
        dataType : 'html',
        beforeSend : function() {
        },
        success : function(data) {
            content.html(data)
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // error del sistema
            content.html(jqXHR.responseText);
        }
    })
}

// al cargar el documento
$(document).ready(function() {
    // se recuperan las referencias de los distintos componentes de la página
    content = $("#content");
});
  • línea 28: al cargar la página, se almacena el campo de identificación [content] y se convierte en una variable global (línea 2);
  • línea 4: la función [gotoPage] recibe como parámetro el número de la página (1 o 2) que se debe mostrar en la vista actual;
  • línea 7: la función URL es el destino de la función POST;
  • línea 8: la función URL de la línea 7 se invoca mediante una POST;
  • línea 9: la cadena enviada. Se envía un parámetro llamado [num]. Su valor es el número de página (línea 4) que se debe mostrar en la vista actual;
  • línea 10: el servidor devolverá HTML, el de la página que se debe mostrar;
  • líneas 13-15: en caso de éxito (estado HTTP igual a 200), el HTML enviado por el servidor se coloca en el campo de identificación [content];
  • líneas 18-20: en caso de error (estado HTTP igual a 500), el HTML enviado por el servidor se coloca en el campo de identificación [content];

7.4.6. La acción [/ajax-07]

El código de la acción [/ajax-07] es el siguiente:


@RequestMapping(value = "/ajax-07", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax07(int num) {
        // num: número de página
        switch (num) {
        case 1:
            return "vue-07";
        case 2:
            return "vue-08";
        default:
            return "vue-07";
        }
    }
  • línea 2: se recupera el parámetro enviado que se llama [num]. Cabe recordar que el parámetro de la línea 2 debe llevar el nombre del parámetro enviado, en este caso [num]. [num] es un número de página o de vista;
  • líneas 5-6: en el caso de que sea [num==1], se devuelve la vista [vue-07.xml];
  • líneas 7-8: en el caso de que sea [num==2], se devuelve la vista [vue-08.xml];
  • líneas 9-10: en los demás casos (lo cual normalmente es imposible), se devuelve la vista [vue-07.xml];

7.4.7. La vista [vue-08.xml]

La vista [vue-08.xml] forma la página n.º 2 de la aplicación:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h4>Page 2</h4>
        <p>
            <a href="javascript:gotoPage(1)">Page 1</a>
        </p>
    </body>
</html>

7.5. Incorporar varios flujos HTML en una respuesta jSON

7.5.1. Introducción

Consideremos la siguiente aplicación:

La página [1] tiene cuatro zonas:

  • [Zone 1, Zone 3] son áreas que aparecen o desaparecen al hacer clic en el botón [Rafraîchir]. Se cuenta el número de veces que aparece cada una de estas dos zonas: [2]. La zona [Zone 1] utiliza el idioma francés, mientras que la zona [Zone 3] utiliza el idioma inglés;
  • la zona [Zone 2] está presente en todo momento;
  • la zona [Saisies] está presente de manera permanente;

El enlace [Valider] muestra la siguiente página [3]:

  • el enlace [Retour à la page 1] devuelve la página n.º 1 al estado en el que se encontraba [4];

La aplicación es de una sola página. El navegador solicita la primera página al servidor. Las siguientes se obtienen del servidor mediante llamadas Ajax.

7.5.2. La acción [/ajax-09]

  

La acción [/ajax-09] es la siguiente:


    @RequestMapping(value = "/ajax-09", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax09() {
        return "vue-09";
}

Simplemente muestra la vista [vue-09.xml].

7.5.3. Las vistas XML

  

La vista [vue-09.xml] es la página maestra de la aplicación:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>Ajax-09</title>
        <link rel="stylesheet" href="/css/ajax01.css" />
        <script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="/js/json3.js"></script>
        <script type="text/javascript" src="/js/local9.js"></script>
    </head>
    <body>
        <h3>Ajax - 09 - Navigation dans une Application à Page Unique</h3>
        <h3>avec des flux HTML embarqués dans des chaînes jSON</h3>
        <hr />
        <div id="content" th:include="vue-09-page1" />
        <img id="loading" src="/images/loading.gif" />
        <div id="erreur" style="background-color:lightgrey"></div>
    </body>
</html>
  • línea 9: el archivo JS utilizado en la aplicación;
  • línea 15: el contenido de la página maestra;
  • línea 16: una imagen animada de espera:
  • línea 17: área para mostrar un posible error;

La vista [vue-09-page1.xml] es la página 1 de la aplicación:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h2>Page 1</h2>
        <!-- zona 1 -->
        <fieldset id="zone1" style="background-color:pink">
            <legend>Zone 1</legend>
            <span id="zone1-content" th:text="xx">xx</span>
        </fieldset>
        <!-- zona 2 -->
        <fieldset id="zone2" style="background-color:lightgreen">
            <legend>Zone 2</legend>
            <span>Ce texte reste toujours présent</span>
        </fieldset>
        <!-- zona 3 -->
        <fieldset id="zone3" style="background-color:yellow">
            <legend>Zone 3</legend>
            <span id="zone3-content" th:text="zz">zz</span>
        </fieldset>
        <br />
        <p>
            <button onclick="javascript:postForm()">Rafraîchir</button>
        </p>
        <hr />
        <div id="saisies" th:include="vue-09-saisies">
        </div>
    </body>
</html>
  • líneas 6-9: el área [Zone 1]. Su contenido se coloca en el componente [id="zone1-content"];
  • líneas 11-14: el campo [Zone 2], que no cambia;
  • líneas 16-19: el campo [Zone 3]. Su contenido se coloca en el componente [id="zone3-content"];
  • línea 22: la función JS que envía el formulario;
  • línea 25: inclusión del área de entrada de datos;

Cabe señalar que la página 1 no tiene la etiqueta [form]. Todo se procesará en JavaScript.

La vista [vue-09-saisies.xml] es la siguiente:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <div id="saisies">
        <h4>Saisies :</h4>
        <p>
            Chaîne de caractères :
            <input type="text" id="text1" size="30" th:value="${value1}" />
        </p>
        <p>
            Nombre entier :
            <input type="text" id="text2" size="10" th:value="${value2}" />
        </p>
        <p>
            <a href="javascript:valider()">Valider</a>
        </p>
    </div>
</html>
  • líneas 5-8: entrada de una cadena de caracteres;
  • líneas 13-16: entrada de un número entero;
  • línea 14: la función JS que envía los valores ingresados;

Una vez más, cabe señalar que el campo de entrada no tiene la etiqueta [form].

En total, la página n.º 1 presenta dos funcionalidades:

  • [Rafraîchir]: que actualiza los campos 1 y 3. Esta acción es procesada por el servidor, que devuelve aleatoriamente:
    • el área 1 con su contador de visitas y nada para el área 3,
    • la zona 3 con su contador de accesos y nada para la zona 1,
    • las dos zonas con sus contadores de acceso;
  • [Valider]: que muestra la página 2 con los valores ingresados o un mensaje de error si los datos ingresados no son válidos;

Nos centraremos primero en el botón [Rafraîchir].

7.5.4. El código JS para la gestión del botón [Rafraîchir]

  

El código del archivo [local9.js] es el siguiente:


// variables globales
var content;
var loading;
var erreur;

// al cargar el documento
$(document).ready(function() {
    // se recuperan las referencias de los distintos componentes de la página
    loading = $("#loading");
    loading.hide();
    erreur = $("#erreur");
    erreur.hide();
    content = $("#content");
});
  • líneas 9-13: cuando se carga la página maestra, se almacenan las referencias de los tres componentes identificados por [loading, erreur, content];
  • líneas 2-4: las referencias de estos tres componentes se almacenan en variables globales. Se mantienen fijas porque las tres áreas en cuestión siempre están presentes en la página que se muestra, sin importar el momento. Como permanecen fijas, pueden calcularse en [$(document).ready] y compartirse con las demás funciones del archivo JS;

La función [postForm] gestiona el clic en el botón [Rafraîchir]:


function postForm() {
    console.log("postForm");
    // se realiza una llamada Ajax manualmente
    $.ajax({
        url : '/ajax-10',
        headers : {
            'Accept: 'application/json'
        },
        type : 'POST',
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}
  • líneas 4-15: la llamada Ajax al servidor;
  • línea 5: es la acción [ajax-10] la que procesará la POST;
  • líneas 6-8: la respuesta será jSON. El cliente JS indica que acepta los documentos jSON;
  • línea 9: se invoca la acción [ajax-10] con una operación POST;
  • línea 10: se recibirá de jSON;
  • línea 11: la función que se ejecuta antes de la llamada Ajax;
  • línea 12: la función que se ejecuta al recibir la respuesta del servidor, cuando esta es exitosa [200 OK];
  • línea 13: la función que se ejecuta al recibir la respuesta del servidor, cuando esta falla: [500 Internal server error, ...];
  • línea 14: la función que se ejecuta después de recibir la respuesta;

La función [onBegin] es la siguiente:


// antes de la llamada Ajax
function onBegin() {
    console.log("onBegin");
    // imagen de espera
    loading.show();
}

Simplemente inicia la imagen animada que muestra la espera del resultado del servidor.

7.5.5. La acción [/ajax-10]

  

La acción [/ajax-10] es la siguiente:


// la sesión
    @Autowired
    private SessionModel1 session;
    // el motor Thymeleaf / Spring
    @Autowired
    private SpringTemplateEngine engine;

    @RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
    ...
    }
  • línea 3: se inyecta la sesión. Esta tiene el siguiente tipo [SessionModel1]:
  

package istia.st.springmvc.models;

import java.io.Serializable;

import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;

@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionModel1 implements Serializable {

    private static final long serialVersionUID = 1L;
    // dos contadores
    private int cpt1 = 0;
    private int cpt3 = 0;
    // las tres zonas
    private String zone1 = "xx";
    private String zone3 = "zz";
    private String saisies;
    private boolean zone1Active = true;
    private boolean zone3Active = true;

    // getters y setters
    ...
}

La sesión [SessionModel1] almacena los siguientes elementos:

  • línea 15: el número de veces que se muestra el campo [Zone 1];
  • línea 16: el número de veces [cpt3] en que se muestra el campo [Zone 3];
  • líneas 18-20: los flujos HTML de las zonas [Zone 1], [Zone 3] y [Saisies]. Esto es necesario en la secuencia [Page 1] --> [Page 2] --> [Page 1]. Al pasar de [Page 2] a [Page 1], es necesario restaurar [Page 1] y, por lo tanto, sus tres campos;
  • líneas 21-22: dos valores booleanos que indican si los campos [Zone 1] y [Zone 3] se muestran (son visibles);

El otro elemento inyectado en el controlador [AjaxController] es el siguiente:


    // el motor Thymeleaf / Spring
    @Autowired
private SpringTemplateEngine engine;

El bean de tipo [SpringTemplateEngine] se define en el archivo de configuración [Config]:

  

Se define de la siguiente manera:


    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
        templateResolver.setPrefix("classpath:/templates/");
        templateResolver.setSuffix(".xml");
        templateResolver.setTemplateMode("HTML5");
        templateResolver.setCacheable(true);
        templateResolver.setCharacterEncoding("UTF-8");
        return templateResolver;
    }

    @Bean
    SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
        return templateEngine;
}
  • líneas 2-10: conocemos el bean de tipo [SpringResourceTemplateResolver], que nos permite definir ciertas características de las vistas;
  • líneas 13-17: el bean de tipo [SpringTemplateEngine] nos permite definir el «motor» de las vistas, la clase encargada de generar las respuestas [Thymeleaf] para los clientes. [Thymeleaf] tiene un «motor» por defecto y otro cuando se utiliza en un entorno [Spring]. Es este último el que utilizamos aquí;

La firma de la acción [/ajax-10] es la siguiente:


@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
    ...
}
  • línea 1: la acción [/ajax-10] solo acepta un POST;
  • línea 2: la acción [/ajax-10] devuelve ella misma la respuesta al cliente. Esta se transformará automáticamente en jSON;
  • línea 3: la respuesta es del tipo [JsonResult10], como se muestra a continuación:
  

package istia.st.springmvc.models;

public class JsonResult10 {

    // datos
    private String content;
    private String zone1;
    private String zone3;
    private String erreur;
    private String saisies;
    private boolean zone1Active;
    private boolean zone3Active;

    public JsonResult10() {
    }

    // getters y setters
...
}
  • línea 6: el contenido HTML del campo identificado por [content];
  • línea 7: el contenido HTML del campo [Zone 1];
  • línea 8: el contenido HTML del campo [Zone 3];
  • línea 9: el contenido HTML del campo [Erreur];
  • línea 10: el contenido HTML del campo [Saisies];
  • línea 11: valor booleano que indica si se debe mostrar el campo [Zone 1];
  • línea 12: valor booleano que indica si se debe mostrar el campo [Zone 3];

El código de la acción [/ajax-10] es el siguiente:


@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
        // contexto de Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // respuesta
        JsonResult10 result = new JsonResult10();
        // sesión
        session.setZone1(null);
        session.setZone3(null);
        session.setZone1Active(false);
        session.setZone3Active(false);
        // se devuelve una respuesta aleatoria
        int cas = new Random().nextInt(3);
        switch (cas) {
        case 0:
            // zona 1 activa
            setZone1(thymeleafContext, result);
            return result;
        case 1:
            // zona 3 activa
            setZone3(thymeleafContext, result);
            return result;
        case 2:
            // zonas 1 y 3 activas
            setZone1(thymeleafContext, result);
            setZone3(thymeleafContext, result);
            return result;
        }
        return null;
    }
  • línea 5: recuperamos el contexto [Thymeleaf]. Más adelante veremos para qué nos servirá;
  • línea 7: creamos una respuesta vacía por el momento;
  • líneas 9-12: asignamos a [null] los dos campos contenidos en la sesión e indicamos que no deben mostrarse. Estos dos campos se generarán pronto, pero es posible que solo se genere uno de ellos;
  • líneas 14-29: se generan los dos campos;
  • líneas 17-19: solo se genera el campo [Zone 1];
  • líneas 21-23: solo se genera el campo [Zone 3];
  • líneas 25-28: se generan las dos zonas [Zone 1] y [Zone 3];

El flujo HTML del campo [Zone 1] se genera mediante el siguiente método:


    private void setZone1(WebContext thymeleafContext, JsonResult10 result) {
        // zona 1 activa
        // flujo HTML
        int cpt1 = session.getCpt1() + 1;
        thymeleafContext.setVariable("cpt1", cpt1);
        thymeleafContext.setLocale(new Locale("fr", "FR"));
        String zone1 = engine.process("vue-09-zone1", thymeleafContext);
        result.setZone1(zone1);
        result.setZone1Active(true);
        // sesión
        session.setCpt1(cpt1);
        session.setZone1(zone1);
        session.setZone1Active(true);
}
  • línea 1: los parámetros son:
    • el contexto [Thymeleaf] de tipo [WebContext],
    • la respuesta al cliente en proceso de construcción de tipo [JsonResult10];
  • línea 3: se incrementa el contador [cpt1] de la sesión, que cuenta el número de veces que se muestra el campo [Zone 1];
  • línea 4: el contexto [Thymeleaf], de tipo [WebContext], se comporta de manera similar al modelo [Model] de Spring MVC. Para agregar un elemento al modelo, se utiliza [WebContext.setVariable]. Aquí, entonces, se coloca el contador [cpt1] en el modelo [Thymeleaf]. Esto permitirá evaluar la expresión Thymeleaf [${cpt1}]
  • línea 5: el contexto [Thymeleaf] tiene una configuración regional. Esto le permite evaluar expresiones del tipo [#{clé_msg}]. Aquí, se asocia el contexto de Thymeleaf a una configuración regional en francés;
  • línea 6: esta es la instrucción más interesante. El motor de Thymeleaf procesará la vista [vue-09-zone1.xml] con la plantilla y la configuración regional que acabamos de calcular y, en lugar de enviar el flujo HTML resultante al cliente, lo devuelve como una cadena de caracteres;
  • líneas 7-9: el flujo HTML del campo [Zone 1] que se acaba de calcular se almacena en la sesión y en el resultado que se enviará al cliente. Además, se indica que se debe mostrar el campo [Zone 1];
  • líneas 11-13: se almacena en la sesión la información relativa al área [Zone 1] para poder regenerarla;

La línea 7 procesa la siguiente vista [vue-09-zone1.xml]:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <span th:text="#{message.zone}"></span>
    <span th:text="${cpt1}"></span>
</html>
  • línea 3: la expresión [#{message.zone}] se evaluará mediante la variable local;
  • línea 4: la expresión [${cpt1}] se evaluará mediante la plantilla Thymeleaf;

El mensaje de clave [message.zone] se define en los archivos de mensajes [messages_fr.properties] y [messages_en.properties]:

  

[messages_fr.properties]


message.zone=Nombre d'accès : 

[messages_en.properties]


message.zone=Number of hits: 

El flujo HTML de la zona [Zone 3] se genera mediante un método similar:


    private void setZone3(WebContext thymeleafContext, JsonResult10 result) {
        // zona 3 activa
        // flujo HTML
        int cpt3 = session.getCpt3() + 1;
        thymeleafContext.setVariable("cpt3", cpt3);
        thymeleafContext.setLocale(new Locale("en", "US"));
        String zone3 = engine.process("vue-09-zone3", thymeleafContext);
        result.setZone3(zone3);
        result.setZone3Active(true);
        // sesión
        session.setCpt3(cpt3);
        session.setZone3(zone3);
        session.setZone3Active(true);
}
  • línea 6: la configuración regional de la zona [Zone 3] es la configuración regional del inglés;

7.5.6. Procesamiento de la respuesta de la acción [/ajax-10]

Volvamos al código JS de [local9.js], que procesará la respuesta del servidor:


// al recibir la respuesta del servidor
// en caso de éxito
function onSuccess(data) {
    console.log("onSuccess");
    // contenido
    if (data.content) {
        content.html(data.content);
    }
    // zona 1
    if (data.zone1Active) {
        $("#zone1").show();
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
        }
    } else {
        $("#zone1").hide();
    }
    // ¿zona 3 activa?
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // ¿Datos ingresados?
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // ¿Error?
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

Recordemos la estructura Java de la respuesta recibida en la línea 3 en la variable [data]:


public class JsonResult10 {

    // datos
    private String content;
    private String zone1;
    private String zone3;
    private String erreur;
    private String saisies;
    private boolean zone1Active;
    private boolean zone3Active;

}
  • líneas 6-8: si [data.content!=null], entonces se inicializa el campo [id=content] con ese valor. Este campo representa [Page 1] o [Page 2] en su totalidad. En este ejemplo, tenemos [data.content==null] y, por lo tanto, el campo [id=content] no se modificará y seguirá mostrando [Page 1];
  • líneas 10-17: se muestra [Zone 1] si es [data.zone1Active==true]. Si además es [data.zone1!=null], entonces se modifica el contenido de [Zone 1]; de lo contrario, permanece igual;
  • líneas 19-26: lo mismo ocurre con [Zone 3];
  • líneas 28-30: si tenemos [data.saisies!=null], entonces se regenera la zona [Saisies]. En la demostración actual, tenemos [data.saisies==null] y, por lo tanto, el área [Saisies] permanece igual que antes;
  • líneas 32-37: razonamiento similar para el campo [Erreur] con las siguientes diferencias:
    • línea 33: [data.erreur] será un mensaje de error en formato de texto;
    • línea 36: si [data.erreur==null], entonces el campo [Erreur] se oculta. De hecho, es posible que se haya mostrado durante la consulta anterior;

En caso de error del lado del servidor (HTTP, estado del tipo «500 Internal server error»), se ejecuta la siguiente función:


// al recibir la respuesta del servidor
// en caso de fallo
function onError(jqXHR) {
    console.log("onError");
    // error del sistema
    erreur.text(jqXHR.responseText);
    erreur.show();
}

Para ver un error de este tipo, modifiquemos la función [postForm] de la siguiente manera:


function postForm() {
    console.log("postForm");
    // se recuperan referencias de la página actual
    ...
    // se realiza una llamada Ajax manualmente
    $.ajax({
        url : '/ajax-10x',
        ...
    })
}
  • línea 7: colocamos un URL que no existe;

Estos son los resultados al hacer clic en el botón [Rafraîchir]:

Es interesante observar que el error también se envió en forma de cadena jSON.

El método que se ejecuta tras recibir la respuesta del servidor es el siguiente:


// después de [onSuccess, onError]
function onComplete() {
    console.log("onComplete");
    // imagen de espera
    loading.hide();
}

Simplemente se oculta la imagen animada de espera.

7.5.7. Visualización de la página [Page 2]

El código HTML del enlace [Valider] es el siguiente:


<a href="javascript:valider()">Valider</a>

La función JS [valider] es la siguiente:


// validación de los valores ingresados
function valider() {
    // valor enviado
    var post = JSON3.stringify({
        "value1" : $("#text1").val().trim(),
        "value2" : $("#text2").val().trim()
    });
    // se realiza una llamada Ajax manualmente
    $.ajax({
        url : '/ajax-11A',
        headers : {
            'Accept: 'application/json',
            'Content-Type: 'application/json'
        },
        type : 'POST',
        data : post,
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}
  • líneas 4-7: tenemos dos valores, v1 y v2, que debemos enviar: los de los componentes de entrada identificados por [#text1] y [#text2]. Vamos a hacer algo nuevo. Vamos a enviar estos dos valores en forma de una cadena jSON {"value1":v1,"value2":v2};
  • línea 10: los valores enviados se remitirán a la acción [ajax-11A];
  • línea 12: como sabemos que vamos a recibir una respuesta jSON, indicamos que podemos recibirla en formato jSON;
  • línea 13: le indicamos al servidor que le enviaremos el valor enviado en forma de cadena jSON;
  • líneas 15-16: se convierte el valor que se va a enviar en un POST;
  • línea 17: vamos a recibir un jSON;

7.5.8. La acción [ajax-11A]

La acción [ajax-11A] que procesa la cadena jSON enviada es la siguiente:


@RequestMapping(value = "/ajax-11A", method = RequestMethod.POST, consumes = "application/json")
    @ResponseBody
    public JsonResult10 ajax11A(@RequestBody @Valid PostAjax11A post, BindingResult bindingResult, Locale locale,     HttpServletRequest request, HttpServletResponse response) {
        ...
    }
  • línea 1: se indica con ["application/json"] que la acción espera un documento en formato jSON. Este documento es el valor enviado por el cliente;
  • línea 3: el valor enviado se recuperará en el siguiente objeto [PostAjax11A post]:
  

package istia.st.springmvc.models;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.Range;

public class PostAjax11A {

    // datos
    @Size(min = 4, max = 6)
    @NotNull
    private String value1;
    @Range(min = 10, max = 14)
    @NotNull
    private Integer value2;

    // getters y setters
    ...
}
  • la estructura del objeto [PostAjax11A] debe reflejar la estructura del objeto enviado {"value1":v1,"value2":v2}. Por lo tanto, se necesitan los campos [value1] (línea 13) y [value2] (línea 16);
  • se han establecido restricciones de integridad en ambos campos;

Volvamos al código de la acción [ajax-11A]:


@RequestMapping(value = "/ajax-11A", method = RequestMethod.POST, consumes = "application/json")
    @ResponseBody
    public JsonResult10 ajax11A(@RequestBody @Valid PostAjax11A post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        // contexto de Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // respuesta
        JsonResult10 result = new JsonResult10();
        // ¿El envío es válido?
        if (bindingResult.hasErrors()) {
            // se devuelve la página 1 con un error
            result.setZone1Active(session.isZone1Active());
            result.setZone3Active(session.isZone3Active());
            result.setErreur(getErreursForModel(bindingResult));
            return result;
        }
        ...
}
  • línea 3: la anotación [@RequestBody] designa el documento enviado por el cliente. Se trata del valor enviado por este en jSON. Por lo tanto, este se utilizará para construir el objeto [PostAjax11A];
  • línea 3: la anotación [@Valid] fuerza la validación del valor enviado;
  • línea 9: si la validación falla:
    • línea 13: se devuelve un mensaje de error;
    • líneas 11-12: los campos 1 y 3 se restablecen al estado en el que se encontraban (visibles o no);

El cálculo del mensaje de error se realiza de la siguiente manera:


    private String getErreursForModel(BindingResult result) {
        StringBuffer buffer = new StringBuffer();
        for (FieldError error : result.getFieldErrors()) {
            StringBuffer bufferCodes = new StringBuffer("(");
            for (String code : error.getCodes()) {
                bufferCodes.append(String.format("%s ", code));
            }
            bufferCodes.append(")");
            buffer.append(String.format("[%s:%s:%s:%s]", error.getField(), error.getRejectedValue(), bufferCodes,
                    error.getDefaultMessage()));
        }
        return buffer.toString();
}

Esta es una función que ya hemos visto antes.

La acción [ajax-11A] continúa de la siguiente manera:


@RequestMapping(value = "/ajax-11A", method = RequestMethod.POST, consumes = "application/json")
    @ResponseBody
    public JsonResult10 ajax11A(@RequestBody @Valid PostAjax11A post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        // contexto de Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // respuesta
        JsonResult10 result = new JsonResult10();
        // ¿Envío válido?
        if (bindingResult.hasErrors()) {
    ...
        }
        // se guarda el campo de entrada
        thymeleafContext.setVariable("value1", post.getValue1());
        thymeleafContext.setVariable("value2", post.getValue2());
        session.setSaisies(engine.process("vue-09-saisies", thymeleafContext));
        // se envía la página 2
        result.setContent(engine.process("vue-09-page2", thymeleafContext));
        return result;
}
  • líneas 13-14: los valores enviados se colocan en el contexto de Thymeleaf;
  • línea 15: con este contexto, se calcula la vista [vue-09-saisies] y se guarda en la sesión para poder regenerarla más adelante;
  • línea 17: la página 2 se incluye en el resultado que se enviará al cliente;

La vista [vue-09-page2.xml] es la siguiente:

  

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h2>Page 2</h2>
        <p>
            <h4>Valeurs saisies :</h4>
            <p>
                Chaîne de caractères :
                <span th:text="${value1}"></span>
            </p>
            <p>
                Nombre entier :
                <span th:text="${value2}"></span>
            </p>
            <a href="javascript:retourPage1()">Retour à la page 1</a>
        </p>
    </body>
</html>
  • En las líneas 9 y 13, se muestran los valores de [value1, value2] que la acción [/ajax-11A] ha colocado en el contexto de Thymeleaf;

7.5.9. Procesamiento de la respuesta de la acción [/ajax-11A]

Del lado del cliente, la respuesta de la acción [/ajax-10] es procesada por la función [onSuccess]:


function onSuccess(data) {
    console.log("onSuccess");
    // contenido
    if (data.content) {
        content.html(data.content);
    }
    // campo 1
    if (data.zone1Active) {
        $("#zone1").show();
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
        }
    } else {
        $("#zone1").hide();
    }
    // ¿Campo 3 activo?
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // ¿Datos ingresados?
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // ¿Error?
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

Ya hemos comentado este código. Consideremos los dos casos: respuesta con o sin error:

Con error

En este caso, la acción [/ajax-11A] envió una respuesta jSON con el formato {"zona1":null, "zona3":null,"entradas":null,"error":error,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":null}. Si seguimos el código anterior, vemos que:

  • el campo [content] no cambia. Contenía la página n.º 1;
  • se muestra el área [Erreur];
  • las zonas [Zone 1], [Zone 3] y [Saisies] se dejan tal como estaban;

Sin error

En este caso, la acción [/ajax-11A] envió una respuesta jSON con el siguiente formato: {"zona1":null, "zona3":null,"entradas":null,"error":null,"zone1Active":false,"zone3Active":false,"content":content}. Si seguimos el código anterior, vemos que:

  • se muestra la zona [content]. Contiene la página n.º 2;

A continuación se muestran tres ejemplos de ejecución:

Un caso con error de validación:

Un caso con error de POST:

Este tipo de error es diferente. Debido a que Spring no pudo convertir la cadena jSON al tipo [PostAjax11A], devolvió una respuesta HTTP con [status=400]. La acción [ajax-11A] no se ejecutó;

Un caso sin error:

7.5.10. Regresar a la página n.º 1

El enlace [Retour vers la page 1] en la página n.º 2 es el siguiente:


<a href="javascript:retourPage1()">Retour à la page 1</a>

El método JS [retourPage1] es el siguiente:


// volver a la página 1
function retourPage1() {
    // se realiza una llamada Ajax manualmente
    $.ajax({
        url : '/ajax-11B',
        headers : {
            'Accept: 'application/json',
        },
        type : 'POST',
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}

Realiza un POST, sin valor contabilizado, hacia la acción [/ajax-11B].

7.5.11. La acción [/ajax-11B]

La acción [/ajax-11B] es la siguiente:


    @RequestMapping(value = "/ajax-11B", method = RequestMethod.POST)
    @ResponseBody
    public JsonResult10 ajax11B(HttpServletRequest request, HttpServletResponse response) {
        // contexto de Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // respuesta
        JsonResult10 result = new JsonResult10();
        // restablecemos la página 1 a su estado original
        result.setContent(engine.process("vue-09-page1", thymeleafContext));
        result.setSaisies(session.getSaisies());
        result.setZone1(session.getZone1());
        result.setZone3(session.getZone3());
        result.setZone1Active(session.isZone1Active());
        result.setZone3Active(session.isZone3Active());
        return result;
}

La acción debe regenerar la página n.º 1 con sus tres zonas [Zone1, Zone3, Erreur]:

  • línea 9: la página n.º 1 se incluye en el resultado;
  • línea 10: el campo de entrada se incluye en el resultado;
  • línea 11: el campo [Zone 1] se incluye en el resultado;
  • línea 12: el campo [Zone 3] se incluye en el resultado;
  • líneas 13-14: se incluyen en el resultado el estado de los campos [Zone 1] y [Zone 3];

7.5.12. Procesamiento de la respuesta de la acción [/ajax-11B]

La respuesta de la acción [/ajax-11B] es procesada por la función [onSuccess]:


function onSuccess(data) {
    console.log("onSuccess");
    // contenido
    if (data.content) {
        content.html(data.content);
    }
    // zona 1
    if (data.zone1Active) {
        $("#zone1").show();
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
        }
    } else {
        $("#zone1").hide();
    }
    // ¿Zona 3 activa?
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // ¿Datos ingresados?
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // ¿Error?
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

La acción [/ajax-11B] envió una respuesta jSON con el formato {"zona1":zona1, "zona3":zona3,"entradas":entradas,"error":null,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":content}. Si se analiza el código anterior, se observa que:

  • el campo [content] se modifica. Anteriormente contenía la página n.º 2. Ahora contendrá la página n.º 1;
  • la zona [Erreur] queda oculta;
  • las zonas [Zone 1], [Zone 3] y [Saisies] se muestran tal como estaban;

7.6. Administrar la sesión del lado del cliente

7.6.1. Introducción

En el párrafo anterior, gestionamos una sesión cuya estructura era la siguiente:


public class SessionModel1 implements Serializable {

    // dos contadores
    private int cpt1 = 0;
    private int cpt3 = 0;
    // las tres zonas
    private String zone1 = "xx";
    private String zone3 = "zz";
    private String saisies;
    private boolean zone1Active = true;
    private boolean zone3Active = true;
...
}

Cuando hay un gran número de usuarios, la memoria ocupada por las sesiones de todos ellos puede suponer un problema. Por lo tanto, la regla es minimizar el tamaño de la memoria. El modelo APU (Aplicación de página única) permite gestionar la sesión del lado del cliente y tener un servidor web sin sesión. De hecho, la página única es cargada inicialmente por el navegador. Junto con ella, llega el archivo JavaScript que la acompaña. Como no hay recarga de la página, este archivo JS permanecerá permanentemente en el navegador tal como se cargó inicialmente. Entonces, podemos utilizar sus variables globales para almacenar información sobre las diferentes acciones del usuario. Eso es lo que veremos ahora. No solo gestionaremos la sesión del lado del cliente, sino que rediseñaremos la aplicación JS para solicitar el menor uso posible del servidor.

7.6.2. La acción [/ajax-12]

  

La acción [/ajax-12] es la siguiente:


    @RequestMapping(value = "/ajax-12", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax12() {
        return "vue-12";
}

La vista [vue-12.xml] es la siguiente:

  

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>Ajax-12</title>
        <link rel="stylesheet" href="/css/ajax01.css" />
        <script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="/js/json3.js"></script>        
        <script type="text/javascript" src="/js/local12.js"></script>
    </head>
    <body>
        <h3>Ajax - 12 - Navigation dans une Application à Page Unique</h3>
        <h3>avec des flux HTML embarqués dans une chaîne jSON</h3>
        <h3>et une session gérée par le client JS</h3>
        <hr />
        <div id="content" th:include="vue-09-page1" />
        <img id="loading" src="/images/loading.gif" />
        <div id="erreur" style="background-color:lightgrey"></div>
    </body>
</html>
  • Esta vista es idéntica a la vista [vue-09], con la única diferencia del script JS utilizado en la línea 9;

La vista que se muestra es la siguiente:

 

7.6.3. El código JS para el manejo del botón [Rafraîchir]

  

El código del archivo [local12.js] es el siguiente:


// variables globales
var content;
var loading;
var erreur;
var page1;
var page2;
var value1;
var value2;
var session = {
        "cpt1" : 0,
        "cpt3" : 0
    };

// al cargar el documento
$(document).ready(function() {
    // se recuperan las referencias de los distintos componentes de la página
    loading = $("#loading");
    loading.hide();
    erreur = $("#erreur");
    erreur.hide();
    content = $("#content");
});
  • líneas 17-21: cuando se carga la página maestra, se almacenan las referencias de los tres componentes identificados por [loading, erreur, content] en las variables globales de las líneas 2-4;
  • líneas 5-6: para almacenar las dos páginas;
  • líneas 7-8: para almacenar los dos valores enviados por el enlace [Valider];
  • línea 9: la sesión. Almacena del lado del cliente los valores de los contadores [cpt1, cpt3];

La función [postForm] gestiona el clic en el botón [Rafraîchir]:


function postForm() {
    console.log("postForm");
    // se guarda la sesión
    var post = JSON3.stringify(session);
    // se realiza una llamada Ajax manualmente
    $.ajax({
        url : '/ajax-13',
        headers : {
            'Accept: 'application/json',
            'Content-Type: 'application/json'
        },
        type : 'POST',
        data : post,
        dataType : 'json',
        beforeSend : onBegin,
        success : function(data) {
            ...
        },
        error : onError,
        complete : onComplete
    })
}

Las diferencias con la versión anterior son las siguientes:

  • la función URL de la línea 7 es diferente;
  • línea 4: se envía un valor, mientras que antes no se enviaba ninguno. Este valor es la cadena jSON de la sesión. El principio es el siguiente:
    • el cliente envía la sesión al servidor,
    • este la modifica y se la devuelve,
    • el cliente almacena la nueva sesión;
  • línea 10: se envía un documento en formato jSON (valor enviado);
  • línea 13: hay algo que enviar;
  • líneas 15-20: las funciones [beforeSend, error, complete] son las mismas que en la versión anterior. Solo cambia la función [success] (líneas 16-18);

7.6.4. La acción [/ajax-13]

  

La acción [/ajax-13] es la siguiente:


    @RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody()
    public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request,    HttpServletResponse response) {
    ...
}
  • línea 3: el parámetro [@RequestBody SessionModel2 session2] recupera la sesión enviada por el cliente. Esta tiene el siguiente tipo [SessionModel2]:
  

package istia.st.springmvc.models;

import java.io.Serializable;

public class SessionModel2 implements Serializable {

    private static final long serialVersionUID = 1L;
    // dos contadores
    private int cpt1 = 0;
    private int cpt3 = 0;

    // getters y setters
    ...
}

La sesión [SessionModel2] almacena los siguientes elementos:

  • línea 9: el número de veces que se muestra el campo [Zone 1];
  • línea 10: el número de veces [cpt3] en que se muestra el campo [Zone 3];

Continuemos con el análisis del código de la acción [/ajax-13]:


    @RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody()
    public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request,    HttpServletResponse response) {
    ...
}
  • línea 3, el tipo [JsonResult13] de la respuesta es el siguiente:
  

package istia.st.springmvc.models;

public class JsonResult13 {

    // datos
    private String page2;
    private String zone1;
    private String zone3;
    private String erreur;
    private String value1;
    private Integer value2;

    // sesión
    private SessionModel2 session;

    // getters y setters
    ...
}
  • línea 14: la sesión. El servidor la devuelve al cliente para que la guarde;
  • línea 6: el contenido HTML de la página n.º 2;
  • línea 7: el contenido HTML del área [Zone 1];
  • línea 8: el contenido HTML del área [Zone 3];
  • línea 9: el posible mensaje de error;
  • líneas 10-11: dos datos calculados por el servidor y mostrados en la página n.º 2;

Continuemos con el análisis del código de la acción [/ajax-13]:


@RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody()
    public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request,
            HttpServletResponse response) {
        // contexto de Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // respuesta
        JsonResult13 result = new JsonResult13();
        result.setSession(session2);
        // se devuelve una respuesta aleatoria
        int cas = new Random().nextInt(3);
        switch (cas) {
        case 0:
            // zona 1 activa
            setZone1B(thymeleafContext, result);
            return result;
        case 1:
            // zona 3 activa
            setZone3B(thymeleafContext, result);
            return result;
        case 2:
            // zonas 1 y 3 activas
            setZone1B(thymeleafContext, result);
            setZone3B(thymeleafContext, result);
            return result;
        }
        return null;
    }
  • línea 9: la sesión se incluye en el resultado de la acción;

El método [setZone1B] que activa el campo [Zone 1] es el siguiente:


    private void setZone1B(WebContext thymeleafContext, JsonResult13 result) {
        // se recupera la sesión
        SessionModel2 session = result.getSession();
        // zona 1 activa
        // flujo HTML
        int cpt1 = session.getCpt1() + 1;
        thymeleafContext.setVariable("cpt1", cpt1);
        thymeleafContext.setLocale(new Locale("fr", "FR"));
        String zone1 = engine.process("vue-09-zone1", thymeleafContext);
        result.setZone1(zone1);
        // sesión
        session.setCpt1(cpt1);
}
  • línea 3: se recupera la sesión. Esta se modificará en la línea 12 con el nuevo contador [cpt1]. Cabe recordar que esta sesión se enviará de vuelta al cliente;
  • línea 10: el nuevo campo [Zone 1];

El método [setZone3B], que activa el campo [Zone 3], es similar:


private void setZone3B(WebContext thymeleafContext, JsonResult13 result) {
        // se recupera la sesión
        SessionModel2 session = result.getSession();
        // zona 3 activa
        // flujo HTML
        int cpt3 = session.getCpt3() + 1;
        thymeleafContext.setVariable("cpt3", cpt3);
        thymeleafContext.setLocale(new Locale("en", "US"));
        String zone3 = engine.process("vue-09-zone3", thymeleafContext);
        result.setZone3(zone3);
        // sesión
        session.setCpt3(cpt3);
    }

7.6.5. Procesamiento de la respuesta de la acción [/ajax-13]

Del lado del cliente, la respuesta jSON de la acción [/ajax-13] se procesa mediante la siguiente función [onSuccess]:


function postForm() {
    console.log("postForm");
    // se envía la sesión
    var post = JSON3.stringify(session);
    // se realiza una llamada Ajax manualmente
    $.ajax({
    ...
        success : function(data) {
            // se guarda la sesión
            session = data.session;
            // se actualizan los dos campos
            if (data.zone1) {
                $("#zone1-content").html(data.zone1);
                $("#zone1").show();
            } else {
                $("#zone1").hide();
            }
            if (data.zone3) {
                $("#zone3").show();
                $("#zone3-content").html(data.zone3);
            } else {
                $("#zone3").hide();
            }
        },
...
    })
}
  • líneas 12-17: si el servidor ha ingresado algo en el campo [zone1] de la respuesta, entonces se debe regenerar el campo [Zone 1] y mostrarlo; de lo contrario, debe ocultarse;
  • líneas 18-23: el mismo razonamiento se aplica al área [Zone 3];

7.6.6. Visualización de la página [Page 2]

El código HTML del enlace [Valider] es el siguiente:


<a href="javascript:valider()">Valider</a>

La función JS [valider] es la siguiente:


// validación de los valores ingresados
function valider() {
    // se guarda la página 1
    page1 = content.html();
    // se guardan los valores ingresados
    value1 = $("#text1").val().trim();
    value2 = $("#text2").val().trim();
    // valor enviado
    var post = JSON3.stringify({
        "value1" : value1,
        "value2" : value2,
        "pageRequired" : page2 ? false : true
    });
    // se realiza una llamada Ajax manualmente
    $.ajax({
        url : '/ajax-14',
        headers : {
            'Accept: 'application/json',
            'Content-Type: 'application/json'
        },
        type : 'POST',
        data : post,
        dataType : 'json',
        beforeSend : onBegin,
        success : function(data) {
        ...
        },
        error : onError,
        complete : onComplete
    })
}
  • vamos a ejecutar un POST que, normalmente, nos llevará a la página n.º 2;
  • línea 4: guardamos la página n.º 1 para poder regresar a ella más adelante;
  • líneas 6-7: la operación anterior no guarda los valores ingresados, solo el código HTML de la página. Por eso, ahora guardamos los dos valores ingresados en el formulario;
  • líneas 9-13: los dos valores ingresados se colocan en una cadena jSON. Esta es la que se enviará;
  • línea 12: un parámetro para indicarle al servidor si necesitamos la página n.º 2. Procederemos de la siguiente manera. Solicitaremos la página n.º 2 una primera vez y luego la almacenaremos en la variable JS [page2]. A partir de ahí, ya no la volveremos a solicitar. Utilizaremos la página almacenada en caché. En la línea 2, [pageRequired] es igual a [true] si la variable [page2] está vacía; de lo contrario, es igual a [false];
  • cabe señalar que la sesión no se envía. De hecho, esta almacena contadores que la acción [/ajax-14] de la línea 20 no modifica;

7.6.7. La acción [/ajax-14]

La acción [/ajax-14] es la siguiente:


@RequestMapping(value = "/ajax-14", method = RequestMethod.POST)
    @ResponseBody
    public JsonResult13 ajax14(@RequestBody @Valid PostAjax14 post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        ...
    }
  • línea 3: la respuesta siempre es de tipo [JsonResult13];
  • línea 3: el valor enviado se encapsula en el siguiente tipo [PostAjax14]:

package istia.st.springmvc.models;

public class PostAjax14 extends PostAjax11A {

    // página 2
    private boolean pageRequired;

    // getters y setters
    ...
}
  • línea 3: la clase [PostAjax14] extiende la clase [PostAjax11A] de la versión anterior. Por lo tanto, tiene una estructura [value1, value2, pageRequired];

La acción [/ajax-14] continúa de la siguiente manera:


    @RequestMapping(value = "/ajax-14", method = RequestMethod.POST)
    @ResponseBody
public JsonResult13 ajax14(@RequestBody @Valid PostAjax14 post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        // contexto de Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // respuesta
        JsonResult13 result = new JsonResult13();
        // ¿El POST es válido?
        if (bindingResult.hasErrors()) {
            // se devuelve un error
            result.setErreur(getErreursForModel(bindingResult));
            return result;
        }
        // ¿Se envía la página 2?
        result.setValue1(post.getValue1());
        result.setValue2(post.getValue2());
        // ¿Página solicitada?
        if (post.isPageRequired()) {
            result.setPage2(engine.process("vue-12-page2", thymeleafContext));
        }
        return result;
}
  • líneas 9-13: si los valores enviados en [value1, value2] no son válidos, se devuelve un mensaje de error;
  • líneas 15-16: normalmente, el servidor debería realizar un cálculo con los valores enviados. En este caso, simplemente los devuelve para indicar que los ha recibido correctamente;
  • líneas 18-20: la página n.º 2 solo se devuelve si el cliente la ha solicitado. En la línea 19, la vista [vue-12-page2] es nueva:
 

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h2>Page 2</h2>
        <p>
            <h4>Valeurs saisies :</h4>
            <p>
                Chaîne de caractères :
                <span id="value1"></span>
            </p>
            <p>
                Nombre entier :
                <span id="value2"></span>
            </p>
            <a href="javascript:retourPage1()">Retour à la page 1</a>
        </p>
    </body>
</html>
  • el código XML ya no contiene valores evaluados por Thymeleaf, como sucedía anteriormente;
  • se han identificado las áreas donde colocar los valores devueltos por el servidor como [value1, value2]. Línea 9, [id='value1'] indica dónde colocar [value1]. Línea 13, lo mismo ocurre con [value2];

7.6.8. Procesamiento de la respuesta de la acción [/ajax-14]

La respuesta de la acción [/ajax-14] se procesa mediante la siguiente función [success]:


// validación de los valores ingresados
function valider() {
    ...
    // se realiza una llamada Ajax manualmente
    $.ajax({
        ...
        success : function(data) {
            // ¿Error?
            if (data.erreur) {
                // visualización del error
                erreur.html(data.erreur);
                erreur.show();
            } else {
                // sin error
                erreur.hide();
                // página 2
                if (page2) {
                    // se utiliza la página almacenada en caché
                    content.html(page2);
                } else {
                    // se guarda la página 2
                    page2 = data.page2;
                    // se muestra
                    content.html(data.page2);
                }
                // se actualiza con la información del servidor
                $("#value1").text(data.value1);
                $("#value2").text(data.value2);
            }
        },
...
    })
}
  • líneas 9-13: si el servidor devolvió un error, se muestra;
  • líneas 14-29: el caso en el que no hubo error. En ese caso, se debe mostrar la página n.º 2;
  • línea 17: se verifica si la página n.º 2 ya está almacenada en la variable [page2];
  • línea 19: en ese caso, se utiliza la variable [page2] para mostrar la página n.º 2;
  • línea 24: de lo contrario, se utiliza el campo [data.page2] proporcionado por el servidor;
  • línea 22: nos aseguramos de guardar la página n.º 2 para no tener que volver a solicitarla posteriormente;
  • líneas 27-28: en la página n.º 2, se muestran los dos datos [value1, value2] enviados por el servidor;

7.6.9. Regresar a la página n.º 1

El enlace [Retour vers la page 1] en la página n.º 2 es el siguiente:


<a href="javascript:retourPage1()">Retour à la page 1</a>

El método JS [retourPage1] es el siguiente:


// regreso a la página 1
function retourPage1() {
    // se regenera la página 1
    content.html(page1);
    // se regeneran los datos ingresados
    $("#text1").val(value1);
    $("#text2").val(value2);
}
  • Se trata de una acción JS sin interacción con el servidor, ya que la página n.º 1 se ha almacenado localmente en la variable [page1];
  • línea 4: se regenera la página n.º 1;
  • líneas 6-7: solo se había almacenado la parte HTML de la página n.º 1. No los datos ingresados. Por lo tanto, hay que regenerar estos;

7.6.10. Conclusión

Al aprovechar las posibilidades del modelo APU, logramos simplificar el servidor web, que ahora es sin estado (sin sesión) y está menos sobrecargado:

  • hemos eliminado la interacción con el servidor en la función JS ([retourPage1]);
  • el servidor genera la página n.º 2 solo una vez;

7.7. Estructuración del código JavaScript en capas

7.7.1. Introducción

El código JavaScript de la aplicación anterior está empezando a volverse complejo. Es hora de estructurarlo en capas. La aplicación seguirá siendo la misma que antes. No vamos a modificar el servidor, salvo para definir una nueva página de inicio. Vamos a rediseñar el código JS.

La nueva arquitectura será la siguiente:

7.7.2. La página de inicio

La acción que inicia la aplicación es la siguiente: [/ajax-16]:


    @RequestMapping(value = "/ajax-16", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax16() {
        return "vue-16";
}

Muestra la siguiente vista [vue-16.xml]:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>Ajax-12</title>
        <link rel="stylesheet" href="/css/ajax01.css" />
        <script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="/js/json3.js"></script>
        <script type="text/javascript" src="/js/local16-dao.js"></script>
        <script type="text/javascript" src="/js/local16-ui.js"></script>
    </head>
    <body>
        <h3>Ajax - 16 - Navigation dans une Application à Page Unique</h3>
        <h3>Structuration du code JS</h3>
        <hr />
        <div id="content" th:include="vue-09-page1" />
        <img id="loading" src="/images/loading.gif" />
        <div id="erreur" style="background-color:lightgrey"></div>
    </body>
</html>
  • líneas 9-10: el código JS se ha colocado en dos archivos diferentes:
    • [local-ui] implementa la capa [présentation],
    • [local-dao] implementa la capa [DAO];
  

7.7.3. Implementación de la capa [DAO]

7.7.4. Interfaz

La capa [DAO] en [local-dao.js] presentará la siguiente interfaz a la capa [présentation]:


function updatePage1(deferred, sendMeBack)
para actualizar la página 1 con el botón [Rafraîchir]

function getPage2(deferred, sendMeBack, value1, value2, pageRequired)
para mostrar la página 2 con el botón [Valider]

JavaScript no tiene el concepto de interfaz. Utilicé este término simplemente para indicar que la capa [présentation] se comprometía a interactuar con la capa [DAO] únicamente a través de las dos funciones anteriores.

7.7.5. Implementación de la interfaz

El esqueleto de la implementación es el siguiente:


var session = {
    "cpt1" : 0,
    "cpt3" : 0
};

// actualizar la página 1
function updatePage1(deferred, sendMeBack) {
...
}

// página 2
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
...
}

El objetivo de la capa [DAO] es ocultar a la capa [présentation] los detalles de las solicitudes HTTP realizadas al servidor web. La sesión forma parte de esos detalles. Por lo tanto, ahora es administrada por la capa [DAO].

7.7.5.1. La función [updatePage1]

La función [updatePage1] es la función que llama la capa [présentation] para actualizar la página 1. Su código es el siguiente:


// Actualizar página 1
function updatePage1(deferred, sendMeBack) {
    // consulta HTTP
    executePost(deferred, sendMeBack, '/ajax-13', session);
}
  • línea 1: la función [updatePage1] recibe dos parámetros:
    1. un objeto de tipo [jQuery.Deferred]. Este tipo de objeto almacena un estado que puede tener tres valores: ['pending', 'resolved', 'rejected']. Cuando llega a la función [updatePage1], se encuentra en el estado [pending];
    2. un objeto JS que se debe devolver en la capa [présentation];

Todas las consultas HTTP se realizan mediante la siguiente función [executePost]:


// solicitud HTTP
function executePost(deferred, sendMeBack, url, post) {
    // se realiza una llamada Ajax manualmente
    $.ajax({
        headers : {
            '«Accept»: 'application/json',
            'Content-Type: 'application/json'
        },
        url : url,
        type : 'POST',
        data : JSON3.stringify(post),
        dataType : 'json',
        success : function(data) {
            // se guarda la sesión
            if (data.session) {
                session = data.session;
            }
            // se devuelve el resultado
            deferred.resolve({
                "status" : 1,
                "data" : data,
                "sendMeBack" : sendMeBack
            });
        },
        error : function(jqXHR) {
            // se muestra el error
            deferred.resolve({
                "status" : 2,
                "data" : jqXHR.responseText,
                "sendMeBack" : sendMeBack
            });
        }
    });
}
  • línea 1: la función [executePost] ejecuta una llamada Ajax de tipo POST. Espera cuatro parámetros:
    1. un objeto de tipo [jQuery.Deferred] en el estado [pending];
    2. un objeto JS para devolver en la capa [présentation];
    3. el URL del POST;
    4. el valor que se debe publicar como objeto JS;
  • líneas 5-8: la función de publicación del jSON (línea 7) y recibe del jSON (línea 6);
  • línea 11: el valor que se va a enviar se transforma en jSON;
  • líneas 13-24: la función que se ejecuta si la llamada Ajax tiene éxito;
  • líneas 19-23: si el servidor ha devuelto una sesión, se almacena;
  • líneas 13-18: transfieren el objeto [deferred] al estado [resolved], pasando además un resultado con los siguientes campos:
    • [status]: 1 si se realiza con éxito, 2 si falla,
    • [data]: la respuesta jSON del servidor,
    • [sendMeBack]: el segundo parámetro de la función, que es un objeto que el solicitante desea recuperar;
  • líneas 17-31: la función que se ejecuta en caso de que falle la llamada Ajax. Se hace lo mismo que antes con dos diferencias:
    • [status] pasa a 2 para indicar un error;
    • [data] es, una vez más, la respuesta jSON del servidor, pero obtenida de una manera diferente;

7.7.5.2. La función [getPage2]

La función [getPage2] es la siguiente:


// página 2
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
    // solicitud HTTP
    executePost(deferred, sendMeBack, '/ajax-14', {
        "value1" : value1,
        "value2" : value2,
        "pageRequired" : pageRequired,
    });
}
  • La función recibe los siguientes parámetros:
    1. [deferred]: un objeto de tipo [jQuery.Deferred] en el estado [pending],
    2. [sendMeBack]: un objeto JS que se devolverá en la capa [présentation],
    3. [value1]: la primera entrada en la página 1,
    4. [value2]: la segunda entrada en la página 2,
    5. [pageRequired]: un valor booleano que indica al servidor si debe o no enviar el flujo HTML de la página n.º 2;
  • se invoca la función [executePost] para ejecutar la consulta HTTP necesaria;

7.7.6. La capa [présentation]

La capa [présentation] se implementa mediante el archivo [local-ui.js]. Este último retoma el código del archivo [local12.js], rediseñado para utilizar la capa [DAO] anterior. Solo cambian dos funciones: [postForm] y [valider].

7.7.6.1. La función [postForm]

La función [postForm] es la siguiente:


// actualización de la página 1
function postForm() {
    // se actualiza la página 1
    var deferred = $.Deferred();
    loading.show();
    updatePage1(deferred, {
        'remitente: «postForm»,
        'info: 10
    });
    // visualización de resultados
    deferred.done(postFormDone);
}
  • línea 4: se crea un objeto [jQuery.Deferred]. Por defecto, se encuentra en el estado [pending];
  • línea 5: se muestra la imagen de espera
  • líneas 6-9: se ejecuta la función [updatePage1]. Se pasa un objeto ficticio [sendMeBack], solo para mostrar para qué puede servir;
  • línea 11: el parámetro de la función [deferred.done] es, a su vez, una función. Es la función que se debe ejecutar cuando el estado del objeto [deferred] pasa al estado [resolved]. Acabamos de ver que la función DAO [executePost] cambiaba el estado de este objeto a [resolved] al recibir la respuesta del servidor. Esto significa que, cuando se ejecuta la función [postFormDone], ya se ha recibido la respuesta del servidor;

La función [postFormDone] es la siguiente:


function postFormDone(result) {
    // fin de espera
    loading.hide();
    // se recuperan los datos
    var data = result.data
    // para demostración
    console.log(JSON3.stringify(result.sendMeBack));
    // se analiza el estado
    switch (result.status) {
    case 1:
        // se actualizan las dos zonas
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
            $("#zone1").show();
        } else {
            $("#zone1").hide();
        }
        if (data.zone3) {
            $("#zone3").show();
            $("#zone3-content").html(data.zone3);
        } else {
            $("#zone3").hide();
        }
        break;
    case 2:
        // se muestra un error
        erreur.html(data);
        break;
    }
}
  • línea 1: el parámetro [result] recibido es el parámetro pasado al método [deferred.resolve] en la función [executePost], por ejemplo:

            // se muestra el resultado
            deferred.resolve({
                "status" : 1,
                "data" : data,
                "sendMeBack" : sendMeBack
});
  • línea 5: se recupera la respuesta del servidor;
  • líneas 10-24: aquí se encuentra el código que, en la versión anterior, estaba en la función [onSuccess] de la función [postForm];
  • líneas 25-28: aquí está el código que en la versión anterior se encontraba en la función [onError] de la función [postForm];

7.7.6.2. La función del parámetro [sendMeBack]

¿Para qué sirve el parámetro [sendMeBack]? Veamos el código de llamada a la función [updatePage1]:


// Actualización de la página 1
function postForm() {
    // se actualiza la página 1
    var deferred = $.Deferred();
    loading.show();
    updatePage1(deferred, {
        'remitente: «postForm»,
        'info: 10
    });
    // visualización de resultados
    deferred.done(postFormDone);
}

y la firma de la función [validerDone]:


function postFormDone(result) {
}

¿Cómo puede la función [postForm] pasar información a la función [postFormDone]? Esta última solo tiene un parámetro: [result]. Este es creado por la función [executePost] de la capa [DAO]. Para transmitir información a la función [postFormDone], la función [postForm] debe primero transmitirla a la función [updatePage1]. Esta es la función del parámetro [sendMeBack]. Se utiliza de la siguiente manera:


function postFormDone(result) {
    // fin de espera
    loading.hide();
    // se recuperan los datos
    var data = result.data
    // para demostración
    console.log(JSON3.stringify(result.sendMeBack));
    // se analiza el estado
    switch (result.status) {
...
  • línea 7, la función [postFormDone] ha recuperado el parámetro [sendMeBack], que inicialmente fue transmitido a la función DAO [updatePage1] por la función [postForm];

7.7.7. La función [valider]

La función [valider] es la siguiente:


// validación de los valores ingresados
function valider() {
    // se guarda la página 1
    page1 = content.html();
    // se guardan los valores ingresados
    value1 = $("#text1").val().trim();
    value2 = $("#text2").val().trim();
    // sin errores
    erreur.hide();
    // se solicita la página 2
    var deferred = $.Deferred();
    loading.show();
    getPage2(deferred, {
        '«remitente»: «validar»,
        '«info»: 20
    }, value1, value2, page2 ? false : true);
    // visualización de resultados
    deferred.done(validerDone);
}

y la función [validerDone] (línea 18) es la siguiente:


function validerDone(result) {
    // fin de la espera
    loading.hide();
    // se recuperan los datos
    var data = result.data
    // para demostración
    console.log(JSON3.stringify(result.sendMeBack));
    // se analiza el estado
    switch (result.status) {
    case 1:
        // ¿Error?
        if (data.erreur) {
            // se muestra el error
            erreur.html(data.erreur);
            erreur.show();
        } else {
            // sin error
            erreur.hide();
            // página 2
            if (page2) {
                // se utiliza la página almacenada en caché
                content.html(page2);
            } else {
                // se guarda la página 2
                page2 = data.page2;
                // se muestra
                content.html(data.page2);
            }
            // se actualiza con la información del servidor
            $("#value1").text(data.value1);
            $("#value2").text(data.value2);
        }
        break;
    case 2:
        // se muestra un error
        erreur.html(data);
        erreur.show();
        break;
    }
}
  • línea 5: se recupera la respuesta del servidor;
  • líneas 10-32: aquí está el código que, en la versión anterior, se encontraba en la función [onSuccess] de la función [valider];
  • líneas 34-38: aquí está el código que en la versión anterior se encontraba en la función [onError] de la función [valider];

7.7.8. Pruebas

La aplicación sigue funcionando como antes y, en la consola de Chrome, se pueden ver los parámetros [sendMeBack] de las funciones [postForm] y [valider]:

 

7.8. Conclusion

Volvamos al esquema general de una aplicación Spring MVC:

Gracias al JavaScript integrado en las páginas HTML y ejecutado en el navegador, y gracias al modelo APU, se puede trasladar código al navegador y lograr la siguiente arquitectura:

  • tenemos una arquitectura cliente [2] / servidor [1] en la que el cliente y el servidor se comunican mediante jSON;
  • en [1], la capa web de Spring MVC entrega vistas, fragmentos de vista y datos en jSON;
  • en [2]: el código JavaScript integrado en la vista que se carga al iniciar la aplicación puede estructurarse en capas:
    • la capa [présentation] se encarga de las interacciones con el usuario,
    • La capa [DAO] se encarga del acceso a los datos a través del servidor web [1],
    • la capa [métier] puede no existir o asumir algunas de las funciones no confidenciales de la capa [métier] del servidor para aliviar su carga;
  • el cliente [2] puede almacenar ciertas vistas en caché para, una vez más, aliviar la carga del servidor. Este gestiona la sesión;