7. Implementação de Ajax numa aplicação Spring MVC
7.1. O papel do AJAX numa aplicação web
Por enquanto, os exemplos de aprendizagem analisados tinham a seguinte arquitetura:
![]() |
Para passar de uma vista [Vue1] para uma vista [Vue2], o navegador:
- envia um pedido à aplicação web;
- recebe a vista [Vue2] e apresenta-a no lugar da vista [Vue1].
Este é o esquema clássico:
- pedido do navegador;
- elaboração de uma vista em resposta ao cliente pelo servidor web;
- exibição dessa nova vista pelo navegador.
Há já alguns anos que existe 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 vista apresentada pelo navegador e o servidor web. O navegador continua a fazer o que sabe fazer, ou seja, apresentar uma vista HTML, mas passa a ser controlado por JavaScript incorporado na vista HTML apresentada. O esquema é o seguinte:
![]() |
- em [1], ocorre um evento na página apresentada no navegador (clique num botão, alteração de um texto, etc.). Este evento é interceptado pelo JavaScript (jS) incorporado na página;
- em [2], o código JavaScript efetua uma solicitação HTTP, tal como o navegador teria feito. A solicitação é assíncrona: o utilizador pode continuar a interagir 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], é enviada uma resposta ao cliente jS. Em vez de uma vista HTML completa, é enviada uma vista HTML parcial, um fluxo XML ou jSON (Notação de Objeto JavaScript) que é enviado;
- em [4], o JavaScript recupera essa resposta e utiliza-a para atualizar uma região da página HTML apresentada.
Para o utilizador, verifica-se uma alteração na visualização, uma vez que o que vê mudou. No entanto, não há um recarregamento total da página, mas sim uma simples modificação parcial da página apresentada. Isto contribui para conferir fluidez e interatividade à página: como não há uma recarga total da página, é possível gerir eventos que anteriormente não eram geridos. Por exemplo, apresentar ao utilizador uma lista de opções à medida que este introduz caracteres numa caixa de entrada. A cada novo caractere digitado, é enviada uma solicitação AJAX ao servidor, que, por sua vez, devolve outras sugestões. Sem o Ajax, este tipo de ajuda à digitação era anteriormente impossível. 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 vistas
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 sobre dois números reais A e B;
- em [3], a resposta do servidor é inserida numa área da página;
- em [4], a hora do cálculo. Esta é diferente da hora de carregamento da página [5]. Esta última é igual a [1], o que demonstra 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 vista [vue-01]
...
}
- linha 2: a ação [/ajax-01] aceita apenas um único parâmetro, [tempo]. Trata-se do tempo, em milissegundos, durante o qual 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 esse o caso, o valor do tempo de espera é guardado na sessão. Isto significa que permanecerá em vigor enquanto não for alterado;
O código da ação [/ajax-01] prossegue 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) {
// período válido?
...
// a preparar o modelo da vista [vue-01]
modèle.addAttribute("actionModel01", new ActionModel01());
...
// vista
return "vue-01";
}
A classe [ActionModel01] serve principalmente para encapsular os valores enviados pela ação [/ajax-01]. Aqui, não há nada a enviar. Cria-se uma classe vazia que é colocada no modelo, uma vez que 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) {
...
// prepara-se o modelo da vista [vue-01]
modèle.addAttribute("actionModel01", new ActionModel01());
Resultats résultats = new Resultats();
modèle.addAttribute("resultats", résultats);
...
// vista
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 sobre os 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 vista que deve ser apresentada, se for o caso;
- linha 14: a cultura da vista, [fr-FR] ou [en-US];
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(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 vista a cultura a utilizar, [fr-FR] ou [en-US]. Esta cultura destina-se ao JavaScript incorporado na vista;
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 cadeia [${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 vista [vue-01.xml]:
7.2.3. A vista [vue-01.xml]
![]() | ![]() |
A vista [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]. É opcional se os registos de validação tiverem sido desativados;
- linha 13: a biblioteca [Unobtrusive Ajax] da Microsoft. Esta biblioteca permite, por vezes, evitar a necessidade de escrever JavaScript;
- linha 16: um ficheiro jS para as nossas próprias necessidades;
- linhas 17-22: para gerir, do lado do cliente, as configurações de formato [fr-FR] e [en-US]. Já nos deparámos com este código;
- linha 27: uma mensagem configurada. Analisámo-las no parágrafo 5.18;
- linhas 36-38: o formulário ao qual voltaremos mais tarde;
- linha 40: a área do documento na qual o JavaScript irá inserir a resposta do servidor;
7.2.4. O formulário
![]() |
Na vista [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 várias mensagens encontram-se nos ficheiros [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 analisar os atributos da baliza [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">
Reconhecem-se os atributos clássicos da baliza [form]:
<form id="formulaire" name="formulaire" method="post" action="/ajax-02.html">
É possível verificar 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 restantes 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 geridos pela biblioteca jS [unobtrusive-ajax], que foi importada pela vista [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 através de uma chamada Ajax da biblioteca [unobtrusive-ajax]. O significado dos parâmetros é o seguinte:
- [data-ajax="true"]: é a presença deste 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 apresentar enquanto se aguarda a resposta do servidor. O campo identificado por [loading] na vista [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 de a área [data-ajax-loading="#loading"] ser exibida. Neste caso, será exibida assim que a espera começar;
- [data-ajax-begin="beforeSend"]: a função jS a executar 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 da área onde será colocado o resultado enviado pelo servidor. A vista [vue-01.xml] possui a seguinte área:
<div id="resultats" />
- [data-ajax-mode="replace"]: o modo de inserção do resultado na zona anterior. O modo [replace] fará com que o resultado «substitua» o que estava anteriormente na zona com o identificador [resultats];
É importante referir que o código 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 escrever JavaScript. Veremos que, neste caso, isso não foi possível.
7.2.5. A ação [/ajax-02]
Vimos que os valores enviados eram encaminhados para a ação [/ajax-02]. Esta é 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 {
// tempo?
Integer tempo = (Integer) session.getAttribute("tempo");
if (tempo != null && tempo > 0) {
Thread.sleep(tempo);
}
// prepara-se o modelo da próxima vista
Resultats résultats = new Resultats();
modèle.addAttribute("resultats", résultats);
// definimos a localização
setLocale(locale, modèle, résultats);
// hora
résultats.setHeurePost(new SimpleDateFormat("hh:mm:ss").format(new Date()));
...
}
- Vamos simplificar, numa primeira fase: partimos do princípio de que a ação POST que ocorre foi efetivamente executada pelo JavaScript da vista [vue-01.xml]. Voltaremos a esta hipótese um pouco mais tarde;
- linha 2: os valores [a,b] enviados são inseridos no modelo [ActionModel01];
- linhas 4-7: se o utilizador tiver definido um tempo de espera num GET anterior, este é recuperado da sessão e o tempo de espera é aplicado (linha 6). O objetivo desta temporização é permitir que o utilizador 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: insere-se a hora do POST no modelo;
Recorde-se o 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 da seguinte forma:
@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 uma vez em cada duas
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 devolver uma página de erro ao cliente jS. Em metade das vezes, devolvemos a seguinte vista [vue-03.xml]:
![]() |
Note-se, na linha 9, que não se trata de uma mensagem inserida no modelo, mas sim 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 vista [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 parametrizada por uma chave de mensagem que, por sua vez, é calculada. Introduzimos este conceito no parágrafo 5.18, página 170.
O código da ação [/ajax-02] continua da seguinte forma:
@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 {
...
// recuperam-se os valores enviados
double a = formulaire.getA();
double b = formulaire.getB();
// constrói-se 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 vista
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: é devolvida a seguinte vista [vue-02.xml]:
![]() |
A vista [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>
Quer o resultado seja a vista [vue-02.xml] ou a vista [vue-03.xml], esse resultado HTML é colocado na zona identificada por [resultats] na vista [vue-01.xml], devido ao atributo [data-ajax-update="#resultats"] do formulário.
7.2.6. O POST dos valores introduzidos
Temos aqui uma dificuldade com os valores lançados. Trabalhamos com duas culturas, [fr-FR] e [en-US], que escrevem os números reais de forma diferente. Já tínhamos abordado esta 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 nessa altura. Mas temos uma dificuldade adicional: não temos acesso ao método que processa os valores introduzidos no POST. É por essa razão que adicionámos os seguintes atributos à baliza do formulário:
- [data-ajax-begin="beforeSend"]: a função jS a executar antes de efetuar 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 irá enviar os valores introduzidos, mas podemos escrever duas funções jS:
- [beforeSend]: uma função jS executada antes da POST;
- [afterComplete]: uma função jS executada após a receção da resposta à função POST;
Estas duas funções estão incluídas num ficheiro [local1.js]:
![]() |
O ficheiro [local1.js] inicializa o ambiente jS da vista [vue-01.xml] da seguinte forma:
// dados globais
var loading;
var formulaire;
var résultats;
var a, b;
// ao carregar o documento
$(document).ready(function() {
// recuperam-se as referências dos diferentes componentes da página
loading = $("#loading");
formulaire = $("#formulaire");
resultats = $('#resultados');
a = $("#a");
b = $("#b");
// ocultam-se alguns elementos
loading.hide();
// analisam-se os validadores do formulário
$.validator.unobtrusive.parse(formulaire);
// são geridas duas configurações regionais [fr_FR, en_US]
// os valores reais [a,b] são enviados pelo servidor no formato anglo-saxónico
// convertem-se 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ónico
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ónico
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 vista é [fr-FR]. Nesse caso, é necessário alterar os valores lançados. Com efeito, se o utilizador tiver introduzido [1,6], deve lançar-se o valor [1.6]; caso contrário, o valor [1,6] será rejeitado pelo servidor. Para tal, basta substituir a vírgula dos valores enviados por um ponto decimal (linhas 18-21);
- mas não podemos ficar por aqui. Com efeito, quando a função [beforeSend] é chamada, a cadeia de valores enviados [a=val1&b=valB] já foi construída. Temos, portanto, de a modificar. Isto é feito através do segundo parâmetro [settings] da função;
- linha 7: [settings.data] (settings é um parâmetro da função) representa a cadeia de caracteres enviada. Recriamos esta cadeia com a expressão [formulaire.serialize()]. Esta expressão percorre o formulário à procura dos valores a enviar e constrói a cadeia de POST. Em seguida, irá obter os novos valores de [a,b] com pontos decimais;
Se não se fizer mais nada, o servidor enviará a sua resposta, que será apresentada corretamente. Só que agora os valores de [a,b] estão com o ponto decimal, enquanto ainda se está na cultura [fr-FR]. Assim, se o utilizador não se aperceber disso e clicar novamente em [Calculer], os validadores respondem-lhe que os valores [a,b] são inválidos. O que está correto. É aqui 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 cultura da vista for [fr-FR], os números [a,b] são convertidos para o formato francês.
7.2.7. Testes
Eis algumas capturas de ecrã 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. Isto significa que o servidor aguardará 5 segundos antes de enviar a sua resposta. Na baliza [form], utilizámos o atributo [data-ajax-loading='#loading']. O parâmetro [loading] é o identificador de uma área que é:
- exibida durante todo o período de espera;
- oculta após a receção da resposta do servidor;
Aqui, [loading] é o identificador de uma imagem animada que se vê 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 introduzidos será efetuada de acordo com a baliza [form], cujos atributos [data-ajax-attr] não serão utilizados. Tudo acontece como se tivéssemos a seguinte baliza [form]:
<form id="formulaire" name="formulaire" method="post" action="/ajax-02.html">
Os valores introduzidos serão, portanto, enviados para a ação [/ajax-02]. Não terão sido verificados do lado do cliente. Serão, portanto, os validadores do lado do servidor que irão intervir. Estes já intervinham anteriormente, mas sobre valores já validados do lado do cliente, ou seja, corretos. Já não é esse o caso.
Alteramos a ação [/ajax-02] da seguinte forma:
@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 {
// pedido Ajax?
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
}
- linha 4: a ação [/ajax-02] pode agora ser chamada através de um POST Ajax ou através de um POST clássico. Temos de saber diferenciar estes dois casos. Fazemo-lo através dos cabeçalhos HTTP enviados pelo navegador do cliente;
Quando se analisa o tráfego de rede na consola de desenvolvimento do Chrome (Ctrl-Shift-I) com o JavaScript ativado, verifica-se que o cliente envia os seguintes cabeçalhos no momento da chamada POST:
![]() |
Como se pode ver acima:
- foi enviado um cabeçalho [X-Requested-With] [1];
- foi adicionado um parâmetro [X-Requested-With] aos valores enviados [2];
Isto não acontece no caso de um POST clássico. Temos, portanto, duas possibilidades para recuperar a informação: recuperá-la nos cabeçalhos HTTP ou nos valores publicados. A linha 4 da ação [/ajax-02] optou pela primeira solução.
Continuemos com o código desta 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 {
// pedido 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 vista
Resultats résultats = new Resultats();
modèle.addAttribute("resultats", résultats);
// define-se a localização
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);
// pedido válido?
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 é devolvida a vista [vue-01.xml] com as mensagens de erro.
Eis um exemplo:
![]() | ![]() |
Continuemos 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 {
// pedido Ajax?
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
// pedido válido?
if (!isAjax && result.hasErrors()) {
return "vue-01";
}
// gera-se um erro uma em cada duas vezes
int val = new Random().nextInt(2);
if (val == 0) {
// é devolvida 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: é gerado um erro aleatório;
- linha 16: no caso de uma chamada Ajax, é devolvida a vista [vue-03.xml], que será colocada na área identificada por [resultats];
- linha 18: no caso de uma chamada não Ajax, coloca-se a vista a apresentar no modelo do tipo [Resultats];
- linha 19: volta a apresentar-se a vista [vue-01.xml];
A vista [vue-01.xml] é alterada 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 vista [vue-03.xml] será inserida abaixo da área [resultats];
Eis um exemplo:
![]() |
Note-se que, a partir de agora, as horas [1] e [2] são idênticas.
Continuemos 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 {
// pedido Ajax?
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
// recuperam-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 vista
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: gera-se a vista [vue-01.xml] (linha 22) inserindo-lhe a vista [vue-02.xml] (linha 22);
Esta inserção é efetuada da seguinte forma 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 zona [resultats];
Eis um exemplo de execução:
![]() |
7.2.9. Desativação do JavaScript com a configuração [fr-FR]
Com a configuração [fr-FR], surge o seguinte problema:
![]() | ![]() |
Os valores introduzidos no formato francês foram considerados inválidos. Com efeito, o servidor espera valores reais no formato anglo-saxónico. A solução é bastante complexa. Vamos criar um filtro que irá:
- interceptar o pedido;
- 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 que deve processá-la;
Em primeiro lugar, introduzimos um campo oculto na vista [vue-01.xml]:
<form ...>
...
</p>
<!-- campos ocultos -->
<input type="hidden" id="culture" name="culture" th:value="${resultats.culture}"></input>
</form>
- linha 5: o valor [fr-FR] ou [en-US] é inserido no campo de atributo [name=culture]. Como a baliza [input] está no formulário, o seu valor será enviado juntamente com os valores de [a] e [b]. Teremos, então, uma cadeia enviada com o seguinte formato:
É importante compreender este ponto.
Em seguida, incluímos um filtro na configuração da aplicação:
![]() |
O ficheiro [Config] é alterado 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 facto de o bean [cultureFilter] devolver um tipo [Filter] torna-o num filtro. O bean, por sua vez, pode ter qualquer nome;
O passo seguinte consiste em 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 {
// manipulador seguinte
filterChain.doFilter(new CultureRequestWrapper(request), response);
}
}
- linha 12: estendemos a classe [OncePerRequestFilter], que é uma classe Spring, e o que temos de fazer é redefinir o método [doFilterInternal] dessa classe;
- linha 15: o método [doFilterInternal] recebe três parâmetros:
- [HttpServletRequest request]: a consulta a filtrar. Esta não pode ser alterada,
- [HttpServletResponse response]: a resposta que será enviada ao servidor. O filtro pode decidir enviá-la ele próprio,
- [FilterChain filterChain]: a cadeia de filtros. Assim que o método [doFilterInternal] terminar o seu trabalho, deve passar a solicitação para o filtro seguinte da cadeia de filtros;
- linha 18: cria-se uma nova solicitação a partir daquela que foi recebida ([new CultureRequestWrapper(request)]) e passa-se essa solicitação para o filtro seguinte. Como não é possível alterar 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 irá redefinir alguns dos seus métodos;
- linhas 8-10: o construtor que recebe a consulta a filtrar e a passa para a classe pai;
- é importante compreender aqui que a solicitação filtrada acabará por ser passada como parâmetro de entrada de uma classe denominada servlet. Com o Spring MVC, esta servlet é do tipo [DispatcherServlet]. Esta classe dispõe de vários métodos para recuperar os parâmetros da solicitação: [getParameter, getParameterMap, getParameterNames, getParameterValues, ...]. É necessário redefinir o método utilizado pelo servlet. Para tal, seria necessário ler o código da classe [DispatcherServlet]. Não o fiz e redefini vários métodos. Acabou por ser o método [getParameterValues] que foi redefinido;
- linha 13: o método [getParameterValues] recebe como parâmetro o nome de um dos parâmetros devolvidos pelo método [getParameterNames] e deve devolver o tabuleiro com os seus valores. Com efeito, sabe-se que um parâmetro pode estar presente em várias instâncias numa solicitação;
- linha 18: substitui-se a vírgula por um ponto decimal;
Eis um exemplo de execução:
![]() |
- no [1], os valores do [a,b] são introduzidos no formato francês;
- em [2], os resultados;
- em [3], o servidor devolveu uma página com números no formato anglo-saxónico.
Este último problema pode ser resolvido com o Thymeleaf da seguinte forma na vista [vue-01.xml]
<tr>
<td>
<input type="text" id="a" name="a" th:value="${resultats.culture}=='fr-FR' and ${actionModel01.a}!=null? ${#strings.replace(actionModel01.a,'.',',')} : ${actionModel01.a}" data-val="true" th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-min=#{actionModel01.a.min},data-val-min-value=#{actionModel01.a.min.value}" />
</td>
<td>
<input type="text" id="b" name="b" th:value="${resultats.culture}=='fr-FR' and ${actionModel01.b}!=null? ${#strings.replace(actionModel01.b,'.',',')} : ${actionModel01.b}" data-val="true" th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-min=#{actionModel01.b.min},data-val-min-value=#{actionModel01.b.min.value}" />
</td>
</tr>
Há várias alterações a fazer 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 baliza HTML [input] gerada. Neste caso, queremos gerir o atributo [value] nós próprios. Por isso, definimos também os atributos [id, name] nós próprios;
- 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 for verdadeira, atribuímos ao atributo [value] o valor de [actionModel01.a], em que 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];
Eis um exemplo de execução:
![]() |
- em [1], os números de [a,b] mantiveram a notação francesa. Não é esse o caso em [2];
Este novo problema é resolvido da mesma forma que o anterior. Altera-se a vista [vue-03.xml] da seguinte forma:
<!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>
Eis um exemplo:
![]() | ![]() |
Agora temos uma aplicação que gere corretamente duas culturas num ambiente que utilize ou não JavaScript. Para tal, foi necessário tornar o código do lado do servidor significativamente mais complexo. Daqui em diante, partiremos sempre do princípio de que o JavaScript do navegador está ativado. Isto permite fazer coisas que seriam impossíveis apenas no modo servidor.
7.2.10. Gestão do link [Calculer]
Analisemos o link [Calculer] da página principal [vue-01.xml]:
![]() | ![]() |
O código do link [Calculer] na vista [vue-01.xml] é o seguinte:
<a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
A função jS [postForm] está definida no ficheiro [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 geridas duas configurações regionais [fr_FR, en_US]
// os valores reais [a,b] devem ser apresentados no formato anglo-saxónico em todos os casos
// isso será feito pelo filtro [CultureFilter]
// é feita 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: recorde-se que estes 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 é anulado;
- linhas 18-38: é efetuada manualmente uma chamada Ajax;
- linha 19: o URL de destino da chamada Ajax;
- linhas 20-22: um conjunto de cabeçalhos HTTP a adicionar aos que estão presentes por predefinição na solicitação HTTP. Aqui, adiciona-se o cabeçalho HTTP, que indicará ao servidor que se está a efetuar uma chamada Ajax;
- linha 23: o método HTTP utilizado;
- linha 24: os dados enviados. [formulaire.serialize] cria a cadeia a enviar [culture=fr-FR&a=12,7&b=20,89] do formulário com o ID [formulaire]. Vamos encontrar aqui o problema analisado anteriormente: os valores [a,b] têm de ser enviados no formato anglo-saxónico. Sabemos que este problema já foi resolvido com a criação do filtro [cultureFilter];
- linha 25: o tipo de dados esperado em resposta. Sabemos que o servidor irá devolver um fluxo HTML;
- linha 26: o método a executar quando a solicitação for iniciada. Aqui, indica-se que é necessário apresentar o componente com o ID [loading]. Trata-se da imagem animada de espera;
- linha 29: o método a executar caso a solicitação Ajax seja bem-sucedida. O parâmetro [data] é a resposta completa do servidor. Sabe-se que se trata de um fluxo HTML;
- linha 30: atualiza-se o componente com o ID [résultats] com o valor HTML do parâmetro [data].
- linha 33: oculta-se o sinal de espera;
- 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 devolveu uma resposta HTTP com um estado a indicar que ocorreu um erro do lado do servidor), exibe-se a resposta HTML do servidor na zona [resultats];
Eis 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 ao pedido Ajax HTTP com um fluxo HTML. Neste fluxo, havia dados acompanhados de formatação HTML. Propomos retomar o exemplo anterior, mas desta vez com respostas jSON (JavaScript Object Notation) que contêm apenas os dados. A vantagem é que, desta forma, são transmitidos menos bytes. Partimos do princípio de que o JavaScript está ativado no navegador.
7.3.1. A ação [/ajax-04]
A ação [/ajax-04] é idêntica à ação [/ajax-01], com a única diferença de que se apresenta a vista [vue-04.xml] em vez da vista [vue-01.xml]:
@RequestMapping(value = "/ajax-04", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax04(Locale locale, Model modèle, HttpSession session, String tempo) {
...
// visualização
return "vue-04";
}
7.3.2. A vista [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 vista encontra-se agora no ficheiro [local4.js];
- linha 16: a baliza [form] já não tem os parâmetros [data-ajax-attr] da biblioteca [Unobtrusive Ajax]. Não vamos utilizá-la aqui. A baliza [form] também não possui os atributos [method] e [action], que indicam como e onde enviar os valores introduzidos no formulário. Isto porque esta será enviada por uma função jS (linha 20);
- linhas 26-57: o campo de identificação [resultats], que anteriormente estava vazio, contém agora o código HTML para apresentar os resultados;
- linhas 26-34: o cabeçalho dos resultados, onde é apresentada 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 vista [vue-04.xm], encontra-se no ficheiro [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");
// ocultam-se 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 erro;
- 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]
A ligação [Calculer] é a 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 ficheiro [local.js] da seguinte forma:
function postForm() {
// formulário válido?
if (!formulaire.validate().form()) {
// formulário inválido - concluído
return;
}
// efetua-se manualmente uma chamada Ajax
$.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 introduzidos, estes são verificados. Se estiverem incorretos, não se executa a função POST do formulário;
- linha 9: os valores introduzidos são enviados para a ação [/ajax-05], que detalhamos um pouco mais adiante;
- linhas 10-12: um cabeçalho HTTP para indicar ao servidor que se espera uma resposta no formato jSON;
- linha 13: os valores introduzidos vão ser enviados;
- linha 14: serialização dos valores introduzidos numa cadeia de caracteres 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 executar antes do POST;
- linha 17: a função a executar após a receção da resposta do servidor, caso esta seja bem-sucedida. O «sucesso» de um pedido HTTP é avaliado com base no estado 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 designa por «estado» de uma resposta HTTP é o código [200] ou [500]. Alguns destes códigos estão associados ao «sucesso», enquanto outros estão associados ao «fracasso»;
- linha 18: a função a executar ao receber a resposta do servidor, quando o estado HTTP dessa resposta for um estado de falha;
- linha 18: a função a executar em último lugar, após as funções [onSuccess, onError] anteriores;
A função [onBegin] é a seguinte:
// antes da chamada Ajax
function onBegin() {
console.log("onBegin");
// é apresentada a imagem animada
loading.show();
// ocultam-se alguns elementos da vista
entete.hide();
résultats.hide();
erreur.hide();
}
Antes de analisarmos as outras funções jS da chamada Ajax, precisamos de 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 vista [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 é apresentado nada
return null;
}
...
}
- linha 2: o atributo [ResponseBody] indica que a ação [/ajax-05] devolve ela própria a resposta ao cliente. Como uma biblioteca jSON está nas dependências do projeto, o Spring Boot configura automaticamente este tipo de ações para que devolvam jSON. Assim, é 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 num tipo [ActionModel01], cuja validação [@Valid ActionModel01] é solicitada. Trata-se apenas de uma formalidade. Partimos do princípio de que o JavaScript estava ativado no navegador do cliente e, por isso, quando chegam, os valores enviados já foram verificados do lado do cliente. No entanto, podemos prever o caso de um POST não autorizado que não utilizasse o nosso cliente jS. Nesse caso, a validação pode falhar;
- linhas 5-7: em caso de erro, devolve-se um fluxo jSON vazio;
Continuemos a análise da ação [/ajax-05]:
@RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
@ResponseBody()
// processa o POST da vista [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);
}
...
// retorna o resultado
return résultats;
}
- linha 8: recupera-se o contexto [ctx] da aplicação Spring. É necessário para recuperar as mensagens dos ficheiros [messages.properties] a partir de uma chave de mensagem e de uma localização. Isto é 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 esta localização for [en_US], será utilizado o ficheiro [messages_en.properties];
- [tableau_de_paramètres]: a mensagem obtida pode ser configurada tal como em [clé=message {0} {1}]. Nesta mensagem existem dois parâmetros [{0} {1}]. Será necessário fornecer, como segundo parâmetro de [ctx.getMessage], uma tabela com dois valores;
- linhas 10-13: se houver uma pausa na sessão, o thread atual é suspenso durante o tempo dessa pausa;
A ação [/ajax-05] prossegue da seguinte forma:
// prepara-se o modelo da próxima vista
JsonResults résultats = new JsonResults();
...
}
- linha 2: criação do modelo da cadeia 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 com o mesmo nome na classe [id] na vista [vue-04.xml]:
A ação [/ajax-05] prossegue da seguinte forma:
// prepara-se o modelo da próxima vista
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()));
// gera um erro uma vez em cada duas
int val = new Random().nextInt(2);
if (val == 0) {
// retorna-se 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 cadeia jSON enviada ao cliente;
- linhas 4-6: criam-se as mensagens do cabeçalho dos resultados;
- linhas 8-14: em média, uma vez em cada duas, é gerada uma mensagem de erro. Nesse caso, o processo não prossegue e a cadeia 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 forma:
// recuperam-se os valores enviados
double a = formulaire.getA();
double b = formulaire.getB();
// constrói-se 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 cadeia jSON [JsonResults] é enviada ao cliente;
Vamos ver o resultado com o cliente [Advanced Rest Client]:
![]() |
- em [1-2], faz-se uma solicitação POST à ação [/ajax-05];
- em [3], são enviados valores incorretos;
- em [4], o servidor devolveu um fluxo vazio;
![]() |
- em [1], são enviadas valores corretos;
- em [2], o objeto jSON devolvido pelo servidor, com uma mensagem de erro;
![]() |
- em [1], são enviadas valores corretos;
- em [2], o objeto jSON devolvido pelo servidor, com os quatro resultados;
![]() |
- em [1], enviam-se valores corretos;
- em [2], conseguimos provocar uma exceção do lado do servidor. Vê-se que o servidor envia novamente um objeto jSON. Nesta mensagem, verifica-se que o estado HTTP da resposta é [500], indicando que ocorreu um erro do lado do servidor;
7.3.5. A função jS [postForm] - 2
Agora que conhecemos o objeto jSON devolvido pelo servidor, podemos utilizá-lo no JavaScript. O método [onSuccess] executado quando o servidor envia uma resposta com o estado HTTP [200] é o seguinte:
// ao receber a resposta do servidor
// em caso de sucesso
function onSuccess(data) {
console.log("onSuccess");
// preenche-se a área dos 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 devolvido pelo servidor:
![]() |
O método [onError] executado quando o estado da resposta HTTP é [500] é o seguinte:
// após a receção da 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 as suas propriedades, as seguintes:
- responseText: o texto da resposta do servidor,
- status: o código de erro devolvido pelo servidor,
- statusText: o texto associado a este código de erro;
- linha 6: o objeto [jqXHR.responseText] é o objeto jSON seguinte:
![]() |
7.3.6. Testes
Vejamos algumas capturas de ecrã da execução da aplicação web:
![]() |
![]() |
![]() |
7.4. Aplicação web de página única
7.4.1. Introdução
A tecnologia Ajax permite criar aplicações de página única:
- a primeira página resulta de um pedido normal do navegador;
- as páginas seguintes são obtidas através de chamadas Ajax. Assim, no final, o navegador nunca muda de URL e nunca carrega uma nova página. A este tipo de aplicação chama-se «Aplicação de Página Única» (APU) ou, em inglês, «Single Page Application» (SPA).
Eis um exemplo básico de uma aplicação deste tipo. A nova aplicação terá duas vistas:
![]() |
![]() |
- em [1], a ação [/ajax-06] permite-nos aceder à primeira página, a página 1;
- em [2], um link permite-nos passar para a página 2 através de uma chamada Ajax;
- em [3], o URL não sofreu alterações. A página apresentada é a página 2;
- em [4], um link permite-nos regressar à página 1 através de uma chamada Ajax;
- em [5], o URL não sofreu alterações. A página apresentada é 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 apresentar a vista [vue-06.xml];
7.4.3. A vista [vue-06.xml]
A vista [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 vista utiliza um script [local6.js];
- linha 12: inclui-se a vista [vue-07.xml] na zona de ID [content] da vista [vue-06.xml];
7.4.4. A vista [vue-07.xml]
A vista [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]
A ligação [Page 2] da vista [vue-07.xml] utiliza a função jS [gotoPage] definida no seguinte ficheiro [local6.js]:
// dados globais
var content;
function gotoPage(num) {
// é efetuada 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() {
// estão a ser recuperadas as referências dos diferentes componentes da página
content = $("#content");
});
- linha 28: ao carregar a página, memoriza-se a zona com o ID [content] e transforma-se numa 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 apresentar na vista atual;
- linha 7: a função URL é o destino da função POST;
- linha 8: a função URL da linha 7 é chamada através de uma função POST;
- linha 9: a cadeia enviada. Trata-se de um parâmetro denominado [num] que é enviado. O seu valor é o número da página (linha 4) a apresentar na vista atual;
- linha 10: o servidor irá devolver o HTML, correspondente à página a apresentar;
- linhas 13-15: em caso de sucesso (estado HTTP igual a 200), o HTML enviado pelo servidor é colocado na zona de ID [content];
- linhas 18-20: em caso de falha (estado HTTP igual a 500), o HTML enviado pelo servidor é colocado 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, denominado [num]. Recorde-se 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 vista;
- linhas 5-6: no caso de [num==1], devolve-se a vista [vue-07.xml];
- linhas 7-8: no caso de [num==2], devolve-se a vista [vue-08.xml];
- linhas 9-10: nos restantes casos (normalmente impossíveis), devolve-se a vista [vue-07.xml];
7.4.7. A vista [vue-08.xml]
A vista [vue-08.xml] constitui a página n.º 2 da aplicação:
<!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 numa resposta jSON
7.5.1. Introdução
Consideramos a seguinte aplicação:
![]() |
A página [1] tem 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 destas duas áreas [2]. A área [Zone 1] utiliza a língua francesa, enquanto a área [Zone 3] utiliza a língua inglesa;
- a zona [Zone 2] está sempre presente;
- a zona [Saisies] está sempre presente;
O link [Valider] apresenta a página seguinte, [3]:
![]() |
- o link [Retour à la page 1] restaura a página n.º 1 ao estado em que se encontrava ([4]);
A aplicação é de página única. A primeira página é solicitada ao servidor pelo navegador. As seguintes são obtidas do servidor através 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";
}
Limita-se a apresentar a vista [vue-09.xml].
7.5.3. As vistas XML
![]() |
A vista [vue-09.xml] é a página principal da aplicação:
<!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 ficheiro JS utilizado na aplicação;
- linha 15: o conteúdo da página principal;
- linha 16: uma imagem animada de espera:
- linha 17: área para apresentar um eventual erro;
A vista [vue-09-page1.xml] é a página 1 da aplicação:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h2>Page 1</h2>
<!-- zona 1 -->
<fieldset id="zone1" style="background-color:pink">
<legend>Zone 1</legend>
<span id="zone1-content" th:text="xx">xx</span>
</fieldset>
<!-- zona 2 -->
<fieldset id="zone2" style="background-color:lightgreen">
<legend>Zone 2</legend>
<span>Ce texte reste toujours présent</span>
</fieldset>
<!-- á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]. O seu conteúdo é colocado no componente [id="zone1-content"];
- linhas 11-14: a zona [Zone 2], que não sofre alterações;
- linhas 16-19: a zona [Zone 3]. O seu conteúdo é colocado no componente [id="zone3-content"];
- linha 22: a função JS que envia o formulário;
- linha 25: inclusão da área de preenchimento;
Note-se que a página 1 não tem a tag [form]. Tudo será processado em JavaScript.
A vista [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: introdução de uma cadeia de caracteres;
- linhas 13-16: introdução de um número inteiro;
- linha 14: a função JS que envia os valores introduzidos;
Mais uma vez, note-se que a área de introdução de dados não possui a baliza [form].
No total, a página n.º 1 apresenta duas funcionalidades:
- [Rafraîchir]: que atualiza as zonas 1 e 3. Esta ação é processada pelo servidor, que devolve aleatoriamente:
- a zona 1 com o seu contador de acessos e nada para a zona 3,
- a zona 3 com o seu contador de acessos e nada para a zona 1,
- as duas zonas com os respetivos contadores de acesso;
- [Valider]: que apresenta a página 2 com os valores introduzidos ou uma mensagem de erro se os dados introduzidos forem inválidos;
Vamos centrar-nos primeiro no botão [Rafraîchir].
7.5.4. O código JS para a gestão do botão [Rafraîchir]
![]() |
O código do ficheiro [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 principal é carregada, memorizam-se as referências aos três componentes identificados por [loading, erreur, content];
- linhas 2-4: as referências destes três componentes são armazenadas em variáveis globais. 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 na função [$(document).ready] e partilhadas com as outras funções do ficheiro JS;
A função [postForm] gere o clique no botão [Rafraîchir]:
function postForm() {
console.log("postForm");
// efetua-se manualmente uma chamada Ajax
$.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 irá 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 falha [500 Internal server error, ...];
- linha 14: a função executada após a receção da resposta;
A função [onBegin] é a seguinte:
// antes da chamada Ajax
function onBegin() {
console.log("onBegin");
// imagem de espera
loading.show();
}
Limita-se a iniciar a imagem animada que mostra 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 tem o tipo [SessionModel1], conforme se segue:
![]() |
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 zonas
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 a zona [Zone 1] é apresentada;
- linha 16: o número de vezes [cpt3] em que a zona [Zone 3] é apresentada;
- linhas 18-20: os fluxos HTML das zonas [Zone 1], [Zone 3] e [Saisies]. Isto é necessário na sequência [Page 1] --> [Page 2] --> [Page 1]. Ao passar de [Page 2] para [Page 1], é necessário restaurar [Page 1] e, consequentemente, as suas três zonas;
- linhas 21-22: dois valores booleanos que indicam se os campos [Zone 1] e [Zone 3] são apresentados (visíveis);
O outro elemento inserido no controlador [AjaxController] é o seguinte:
// o motor Thymeleaf / Spring
@Autowired
private SpringTemplateEngine engine;
O bean do tipo [SpringTemplateEngine] está definido no ficheiro de configuração [Config]:
![]() |
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 vistas;
- linhas 13-17: o bean do tipo [SpringTemplateEngine] permite-nos definir o «motor» das vistas, a classe responsável por gerar as respostas [Thymeleaf] para os clientes. O [Thymeleaf] tem um «motor» por predefinição e outro quando é utilizado num 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] só aceita um POST;
- linha 2: a ação [/ajax-10] devolve ela própria a resposta ao cliente. Esta será transformada automaticamente em jSON;
- linha 3: a resposta é do tipo [JsonResult10], conforme se 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 da zona identificada 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 que indica se a zona [Zone 1] deve ser apresentada;
- linha 12: valor booleano que indica se a zona [Zone 3] deve ser apresentada;
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);
// retorna 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 tarde para que nos 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 não devem ser apresentados. Estes dois campos serão gerados em breve, mas é possível que apenas um deles venha a ser gerado;
- linhas 14-29: os dois campos são gerados;
- linhas 17-19: apenas a zona [Zone 1] é gerada;
- linhas 21-23: apenas a zona [Zone 3] é gerada;
- linhas 25-28: são geradas as duas zonas [Zone 1] e [Zone 3];
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 fase de construção do tipo [JsonResult10];
- linha 3: incrementa-se o contador [cpt1] da sessão, que conta o número de vezes que a zona [Zone 1] é apresentada;
- linha 4: o contexto [Thymeleaf], do tipo [WebContext], comporta-se de forma semelhante ao modelo [Model] do Spring MVC. Para adicionar um elemento ao modelo, utiliza-se o [WebContext.setVariable]. Aqui, insere-se, portanto, o contador [cpt1] no modelo [Thymeleaf]. Isto permitirá avaliar a expressão Thymeleaf [${cpt1}]
- linha 5: o contexto [Thymeleaf] tem uma configuração regional. Isso permite-lhe avaliar expressões do tipo [#{clé_msg}]. Aqui, associamos o contexto Thymeleaf a uma configuração regional francesa;
- linha 6: esta é a instrução mais interessante. O motor Thymeleaf irá processar a vista [vue-09-zone1.xml] com o modelo e a localização que acabámos de calcular e, em vez de enviar o fluxo HTML resultante ao cliente, devolve-o como uma cadeia de caracteres;
- linhas 7-9: o fluxo HTML da zona [Zone 1], que acabou de ser calculado, é armazenado na sessão e no resultado que será enviado ao cliente. Além disso, indica-se que a zona [Zone 1] deve ser apresentada;
- linhas 11-13: guardam-se na sessão as informações relativas à zona [Zone 1], para que seja possível regenerá-la;
A linha 7 trata a seguinte vista [vue-09-zone1.xml]:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<span th:text="#{message.zone}"></span>
<span th:text="${cpt1}"></span>
</html>
- linha 3: a expressão [#{message.zone}] será avaliada através da localização;
- linha 4: a expressão [${cpt1}] será avaliada através do modelo Thymeleaf;
A mensagem de chave [message.zone] está definida nos ficheiros 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 zona [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 localização da zona [Zone 3] é a localização em inglês;
7.5.6. Processamento da resposta da ação [/ajax-10]
Voltemos ao código JS de [local9.js], que irá 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();
}
}
Recorde-se a estrutura Java da resposta recebida na linha 3 na variável [data]:
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;
}
- linhas 6-8: se [data.content!=null], então inicializa-se a zona [id=content] com esse valor. Esta zona representa [Page 1] ou [Page 2] na sua totalidade. Na presente demonstração, temos [data.content==null] e, por isso, a zona [id=content] não será alterada e continuará a apresentar [Page 1];
- linhas 10-17: exibição de [Zone 1] se [data.zone1Active==true]. Se, além disso, for [data.zone1!=null], então o conteúdo de [Zone 1] é alterado; caso contrário, permanece como estava;
- linhas 19-26: o mesmo se aplica a [Zone 3];
- linhas 28-30: se tivermos [data.saisies!=null], então a zona [Saisies] é regenerada. Na demonstração atual, temos [data.saisies==null] e, por isso, 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 em formato de texto;
- linha 36: se [data.erreur==null], então a zona [Erreur] é ocultada. Com efeito, esta pode ter sido apresentada na consulta anterior;
Em caso de erro do lado do servidor (HTTP, com um estado do tipo «500 Internal server error»), é executada a seguinte função:
// 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 ver um erro deste tipo, vamos alterar a função [postForm] da seguinte forma:
function postForm() {
console.log("postForm");
// estão a ser recuperadas referências na página atual
...
// é efetuada uma chamada Ajax manualmente
$.ajax({
url : '/ajax-10x',
...
})
}
- linha 7: inserimos um URL que não existe;
Eis os resultados quando clicamos no botão [Rafraîchir]:
![]() |
É interessante verificar que o erro também foi enviado sob a forma de uma cadeia jSON.
O método executado após a receção 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 introduzidos
function valider() {
// valor enviado
var post = JSON3.stringify({
"value1" : $("#text1").val().trim(),
"value2" : $("#text2").val().trim()
});
// faz-se 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, para enviar: os dos componentes de entrada identificados por [#text1] e [#text2]. Vamos fazer algo novo. Vamos enviar estes dois valores na forma de uma cadeia jSON {"value1":v1,"value2":v2};
- linha 10: os valores enviados serão encaminhados para a ação [ajax-11A];
- linha 12: como sabemos que vamos receber uma resposta jSON, indicamos que podemos recebê-la com o formato jSON;
- linha 13: indicamos ao servidor que lhe vamos enviar o valor enviado na forma de uma cadeia jSON;
- linhas 15-16: converte-se o valor a enviar num POST;
- linha 17: vamos receber um jSON;
7.5.8. A ação [ajax-11A]
A ação [ajax-11A], que processa a cadeia 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. Este documento é o valor enviado pelo cliente;
- linha 3: o valor lançado será recuperado no objeto [PostAjax11A post] seguinte:
![]() |
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 corresponder à estrutura do objeto enviado {"value1":v1,"value2":v2}. É, portanto, necessário um campo [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();
// o POST é válido?
if (bindingResult.hasErrors()) {
// é devolvida 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. Este 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: é devolvida uma mensagem de erro,
- linhas 11-12: os campos 1 e 3 são repostos no estado em que se encontravam (exibidos ou não);
O cálculo da mensagem de erro é feito da seguinte forma:
private String getErreursForModel(BindingResult result) {
StringBuffer buffer = new StringBuffer();
for (FieldError error : result.getFieldErrors()) {
StringBuffer bufferCodes = new StringBuffer("(");
for (String code : error.getCodes()) {
bufferCodes.append(String.format("%s ", code));
}
bufferCodes.append(")");
buffer.append(String.format("[%s:%s:%s:%s]", error.getField(), error.getRejectedValue(), bufferCodes,
error.getDefaultMessage()));
}
return buffer.toString();
}
Esta é uma função que já conhecemos.
A ação [ajax-11A] prossegue da seguinte forma:
@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();
// envio válido?
if (bindingResult.hasErrors()) {
...
}
// memoriza-se o campo de introdução
thymeleafContext.setVariable("value1", post.getValue1());
thymeleafContext.setVariable("value2", post.getValue2());
session.setSaisies(engine.process("vue-09-saisies", thymeleafContext));
// envia-se a página 2
result.setContent(engine.process("vue-09-page2", thymeleafContext));
return result;
}
- linhas 13-14: os valores enviados são colocados no contexto Thymeleaf;
- linha 15: com este contexto, calcula-se a vista [vue-09-saisies] e coloca-se na sessão para poder regenerá-la posteriormente;
- linha 17: a página 2 é inserida no resultado que será enviado ao cliente;
A vista [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, exibem-se os valores [value1, value2] que a ação [/ajax-11A] colocou no contexto Thymeleaf;
7.5.9. Processamento da resposta da ação [/ajax-11A]
Do 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();
}
// área 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();
}
}
Já comentámos este código. Consideremos os dois casos: resposta com ou sem erro:
Com erro
Neste caso, a ação [/ajax-11A] enviou uma resposta jSON com o seguinte formato: {"zona1":null, "zona3":null,"entradas":null,"erro":erro,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":null}. Se seguirmos o código acima, verificamos que:
- a zona [content] não se altera. Con continha a página n.º 1;
- a zona [Erreur] é apresentada;
- as zonas [Zone 1], [Zone 3] e [Saisies] são mantidas tal como estavam;
Sem erros
Neste caso, a ação [/ajax-11A] enviou uma resposta jSON com o seguinte formato: {"zona1":null, "zona3":null,"entradas":null,"erro":null,"zone1Active":false,"zone3Active":false,"content":content}. Se analisarmos o código acima, verificamos que:
- a zona [content] é apresentada. Contém a página n.º 2;
Eis três exemplos de execução:
Um caso com erro de validação:
![]() | ![]() |
Um caso com erro de POST:
![]() | ![]() |
Este tipo de erro é diferente. Como o Spring não conseguiu converter a cadeia jSON para o tipo [PostAjax11A], devolveu 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
})
}
Este método executa 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 as suas três zonas [Zone1, Zone3, Erreur]:
- linha 9: a página n.º 1 é incluída no resultado;
- linha 10: a zona de entradas é incluída no resultado;
- linha 11: a zona [Zone 1] é incluída no resultado;
- linha 12: a zona [Zone 3] é incluída no resultado;
- linhas 13-14: o estado 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();
}
// 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();
}
}
A ação [/ajax-11B] enviou uma resposta jSON com o seguinte formato: {"zona1":zona1, "zona3":zona3,"entradas":entradas,"erro":null,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":content}. Se analisarmos o código acima, verificamos que:
- a zona [content] é alterada. Anteriormente, continha a página n.º 2. Passará agora a conter a página n.º 1;
- a zona [Erreur] está oculta;
- as zonas [Zone 1], [Zone 3] e [Saisies] são apresentadas tal como estavam;
7.6. Gerir a sessão do lado do cliente
7.6.1. Introdução
No parágrafo anterior, gerimos 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 utilizadores, a memória ocupada pelas sessões de todos esses utilizadores pode constituir um problema. A regra é, portanto, minimizar o tamanho dessa memória. O modelo APU (Aplicação de Página Única) permite gerir a sessão do lado do cliente e ter um servidor web sem sessão. Com efeito, a página única é carregada inicialmente pelo navegador. Juntamente com ela, é carregado o ficheiro JavaScript que a acompanha. Como não há recarregamento da página, este ficheiro JS permanecerá permanentemente no navegador, tal como foi carregado inicialmente. É então possível utilizar as suas variáveis globais para armazenar informações sobre as diferentes ações do utilizador. É isso que vamos ver agora. Não só iremos gerir a sessão do lado do cliente, como também repensar a aplicação JS de forma a sobrecarregar o servidor o menos 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 vista [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>
- esta vista é idêntica à vista [vue-09], com a única diferença do script JS utilizado na linha 9;
A vista apresentada é a seguinte:
![]() |
7.6.3. O código JS para a gestão do botão [Rafraîchir]
![]() |
O código do ficheiro [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. Armazena, do lado do cliente, os valores dos contadores [cpt1, cpt3];
A função [postForm] gere o clique no botão [Rafraîchir]:
function postForm() {
console.log("postForm");
// armazenamos a sessão
var post = JSON3.stringify(session);
// efetua-se manualmente uma chamada Ajax
$.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 é diferente;
- linha 4: é enviado um valor, ao passo que anteriormente não era enviado nenhum. Este valor é a cadeia jSON da sessão. O princípio é o seguinte:
- o cliente envia a sessão para o servidor,
- este altera-a e devolve-a,
- o cliente guarda a nova sessão;
- linha 10: envia-se um documento no formato jSON (valor enviado);
- linha 13: há algo para enviar;
- 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. Esta tem o tipo [SessionModel2] seguinte:
![]() |
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 a zona [Zone 1] é apresentada;
- linha 10: o número de vezes [cpt3] em que a zona [Zone 3] é apresentada;
Continuemos 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 reenvia-a 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 zona [Zone 3];
- linha 9: a eventual mensagem de erro;
- linhas 10-11: duas informações calculadas pelo servidor e apresentadas na página n.º 2;
Continuemos 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 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 é incluída no resultado da ação;
O método [setZone1B], que ativa a zona [Zone 1], é o seguinte:
private void setZone1B(WebContext thymeleafContext, JsonResult13 result) {
// recupera-se 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. Esta será alterada na linha 12 com o novo contador [cpt1]. Recorde-se que esta 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) {
// recuperação da 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]
Do 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 é enviada
var post = JSON3.stringify(session);
// faz-se uma chamada Ajax manualmente
$.ajax({
...
success : function(data) {
// guardamos a sessão
session = data.session;
// atualizam-se 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 tiver inserido algum valor no campo [zone1] da resposta, então é necessário regenerar a zona [Zone 1] e exibi-la; caso contrário, esta deve ser ocultada;
- linhas 18-23: o mesmo raciocínio aplica-se à zona [Zone 3];
7.6.6. Exibição da página [Page 2]
O código HTML da ligação [Valider] é o seguinte:
<a href="javascript:valider()">Valider</a>
A função JS [valider] é a seguinte:
// validação dos valores introduzidos
function valider() {
// guardamos a página 1
page1 = content.html();
// guardam-se os valores introduzidos
value1 = $("#text1").val().trim();
value2 = $("#text2").val().trim();
// valor enviado
var post = JSON3.stringify({
"value1" : value1,
"value2" : value2,
"pageRequired" : page2 ? false : true
});
// efetua-se manualmente uma chamada Ajax
$.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: guardamos a página n.º 1 para podermos voltar a ela mais tarde;
- linhas 6-7: a operação anterior não guarda os valores introduzidos, apenas o código HTML da página. Por isso, guardamos agora os dois valores introduzidos no formulário;
- linhas 9-13: os dois valores introduzidos são colocados numa cadeia jSON. É esta cadeia que será enviada;
- linha 12: um parâmetro para indicar ao servidor se precisamos da página n.º 2. Vamos proceder da seguinte forma. Vamos solicitar a página n.º 2 uma primeira vez e, em seguida, guardá-la na variável JS [page2]. Depois disso, não a voltaremos a solicitar. Iremos utilizar a página em cache. Na linha 2, [pageRequired] é igual a [true] se a variável [page2] estiver vazia; caso contrário, é igual a [false];
- note-se que a sessão não é enviada. Com efeito, esta memoriza 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. Por conseguinte, tem 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 Thymeleaf
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// resposta
JsonResult13 result = new JsonResult13();
// o POST é válido?
if (bindingResult.hasErrors()) {
// é devolvido 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 para [value1, value2] forem inválidos, é devolvida uma mensagem de erro;
- linhas 15-16: normalmente, o servidor deveria efetuar um cálculo com os valores enviados. Aqui, limita-se a reenviá-los para indicar que os recebeu corretamente;
- linhas 18-20: a página n.º 2 só é devolvida se tiver sido solicitada pelo cliente. Na linha 19, a vista [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 já não contém valores avaliados pelo Thymeleaf, como acontecia anteriormente;
- foram identificadas as áreas onde colocar os valores devolvidos pelo servidor, [value1, value2]. Linha 9, [id='value1'] indica o local onde colocar [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 introduzidos
function valider() {
...
// é efetuada 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á a ser memorizada
page2 = data.page2;
// a página é apresentada
content.html(data.page2);
}
// atualiza-se com as informações do servidor
$("#value1").text(data.value1);
$("#value2").text(data.value2);
}
},
...
})
}
- linhas 9-13: se o servidor tiver devolvido um erro, este é apresentado;
- linhas 14-29: o caso em que não houve erro. Nesse caso, deve-se apresentar a página n.º 2;
- linha 17: verifica-se se a página n.º 2 já está registada na variável [page2];
- linha 19: nesse caso, utiliza-se a variável [page2] para apresentar a página n.º 2;
- linha 24: caso contrário, utiliza-se o campo [data.page2] fornecido pelo servidor;
- linha 22: tem-se o cuidado de memorizar a página n.º 2 para não ter de a solicitar 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:
// regresso à página 1
function retourPage1() {
// regenera-se a página 1
content.html(page1);
// regenera os dados introduzidos
$("#text1").val(value1);
$("#text2").val(value2);
}
- trata-se de uma ação JS sem interação com o servidor, uma vez que 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 tinha sido armazenada. Não as entradas. Por isso, é necessário regenerar estas últimas;
7.6.10. Conclusão
Ao tirar partido das possibilidades do modelo APU, conseguimos simplificar o servidor web, que agora é sem estado (sem sessão) e está menos sobrecarregado:
- eliminámos 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 da aplicação anterior começa a tornar-se complexo. Está na hora de o estruturarmos em camadas. A aplicação permanecerá a mesma que anteriormente. Não iremos alterar o servidor, exceto no que diz respeito à definição de uma nova página inicial. Iremos reformular o código JS.
A nova arquitetura será a seguinte:
![]() |
7.7.2. A página inicial
A ação que inicia a aplicação é a seguinte ação [/ajax-16]:
@RequestMapping(value = "/ajax-16", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax16() {
return "vue-16";
}
Esta ação apresenta a seguinte vista [vue-16.xml]:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta name="viewport" content="width=device-width" />
<title>Ajax-12</title>
<link rel="stylesheet" href="/css/ajax01.css" />
<script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="/js/json3.js"></script>
<script type="text/javascript" src="/js/local16-dao.js"></script>
<script type="text/javascript" src="/js/local16-ui.js"></script>
</head>
<body>
<h3>Ajax - 16 - Navigation dans une Application à Page Unique</h3>
<h3>Structuration du code JS</h3>
<hr />
<div id="content" th:include="vue-09-page1" />
<img id="loading" src="/images/loading.gif" />
<div id="erreur" style="background-color:lightgrey"></div>
</body>
</html>
- linhas 9-10: o código JS foi colocado em dois ficheiros 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]:
| para atualizar a página 1 com o botão [Rafraîchir] |
| para apresentar a página 2 com o botão [Valider] |
O JavaScript não tem o conceito de interface. Utilizei este termo simplesmente para indicar que a camada [présentation] se comprometia a interagir com a camada [DAO] exclusivamente através das duas funções anteriores.
7.7.5. Implementação da interface
A estrutura 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. Por conseguinte, passa a ser gerida 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. O seu código é o seguinte:
// atualizar 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:
- um objeto do tipo [jQuery.Deferred]. Este tipo de objeto armazena um estado que pode assumir três valores: ['pending', 'resolved', 'rejected']. Quando chega à função [updatePage1], encontra-se no estado [pending];
- um objeto JS a ser devolvido na camada [présentation];
Todas as consultas HTTP são efetuadas pela função [executePost] seguinte:
// pedido 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) {
// guardamos a sessão
if (data.session) {
session = data.session;
}
// retornamos o resultado
deferred.resolve({
"status" : 1,
"data" : data,
"sendMeBack" : sendMeBack
});
},
error : function(jqXHR) {
// retorna o erro
deferred.resolve({
"status" : 2,
"data" : jqXHR.responseText,
"sendMeBack" : sendMeBack
});
}
});
}
- linha 1: a função [executePost] executa uma chamada Ajax do tipo POST. Espera quatro parâmetros:
- um objeto do tipo [jQuery.Deferred] no estado [pending];
- um objeto JS a ser devolvido na camada [présentation];
- o URL do POST;
- o valor a lançar como objeto JS;
- linhas 5-8: a função de publicação do jSON (linha 7) e recebe do jSON (linha 6);
- linha 11: o valor a enviar é transformado em jSON;
- linhas 13-24: a função executada caso a chamada Ajax seja bem-sucedida;
- linhas 19-23: se o servidor tiver devolvido uma sessão, esta é memorizada;
- 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 pretende recuperar;
- linhas 17-31: a função executada em caso de falha da chamada Ajax. Faz-se 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 forma 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) {
// pedido HTTP
executePost(deferred, sendMeBack, '/ajax-14', {
"value1" : value1,
"value2" : value2,
"pageRequired" : pageRequired,
});
}
- A função recebe os seguintes parâmetros:
- [deferred]: um objeto do tipo [jQuery.Deferred] no estado [pending],
- [sendMeBack]: um objeto JS a ser devolvido na camada [présentation],
- [value1]: a primeira entrada na página 1,
- [value2]: a segunda entrada na página 2,
- [pageRequired]: um valor booleano que indica ao servidor se 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 ficheiro [local-ui.js]. Este último retoma o código do ficheiro [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() {
// atualiza-se 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 predefinição, este encontra-se no estado [pending];
- linha 5: é apresentada a imagem de espera
- linhas 6-9: a função [updatePage1] é executada. Passa-se um objeto [sendMeBack] fictício, apenas para mostrar para que pode servir;
- linha 11: o parâmetro da função [deferred.done] é, por sua vez, uma função. É a função a executar quando o estado do objeto [deferred] passa para o estado [resolved]. Acabámos de ver que a função DAO [executePost] passava o estado deste objeto para [resolved] ao receber a resposta do servidor. Isto 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();
// recuperação dos dados
var data = result.data
// para demonstração
console.log(JSON3.stringify(result.sendMeBack));
// análise do estado
switch (result.status) {
case 1:
// atualização das duas zonas
if (data.zone1) {
$("#zone1-content").html(data.zone1);
$("#zone1").show();
} else {
$("#zone1").hide();
}
if (data.zone3) {
$("#zone3").show();
$("#zone3-content").html(data.zone3);
} else {
$("#zone3").hide();
}
break;
case 2:
// 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:
// apresenta-se o resultado
deferred.resolve({
"status" : 1,
"data" : data,
"sendMeBack" : sendMeBack
});
- linha 5: recupera-se a resposta do servidor;
- linhas 10-24: aqui está o código que, na versão anterior, se encontrava na função [onSuccess] da função [postForm];
- linhas 25-28: temos o código que, na versão anterior, se encontrava 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 é que a função [postForm] consegue passar informações para a função [postFormDone]? Esta última tem apenas um parâmetro: [result]. Este é 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]. Utiliza-se da seguinte forma:
function postFormDone(result) {
// fim da espera
loading.hide();
// recuperação dos dados
var data = result.data
// para demonstração
console.log(JSON3.stringify(result.sendMeBack));
// análise do estado
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 introduzidos
function valider() {
// guardar a página 1
page1 = content.html();
// guardam-se os valores introduzidos
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: «validar»,
'«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();
// recuperação dos dados
var data = result.data
// para demonstração
console.log(JSON3.stringify(result.sendMeBack));
// análise do estado
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) {
// utiliza-se a página em cache
content.html(page2);
} else {
// a página 2 está a ser memorizada
page2 = data.page2;
// a página é apresentada
content.html(data.page2);
}
// atualiza-se 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: aqui está o código que, na versão anterior, se encontrava na função [onSuccess] da função [valider];
- linhas 34-38: temos o código que, na versão anterior, se encontrava na função [onError] da função [valider];
7.7.8. Testes
A aplicação continua a funcionar como antes e, na consola 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 obter a seguinte arquitetura:
![]() |
- temos uma arquitetura cliente [2] / servidor [1], em que o cliente e o servidor comunicam em jSON;
- em [1], a camada web Spring MVC fornece vistas, fragmentos de vista e dados em jSON;
- em [2]: o código JavaScript incorporado na vista carregada no arranque da aplicação pode ser estruturado em camadas:
- a camada [présentation] encarrega-se das interações com o utilizador,
- A camada [DAO] é responsável pelo acesso aos dados através 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 determinadas visualizações em cache, mais uma vez com o objetivo de aliviar a carga do servidor. Este gere a sessão;













































































