Skip to content

7. Implementação de Ajax em uma aplicação Spring MVC

7.1. O papel do AJAX em uma aplicação web

Até o momento, os exemplos de aprendizado estudados apresentavam a seguinte arquitetura:

Para passar de uma visualização [Vue1] para uma visualização [Vue2], o navegador:

  • envia uma solicitação para a aplicação web;
  • recebe a visualização [Vue2] e a exibe no lugar da visualização [Vue1].

Esse é o esquema clássico:

  • solicitação do navegador;
  • geração de uma visualização pelo servidor web em resposta ao cliente;
  • exibição dessa nova visualização pelo navegador.

Há alguns anos, surgiu outro modo de interação entre o navegador e o servidor web: AJAX (Asynchronous JavaScript and XML). Trata-se, na verdade, de interações entre a visualização exibida pelo navegador e o servidor web. O navegador continua fazendo o que sabe fazer, ou seja, exibir uma visualização HTML, mas agora é controlado por JavaScript incorporado na visualização HTML exibida. O esquema é o seguinte:

  • em [1], ocorre um evento na página exibida no navegador (clique em um botão, alteração de um texto, etc.). Esse evento é interceptado pelo JavaScript (jS) incorporado à página;
  • em [2], o código JavaScript faz uma solicitação HTTP, da mesma forma que o navegador teria feito. A solicitação é assíncrona: o usuário pode continuar interagindo com a página sem ficar bloqueado pela espera pela resposta à solicitação HTTP. A solicitação segue o processo clássico de processamento. Nada (ou quase nada) a distingue de uma solicitação clássica;
  • em [3], uma resposta é enviada ao cliente jS. Em vez de uma visualização HTML completa, trata-se, na verdade, de uma visualização HTML parcial, um fluxo XML ou jSON (Notação de Objeto JavaScript) que é enviado;
  • em [4], o JavaScript recupera essa resposta e a utiliza para atualizar uma região da página HTML exibida.

Para o usuário, há uma mudança na visualização, pois o que ele vê mudou. No entanto, não há recarregamento total da página, mas simplesmente uma modificação parcial da página exibida. Isso contribui para dar fluidez e interatividade à página: como não há recarregamento total da página, é possível gerenciar eventos que antes não eram gerenciados. Por exemplo, oferecer ao usuário uma lista de opções à medida que ele digita caracteres em um campo de entrada. A cada novo caractere digitado, é enviada uma solicitação AJAX ao servidor, que então retorna outras sugestões. Sem o Ajax, esse tipo de auxílio à digitação era impossível anteriormente. Não era possível recarregar uma nova página a cada caractere digitado.

7.2. Atualização de uma página com um fluxo HTML

7.2.1. As visualizações

Propomos estudar a seguinte aplicação:

  • em [1], a hora de carregamento da página;
  • em [2], realizam-se as quatro operações aritméticas com dois números reais A e B;
  • em [3], a resposta do servidor é exibida em uma área da página;
  • em [4], a hora do cálculo. Essa hora é diferente da hora de carregamento da página [5]. Esta última é igual a [1], o que mostra que a região [6] não foi recarregada. Além disso, as regiões URL e [7] da página não sofreram alterações.

7.2.2. A ação [/ajax-01]

  

O controlador [Ajax.java] define a seguinte ação [/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) {
        // tempo 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));
            }
        }
        // prepara-se o modelo da visualização [vue-01]
        ...
}
  • linha 2: a ação [/ajax-01] aceita apenas um único parâmetro, [tempo]. Trata-se do tempo, em milissegundos, que o servidor deverá aguardar antes de enviar os resultados das operações aritméticas;
  • linha 4: o parâmetro [tempo] é opcional;
  • linhas 5-12: verifica-se se o valor do parâmetro [tempo] é válido;
  • linhas 13-15: se for o caso, o valor do tempo de espera é armazenado na sessão. Isso significa que ele permanecerá válido até que seja alterado;

O código da ação [/ajax-01] continua da seguinte forma:


    @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) {
        // tempo válido?
...
        // preparando o modelo da visualização [vue-01]
        modèle.addAttribute("actionModel01", new ActionModel01());
...
        // visualização
        return "vue-01";
}

A classe [ActionModel01] serve principalmente para encapsular os valores enviados pela ação [/ajax-01]. Aqui, não há nada sendo enviado. Cria-se uma classe vazia que é inserida no modelo, pois a vista [vue-01.xml] a utiliza. A classe [ActionModel01] é a seguinte:


package istia.st.springmvc.models;

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

public class ActionModel01 {

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

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

    // getters e setters
    ...
}
  • linhas 11 e 15: dois valores reais [a,b] que serão enviados por um formulário;

Voltemos ao código da ação:


    @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) {
...
        // preparando o modelo da visualização [vue-01]
        modèle.addAttribute("actionModel01", new ActionModel01());
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
...
        // visualização
        return "vue-01";
}
  • linhas 6-7: insere-se uma instância do tipo [Resultats] no modelo;

O tipo [Resultats] inserido no modelo é o seguinte:

  

package istia.st.springmvc.models;

public class Resultats {

    // dados
    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 e setters
    ...
}
  • linhas 6-9: o resultado das quatro operações aritméticas nos números [a,b];
  • linha 10: a hora do carregamento inicial da página;
  • linha 11: a hora de execução das quatro operações aritméticas;
  • linha 12: uma eventual mensagem de erro;
  • linha 13: a possível visualização a ser exibida;
  • linha 14: a cultura da visualização, [fr-FR] ou [en-US];

O código da ação [/ajax-01] continua assim:


    @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) {
        ...
        // configuração regional
        setLocale(locale, modèle, résultats);
...
}
  • linha 5: o método [setLocale] serve para inserir no modelo da visualização a cultura a ser utilizada, [fr-FR] ou [en-US]. Essa cultura é destinada ao JavaScript incorporado na visualização;

O método [setLocale] é o seguinte:


    private void setLocale(Locale locale, Model modèle, Resultats résultats) {
        // só são suportadas as configurações regionais fr-FR e 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));
}

No modelo, teremos a string [${resultats.culture}] igual a 'fr-FR' ou 'en-US'.

Voltemos à ação [/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) {
...
        // configuração regional
        setLocale(locale, modèle, résultats);
        // hora
        résultats.setHeureGet(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        // visualização
        return "vue-01";
    }
  • linha 7: insere-se a hora do GET no modelo;
  • linha 9: exibe-se a visualização [vue-01.xml]:

7.2.3. A visualização [vue-01.xml]

A visualização [vue-01.xml] é a seguinte:


<!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>
  • linhas 7-12: as bibliotecas jQuery de validação e internacionalização (culturas);
  • linha 15: a biblioteca [client-validation] criada no parágrafo 6.3;
  • linha 14: a biblioteca jSON utilizada pela biblioteca [client-validation]. Ela é opcional caso os registros de validação tenham sido desativados;
  • linha 13: a biblioteca [Unobtrusive Ajax] da Microsoft. Essa biblioteca permite, em alguns casos, evitar a necessidade de escrever JavaScript;
  • linha 16: um arquivo jS para nossas próprias necessidades;
  • linhas 17-22: para gerenciar, no lado do cliente, as configurações de formato [fr-FR] e [en-US]. Já nos deparamos com esse código;
  • linha 27: uma mensagem configurada. Analisamos isso no parágrafo 5.18;
  • linhas 36-38: o formulário ao qual voltaremos mais tarde;
  • linha 40: o campo do documento no qual o JavaScript colocará a resposta do servidor;

7.2.4. O formulário

 

Na visualização [vue-01.xml], o formulário é o seguinte:


<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 gera o seguinte 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>
  • linha 16: ao campo [a] estão associados os validadores [required], [number] e [min];
  • linha 19: o mesmo se aplica ao campo [b];

As diversas mensagens podem ser encontradas nos arquivos [messages.properties] do projeto:

  

[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:

Agora, vamos examinar os atributos da tag [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">

Reconhecemos os atributos clássicos da tag [form]:


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

Pode-se notar imediatamente que, se no navegador que exibe a página o JavaScript estiver desativado, o formulário será enviado para o URL [/ajax-02.html]. Agora, vamos analisar os demais 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">

Os atributos [data-ajax-xxx] são gerenciados pela biblioteca jS [unobtrusive-ajax], que foi importada pela visualização [vue-01.xml]:


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

Quando os atributos [data-ajax-xxx] estão presentes, o [submit] do formulário será executado por uma chamada Ajax da biblioteca [unobtrusive-ajax]. O significado dos parâmetros é o seguinte:

  • [data-ajax="true"]: é a presença desse atributo que faz com que o [submit] do formulário seja executado via Ajax;
  • [data-ajax-method="post"]: o método do [submit]. O URL do POST será o do atributo [action="/ajax-02.html"];
  • [data-ajax-loading="#loading"]: o ID de um campo a ser exibido enquanto se aguarda a resposta do servidor. O campo identificado por [loading] na visualização [vue-01.xml] é o seguinte:

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

Trata-se de uma imagem animada de espera que será exibida enquanto a resposta do servidor não for recebida;

  • [data-ajax-loading-duration="0"]: o tempo de espera em milissegundos antes que a área [data-ajax-loading="#loading"] seja exibida. Neste caso, ela será exibida assim que a espera começar;
  • [data-ajax-begin="beforeSend"]: a função jS a ser executada antes de executar a função [submit];
  • [data-ajax-complete="afterComplete"]: a função jS a ser executada quando a resposta for recebida;
  • [data-ajax-update="#resultats"]: o identificador do campo onde o resultado enviado pelo servidor será inserido. A visualização [vue-01.xml] possui o seguinte campo:

<div id="resultats" />
  • [data-ajax-mode="replace"]: o modo de inserção do resultado na área anterior. O modo [replace] fará com que o resultado “substitua” o que havia anteriormente na área com o ID [resultats];

É importante observar que o JavaScript [submit] só será executado se os validadores tiverem declarado válidos os valores testados.

A biblioteca jS [unobtrusive-ajax] tem dois objetivos:

  • garantir que o formulário se adapte corretamente às duas possibilidades: ativação ou não do JavaScript no navegador;
  • evitar a escrita de JavaScript. Veremos que, neste caso, isso não pôde ser evitado.

7.2.5. A ação [/ajax-02]

Vimos que os valores enviados eram direcionados para a ação [/ajax-02]. Ela é a seguinte:


@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 {
        // clima?
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
        // preparando o modelo da próxima vista
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
        // definindo a localidade
        setLocale(locale, modèle, résultats);
        // hora
        résultats.setHeurePost(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        ...
}
  • Vamos simplificar inicialmente: supomos que a ação POST que ocorre tenha sido de fato executada pelo JavaScript da visualização [vue-01.xml]. Voltaremos a essa hipótese um pouco mais adiante;
  • linha 2: os valores [a,b] enviados são inseridos no modelo [ActionModel01];
  • linhas 4-7: se o usuário tiver definido um tempo de espera em um GET anterior, esse valor é recuperado da sessão e o tempo de espera é aplicado (linha 6). O objetivo disso é permitir que o usuário veja o efeito do atributo [data-ajax-loading="#loading"] no formulário;
  • linhas 9-10: insere-se um atributo [resultats] no modelo;
  • linha 12: insere-se a cultura [fr-FR] ou [en-US] no modelo;
  • linha 14: insira a hora do POST no modelo;

Lembre-se do tipo [Resultats] inserido no modelo:


public class Resultats {

    // dados
    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 e setters
...
}

O código da ação [/ajax-02] continua assim:


@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()));
        // gera-se um erro a cada duas vezes
        int val = new Random().nextInt(2);
        if (val == 0) {
            // retorna-se uma mensagem de erro
            résultats.setErreur("erreur.aleatoire");
            return "vue-03";
        }
...
    }
  • linhas 6-11: para o exemplo, mostramos como retornar uma página de erro ao cliente jS. Em uma de cada duas vezes, retornamos a seguinte visualização [vue-03.xml]:

Observe, na linha 9, que não se trata de uma mensagem inserida no modelo, mas de uma chave de mensagem:

[messages_fr.properties]


erreur.aleatoire=erreur aléatoire

[messages_fr.properties]


erreur.aleatoire=randomly generated error

O código da visualização [vue-03.xml] é o seguinte:


<!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="erro" th:text="${resultats.erreur}"></span> -->
        </p>
    </body>
</html>

  • na linha 12, observa-se uma mensagem configurada por uma chave de mensagem que, por sua vez, é calculada. Introduzimos esse conceito no parágrafo 5.18, página 170.

O código da ação [/ajax-02] continua assim:


@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 {
...
        // recupera-se os valores enviados
        double a = formulaire.getA();
        double b = formulaire.getB();
        // construímos o 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");
        }
        // exibe-se a visualização
        return "vue-02";
    }
  • linhas 5-15: as quatro operações aritméticas são realizadas sobre os números [a,b] e encapsuladas na instância [Resultats] do modelo;
  • linha 17: é retornada a seguinte visualização [vue-02.xml]:

A visualização [vue-02.xml] é a seguinte:


<!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>

Seja o resultado a visualização [vue-02.xml] ou a visualização [vue-03.xml], esse resultado HTML é inserido no campo identificado por [resultats] na visualização [vue-01.xml], devido ao atributo [data-ajax-update="#resultats"] do formulário.

7.2.6. O POST dos valores inseridos

Temos aqui uma dificuldade com os valores lançados. Trabalhamos com duas culturas, [fr-FR] e [en-US], que representam os números reais de maneiras diferentes. Já havíamos abordado essa dificuldade quando, no parágrafo 6.3, página 190, foi necessário lançar valores reais em duas culturas diferentes. Vamos retomar aqui as ferramentas utilizadas naquela ocasião. Mas temos uma dificuldade adicional: não temos acesso ao método que processa os valores inseridos no POST. É por isso que adicionamos os seguintes atributos à tag do formulário:

  • [data-ajax-begin="beforeSend"]: a função jS a ser executada antes de realizar o [submit];
  • [data-ajax-complete="afterComplete"]: a função jS a ser executada quando a resposta for recebida;

Não temos acesso à função jS, que enviará os valores inseridos, mas podemos escrever duas funções jS:

  • [beforeSend]: uma função jS executada antes da POST;
  • [afterComplete]: uma função jS executada ao receber a resposta à função POST;

Essas duas funções estão contidas no arquivo [local1.js]:

  

O arquivo [local1.js] inicializa o ambiente jS da visualização [vue-01.xml] da seguinte maneira:


// dados globais
var loading;
var formulaire;
var résultats;
var a, b;

// ao carregar o documento
$(document).ready(function() {
    // recupera-se as referências dos diferentes componentes da página
    loading = $("#loading");
    formulaire = $("#formulaire");
    resultats = $('#resultados');
    a = $("#a");
    b = $("#b");
    // ocultamos alguns elementos
    loading.hide();
    // analisam-se os validadores do formulário
    $.validator.unobtrusive.parse(formulaire);
    // gerencia-se duas configurações regionais [fr_FR, en_US]
    // os valores reais [a,b] são enviados pelo servidor no formato anglo-saxão
    // eles são convertidos para o formato francês, se necessário
    checkCulture(2);
});
  • linha 22: a função [checkCulture] é apresentada um pouco mais adiante;

A função jS [beforeSend] será a seguinte:


function beforeSend(jqXHR, settings) {
    // antes do POST
    // os números devem ser apresentados no formato anglo-saxão
    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) {
        // colocamos os números [a,b] no formato anglo-saxão
        var value1 = a.val().replace(",", ".");
        a.val(value1);
        var value2 = b.val().replace(",", ".");
        b.val(value2);
    }
    if (mode == 2) {
...
    }
}
  • linhas 4-6: verifica-se se a cultura da visualização é [fr-FR]. Nesse caso, é preciso alterar os valores lançados. De fato, se o usuário digitou [1,6], deve-se lançar o valor [1.6]; caso contrário, o valor [1,6] será rejeitado pelo servidor. Para isso, basta substituir a vírgula dos valores enviados por um ponto decimal (linhas 18-21);
  • mas não podemos parar por aí. De fato, quando a função [beforeSend] é chamada, a sequência de valores enviados [a=val1&b=valB] já foi construída. Portanto, precisamos modificá-la. Isso é feito por meio do segundo parâmetro [settings] da função;
  • linha 7: [settings.data] (settings é um parâmetro da função) representa a sequência de caracteres enviada. Recriamos essa string com a expressão [formulaire.serialize()]. Essa expressão percorre o formulário em busca dos valores a serem enviados e constrói a string de POST. Em seguida, ela irá obter os novos valores de [a,b] com pontos decimais;

Se não for feito mais nada, o servidor enviará sua resposta, que será exibida corretamente. Só que agora os valores de [a,b] estão com o ponto decimal, enquanto ainda estamos na configuração cultural de [fr-FR]. Portanto, se o usuário não perceber isso e clicar novamente em [Calculer], os validadores informarão que os valores [a,b] são inválidos. O que está correto. É aí que entra a função [afterComplete], executada ao receber o resultado:


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

function afterComplete(jqXHR, settings) {
    // depois do POST
    // os números devem ser convertidos para o formato francês, se necessário
    var culture = Globalize.culture().name;
    if (culture === 'fr-FR') {
        checkCulture(2);
    }
}

function checkCulture(mode) {
    if (mode == 1) {
...
    }
    if (mode == 2) {
        // colocamos os números no formato francês
        var value1 = a.val().replace(".", ",");
        a.val(value1);
        var value2 = b.val().replace(".", ",");
        b.val(value2);
    }
}
  • linhas 9-12: se a configuração da visualização for [fr-FR], os números [a,b] são convertidos para o formato francês.

7.2.7. Testes

Aqui estão algumas capturas de tela dos testes:

  • em [1], a resposta do servidor;
  • em [2], a resposta do servidor com uma mensagem de erro;
  • em [3], define-se um tempo de espera de 5 segundos. Isso significa que o servidor aguardará 5 segundos antes de enviar sua resposta. Na tag [form], utilizamos o atributo [data-ajax-loading='#loading']. O parâmetro [loading] é o identificador de uma área que fica:
    • exibida durante todo o tempo de espera;
    • oculta após o recebimento da resposta do servidor;

Aqui, [loading] é o identificador de uma imagem animada que pode ser vista em [4].

7.2.8. Desativação do JavaScript com o código [en-US]

O que acontece se desativarmos o JavaScript do navegador?

A POST dos valores inseridos será realizada de acordo com a tag [form], cujos atributos [data-ajax-attr] não serão utilizados. Tudo ocorre como se tivéssemos a seguinte tag [form]:


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

Os valores inseridos serão, portanto, lançados na ação [/ajax-02]. Eles não terão sido verificados no lado do cliente. Portanto, serão os validadores do lado do servidor que entrarão em ação. Eles já atuavam anteriormente, mas sobre valores já validados no lado do cliente, portanto corretos. Esse não é mais o caso.

Modificamos a ação [/ajax-02] da seguinte maneira:


@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 {
        // solicitação Ajax?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        ...
    }
  • linha 4: a ação [/ajax-02] agora pode ser chamada por meio de um POST Ajax ou por meio de um POST convencional. Precisamos saber diferenciar esses dois casos. Isso é feito por meio dos cabeçalhos HTTP enviados pelo navegador do cliente;

Ao observar as trocas de dados na rede no console de desenvolvimento do Chrome (Ctrl+Shift+I) com o JavaScript ativado, percebe-se que o cliente envia os seguintes cabeçalhos no momento da chamada POST:

Observa-se acima que:

  • um cabeçalho [X-Requested-With] foi enviado junto com o [1];
  • um parâmetro [X-Requested-With] foi adicionado aos valores postados [2];

Isso não ocorre no caso de um POST clássico. Portanto, temos duas possibilidades para recuperar a informação: recuperá-la nos cabeçalhos HTTP ou nos valores postados. A linha 4 da ação [/ajax-02] optou pela primeira solução.

Vamos continuar com o código dessa ação:


@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 {
        // solicitação Ajax?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        // tempo?
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
        // prepara-se o modelo da próxima visualização
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
        // define-se a localidade
        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);
        // solicitação válida?
        if (!isAjax && result.hasErrors()) {
            return "vue-01";
        }
...
  • linha 2: o parâmetro [@Valid ActionModel01 formulaire] aciona os validadores do lado do servidor;
  • linhas 20-22: se a chamada não for uma chamada Ajax e a validação falhar, então é retornada a visualização [vue-01.xml] com as mensagens de erro.

Veja um exemplo:

Vamos continuar a análise da ação [/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 {
        // solicitação Ajax?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
        // solicitação válida?
        if (!isAjax && result.hasErrors()) {
            return "vue-01";
        }
        // gera um erro a cada duas vezes
        int val = new Random().nextInt(2);
        if (val == 0) {
            // é retornada uma mensagem de erro
            résultats.setErreur("erreur.aleatoire");
            if (isAjax) {
                return "vue-03";
            } else {
                résultats.setVue("vue-03");
                return "vue-01";
            }
        }
...
  • linha 14: gera-se um erro aleatório;
  • linha 16: no caso de uma chamada Ajax, retorna-se a visualização [vue-03.xml], que será colocada na área identificada por [resultats];
  • linha 18: no caso de uma chamada que não seja Ajax, a visualização a ser exibida é colocada no modelo do tipo [Resultats];
  • linha 19: exibe-se novamente a visualização [vue-01.xml];

A visualização [vue-01.xml] é modificada da seguinte forma:


<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" />
  • linha 3: a visualização [vue-03.xml] será inserida abaixo da área [resultats];

Veja um exemplo:

Observe que, a partir de agora, as horas [1] e [2] são idênticas.

Vamos continuar a análise da ação [/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 {
        // solicitação Ajax?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
        // recuperamos os valores enviados
        double a = formulaire.getA();
        double b = formulaire.getB();
        // construímos o 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");
        }
        // exibindo a visualização
        if (isAjax) {
            return "vue-02";
        } else {
            résultats.setVue("vue-02");
            return "vue-01";
        }
}
  • linhas 7-17: os resultados das quatro operações aritméticas são inseridos no modelo;
  • linhas 22-23: a visualização [vue-01.xml] (linha 22) é gerada mediante a inserção da visualização [vue-02.xml] (linha 22);

Essa inserção é feita da seguinte maneira em [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" />
  • linha 2: a vista [vue-02.xml] será inserida abaixo da área [resultats];

Veja um exemplo de execução:

 

7.2.9. Desativação do JavaScript com a configuração [fr-FR]

Com a configuração [fr-FR], temos o seguinte problema:

Os valores inseridos no formato francês foram considerados inválidos. Na verdade, o servidor espera valores reais no formato anglo-saxão. A solução é bastante complexa. Vamos criar um filtro que irá:

  • interceptar a solicitação;
  • substituir as vírgulas nos valores enviados [a] e [b] por ponto decimal;
  • e, em seguida, encaminhará a nova solicitação para a ação responsável por processá-la;

Primeiramente, inserimos um campo oculto na visualização [vue-01.xml]:


<form ...>
...
</p>
    <!-- campos ocultos -->
    <input type="hidden" id="culture" name="culture" th:value="${resultats.culture}"></input>
</form>
  • linha 5: o valor de [fr-FR] ou [en-US] é inserido no campo de atributo [name=culture]. Como a tag [input] está no formulário, seu valor será enviado junto com os valores de [a] e [b]. Assim, teremos uma string enviada no formato:
culture=fr-FR&a=12,7&b=20,78

É importante compreender esse ponto.

Em seguida, incluímos um filtro na configuração do aplicativo:

  

O arquivo [Config] é modificado da seguinte forma:


@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
...
    @Bean
    public Filter cultureFilter() {
        return new CultureFilter();
    }
}
  • linha 7: o fato de o bean [cultureFilter] retornar um tipo [Filter] faz dele um filtro. O bean, por sua vez, pode ter qualquer nome;

O próximo passo é criar o próprio filtro:

  

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 {
        // próximo manipulador
        filterChain.doFilter(new CultureRequestWrapper(request), response);
    }
}
  • linha 12: estendemos a classe [OncePerRequestFilter], que é uma classe do Spring, e o que precisamos fazer é redefinir o método [doFilterInternal] dessa classe;
  • linha 15: o método [doFilterInternal] recebe três informações:
    • [HttpServletRequest request]: a consulta a ser filtrada. Ela não pode ser alterada;
    • [HttpServletResponse response]: a resposta que será enviada ao servidor. O filtro pode decidir enviá-la por conta própria,
    • [FilterChain filterChain]: a cadeia de filtros. Assim que o método [doFilterInternal] concluir seu trabalho, ele deve passar a solicitação para o próximo filtro da cadeia de filtros;
  • linha 18: cria-se uma nova solicitação a partir daquela recebida ([new CultureRequestWrapper(request)]) e ela é encaminhada ao próximo filtro. Como não é possível modificar a solicitação inicial ([HttpServletRequest request]), cria-se uma nova;

A classe [CultureRequestWrapper] é a seguinte:

  

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 e 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;
        }
        // outros casos
        return super.getParameterValues(name);
    }

}
  • linha 6: a classe [CultureRequestWrapper] estende a classe [HttpServletRequestWrapper] e redefinirá alguns desses métodos;
  • linhas 8-10: o construtor que recebe a consulta a ser filtrada e a repassa para a classe pai;
  • é importante entender aqui que a solicitação filtrada acabará servindo como parâmetro de entrada de uma classe chamada servlet. Com o Spring MVC, esse servlet é do tipo [DispatcherServlet]. Essa classe dispõe de vários métodos para recuperar os parâmetros da solicitação: [getParameter, getParameterMap, getParameterNames, getParameterValues, ...]. É preciso redefinir o método utilizado pelo servlet. Para isso, seria necessário ler o código da classe [DispatcherServlet]. Eu não fiz isso e redefini vários métodos. No fim das contas, foi o método [getParameterValues] que foi redefinido;
  • linha 13: o método [getParameterValues] recebe como parâmetro o nome de um dos parâmetros retornados pelo método [getParameterNames] e deve retornar a matriz de seus valores. De fato, sabe-se que um parâmetro pode estar presente em várias instâncias em uma solicitação;
  • linha 18: substitui-se a vírgula por um ponto decimal;

Aqui está um exemplo de execução:

  • no [1], os valores do [a,b] são inseridos no formato francês;
  • em [2], os resultados;
  • em [3], o servidor retornou uma página com números no formato anglo-saxão.

Esse último problema pode ser resolvido com o Thymeleaf da seguinte maneira na visualização [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>

Há várias alterações a serem feitas nas linhas 3 e 6. Vamos analisar a linha 3:

  • tínhamos escrito [th:field="*{a}"]. O parâmetro [th:field] define os atributos [id, name, value] da tag HTML [input] gerada. Aqui, queremos gerenciar o atributo [value] por conta própria. Portanto, também definimos os atributos [id, name] por conta própria;
  • o atributo [th:value] avalia uma expressão que utiliza o operador ternário ?. Testamos a expressão [${resultats.culture}=='fr-FR' and ${actionModel01.b}!=null]. Se ela for verdadeira, atribuímos ao atributo [value] o valor de [actionModel01.a], onde o ponto decimal é substituído pela vírgula. Se for falsa, atribui-se ao atributo [value] o valor de [actionModel01.a], sem alterações;
  • linha 6: repete-se o mesmo procedimento para o campo [b];

Aqui está um exemplo de execução:

  • em [1], os números de [a,b] mantiveram a notação francesa. Esse não é o caso em [2];

Esse novo problema é resolvido da mesma forma que o anterior. Modifica-se a visualização [vue-03.xml] da seguinte maneira:


<!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>

Veja um exemplo:

Agora temos um aplicativo que lida corretamente com duas culturas em um ambiente que utilize ou não JavaScript. Para isso, foi necessário tornar o código do lado do servidor significativamente mais complexo. Daqui em diante, sempre assumiremos que o JavaScript do navegador está ativado. Isso permite funcionalidades impossíveis no modo exclusivamente de servidor.

Vamos examinar o link [Calculer] da página principal [vue-01.xml]:

O código do link [Calculer] na visualização [vue-01.xml] é o seguinte:


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

A função jS [postForm] está definida no arquivo [local1.js] da seguinte forma:


// dados globais
var loading;
var formulaire;
var résultats;
var a, b;

function postForm() {
    // formulário válido?
    if (!formulaire.validate().form()) {
        // formulário inválido – concluído
        return;
    }
    // são gerenciadas duas configurações regionais [fr_FR, en_US]
    // os valores reais [a,b] devem ser lançados no formato anglo-saxão em todos os casos
    // isso será feito pelo filtro [CultureFilter]

    // faz-se uma chamada 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);
        }
    })
}
  • linhas 2-5: vale lembrar que esses elementos foram inicializados pela função [$(document).ready];
  • linhas 9-12: são executados os validadores jS do formulário. Se algum dos valores for inválido, a expressão [formulaire.validate().form()] retorna o valor false. Nesse caso, o [submit] do formulário é cancelado;
  • linhas 18-38: é feita uma chamada Ajax manualmente;
  • linha 19: o URL é o destino da chamada Ajax;
  • linhas 20-22: um array de cabeçalhos HTTP a ser adicionado aos que já estão presentes por padrão na solicitação HTTP. Aqui, adicionamos o cabeçalho HTTP, que indicará ao servidor que estamos realizando uma chamada Ajax;
  • linha 23: o método HTTP utilizado;
  • linha 24: os dados enviados. [formulaire.serialize] cria a string a ser enviada [culture=fr-FR&a=12,7&b=20,89] do formulário com ID [formulaire]. Encontramos aqui o problema analisado anteriormente: os valores [a,b] precisam ser enviados no formato anglo-saxão. Sabemos que esse problema já foi resolvido com a criação do filtro [cultureFilter];
  • linha 25: o tipo de dados esperado como retorno. Sabemos que o servidor retornará um fluxo HTML;
  • linha 26: o método a ser executado quando a solicitação for iniciada. Aqui, indica-se que é necessário exibir o componente com o ID [loading]. Trata-se da imagem animada de espera;
  • linha 29: o método a ser executado caso a solicitação Ajax seja bem-sucedida. O parâmetro [data] é a resposta completa do servidor. Sabemos que se trata de um fluxo HTML;
  • linha 30: atualiza-se o componente com id [résultats] com o valor HTML do parâmetro [data].
  • linha 33: o sinal de espera é ocultado;
  • linha 35: função executada quando a resposta do servidor é recebida, independentemente de ser um sucesso ou um erro;
  • linhas 35-37: em caso de erro (o servidor retornou uma resposta HTTP com um status indicando que houve erro no lado do servidor), exibe-se a resposta HTML do servidor na área [resultats];

Veja um exemplo de execução:

7.3. Atualização de uma página HTML com um fluxo jSON

No exemplo anterior, o servidor web respondia à solicitação Ajax HTTP com um fluxo HTML. Nesse fluxo, havia dados acompanhados de formatação HTML. Propomos retomar o exemplo anterior, mas desta vez com respostas jSON (JavaScript Object Notation) contendo apenas os dados. A vantagem é que, dessa forma, são transmitidos menos bytes. Supõe-se que o JavaScript esteja ativado no navegador.

7.3.1. A ação [/ajax-04]

A ação [/ajax-04] é idêntica à ação [/ajax-01], com a diferença de que exibe a visualização [vue-04.xml] em vez da visualização [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) {
        ...
        // visualização
        return "vue-04";
    }

7.3.2. A visualização [vue-04.xml]

 

A vista [vue-04.xml] retoma o corpo da vista [vue-01.xml] com as seguintes diferenças:


<!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>
  • linha 5: o JavaScript da visualização agora está no arquivo [local4.js];
  • linha 16: a tag [form] não possui mais os parâmetros [data-ajax-attr] da biblioteca [Unobtrusive Ajax]. Não vamos utilizá-la aqui. A tag [form] também não possui os atributos [method] e [action], que indicam como e onde enviar os valores inseridos no formulário. Isso ocorre porque ela será enviada por uma função jS (linha 20);
  • linhas 26-57: o campo de ID [resultats], que antes era vazio, agora contém o código HTML para exibir os resultados;
  • linhas 26-34: o cabeçalho dos resultados, onde é exibida a hora do cálculo;
  • linhas 35-52: os resultados das quatro operações aritméticas;
  • linhas 53-57: uma eventual mensagem de erro enviada pelo servidor;

O código jS, executado ao carregar a visualização [vue-04.xm], está no arquivo [local4.js]. É o seguinte:


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

// ao carregar o documento
$(document).ready(function() {
    // recuperam-se as referências dos diferentes componentes da 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");
    // ocultamos alguns elementos
    résultats.hide();
    erreur.hide();
    loading.hide();
});
  • linhas 17-27: recuperam-se as referências jQuery de todos os elementos da página;
  • linha 29: a área de resultados é ocultada;
  • linha 30: assim como a área de erros;
  • linha 31: assim como a imagem animada de espera;
  • linhas 2-12: as referências recuperadas são definidas como globais para que as outras funções possam utilizá-las;

7.3.3. A função jS [postForm]

O link [Calculer] é o seguinte:


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

A função jS [postForm] está definida no arquivo [local.js] da seguinte forma:


function postForm() {
    // formulário válido?
    if (!formulaire.validate().form()) {
        // formulário inválido – concluído
        return;
    }
    // faz-se uma chamada 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 da chamada Ajax
function onBegin() {
...
}

// ao receber a resposta do servidor
// em caso de sucesso
function onSuccess(data) {
...
}

// ao receber a resposta do servidor
// em caso de falha
function onError(jqXHR) {
...
}

// após [onSuccess, onError]
function onComplete() {
...
}
  • linhas 3-6: antes de enviar os valores inseridos, eles são verificados. Se estiverem incorretos, a ação POST do formulário não é executada;
  • linha 9: os valores inseridos são enviados para a ação [/ajax-05], que detalharemos um pouco mais adiante;
  • linhas 10-12: um cabeçalho HTTP para informar ao servidor que se espera uma resposta no formato jSON;
  • linha 13: os valores inseridos serão enviados;
  • linha 14: serialização dos valores inseridos em uma string pronta para ser enviada ([a=1,6&b=2,4&culture=fr-FR]);
  • linha 15: o tipo da resposta enviada pelo servidor. Será jSON;
  • linha 16: a função a ser executada antes do POST;
  • linha 17: a função a ser executada ao receber a resposta do servidor, caso a solicitação seja bem-sucedida. O “sucesso” de uma solicitação HTTP é avaliado com base no status da resposta HTTP do servidor. Uma resposta [HTTP/1.1 200 OK ] é uma resposta bem-sucedida. Uma resposta [HTTP/1.1 500 Internal Server Error] é uma resposta com falha. O que se denomina status de uma resposta HTTP é o código [200] ou [500]. Alguns desses códigos estão associados ao “sucesso”, enquanto outros estão associados à “falha”;
  • linha 18: a função a ser executada ao receber a resposta do servidor quando o status HTTP dessa resposta for um status de falha;
  • linha 18: a função a ser executada por último, após as funções [onSuccess, onError] anteriores;

A função [onBegin] é a seguinte:


// antes da chamada Ajax
function onBegin() {
    console.log("onBegin");
    // exibe-se a imagem animada
    loading.show();
    // ocultam-se alguns elementos da visualização
    entete.hide();
    résultats.hide();
    erreur.hide();
}

Antes de analisarmos as demais funções jS da chamada Ajax, precisamos conhecer a resposta enviada pela ação [/ajax-05].

7.3.4. A ação [/ajax-05]

A ação [/ajax-05] é a seguinte:


    @RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
    @ResponseBody()
    // processa o POST da visualização [vue-04]
    public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale,    HttpServletRequest request, HttpSession session) throws InterruptedException {
        if(result.hasErrors()){
            // caso anormal — não é exibido nada
            return null;
        }
        ...
}
  • linha 2: o atributo [ResponseBody] indica que a ação [/ajax-05] retorna ela mesma a resposta ao cliente. Como uma biblioteca jSON está entre as dependências do projeto, o Spring Boot configura automaticamente esse tipo de ação para que elas retornem jSON. Portanto, é a cadeia jSON de um tipo [JsonResults] (linha 4) que será enviada ao cliente;
  • linha 2: os valores enviados [a, b, culture] serão encapsulados em um tipo [ActionModel01], cuja validação [@Valid ActionModel01] é solicitada. Isso é apenas por uma questão de formalidade. Partimos da hipótese de que o JavaScript estava ativado no navegador do cliente e, portanto, quando chegam, os valores enviados já foram verificados no lado do cliente. No entanto, podemos prever o caso de um POST não autorizado que não utilizaria nosso cliente jS. Nesse caso, a validação pode falhar;
  • linhas 5-7: em caso de erro, retorna-se um fluxo jSON vazio;

Vamos continuar a análise da ação [/ajax-05]:


    @RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
    @ResponseBody()
    // processa o POST da visualização [vue-04]
    public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale,
            HttpServletRequest request, HttpSession session) throws InterruptedException {
...
        // o contexto da aplicação Spring
        WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
        // tempo?
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
    ...
        // retornamos o resultado
        return résultats;
}
  • linha 8: recuperamos o contexto [ctx] do aplicativo Spring. Precisamos dele para recuperar as mensagens dos arquivos [messages.properties] a partir de uma chave de mensagem e de uma localidade. Isso é feito com a seguinte sintaxe:

ctx.getMessage(clé_message, tableau_de_paramètres, locale)
    • [clé_message]: a chave da mensagem procurada;
    • [locale]: a localização utilizada. Assim, se essa localização for [en_US], será utilizado o arquivo [messages_en.properties];
    • [tableau_de_paramètres]: a mensagem obtida pode ser configurada da mesma forma que em [clé=message {0} {1}]. Nessa mensagem, há dois parâmetros [{0} {1}]. Será necessário fornecer, como segundo parâmetro de [ctx.getMessage], uma matriz com dois valores;
  • linhas 10-13: se houver uma espera na sessão, o thread atual é suspenso durante o tempo dessa espera;

A ação [/ajax-05] prossegue da seguinte maneira:


        // preparamos o modelo da próxima visualização
        JsonResults résultats = new JsonResults();
        ...
}
  • linha 2: criação do modelo da string jSON enviada ao cliente;

O modelo [JsonResults] é o seguinte:

 

package istia.st.springmvc.models;

public class JsonResults {

    // dados
    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 e setters
...

}
  • linhas 6-13: cada um dos campos da classe [JsonResult] corresponde a um campo da mesma classe [id] na visão [vue-04.xml]:

A ação [/ajax-05] prossegue da seguinte forma:


        // preparando o modelo da próxima visualização
        JsonResults résultats = new JsonResults();
        // cabeçalho
        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()));
        // geramos um erro a cada duas vezes
        int val = new Random().nextInt(2);
        if (val == 0) {
            // retornando uma mensagem de erro
            résultats.setMsgErreur(ctx.getMessage("resultats.erreur",
                    new Object[] { ctx.getMessage("erreur.aleatoire", null, locale) }, locale));
            return résultats;
}
  • linha 2: criação do modelo da string jSON enviada ao cliente;
  • linhas 4-6: cria-se as mensagens do cabeçalho dos resultados;
  • linhas 8-14: em média, a cada duas vezes, é gerada uma mensagem de erro. Nesse caso, o processo não prossegue e a string jSON é devolvida ao cliente (linha 13);
  • linha 11: aqui temos um exemplo de mensagem configurada:

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

A ação [/ajax-05] prossegue da seguinte maneira:


        // recuperando os valores enviados
        double a = formulaire.getA();
        double b = formulaire.getB();
        // construímos o 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");
        }
        // retorna o resultado
return résultats;
  • linhas 2-3: recuperam-se os valores de [a] e [b];
  • linhas 5-12: constroem-se os quatro resultados;
  • linha 14: a sequência jSON [JsonResults] é enviada ao cliente;

Vamos ver como fica com o cliente [Advanced Rest Client]:

  • em [1-2], faz-se uma solicitação POST para a ação [/ajax-05];
  • em [3], são enviadas valores incorretos;
  • em [4], o servidor retornou um fluxo vazio;
  • em [1], são enviadas valores corretos;
  • em [2], o objeto jSON retornado pelo servidor, com uma mensagem de erro;
  • em [1], são enviadas valores corretos;
  • em [2], o objeto jSON retornado pelo servidor, com os quatro resultados;
  • em [1], enviamos valores corretos;
  • em [2], conseguimos provocar uma exceção no lado do servidor. Vemos que o servidor ainda envia um objeto jSON. Nessa mensagem, percebemos que o status HTTP da resposta é [500], indicando que houve um erro no lado do servidor;

7.3.5. A função jS [postForm] - 2

Agora que conhecemos o objeto jSON retornado pelo servidor, podemos utilizá-lo no JavaScript. O método [onSuccess] executado quando o servidor envia uma resposta com o status HTTP [200] é o seguinte:


// ao receber a resposta do servidor
// em caso de sucesso
function onSuccess(data) {
    console.log("onSuccess");
    // preenche-se a área de resultados
    titre.text(data.titre);
    labelHeureCalcul.text(data.labelHeureCalcul);
    heureCalcul.text(data.heureCalcul);
    entete.show();
    // resultados sem erros
    if (!data.msgErreur) {
        aplusb.text(data.aplusb);
        amoinsb.text(data.amoinsb);
        afoisb.text(data.afoisb);
        adivb.text(data.adivb);
        résultats.show();
        return;
    }
    // resultados com erro
    msgErreur.text(data.msgErreur);
    erreur.show();
}
  • linha 3: o parâmetro [data] é o objeto jSON retornado pelo servidor:
 

O método [onError] executado quando o status da resposta HTTP é [500] é o seguinte:


// ao receber a resposta do servidor
// em caso de falha
function onError(jqXHR) {
    console.log("onError");
    // erro do sistema
    msgErreur.text(jqXHR.responseText);
    erreur.show();
}
  • linha 3: o objeto JQuery [jqXHR] possui, entre suas propriedades, as seguintes:
    • responseText: o texto da resposta do servidor,
    • status: o código de erro retornado pelo servidor,
    • statusText: o texto associado a esse código de erro;
  • linha 6: o objeto [jqXHR.responseText] é o objeto jSON a seguir:
 

7.3.6. Testes

Vejamos algumas capturas de tela da execução do aplicativo web:

 
 
 

7.4. Aplicativo web de página única

7.4.1. Introdução

A tecnologia Ajax permite criar aplicativos de página única:

  • a primeira página é carregada por meio de uma solicitação convencional do navegador;
  • as páginas seguintes são obtidas por meio de chamadas Ajax. Assim, no final, o navegador nunca muda de URL e nunca carrega uma nova página. Esse tipo de aplicativo é chamado de Aplicativo de Página Única (APU) ou, em inglês, Single Page Application (SPA).

Aqui está um exemplo básico desse tipo de aplicação. A nova aplicação terá duas visualizações:

  • em [1], a ação [/ajax-06] nos permite acessar a primeira página, a página 1;
  • em [2], um link nos permite passar para a página 2 por meio de uma chamada Ajax;
  • em [3], o URL não mudou. A página exibida é a página 2;
  • em [4], um link permite que voltemos à página 1 por meio de uma chamada Ajax;
  • em [5], o URL não sofreu alterações. A página exibida é a página 1.

7.4.2. A ação [/ajax-06]

O código da ação [/ajax-06] é o seguinte:


    @RequestMapping(value = "/ajax-06", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax06() {
        return "vue-06";
}
  • linhas 1-4: a ação [/ajax-06] limita-se a renderizar a visualização [vue-06.xml];

7.4.3. A visualização [vue-06.xml]

A visualização [vue-06.xml] é a seguinte:


<!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>
  • linha 8: a visualização utiliza um script [local6.js];
  • linha 12: a visualização [vue-07.xml] é incluída na área de ID [content] da visualização [vue-06.xml];

7.4.4. A visualização [vue-07.xml]

A visualização [vue-07.xml] é a seguinte:


<!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. A função jS [gotoPage]

O link [Page 2] da visualização [vue-07.xml] utiliza a função jS [gotoPage] definida no seguinte arquivo [local6.js]:


// dados globais
var content;

function gotoPage(num) {
    // é feita uma chamada 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) {
            // erro de sistema
            content.html(jqXHR.responseText);
        }
    })
}

// ao carregar o documento
$(document).ready(function() {
    // recuperando as referências dos diferentes componentes da página
    content = $("#content");
});
  • linha 28: ao carregar a página, memoriza-se a zona de ID [content] e transforma-se em uma variável global (linha 2);
  • linha 4: a função [gotoPage] recebe como parâmetro o número da página (1 ou 2) a ser exibida na visualização atual;
  • linha 7: a função URL é a função de destino da função POST;
  • linha 8: a função URL da linha 7 é chamada por meio de uma função POST;
  • linha 9: a string enviada. Trata-se de um parâmetro chamado [num] que é enviado. Seu valor é o número da página (linha 4) a ser exibida na visualização atual;
  • linha 10: o servidor retornará o HTML, que corresponde à página a ser exibida;
  • linhas 13-15: em caso de sucesso (status HTTP igual a 200), o HTML enviado pelo servidor é inserido no campo de ID [content];
  • linhas 18-20: em caso de falha (status HTTP igual a 500), o HTML enviado pelo servidor é inserido no campo de identificação [content];

7.4.6. A ação [/ajax-07]

O código da ação [/ajax-07] é o seguinte:


@RequestMapping(value = "/ajax-07", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax07(int num) {
        // num: número da página
        switch (num) {
        case 1:
            return "vue-07";
        case 2:
            return "vue-08";
        default:
            return "vue-07";
        }
    }
  • linha 2: recupera-se o parâmetro enviado, chamado [num]. Vale lembrar que o parâmetro da linha 2 deve ter o nome do parâmetro enviado, neste caso, [num]. [num] é um número de página ou de visualização;
  • linhas 5-6: caso [num==1], retorna-se a visualização [vue-07.xml];
  • linhas 7-8: no caso de [num==2], retorna-se a visualização [vue-08.xml];
  • linhas 9-10: nos demais casos (o que normalmente é impossível), retorna-se a visualização [vue-07.xml];

7.4.7. A visualização [vue-08.xml]

A visualização [vue-08.xml] compõe a página nº 2 do aplicativo:


<!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 vários fluxos HTML em uma resposta jSON

7.5.1. Introdução

Consideramos a seguinte aplicação:

A página [1] possui quatro áreas:

  • [Zone 1, Zone 3] são áreas que aparecem/desaparecem ao clicar no botão [Rafraîchir]. Conta-se o número de aparições de cada uma dessas duas áreas [2]. A área [Zone 1] utiliza o idioma francês, enquanto a área [Zone 3] utiliza o idioma inglês;
  • a zona [Zone 2] está sempre presente;
  • a área [Saisies] está sempre presente;

O link [Valider] exibe a página seguinte, [3]:

  • o link [Retour à la page 1] retorna a página nº 1 ao estado em que se encontrava ([4]);

O aplicativo é de página única. A primeira página é solicitada ao servidor pelo navegador. As seguintes são obtidas do servidor por meio de chamadas Ajax.

7.5.2. A ação [/ajax-09]

  

A ação [/ajax-09] é a seguinte:


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

Ela se limita a exibir a visualização [vue-09.xml].

7.5.3. As visualizações XML

  

A visualização [vue-09.xml] é a página principal do aplicativo:


<!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>
  • linha 9: o arquivo JS utilizado na aplicação;
  • linha 15: o conteúdo da página mestre;
  • linha 16: uma imagem animada de espera:
  • linha 17: área para exibir um possível erro;

A visualização [vue-09-page1.xml] é a página 1 do aplicativo:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h2>Page 1</h2>
        <!-- área 1 -->
        <fieldset id="zone1" style="background-color:pink">
            <legend>Zone 1</legend>
            <span id="zone1-content" th:text="xx">xx</span>
        </fieldset>
        <!-- área 2 -->
        <fieldset id="zone2" style="background-color:lightgreen">
            <legend>Zone 2</legend>
            <span>Ce texte reste toujours présent</span>
        </fieldset>
        <!-- área 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>
  • linhas 6-9: a área [Zone 1]. Seu conteúdo é inserido no componente [id="zone1-content"];
  • linhas 11-14: a área [Zone 2], que não sofre alterações;
  • linhas 16-19: o campo [Zone 3]. Seu conteúdo é inserido no componente [id="zone3-content"];
  • linha 22: a função JS, que envia o formulário;
  • linha 25: inclusão da área de preenchimento;

Observe-se que a página 1 não possui a tag [form]. Tudo será processado em JavaScript.

A visualização [vue-09-saisies.xml] é a seguinte:


<!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>
  • linhas 5-8: inserção de uma sequência de caracteres;
  • linhas 13-16: inserção de um número inteiro;
  • linha 14: a função JS que envia os valores inseridos;

Mais uma vez, observe-se que a área de entrada não possui a tag [form].

No total, a página nº 1 apresenta duas funcionalidades:

  • [Rafraîchir]: que atualiza os campos 1 e 3. Essa ação é processada pelo servidor, que retorna aleatoriamente:
    • a área 1 com seu contador de acessos e nada para a área 3,
    • a zona 3 com seu contador de acessos e nada para a zona 1,
    • as duas áreas com seus contadores de acesso;
  • [Valider]: que exibe a página 2 com os valores inseridos ou uma mensagem de erro caso os dados inseridos sejam inválidos;

Vamos nos concentrar primeiro no botão [Rafraîchir].

7.5.4. O código JS para gerenciamento do botão [Rafraîchir]

  

O código do arquivo [local9.js] é o seguinte:


// variáveis globais
var content;
var loading;
var erreur;

// ao carregar o documento
$(document).ready(function() {
    // recuperam-se as referências dos diferentes componentes da página
    loading = $("#loading");
    loading.hide();
    erreur = $("#erreur");
    erreur.hide();
    content = $("#content");
});
  • linhas 9-13: quando a página mestre é carregada, as referências aos três componentes identificados por [loading, erreur, content] são armazenadas;
  • linhas 2-4: as referências desses três componentes são armazenadas em variáveis globais. Elas permanecem fixas porque as três áreas em questão estão sempre presentes na página exibida, independentemente do momento. Como permanecem fixas, podem ser calculadas em [$(document).ready] e compartilhadas com as outras funções do arquivo JS;

A função [postForm] gerencia o clique no botão [Rafraîchir]:


function postForm() {
    console.log("postForm");
    // é feita uma chamada Ajax manualmente
    $.ajax({
        url : '/ajax-10',
        headers : {
            'Accept': 'application/json'
        },
        type : 'POST',
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}
  • linhas 4-15: a chamada Ajax ao servidor;
  • linha 5: é a ação [ajax-10] que processará a POST;
  • linhas 6-8: a resposta será a ação jSON. O cliente JS indica que aceita os documentos jSON;
  • linha 9: a ação [ajax-10] é chamada com uma operação POST;
  • linha 10: vamos receber o jSON;
  • linha 11: a função executada antes da chamada Ajax;
  • linha 12: a função executada ao receber a resposta do servidor, quando esta for bem-sucedida [200 OK];
  • linha 13: a função executada ao receber a resposta do servidor, quando esta falhar [500 Internal server error, ...];
  • linha 14: a função executada após o recebimento da resposta;

A função [onBegin] é a seguinte:


// antes da chamada Ajax
function onBegin() {
    console.log("onBegin");
    // imagem de espera
    loading.show();
}

Ela se limita a iniciar a imagem animada que indica a espera pelo resultado do servidor.

7.5.5. A ação [/ajax-10]

  

A ação [/ajax-10] é a seguinte:


// a sessão
    @Autowired
    private SessionModel1 session;
    // o motor Thymeleaf / Spring
    @Autowired
    private SpringTemplateEngine engine;

    @RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
    ...
    }
  • linha 3: insere-se a sessão. Esta possui o tipo [SessionModel1] a seguir:
  

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;
    // dois contadores
    private int cpt1 = 0;
    private int cpt3 = 0;
    // as três áreas
    private String zone1 = "xx";
    private String zone3 = "zz";
    private String saisies;
    private boolean zone1Active = true;
    private boolean zone3Active = true;

    // getters e setters
    ...
}

A sessão [SessionModel1] armazena os seguintes elementos:

  • linha 15: o número de vezes que [cpt1] em que o campo [Zone 1] é exibido;
  • linha 16: o número de vezes [cpt3] em que a área [Zone 3] é exibida;
  • linhas 18-20: os fluxos HTML das áreas [Zone 1], [Zone 3] e [Saisies]. Isso é necessário na sequência [Page 1] --> [Page 2] --> [Page 1]. Ao passar de [Page 2] para [Page 1], é preciso restaurar [Page 1] e, portanto, seus três campos;
  • linhas 21-22: dois valores booleanos que indicam se os campos [Zone 1] e [Zone 3] estão exibidos (visíveis);

O outro elemento inserido no controlador [AjaxController] é o seguinte:


    // o mecanismo Thymeleaf / Spring
    @Autowired
private SpringTemplateEngine engine;

O bean do tipo [SpringTemplateEngine] está definido no arquivo de configuração [Config]:

  

Ele está definido da seguinte forma:


    @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;
}
  • linhas 2-10: conhecemos o bean do tipo [SpringResourceTemplateResolver], que nos permite definir certas características das visualizações;
  • linhas 13-17: o bean do tipo [SpringTemplateEngine] nos permite definir o “motor” das visualizações, a classe responsável por gerar as respostas [Thymeleaf] para os clientes. [Thymeleaf] possui um “motor” padrão e outro quando utilizado em um ambiente [Spring]. É este último que utilizamos aqui;

A assinatura da ação [/ajax-10] é a seguinte:


@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
    ...
}
  • linha 1: a ação [/ajax-10] aceita apenas um POST;
  • linha 2: a ação [/ajax-10] retorna ela mesma a resposta ao cliente. Essa resposta será transformada automaticamente em jSON;
  • linha 3: a resposta é do tipo [JsonResult10], conforme segue:
  

package istia.st.springmvc.models;

public class JsonResult10 {

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

    public JsonResult10() {
    }

    // getters e setters
...
}
  • linha 6: o conteúdo HTML do campo identificado por [content];
  • linha 7: o conteúdo HTML da zona [Zone 1];
  • linha 8: o conteúdo HTML da zona [Zone 3];
  • linha 9: o conteúdo HTML da zona [Erreur];
  • linha 10: o conteúdo HTML da zona [Saisies];
  • linha 11: valor booleano indicando se a zona [Zone 1] deve ser exibida;
  • linha 12: valor booleano indicando se a zona [Zone 3] deve ser exibida;

O código da ação [/ajax-10] é o seguinte:


@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
        // contexto Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // resposta
        JsonResult10 result = new JsonResult10();
        // sessão
        session.setZone1(null);
        session.setZone3(null);
        session.setZone1Active(false);
        session.setZone3Active(false);
        // retornamos uma resposta aleatória
        int cas = new Random().nextInt(3);
        switch (cas) {
        case 0:
            // zona 1 ativa
            setZone1(thymeleafContext, result);
            return result;
        case 1:
            // zona 3 ativa
            setZone3(thymeleafContext, result);
            return result;
        case 2:
            // zonas 1 e 3 ativas
            setZone1(thymeleafContext, result);
            setZone3(thymeleafContext, result);
            return result;
        }
        return null;
    }
  • linha 5: recuperamos o contexto [Thymeleaf]. Veremos mais adiante para que ele servirá;
  • linha 7: criamos uma resposta vazia por enquanto;
  • linhas 9-12: colocamos em [null] os dois campos contidos na sessão e indicamos que eles não devem ser exibidos. Esses dois campos serão gerados em breve, mas é possível que apenas um deles seja gerado;
  • linhas 14-29: os dois campos são gerados;
  • linhas 17-19: apenas a zona [Zone 1] é gerada;
  • linhas 21-23: apenas o campo [Zone 3] é gerado;
  • linhas 25-28: as duas áreas [Zone 1] e [Zone 3] são geradas;

O fluxo HTML da zona [Zone 1] é gerado pelo seguinte método:


    private void setZone1(WebContext thymeleafContext, JsonResult10 result) {
        // zona 1 ativa
        // fluxo 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);
        // sessão
        session.setCpt1(cpt1);
        session.setZone1(zone1);
        session.setZone1Active(true);
}
  • linha 1: os parâmetros são:
    • o contexto [Thymeleaf] do tipo [WebContext],
    • a resposta ao cliente em andamento, do tipo [JsonResult10];
  • linha 3: incrementa-se o contador [cpt1] da sessão, que conta o número de vezes que o campo [Zone 1] é exibido;
  • linha 4: o contexto [Thymeleaf], do tipo [WebContext], funciona de maneira semelhante ao modelo [Model] do Spring MVC. Para adicionar um elemento ao modelo, utiliza-se [WebContext.setVariable]. Aqui, insere-se, portanto, o contador [cpt1] no modelo [Thymeleaf]. Isso permitirá avaliar a expressão Thymeleaf [${cpt1}]
  • linha 5: o contexto [Thymeleaf] possui uma localização. Isso permite que ele avalie expressões do tipo [#{clé_msg}]. Aqui, associamos o contexto Thymeleaf a uma localização em francês;
  • linha 6: esta é a instrução mais interessante. O mecanismo Thymeleaf processará a visualização [vue-09-zone1.xml] com o modelo e a localidade que acabamos de calcular e, em vez de enviar o fluxo HTML resultante ao cliente, ele o retorna como uma sequência de caracteres;
  • linhas 7-9: o fluxo HTML da área [Zone 1], que acaba de ser calculado, é armazenado na sessão e no resultado que será enviado ao cliente. Além disso, indica-se que a área [Zone 1] deve ser exibida;
  • linhas 11-13: as informações relativas à área [Zone 1] são armazenadas na sessão para que seja possível regenerá-la;

A linha 7 trata a seguinte visão [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>
  • linha 3: a expressão [#{message.zone}] será avaliada por meio da localização;
  • linha 4: a expressão [${cpt1}] será avaliada por meio do modelo Thymeleaf;

A mensagem de chave [message.zone] está definida nos arquivos de mensagens [messages_fr.properties] e [messages_en.properties]:

  

[messages_fr.properties]


message.zone=Nombre d'accès : 

[messages_en.properties]


message.zone=Number of hits: 

O fluxo HTML da área [Zone 3] é gerado por um método semelhante:


    private void setZone3(WebContext thymeleafContext, JsonResult10 result) {
        // zona 3 ativa
        // fluxo 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);
        // sessão
        session.setCpt3(cpt3);
        session.setZone3(zone3);
        session.setZone3Active(true);
}
  • linha 6: a localidade da zona [Zone 3] é a localidade em inglês;

7.5.6. Processamento da resposta da ação [/ajax-10]

Voltemos ao código JS de [local9.js], que processará a resposta do servidor:


// ao receber a resposta do servidor
// em caso de sucesso
function onSuccess(data) {
    console.log("onSuccess");
    // conteúdo
    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 ativa?
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // entradas?
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // erro?
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

Vamos relembrar a estrutura Java da resposta recebida na linha 3 na variável [data]:


public class JsonResult10 {

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

}
  • linhas 6-8: se [data.content!=null], então se inicializa o campo [id=content] com esse valor. Esse campo representa [Page 1] ou [Page 2] na íntegra. Na demonstração atual, temos [data.content==null] e, portanto, a zona [id=content] não será alterada e continuará exibindo [Page 1];
  • linhas 10-17: exibição de [Zone 1] se [data.zone1Active==true]. Se, além disso, for [data.zone1!=null], o conteúdo de [Zone 1] é alterado; caso contrário, permanece como estava;
  • linhas 19-26: o mesmo vale para [Zone 3];
  • linhas 28-30: se houver [data.saisies!=null], a área [Saisies] é regenerada. Na demonstração atual, temos [data.saisies==null] e, portanto, a zona [Saisies] permanece como estava;
  • linhas 32-37: raciocínio análogo para a zona [Erreur], com as seguintes nuances:
    • linha 33: [data.erreur] será uma mensagem de erro no formato de texto;
    • linha 36: se [data.erreur==null], então o campo [Erreur] fica oculto. De fato, ele pode ter sido exibido na consulta anterior;

Em caso de erro no servidor (HTTP, status do tipo 500 Internal server error), a seguinte função é executada:


// ao receber a resposta do servidor
// em caso de falha
function onError(jqXHR) {
    console.log("onError");
    // erro do sistema
    erreur.text(jqXHR.responseText);
    erreur.show();
}

Para visualizar esse erro, vamos modificar a função [postForm] da seguinte maneira:


function postForm() {
    console.log("postForm");
    // recuperando referências na página atual
    ...
    // é feita uma chamada Ajax manualmente
    $.ajax({
        url : '/ajax-10x',
        ...
    })
}
  • linha 7: inserimos um URL que não existe;

Veja os resultados ao clicar no botão [Rafraîchir]:

É interessante observar que o erro também foi enviado na forma de uma string jSON.

O método executado após o recebimento da resposta do servidor é o seguinte:


// após [onSuccess, onError]
function onComplete() {
    console.log("onComplete");
    // imagem de espera
    loading.hide();
}

Limita-se a ocultar a imagem animada de espera.

7.5.7. Exibição da página [Page 2]

O código HTML do link [Valider] é o seguinte:


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

A função JS [valider] é a seguinte:


// validação dos valores inseridos
function valider() {
    // valor enviado
    var post = JSON3.stringify({
        "value1" : $("#text1").val().trim(),
        "value2" : $("#text2").val().trim()
    });
    // fazemos uma chamada 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
    })
}
  • linhas 4-7: temos dois valores, v1 e v2, a serem postados: os dos componentes de entrada identificados por [#text1] e [#text2]. Vamos fazer algo novo. Vamos enviar esses dois valores na forma de uma string jSON {"value1":v1,"value2":v2};
  • linha 10: os valores enviados serão direcionados para a ação [ajax-11A];
  • linha 12: como sabemos que receberemos uma resposta jSON, indicamos que podemos recebê-la como jSON;
  • linha 13: indicamos ao servidor que vamos enviar a ele o valor postado na forma de uma string jSON;
  • linhas 15-16: converte-se o valor a ser enviado no formato POST;
  • linha 17: vamos receber um jSON;

7.5.8. A ação [ajax-11A]

A ação [ajax-11A], que processa a sequência jSON enviada, é a seguinte:


@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) {
        ...
    }
  • linha 1: indica-se com ["application/json"] que a ação aguarda um documento no formato jSON. Esse documento é o valor enviado pelo cliente;
  • linha 3: o valor lançado será recuperado no objeto [PostAjax11A post] a seguir:
  

package istia.st.springmvc.models;

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

import org.hibernate.validator.constraints.Range;

public class PostAjax11A {

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

    // getters e setters
    ...
}
  • a estrutura do objeto [PostAjax11A] deve reproduzir a estrutura do objeto enviado {"value1":v1,"value2":v2}. Portanto, são necessários os campos [value1] (linha 13) e [value2] (linha 16);
  • foram definidas restrições de integridade para ambos os campos;

Voltemos ao código da ação [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 Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // resposta
        JsonResult10 result = new JsonResult10();
        // post válido?
        if (bindingResult.hasErrors()) {
            // retornamos a página 1 com um erro
            result.setZone1Active(session.isZone1Active());
            result.setZone3Active(session.isZone3Active());
            result.setErreur(getErreursForModel(bindingResult));
            return result;
        }
        ...
}
  • linha 3: a anotação [@RequestBody] designa o documento enviado pelo cliente. Trata-se do valor enviado pelo cliente em jSON. Esse valor será, portanto, utilizado para construir o objeto [PostAjax11A];
  • linha 3: a anotação [@Valid] força a validação do valor enviado;
  • linha 9: se a validação falhar:
    • linha 13: é retornada uma mensagem de erro;
    • linhas 11-12: os campos 1 e 3 são restaurados ao estado em que se encontravam (exibidos ou não);

O cálculo da mensagem de erro é feito da seguinte maneira:


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

Essa é uma função que já vimos anteriormente.

A ação [ajax-11A] prossegue da seguinte maneira:


@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 Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // resposta
        JsonResult10 result = new JsonResult10();
        // post válido?
        if (bindingResult.hasErrors()) {
    ...
        }
        // o campo de entrada é salvo
        thymeleafContext.setVariable("value1", post.getValue1());
        thymeleafContext.setVariable("value2", post.getValue2());
        session.setSaisies(engine.process("vue-09-saisies", thymeleafContext));
        // enviando a página 2
        result.setContent(engine.process("vue-09-page2", thymeleafContext));
        return result;
}
  • linhas 13-14: os valores enviados são inseridos no contexto do Thymeleaf;
  • linha 15: com esse contexto, calcula-se a visualização [vue-09-saisies] e ela é inserida na sessão para que possa ser regenerada posteriormente;
  • linha 17: a página 2 é inserida no resultado que será enviado ao cliente;

A visualização [vue-09-page2.xml] é a seguinte:

  

<!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>
  • nas linhas 9 e 13, exibimos os valores [value1, value2] que a ação [/ajax-11A] inseriu no contexto do Thymeleaf;

7.5.9. Processamento da resposta da ação [/ajax-11A]

No lado do cliente, a resposta da ação [/ajax-10] é processada pela função [onSuccess]:


function onSuccess(data) {
    console.log("onSuccess");
    // conteúdo
    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 ativo?
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // dados inseridos?
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // erro?
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

Já comentamos esse código. Vamos considerar os dois casos: resposta com ou sem erro:

Com erro

Nesse caso, a ação [/ajax-11A] enviou uma resposta jSON no formato {"zona1":null, "zona3":null,"entradas":null,"erro":erro,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":null}. Se seguirmos o código acima, vemos que:

  • a zona [content] não muda. Ela continha a página nº 1;
  • a área [Erreur] é exibida;
  • as áreas [Zone 1], [Zone 3] e [Saisies] permanecem como estavam;

Sem erros

Nesse caso, a ação [/ajax-11A] enviou uma resposta jSON no formato {"zona1":null, "zona3":null,"entradas":null,"erro":null,"zone1Active":false,"zone3Active":false,"content":content}. Se seguirmos o código acima, vemos que:

  • a zona [content] é exibida. Ela contém a página nº 2;

Aqui estão três exemplos de execução:

Um caso com erro de validação:

Um caso com erro de POST:

Esse tipo de erro é diferente. Como o Spring não conseguiu converter a string jSON para o tipo [PostAjax11A], ele retornou uma resposta HTTP com [status=400]. A ação [ajax-11A] não foi executada;

Um caso sem erros:

7.5.10. Voltar à página nº 1

O link [Retour vers la page 1] na página nº 2 é o seguinte:


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

O método JS [retourPage1] é o seguinte:


// voltar à página 1
function retourPage1() {
    // fazemos uma chamada Ajax manualmente
    $.ajax({
        url : '/ajax-11B',
        headers : {
            'Accept': 'application/json',
        },
        type : 'POST',
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}

Ele gera um POST, sem valor lançado, para a ação [/ajax-11B].

7.5.11. A ação [/ajax-11B]

A ação [/ajax-11B] é a seguinte:


    @RequestMapping(value = "/ajax-11B", method = RequestMethod.POST)
    @ResponseBody
    public JsonResult10 ajax11B(HttpServletRequest request, HttpServletResponse response) {
        // contexto Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // resposta
        JsonResult10 result = new JsonResult10();
        // restabelecemos a página 1 ao seu 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;
}

A ação deve regenerar a página nº 1 com suas três áreas [Zone1, Zone3, Erreur]:

  • linha 9: a página nº 1 é inserida no resultado;
  • linha 10: a área de entradas é incluída no resultado;
  • linha 11: a área [Zone 1] é incluída no resultado;
  • linha 12: a área [Zone 3] é incluída no resultado;
  • linhas 13-14: o status dos campos [Zone 1] e [Zone 3] é incluído no resultado;

7.5.12. Processamento da resposta da ação [/ajax-11B]

A resposta da ação [/ajax-11B] é processada pela função [onSuccess]:


function onSuccess(data) {
    console.log("onSuccess");
    // conteúdo
    if (data.content) {
        content.html(data.content);
    }
    // área 1
    if (data.zone1Active) {
        $("#zone1").show();
        if (data.zone1) {
            $("#zone1-content").html(data.zone1);
        }
    } else {
        $("#zone1").hide();
    }
    // área 3 está ativa?
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // entradas?
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // erro?
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

A ação [/ajax-11B] enviou uma resposta jSON no formato {"zona1":zona1, "zona3":zona3,"entradas":entradas,"erro":null,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":content}. Ao analisar o código acima, percebe-se que:

  • a zona [content] foi alterada. Ela continha a página nº 2. Agora, ela conterá a página nº 1;
  • a zona [Erreur] está oculta;
  • as áreas [Zone 1], [Zone 3] e [Saisies] são exibidas exatamente como estavam;

7.6. Gerenciar a sessão no lado do cliente

7.6.1. Introdução

No parágrafo anterior, gerenciamos uma sessão cuja estrutura era a seguinte:


public class SessionModel1 implements Serializable {

    // dois contadores
    private int cpt1 = 0;
    private int cpt3 = 0;
    // as três zonas
    private String zone1 = "xx";
    private String zone3 = "zz";
    private String saisies;
    private boolean zone1Active = true;
    private boolean zone3Active = true;
...
}

Quando há um grande número de usuários, a memória ocupada pelas sessões de todos esses usuários pode representar um problema. A regra, portanto, é minimizar o tamanho dessa memória. O modelo APU (Aplicativo de Página Única) permite gerenciar a sessão no lado do cliente e ter um servidor web sem sessão. De fato, a página única é carregada inicialmente pelo navegador. Junto com ela, é carregado o arquivo JavaScript que a acompanha. Como não há recarregamento da página, esse arquivo JS permanecerá permanentemente no navegador, tal como foi carregado inicialmente. É possível, então, utilizar suas variáveis globais para armazenar informações sobre as diferentes ações do usuário. É isso que veremos agora. Não apenas gerenciaremos a sessão no lado do cliente, mas também repensaremos a aplicação JS para sobrecarregar o servidor o mínimo possível.

7.6.2. A ação [/ajax-12]

  

A ação [/ajax-12] é a seguinte:


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

A visualização [vue-12.xml] é a seguinte:

  

<!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>
  • essa visualização é idêntica à visualização [vue-09], com a única diferença do script JS utilizado na linha 9;

A visualização exibida é a seguinte:

 

7.6.3. O código JS para gerenciamento do botão [Rafraîchir]

  

O código do arquivo [local12.js] é o seguinte:


// variáveis globais
var content;
var loading;
var erreur;
var page1;
var page2;
var value1;
var value2;
var session = {
        "cpt1" : 0,
        "cpt3" : 0
    };

// ao carregar o documento
$(document).ready(function() {
    // recuperam-se as referências dos diferentes componentes da página
    loading = $("#loading");
    loading.hide();
    erreur = $("#erreur");
    erreur.hide();
    content = $("#content");
});
  • linhas 17-21: quando a página mestre é carregada, as referências dos três componentes identificados por [loading, erreur, content] são armazenadas nas variáveis globais das linhas 2-4;
  • linhas 5-6: para armazenar as duas páginas;
  • linhas 7-8: para armazenar os dois valores enviados pelo link [Valider];
  • linha 9: a sessão. Ela armazena, no lado do cliente, os valores dos contadores [cpt1, cpt3];

A função [postForm] gerencia o clique no botão [Rafraîchir]:


function postForm() {
    console.log("postForm");
    // a sessão é gravada
    var post = JSON3.stringify(session);
    // faz-se uma chamada 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
    })
}

As diferenças em relação à versão anterior são as seguintes:

  • a função URL da linha 7 está diferente;
  • linha 4: envia-se um valor, enquanto antes não era enviado. Esse valor é a string jSON da sessão. O princípio é o seguinte:
    • o cliente envia a sessão ao servidor,
    • este a modifica e a reenvia,
    • o cliente armazena a nova sessão;
  • linha 10: enviamos um documento no formato jSON (valor enviado);
  • linha 13: há algo a ser postado;
  • linhas 15-20: as funções [beforeSend, error, complete] são as da versão anterior. Apenas a função [success] muda (linhas 16-18);

7.6.4. A ação [/ajax-13]

  

A ação [/ajax-13] é a seguinte:


    @RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody()
    public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request,    HttpServletResponse response) {
    ...
}
  • linha 3: o parâmetro [@RequestBody SessionModel2 session2] recupera a sessão enviada pelo cliente. Essa sessão tem o tipo [SessionModel2] a seguir:
  

package istia.st.springmvc.models;

import java.io.Serializable;

public class SessionModel2 implements Serializable {

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

    // getters e setters
    ...
}

A sessão [SessionModel2] armazena os seguintes elementos:

  • linha 9: o número de vezes que [cpt1] em que o campo [Zone 1] é exibido;
  • linha 10: o número de vezes [cpt3] em que o campo [Zone 3] é exibido;

Vamos continuar a análise do código da ação [/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) {
    ...
}
  • linha 3, o tipo [JsonResult13] da resposta é o seguinte:
  

package istia.st.springmvc.models;

public class JsonResult13 {

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

    // sessão
    private SessionModel2 session;

    // getters e setters
    ...
}
  • linha 14: a sessão. O servidor a reenvia ao cliente para armazenamento;
  • linha 6: o conteúdo HTML da página nº 2;
  • linha 7: o conteúdo HTML da área [Zone 1];
  • linha 8: o conteúdo HTML da área [Zone 3];
  • linha 9: a eventual mensagem de erro;
  • linhas 10-11: duas informações calculadas pelo servidor e exibidas pela página nº 2;

Vamos continuar a análise do código da ação [/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 do Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // resposta
        JsonResult13 result = new JsonResult13();
        result.setSession(session2);
        // retornamos uma resposta aleatória
        int cas = new Random().nextInt(3);
        switch (cas) {
        case 0:
            // zona 1 ativa
            setZone1B(thymeleafContext, result);
            return result;
        case 1:
            // zona 3 ativa
            setZone3B(thymeleafContext, result);
            return result;
        case 2:
            // zonas 1 e 3 ativas
            setZone1B(thymeleafContext, result);
            setZone3B(thymeleafContext, result);
            return result;
        }
        return null;
    }
  • linha 9: a sessão é inserida no resultado da ação;

O método [setZone1B], que ativa a área [Zone 1], é o seguinte:


    private void setZone1B(WebContext thymeleafContext, JsonResult13 result) {
        // recuperando a sessão
        SessionModel2 session = result.getSession();
        // zona 1 ativa
        // fluxo 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);
        // sessão
        session.setCpt1(cpt1);
}
  • linha 3: recupera-se a sessão. Ela será modificada na linha 12 com o novo contador [cpt1]. Vale lembrar que essa sessão será devolvida ao cliente;
  • linha 10: o novo campo [Zone 1];

O método [setZone3B], que ativa o campo [Zone 3], é semelhante:


private void setZone3B(WebContext thymeleafContext, JsonResult13 result) {
        // recuperando a sessão
        SessionModel2 session = result.getSession();
        // zona 3 ativa
        // fluxo 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);
        // sessão
        session.setCpt3(cpt3);
    }

7.6.5. Processamento da resposta da ação [/ajax-13]

No lado do cliente, a resposta jSON da ação [/ajax-13] é processada pela seguinte função [onSuccess]:


function postForm() {
    console.log("postForm");
    // a sessão é lançada
    var post = JSON3.stringify(session);
    // fazemos uma chamada Ajax manualmente
    $.ajax({
    ...
        success : function(data) {
            // armazenamos a sessão
            session = data.session;
            // atualizamos os dois 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();
            }
        },
...
    })
}
  • linhas 12-17: se o servidor inseriu algum valor no campo [zone1] da resposta, é necessário regenerar o campo [Zone 1] e exibi-lo; caso contrário, ele deve ser ocultado;
  • linhas 18-23: o mesmo raciocínio se aplica à área [Zone 3];

7.6.6. Exibição da página [Page 2]

O código HTML do link [Valider] é o seguinte:


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

A função JS [valider] é a seguinte:


// validação dos valores inseridos
function valider() {
    // a página 1 é armazenada
    page1 = content.html();
    // os valores inseridos são salvos
    value1 = $("#text1").val().trim();
    value2 = $("#text2").val().trim();
    // valor enviado
    var post = JSON3.stringify({
        "value1" : value1,
        "value2" : value2,
        "pageRequired" : page2 ? false : true
    });
    // é feita uma chamada 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 executar um POST, que normalmente nos levará para a página nº 2;
  • linha 4: memorizamos a página nº 1 para podermos voltar a ela posteriormente;
  • linhas 6-7: a operação anterior não armazena os valores inseridos, apenas o código HTML da página. Por isso, agora armazenamos os dois valores inseridos no formulário;
  • linhas 9-13: os dois valores inseridos são colocados em uma string jSON. É essa string que será enviada;
  • linha 12: um parâmetro para indicar ao servidor se precisamos da página nº 2. Procederemos da seguinte forma: solicitaremos a página nº 2 uma primeira vez e, em seguida, a armazenaremos na variável JS [page2]. Depois disso, não a solicitaremos novamente. Usaremos a página armazenada em cache. Na linha 2, [pageRequired] é igual a [true] se a variável [page2] estiver vazia; caso contrário, é igual a [false];
  • vale ressaltar que a sessão não é enviada. De fato, ela armazena contadores que a ação [/ajax-14] da linha 20 não altera;

7.6.7. A ação [/ajax-14]

A ação [/ajax-14] é a seguinte:


@RequestMapping(value = "/ajax-14", method = RequestMethod.POST)
    @ResponseBody
    public JsonResult13 ajax14(@RequestBody @Valid PostAjax14 post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        ...
    }
  • linha 3: a resposta é sempre do tipo [JsonResult13];
  • linha 3: o valor enviado é encapsulado no seguinte tipo [PostAjax14]:

package istia.st.springmvc.models;

public class PostAjax14 extends PostAjax11A {

    // página 2
    private boolean pageRequired;

    // getters e setters
    ...
}
  • linha 3: a classe [PostAjax14] estende a classe [PostAjax11A] da versão anterior. Portanto, ela possui uma estrutura [value1, value2, pageRequired];

A ação [/ajax-14] prossegue da seguinte forma:


    @RequestMapping(value = "/ajax-14", method = RequestMethod.POST)
    @ResponseBody
public JsonResult13 ajax14(@RequestBody @Valid PostAjax14 post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        // contexto do Thymeleaf
        WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
        // resposta
        JsonResult13 result = new JsonResult13();
        // post válido?
        if (bindingResult.hasErrors()) {
            // retornamos um erro
            result.setErreur(getErreursForModel(bindingResult));
            return result;
        }
        // envia-se a 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;
}
  • linhas 9-13: se os valores enviados por [value1, value2] forem inválidos, é retornada uma mensagem de erro;
  • linhas 15-16: normalmente, o servidor deveria realizar um cálculo com os valores enviados. Aqui, ele se limita a reenviá-los para confirmar que os recebeu corretamente;
  • linhas 18-20: a página nº 2 só é retornada se tiver sido solicitada pelo cliente. Na linha 19, a visualização [vue-12-page2] é nova:
 

<!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>
  • o código XML não contém mais valores avaliados pelo Thymeleaf, como acontecia anteriormente;
  • foram identificadas as áreas onde inserir os valores retornados pelo servidor como [value1, value2]. Linha 9, [id='value1'] indica o local onde inserir [value1]. Na linha 13, o mesmo se aplica a [value2];

7.6.8. Processamento da resposta da ação [/ajax-14]

A resposta da ação [/ajax-14] é processada pela seguinte função [success]:


// validação dos valores inseridos
function valider() {
    ...
    // é feita uma chamada Ajax manualmente
    $.ajax({
        ...
        success : function(data) {
            // erro?
            if (data.erreur) {
                // exibição do erro
                erreur.html(data.erreur);
                erreur.show();
            } else {
                // sem erro
                erreur.hide();
                // página 2
                if (page2) {
                    // utiliza-se a página em cache
                    content.html(page2);
                } else {
                    // a página 2 está sendo armazenada
                    page2 = data.page2;
                    // a página está sendo exibida
                    content.html(data.page2);
                }
                // atualizando-a com as informações do servidor
                $("#value1").text(data.value1);
                $("#value2").text(data.value2);
            }
        },
...
    })
}
  • linhas 9-13: se o servidor retornou um erro, ele é exibido;
  • linhas 14-29: caso não tenha ocorrido erro. Nesse caso, deve-se exibir a página nº 2;
  • linha 17: verifica-se se a página nº 2 já está armazenada na variável [page2];
  • linha 19: nesse caso, utiliza-se a variável [page2] para exibir a página nº 2;
  • linha 24: caso contrário, utiliza-se o campo [data.page2] fornecido pelo servidor;
  • linha 22: certifique-se de armazenar a página nº 2 para não precisar solicitá-la novamente posteriormente;
  • linhas 27-28: na página nº 2, exibem-se as duas informações [value1, value2] enviadas pelo servidor;

7.6.9. Voltar à página nº 1

O link [Retour vers la page 1] na página nº 2 é o seguinte:


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

O método JS [retourPage1] é o seguinte:


// retorno à página 1
function retourPage1() {
    // regenera-se a página 1
    content.html(page1);
    // regenera os dados inseridos
    $("#text1").val(value1);
    $("#text2").val(value2);
}
  • trata-se de uma ação JS sem interação com o servidor, pois a página nº 1 foi armazenada localmente na variável [page1];
  • linha 4: regenera-se a página nº 1;
  • linhas 6-7: apenas a parte HTML da página nº 1 havia sido armazenada. Não as entradas. Portanto, é preciso regenerá-las;

7.6.10. Conclusão

Ao aproveitar as possibilidades do modelo APU, conseguimos simplificar o servidor web, que agora é sem estado (sem sessão) e está menos sobrecarregado:

  • eliminamos a interação com o servidor na função JS ([retourPage1]);
  • o servidor gera a página nº 2 apenas uma vez;

7.7. Estruturação do código JavaScript em camadas

7.7.1. Introdução

O código JavaScript do aplicativo anterior está começando a ficar complexo. É hora de estruturá-lo em camadas. O aplicativo permanecerá o mesmo de antes. Não vamos alterar o servidor, exceto para definir uma nova página inicial. Vamos reformular o código JS.

A nova arquitetura será a seguinte:

7.7.2. A página inicial

A ação que inicia o aplicativo é a ação [/ajax-16] a seguir:


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

Ela exibe a seguinte visualização [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>
  • linhas 9-10: o código JS foi colocado em dois arquivos diferentes:
    • [local-ui] implementa a camada [présentation],
    • [local-dao] implementa a camada [DAO];
  

7.7.3. Implementação da camada [DAO]

7.7.4. Interface

A camada [DAO] em [local-dao.js] apresentará a seguinte interface à camada [présentation]:


function updatePage1(deferred, sendMeBack)
para atualizar a página 1 com o botão [Rafraîchir]

function getPage2(deferred, sendMeBack, value1, value2, pageRequired)
para exibir a página 2 com o botão [Valider]

O JavaScript não possui o conceito de interface. Utilizei esse termo simplesmente para indicar que a camada [présentation] se comprometia a interagir com a camada [DAO] exclusivamente por meio das duas funções anteriores.

7.7.5. Implementação da interface

A estrutura básica da implementação é a seguinte:


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

// atualizar a página 1
function updatePage1(deferred, sendMeBack) {
...
}

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

O objetivo da camada [DAO] é ocultar da camada [présentation] os detalhes das solicitações HTTP feitas ao servidor web. A sessão faz parte desses detalhes. Portanto, ela agora é gerenciada pela camada [DAO].

7.7.5.1. A função [updatePage1]

A função [updatePage1] é a função chamada pela camada [présentation] para atualizar a página 1. Seu código é o seguinte:


// atualização da página 1
function updatePage1(deferred, sendMeBack) {
    // consulta HTTP
    executePost(deferred, sendMeBack, '/ajax-13', session);
}
  • linha 1: a função [updatePage1] recebe dois parâmetros:
    1. um objeto do tipo [jQuery.Deferred]. Esse tipo de objeto armazena um estado que pode assumir três valores: ['pending', 'resolved', 'rejected']. Quando chega à função [updatePage1], ele está no estado [pending];
    2. um objeto JS a ser devolvido na camada [présentation];

Todas as consultas HTTP são realizadas pela função [executePost] a seguir:


// solicitação HTTP
function executePost(deferred, sendMeBack, url, post) {
    // fazemos uma chamada Ajax manualmente
    $.ajax({
        headers : {
            'Accept: 'application/json',
            'Content-Type: 'application/json'
        },
        url : url,
        type : 'POST',
        data : JSON3.stringify(post),
        dataType : 'json',
        success : function(data) {
            // armazenamos a sessão
            if (data.session) {
                session = data.session;
            }
            // retornamos o resultado
            deferred.resolve({
                "status" : 1,
                "data" : data,
                "sendMeBack" : sendMeBack
            });
        },
        error : function(jqXHR) {
            // retornamos o erro
            deferred.resolve({
                "status" : 2,
                "data" : jqXHR.responseText,
                "sendMeBack" : sendMeBack
            });
        }
    });
}
  • linha 1: a função [executePost] executa uma chamada Ajax do tipo POST. Ela espera quatro parâmetros:
    1. um objeto do tipo [jQuery.Deferred] no estado [pending];
    2. um objeto JS a ser retornado na camada [présentation];
    3. o URL do POST;
    4. o valor a ser lançado como objeto JS;
  • linhas 5-8: a função de postagem do jSON (linha 7) recebe do jSON (linha 6);
  • linha 11: o valor a ser enviado é transformado em jSON;
  • linhas 13-24: a função executada caso a chamada Ajax seja bem-sucedida;
  • linhas 19-23: se o servidor tiver retornado uma sessão, ela é armazenada;
  • linhas 13-18: transferem o objeto [deferred] para o estado [resolved], passando também um resultado com os seguintes campos:
    • [status]: 1 para sucesso, 2 para falha,
    • [data]: a resposta jSON do servidor,
    • [sendMeBack]: o segundo parâmetro da função, que é um objeto que o chamador deseja recuperar;
  • linhas 17-31: a função executada em caso de falha na chamada Ajax. Fazemos o mesmo que anteriormente, com duas diferenças:
    • [status] passa a 2 para sinalizar um erro;
    • [data] é, mais uma vez, a resposta jSON do servidor, mas obtida de maneira diferente;

7.7.5.2. A função [getPage2]

A função [getPage2] é a seguinte:


// página 2
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
    // solicitação HTTP
    executePost(deferred, sendMeBack, '/ajax-14', {
        "value1" : value1,
        "value2" : value2,
        "pageRequired" : pageRequired,
    });
}
  • A função recebe os seguintes parâmetros:
    1. [deferred]: um objeto do tipo [jQuery.Deferred] no estado [pending],
    2. [sendMeBack]: um objeto JS a ser retornado na camada [présentation],
    3. [value1]: a primeira entrada na página 1,
    4. [value2]: a segunda entrada na página 2,
    5. [pageRequired]: um valor booleano que indica ao servidor se ele deve ou não enviar o fluxo HTML da página nº 2;
  • a função [executePost] é chamada para executar a consulta HTTP necessária;

7.7.6. A camada [présentation]

A camada [présentation] é implementada pelo arquivo [local-ui.js]. Este último utiliza o código do arquivo [local12.js], adaptado para utilizar a camada [DAO] anterior. Apenas duas funções foram alteradas: [postForm] e [valider].

7.7.6.1. A função [postForm]

A função [postForm] é a seguinte:


// atualização da página 1
function postForm() {
    // atualizando a página 1
    var deferred = $.Deferred();
    loading.show();
    updatePage1(deferred, {
        'remetente: “postForm”,
        'info: 10
    });
    // exibição dos resultados
    deferred.done(postFormDone);
}
  • linha 4: cria-se um objeto [jQuery.Deferred]. Por padrão, ele está no estado [pending];
  • linha 5: a imagem de espera é exibida
  • linhas 6-9: a função [updatePage1] é executada. Passa-se um objeto fictício [sendMeBack], apenas para mostrar para que isso pode servir;
  • linha 11: o parâmetro da função [deferred.done] é, por sua vez, uma função. É a função a ser executada quando o estado do objeto [deferred] passa para o estado [resolved]. Acabamos de ver que a função DAO [executePost] alterava o estado desse objeto para [resolved] ao receber a resposta do servidor. Isso significa que, quando a função [postFormDone] é executada, a resposta do servidor já foi recebida;

A função [postFormDone] é a seguinte:


function postFormDone(result) {
    // fim da espera
    loading.hide();
    // recuperando os dados
    var data = result.data
    // para demonstração
    console.log(JSON3.stringify(result.sendMeBack));
    // analisando o status
    switch (result.status) {
    case 1:
        // atualizando os dois 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();
        }
        break;
    case 2:
        // exibição de erro
        erreur.html(data);
        break;
    }
}
  • linha 1: o parâmetro [result] recebido é o parâmetro passado para o método [deferred.resolve] na função [executePost], por exemplo:

            // retornando o resultado
            deferred.resolve({
                "status" : 1,
                "data" : data,
                "sendMeBack" : sendMeBack
});
  • linha 5: recupera-se a resposta do servidor;
  • linhas 10-24: temos o código que, na versão anterior, estava na função [onSuccess] da função [postForm];
  • linhas 25-28: temos o código que, na versão anterior, estava na função [onError] da função [postForm];

7.7.6.2. A função do parâmetro [sendMeBack]

Para que serve o parâmetro [sendMeBack]? Vejamos o código de chamada da função [updatePage1]:


// atualização da página 1
function postForm() {
    // atualiza-se a página 1
    var deferred = $.Deferred();
    loading.show();
    updatePage1(deferred, {
        'remetente: "postForm",
        'info: 10
    });
    // exibição dos resultados
    deferred.done(postFormDone);
}

e a assinatura da função [validerDone]:


function postFormDone(result) {
}

Como a função [postForm] pode passar informações para a função [postFormDone]? Esta última possui apenas um parâmetro: [result]. Esse parâmetro é criado pela função [executePost] da camada [DAO]. Para transmitir informações à função [postFormDone], a função [postForm] deve primeiro transmiti-las à função [updatePage1]. Essa é a função do parâmetro [sendMeBack]. Ele é utilizado da seguinte maneira:


function postFormDone(result) {
    // fim da espera
    loading.hide();
    // recuperando os dados
    var data = result.data
    // para demonstração
    console.log(JSON3.stringify(result.sendMeBack));
    // analisando o status
    switch (result.status) {
...
  • linha 7, a função [postFormDone] recuperou o parâmetro [sendMeBack] inicialmente transmitido à função DAO [updatePage1] pela função [postForm];

7.7.7. A função [valider]

A função [valider] é a seguinte:


// validação dos valores inseridos
function valider() {
    // armazenando a página 1
    page1 = content.html();
    // salva-se os valores inseridos
    value1 = $("#text1").val().trim();
    value2 = $("#text2").val().trim();
    // sem erros
    erreur.hide();
    // solicita-se a página 2
    var deferred = $.Deferred();
    loading.show();
    getPage2(deferred, {
        'remetente: 'confirmar',
        '‘info’: 20
    }, value1, value2, page2 ? false : true);
    // exibição dos resultados
    deferred.done(validerDone);
}

e a função [validerDone] (linha 18) é a seguinte:


function validerDone(result) {
    // fim da espera
    loading.hide();
    // recuperando os dados
    var data = result.data
    // para demonstração
    console.log(JSON3.stringify(result.sendMeBack));
    // analisando o status
    switch (result.status) {
    case 1:
        // erro?
        if (data.erreur) {
            // exibição de erro
            erreur.html(data.erreur);
            erreur.show();
        } else {
            // sem erro
            erreur.hide();
            // página 2
            if (page2) {
                // utilizando a página em cache
                content.html(page2);
            } else {
                // a página 2 está sendo armazenada
                page2 = data.page2;
                // a página está sendo exibida
                content.html(data.page2);
            }
            // atualizando-a com as informações do servidor
            $("#value1").text(data.value1);
            $("#value2").text(data.value2);
        }
        break;
    case 2:
        // exibição de erro
        erreur.html(data);
        erreur.show();
        break;
    }
}
  • linha 5: recupera-se a resposta do servidor;
  • linhas 10-32: temos o código que, na versão anterior, estava na função [onSuccess] da função [valider];
  • linhas 34-38: temos o código que, na versão anterior, estava na função [onError] da função [valider];

7.7.8. Testes

O aplicativo continua funcionando como antes e, no console do Chrome, é possível ver os parâmetros [sendMeBack] das funções [postForm] e [valider]:

 

7.8. Conclusion

Voltemos ao esquema geral de uma aplicação Spring MVC:

Graças ao JavaScript incorporado nas páginas HTML e executado no navegador, e graças ao modelo APU, é possível transferir código para o navegador e chegar à seguinte arquitetura:

  • temos uma arquitetura cliente [2] / servidor [1], na qual o cliente e o servidor se comunicam em jSON;
  • em [1], a camada web Spring MVC fornece visualizações, fragmentos de visualização e dados em jSON;
  • em [2]: o código JavaScript incorporado na visualização carregada na inicialização do aplicativo pode ser estruturado em camadas:
    • a camada [présentation] lida com as interações com o usuário,
    • A camada [DAO] é responsável pelo acesso aos dados por meio do servidor web [1];
    • a camada [métier] pode não existir ou assumir algumas das funcionalidades não confidenciais da camada [métier] do servidor, a fim de aliviar a carga deste;
  • o cliente [2] pode armazenar certas visualizações em cache para, mais uma vez, aliviar a carga do servidor. Ele gerencia a sessão;