7. Ajaxificatie van een Spring-applicatie MVC
7.1. De rol van AJAX in een webapplicatie
Tot nu toe hadden de bestudeerde leervoorbeelden de volgende architectuur:
![]() |
Om van een weergave [Vue1] naar een weergave [Vue2] te gaan, doet de browser het volgende:
- een verzoek naar de webapplicatie;
- ontvangt de weergave [Vue2] en geeft deze weer in plaats van de weergave [Vue1].
Dit is het klassieke patroon:
- verzoek van de browser;
- de webserver stelt een weergave op als antwoord voor de klant;
- weergave van deze nieuwe weergave door de browser.
Sinds enkele jaren bestaat er een andere manier van interactie tussen de browser en de webserver: AJAX (Asynchronous JavaScript and XML). Het gaat in feite om interacties tussen de door de browser weergegeven weergave en de webserver. De browser blijft doen wat hij altijd al doet, namelijk een weergave HTML weergeven, maar wordt nu aangestuurd door JavaScript dat is ingebed in de weergegeven weergave HTML. Het schema ziet er als volgt uit:
![]() |
- in [1] vindt er een gebeurtenis plaats op de pagina die in de browser wordt weergegeven (klik op een knop, wijziging van tekst, ...). Deze gebeurtenis wordt onderschept door JavaScript (jS) dat in de pagina is ingebed;
- in [2] voert de JavaScript-code een verzoek uit (HTTP), net zoals de browser dat zou hebben gedaan. Het verzoek is asynchroon: de gebruiker kan blijven interageren met de pagina zonder te worden geblokkeerd door het wachten op het antwoord op het verzoek HTTP. Het verzoek volgt het klassieke verwerkingsproces. Er is geen (of nauwelijks) verschil met een klassiek verzoek;
- in [3] wordt een antwoord naar de client jS verzonden. In plaats van een volledige weergave HTML wordt er eerder een gedeeltelijke weergave HTML, een stream XML of jSON (JavaScript Object Notation) die wordt verzonden;
- in [4] haalt het JavaScript dit antwoord op en gebruikt het om een deel van de weergegeven pagina HTML bij te werken.
Voor de gebruiker is er sprake van een verandering in de weergave, omdat wat hij ziet is veranderd. Er vindt echter geen volledige herlaadbeurt van de pagina plaats, maar slechts een gedeeltelijke wijziging van de weergegeven pagina. Dit draagt bij aan de vloeiendheid en interactiviteit van de pagina: omdat de pagina niet volledig opnieuw wordt geladen, kunnen we gebeurtenissen verwerken die voorheen niet mogelijk waren. Bijvoorbeeld door de gebruiker een lijst met opties aan te bieden terwijl hij tekens invoert in een invoerveld. Bij elk nieuw ingevoerd teken wordt een verzoek AJAX naar de server gestuurd, die vervolgens nieuwe suggesties terugstuurt. Zonder Ajax was dit soort invoerhulp voorheen onmogelijk. Het was niet mogelijk om bij elk ingevoerd teken een nieuwe pagina te laden.
7.2. Een pagina bijwerken met een feed HTML
7.2.1. De weergaven
We gaan de volgende applicatie bestuderen:
![]() |
- in [1], het tijdstip waarop de pagina is geladen;
- in [2] worden de vier rekenkundige bewerkingen uitgevoerd op twee reële getallen A en B;
- in [3] wordt het antwoord van de server in een deel van de pagina weergegeven;
- in [4], het tijdstip van de berekening. Dit verschilt van het tijdstip waarop de pagina is geladen, [5]. Dit laatste is gelijk aan [1], wat aantoont dat het gebied [6] niet opnieuw is geladen. Bovendien is het URL [7] van de pagina niet veranderd.
7.2.2. De actie [/ajax-01]
![]() |
De controller [Ajax.java] definieert de volgende actie [/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) {
// geldige tijd?
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));
}
}
// het weergavesjabloon [vue-01] wordt voorbereid
...
}
- regel 2: de actie [/ajax-01] accepteert slechts één parameter, namelijk [tempo]. Dit is de tijd in milliseconden die de server moet wachten voordat de resultaten van de rekenkundige bewerkingen worden verzonden;
- regel 4: de parameter [tempo] is optioneel;
- regels 5-12: er wordt gecontroleerd of de waarde van de parameter [tempo] aanvaardbaar is;
- regels 13-15: als dat het geval is, wordt de waarde van de vertraging in de sessie opgeslagen. Dit betekent dat deze van kracht blijft zolang deze niet wordt gewijzigd;
De code van de actie [/ajax-01] gaat als volgt verder:
@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) {
// geldige tijd?
...
// het sjabloon van de weergave [vue-01] wordt voorbereid
modèle.addAttribute("actionModel01", new ActionModel01());
...
// weergave
return "vue-01";
}
De klasse [ActionModel01] dient voornamelijk om de waarden in te kapselen die door de actie [/ajax-01] worden verzonden. Hier wordt niets verzonden. We maken een lege klasse aan die we in het model plaatsen, omdat de weergave [vue-01.xml] deze gebruikt. De klasse [ActionModel01] ziet er als volgt uit:
package istia.st.springmvc.models;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotNull;
public class ActionModel01 {
// verzonden gegevens
@NotNull
@DecimalMin(value = "0.0")
private Double a;
@NotNull
@DecimalMin(value = "0.0")
private Double b;
// getters en setters
...
}
- regels 11 en 15: twee reële getallen [a,b] die via een formulier zullen worden verzonden;
Laten we teruggaan naar de code van de actie:
@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) {
...
// we bereiden het model van de weergave [vue-01] voor
modèle.addAttribute("actionModel01", new ActionModel01());
Resultats résultats = new Resultats();
modèle.addAttribute("resultats", résultats);
...
// weergave
return "vue-01";
}
- regels 6-7: we plaatsen een instantie van het type [Resultats] in het sjabloon;
Het type [Resultats] dat in het model wordt geplaatst, is als volgt:
![]() |
package istia.st.springmvc.models;
public class Resultats {
// gegevens
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 en setters
...
}
- regels 6-9: het resultaat van de vier rekenkundige bewerkingen op de getallen [a,b];
- regel 10: het tijdstip waarop de pagina voor het eerst werd geladen;
- regel 11: het tijdstip waarop de vier rekenkundige bewerkingen zijn uitgevoerd;
- regel 12: een eventuele foutmelding;
- regel 13: de eventuele weergave die moet worden getoond;
- regel 14: de weergavestijl, [fr-FR] of [en-US];
De code van de actie [/ajax-01] gaat als volgt verder:
@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) {
...
// lokale instellingen
setLocale(locale, modèle, résultats);
...
}
- regel 5: de methode [setLocale] dient om de te gebruiken cultuur, [fr-FR] of [en-US], in het sjabloon van de weergave op te nemen. Deze cultuur is bestemd voor de in de weergave ingebedde JavaScript-code;
De methode [setLocale] is als volgt:
private void setLocale(Locale locale, Model modèle, Resultats résultats) {
// we ondersteunen alleen de locale-instellingen fr-FR en en-US
String language = locale.getLanguage();
String country = null;
switch (language) {
case "fr":
country = "FR";
break;
default:
language = "en";
country = "US";
break;
}
// cultuur
résultats.setCulture(String.format("%s-%s", language, country));
}
In het model staat de tekenreeks [${resultats.culture}], die gelijk is aan 'fr-FR' of 'en-US'.
Laten we teruggaan naar de actie [/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) {
...
// locale
setLocale(locale, modèle, résultats);
// tijd
résultats.setHeureGet(new SimpleDateFormat("hh:mm:ss").format(new Date()));
// weergave
return "vue-01";
}
- regel 7: we plaatsen de tijd van GET in het sjabloon;
- regel 9: we geven de weergave [vue-01.xml] weer:
7.2.3. De weergave [vue-01.xml]
![]() | ![]() |
De weergave [vue-01.xml] ziet er als volgt uit:
<!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>
- regels 7-12: de bibliotheken jQuery voor validatie en internationalisering (culturen);
- regel 15: de bibliotheek [client-validation] die in paragraaf 6.3 is opgebouwd;
- regel 14: de bibliotheek jSON die wordt gebruikt door de bibliotheek [client-validation]. Deze is optioneel als de validatielogboeken zijn uitgeschakeld;
- regel 13: de bibliotheek [Unobtrusive Ajax] van Microsoft. Met deze bibliotheek kan het soms worden vermeden om JavaScript te schrijven;
- regel 16: een bestand jS voor onze eigen doeleinden;
- regels 17-22: voor het beheer aan de clientzijde van de culturen [fr-FR] en [en-US]. We zijn deze code al eerder tegengekomen;
- regel 27: een geconfigureerd bericht. We hebben deze in paragraaf 5.18 besproken;
- regels 36-38: het formulier waar we later op terugkomen;
- regel 40: het veld in het document waarin JavaScript het antwoord van de server zal plaatsen;
7.2.4. Het formulier
![]() |
In de weergave [vue-01.xml] ziet het formulier er als volgt uit:
<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>
dat het volgende HTML oplevert:
<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>
- regel 16: aan het veld [a] zijn de validatoren [required], [number] en [min] gekoppeld;
- regel 19: hetzelfde geldt voor het veld [b];
De verschillende berichten zijn te vinden in de bestanden [messages.properties] van het project:
![]() |
[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:
Laten we nu eens kijken naar de attributen van de tag [form]:
<form id="formulaire" name="formulaire" method="post" data-ajax-update="#resultats" data-ajax-complete="afterComplete" data-ajax-begin="beforeSend" data-ajax-loading-duration="0" data-ajax-mode="replace" data-ajax="true" data-ajax-method="post" data-ajax-loading="#loading" action="/ajax-02.html">
We herkennen de klassieke attributen van de tag [form]:
<form id="formulaire" name="formulaire" method="post" action="/ajax-02.html">
Het valt meteen op dat als JavaScript is uitgeschakeld in de browser die de pagina weergeeft, het formulier wordt verzonden naar URL [/ajax-02.html]. Laten we nu de andere attributen analyseren:
<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">
De attributen [data-ajax-xxx] worden beheerd door de bibliotheek jS [unobtrusive-ajax], die is geïmporteerd door de weergave [vue-01.xml]:
<script type="text/javascript" src="/js/jquery/jquery.unobtrusive-ajax.js"></script>
Wanneer de attributen [data-ajax-xxx] aanwezig zijn, wordt de [submit] van het formulier uitgevoerd via een Ajax-aanroep van de bibliotheek [unobtrusive-ajax]. De betekenis van de parameters is als volgt:
- [data-ajax="true"]: de aanwezigheid van dit attribuut zorgt ervoor dat de [submit] van het formulier via Ajax wordt uitgevoerd;
- [data-ajax-method="post"]: de methode van het [submit]. Het URL van de POST-verzoek zal overeenkomen met die van het attribuut [action="/ajax-02.html"];
- [data-ajax-loading="#loading"]: de id van een veld dat moet worden weergegeven in afwachting van het antwoord van de server. Het veld dat wordt aangeduid met [loading] in de weergave [vue-01.xml] is het volgende:
<img id="loading" style="display: none" src="/images/loading.gif" />
Dit is een geanimeerde wachtafbeelding die wordt weergegeven zolang het antwoord van de server nog niet is ontvangen;
- [data-ajax-loading-duration="0"]: de wachttijd in milliseconden voordat het gebied [data-ajax-loading="#loading"] wordt weergegeven. Hier wordt het weergegeven zodra het wachten begint;
- [data-ajax-begin="beforeSend"]: de functie jS die moet worden uitgevoerd voordat [submit] wordt uitgevoerd;
- [data-ajax-complete="afterComplete"]: de functie jS die moet worden uitgevoerd wanneer het antwoord is ontvangen;
- [data-ajax-update="#resultats"]: de ID van het veld waarin het door de server verzonden resultaat wordt geplaatst. De weergave [vue-01.xml] bevat het volgende veld:
<div id="resultats" />
- [data-ajax-mode="replace"]: de wijze waarop het resultaat in het voorgaande veld wordt ingevoegd. De modus [replace] zorgt ervoor dat het resultaat de eerdere inhoud van het veld met id [resultats] 'overschrijft';
Let op: de JavaScript-code [submit] wordt alleen uitgevoerd als de validators de geteste waarden als geldig hebben aangemerkt.
De bibliotheek jS [unobtrusive-ajax] heeft twee doelen:
- ervoor zorgen dat het formulier zich correct aanpast aan beide mogelijkheden: of JavaScript in de browser is ingeschakeld of niet;
- het schrijven van JavaScript te vermijden. We zullen zien dat dit hier niet kon worden vermeden.
7.2.5. De actie [/ajax-02]
We hebben gezien dat de verzonden waarden naar de actie [/ajax-02] werden gestuurd. Deze ziet er als volgt uit:
@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);
}
// we bereiden het sjabloon voor de volgende weergave voor
Resultats résultats = new Resultats();
modèle.addAttribute("resultats", résultats);
// de lokale tijd instellen
setLocale(locale, modèle, résultats);
// tijd
résultats.setHeurePost(new SimpleDateFormat("hh:mm:ss").format(new Date()));
...
}
- We gaan het in eerste instantie even vereenvoudigen: we gaan ervan uit dat de POST die plaatsvindt, inderdaad is uitgevoerd door het JavaScript van de weergave [vue-01.xml]. We komen later nog terug op deze aanname;
- regel 2: de verzonden waarden van [a,b] worden in het model [ActionModel01] geplaatst;
- regels 4-7: als de gebruiker bij een eerdere GET een vertraging had ingesteld, wordt deze uit de sessie opgehaald en wordt de vertraging toegepast (regel 6). Het doel hiervan is om de gebruiker in staat te stellen het effect van het attribuut [data-ajax-loading="#loading"] in het formulier te zien;
- regels 9-10: er wordt een attribuut [resultats] in het sjabloon geplaatst;
- regel 12: de cultuur [fr-FR] of [en-US] wordt in het sjabloon geplaatst;
- regel 14: voeg de tijd van POST toe aan het sjabloon;
Ter herinnering: het type [Resultats] dat in het sjabloon is ingevoerd:
public class Resultats {
// gegevens
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 en setters
...
}
De code van de actie [/ajax-02] gaat als volgt verder:
@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()));
// er wordt om de beurt een fout gegenereerd
int val = new Random().nextInt(2);
if (val == 0) {
// er wordt een foutmelding teruggestuurd
résultats.setErreur("erreur.aleatoire");
return "vue-03";
}
...
}
- regels 6-11: in dit voorbeeld wordt getoond hoe een foutpagina naar de klant jS wordt teruggestuurd. In de helft van de gevallen wordt de volgende weergave [vue-03.xml] teruggestuurd:
![]() |
Let op regel 9: dit is geen bericht dat in het sjabloon wordt geplaatst, maar een berichtcode:
[messages_fr.properties]
erreur.aleatoire=erreur aléatoire
[messages_fr.properties]
erreur.aleatoire=randomly generated error
De code van de weergave [vue-03.xml] is als volgt:
<!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="fout" th:text="${resultats.erreur}"></span> -->
</p>
</body>
</html>
- op regel 12 zien we een bericht dat is ingesteld door een berichtcode die zelf wordt berekend. We hebben dit begrip geïntroduceerd in paragraaf 5.18, pagina 170.
De code van de actie [/ajax-02] gaat als volgt verder:
@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 {
...
// de verzonden waarden worden opgehaald
double a = formulaire.getA();
double b = formulaire.getB();
// het model wordt opgebouwd
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");
}
// de weergave wordt weergegeven
return "vue-02";
}
- regels 5-15: de vier rekenkundige bewerkingen worden uitgevoerd op de getallen [a,b] en ingekapseld in de instantie [Resultats] van het model;
- regel 17: de volgende weergave [vue-02.xml] wordt geretourneerd:
![]() |
De weergave [vue-02.xml] is als volgt:
<!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>
Of het resultaat nu de weergave [vue-02.xml] of de weergave [vue-03.xml] is, wordt dit resultaat HTML geplaatst in het veld met de identificatie [resultats] in de weergave [vue-01.xml], dit vanwege het attribuut [data-ajax-update="#resultats"] van het formulier.
7.2.6. De POST van de ingevoerde waarden
We hebben hier een probleem met de ingevoerde waarden. We werken met twee culturen, [fr-FR] en [en-US], die reële getallen op verschillende manieren weergeven. We hebben dit probleem al aangepakt toen we in paragraaf 6.3, pagina 190, reële getallen in twee verschillende culturen moesten invoeren. We gaan hier de toen gebruikte hulpmiddelen opnieuw gebruiken. Maar we hebben een extra probleem: we hebben geen toegang tot de methode die de POST-conversie van de ingevoerde waarden uitvoert. Daarom hebben we de volgende attributen toegevoegd aan de formulier-tag:
- [data-ajax-begin="beforeSend"]: de functie jS die moet worden uitgevoerd voordat de [submit] wordt uitgevoerd;
- [data-ajax-complete="afterComplete"]: de functie jS die moet worden uitgevoerd zodra het antwoord is ontvangen;
We hebben geen toegang tot de functie jS die de ingevoerde waarden zal verzenden, maar we kunnen twee functies jS schrijven:
- [beforeSend]: een functie jS die wordt uitgevoerd vóór de functie POST;
- [afterComplete]: een functie jS die wordt uitgevoerd bij ontvangst van het antwoord op de functie POST;
Deze twee functies zijn ondergebracht in een bestand [local1.js]:
![]() |
Het bestand [local1.js] initialiseert de omgeving jS van de weergave [vue-01.xml] als volgt:
// globale gegevens
var loading;
var formulaire;
var résultats;
var a, b;
// bij het laden van het document
$(document).ready(function() {
// de referenties van de verschillende componenten van de pagina worden opgehaald
loading = $("#loading");
formulaire = $("#formulaire");
resultats = $('#resultaten');
a = $("#a");
b = $("#b");
// bepaalde elementen worden in de cache opgeslagen
loading.hide();
// de validatieregels van het formulier worden geparseerd
$.validator.unobtrusive.parse(formulaire);
// er worden twee locales beheerd [fr_FR, en_US]
// de daadwerkelijke gegevens [a,b] worden door de server in het Angelsaksische formaat verzonden
// indien nodig worden ze in het Franse formaat omgezet
checkCulture(2);
});
- regel 22: de functie [checkCulture] wordt iets verderop beschreven;
De functie jS [beforeSend] ziet er als volgt uit:
function beforeSend(jqXHR, settings) {
// vóór de POST
// de getallen moeten in het Angelsaksische formaat worden verzonden
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) {
// de getallen [a,b] worden in het Angelsaksische formaat weergegeven
var value1 = a.val().replace(",", ".");
a.val(value1);
var value2 = b.val().replace(",", ".");
b.val(value2);
}
if (mode == 2) {
...
}
}
- regels 4-6: er wordt gecontroleerd of de weergavecultuur [fr-FR] is. In dat geval moeten de verzonden waarden worden aangepast. Als de gebruiker namelijk [1,6] heeft ingevoerd, moet de waarde [1.6] worden verzonden; anders wordt de waarde [1,6] door de server geweigerd. Hiervoor volstaat het om de komma in de verzonden waarden te vervangen door een decimaalteken (regels 18-21);
- maar daar blijft het niet bij. Wanneer de functie [beforeSend] wordt aangeroepen, is de reeks van verzonden waarden [a=val1&b=valB] namelijk al samengesteld. We moeten deze dus aanpassen. Dit gebeurt met behulp van de tweede parameter [settings] van de functie;
- regel 7: [settings.data] (settings is een parameter van de functie) vertegenwoordigt de verzonden tekenreeks. Deze tekenreeks wordt opnieuw gegenereerd met de uitdrukking [formulaire.serialize()]. Deze uitdrukking doorloopt het formulier op zoek naar de te verzenden waarden en stelt de tekenreeks van POST samen. Vervolgens neemt deze de nieuwe waarden van [a,b] over, inclusief de decimalen;
Als we verder niets doen, zal de server zijn antwoord versturen, dat correct wordt weergegeven. Alleen zijn de waarden van [a,b] nu met een decimaalteken, terwijl we ons nog steeds in de cultuur van [fr-FR] bevinden. Als de gebruiker dit dus niet opmerkt en opnieuw op [Calculer] klikt, melden de validators dat de waarden [a,b] ongeldig zijn. En dat klopt. Hier komt de functie [afterComplete] in beeld, die wordt uitgevoerd zodra het resultaat wordt ontvangen:
function beforeSend(jqXHR, settings) {
// vóór de POST
...
}
function afterComplete(jqXHR, settings) {
// na de POST
// de getallen moeten indien nodig weer in het Franse formaat worden gezet
var culture = Globalize.culture().name;
if (culture === 'fr-FR') {
checkCulture(2);
}
}
function checkCulture(mode) {
if (mode == 1) {
...
}
if (mode == 2) {
// de getallen worden in het Franse formaat weergegeven
var value1 = a.val().replace(".", ",");
a.val(value1);
var value2 = b.val().replace(".", ",");
b.val(value2);
}
}
- regels 9-12: als de taal van de weergave [fr-FR] is, worden de getallen [a,b] weer in het Franse formaat omgezet.
7.2.7. Tests
Hieronder volgen enkele schermafbeeldingen van tests:
![]() |
- in [1], het antwoord van de server;
![]() |
- en [2], het antwoord van de server met een foutmelding;
![]() |
- in [3] stellen we een vertraging van 5 seconden in. Dit betekent dat de server 5 seconden wacht voordat hij zijn antwoord verstuurt. In de tag [form] hebben we het attribuut [data-ajax-loading='#loading'] gebruikt. De parameter [loading] is de identificatiecode van een zone die:
- gedurende de gehele wachttijd wordt weergegeven;
- verborgen wordt na ontvangst van het antwoord van de server;
Hier is [loading] de identificatiecode van een geanimeerde afbeelding die te zien is in [4].
7.2.8. Javascript uitschakelen met de culture [en-US]
Wat gebeurt er als JavaScript in de browser wordt uitgeschakeld?
De POST van de ingevoerde waarden vindt plaats volgens de tag [form], waarvan de attributen [data-ajax-attr] niet worden gebruikt. Alles verloopt alsof we de volgende tag [form] zouden hebben:
<form id="formulaire" name="formulaire" method="post" action="/ajax-02.html">
De ingevoerde waarden worden dus naar de actie [/ajax-02] verzonden. Ze zijn niet aan de clientzijde gecontroleerd. Het zijn dus de validators aan de serverzijde die in actie komen. Deze waren eerder al actief, maar dan op waarden die al aan de clientzijde waren gevalideerd en dus correct waren. Dat is nu niet meer het geval.
We passen de actie [/ajax-02] als volgt aan:
@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 {
// Ajax-verzoek?
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
}
- regel 4: de actie [/ajax-02] kan nu dus worden aangeroepen via een Ajax-verzoek POST of via een klassieke POST. We moeten deze twee gevallen van elkaar kunnen onderscheiden. Dat doen we aan de hand van de HTTP-headers die door de clientbrowser worden verzonden;
Als we de netwerkverkeer in de ontwikkelingsconsole van Chrome (Ctrl-Shift-I) bekijken terwijl JavaScript is ingeschakeld, zien we dat de client de volgende headers verstuurt op het moment van de POST:
![]() |
Hierboven is te zien dat:
- er een header [X-Requested-With] is verzonden na [1];
- een parameter [X-Requested-With] is toegevoegd aan de verzonden waarden van [2];
Dit gebeurt niet bij een klassieke POST. Er zijn dus twee mogelijkheden om de informatie op te halen: uit de headers HTTP of uit de geposte waarden. In regel 4 van de actie [/ajax-02] is voor de eerste oplossing gekozen.
Laten we verdergaan met de code van deze actie:
@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 {
// Ajax-verzoek?
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
// tempo?
Integer tempo = (Integer) session.getAttribute("tempo");
if (tempo != null && tempo > 0) {
Thread.sleep(tempo);
}
// het sjabloon voor de volgende weergave wordt voorbereid
Resultats résultats = new Resultats();
modèle.addAttribute("resultats", résultats);
// de landinstelling wordt ingesteld
setLocale(locale, modèle, résultats);
// tijd
String heure = new SimpleDateFormat("hh:mm:ss").format(new Date());
résultats.setHeurePost(heure);
résultats.setHeureGet(heure);
// geldig verzoek?
if (!isAjax && result.hasErrors()) {
return "vue-01";
}
...
- regel 2: de parameter [@Valid ActionModel01 formulaire] activeert de validaties aan de serverzijde;
- regels 20-22: als de aanroep geen Ajax-aanroep is en de validatie is mislukt, wordt de weergave [vue-01.xml] met de foutmeldingen teruggestuurd.
Hier volgt een voorbeeld:
![]() | ![]() |
Laten we verdergaan met de analyse van de actie [/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 {
// Ajax-verzoek?
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
// geldig verzoek?
if (!isAjax && result.hasErrors()) {
return "vue-01";
}
// er wordt om de beurt een fout gegenereerd
int val = new Random().nextInt(2);
if (val == 0) {
// er wordt een foutmelding teruggestuurd
résultats.setErreur("erreur.aleatoire");
if (isAjax) {
return "vue-03";
} else {
résultats.setVue("vue-03");
return "vue-01";
}
}
...
- regel 14: er wordt een willekeurige fout gegenereerd;
- regel 16: in het geval van een Ajax-aanroep wordt de weergave [vue-03.xml] geretourneerd, die in het gebied wordt geplaatst dat wordt aangeduid met [resultats];
- regel 18: bij een niet-Ajax-aanroep wordt de weer te geven weergave in het model van het type [Resultats] geplaatst;
- regel 19: de weergave [vue-01.xml] wordt opnieuw weergegeven;
De weergave [vue-01.xml] wordt als volgt gewijzigd:
<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" />
- regel 3: de weergave [vue-03.xml] wordt onder het gebied [resultats] ingevoegd;
Hier volgt een voorbeeld:
![]() |
Merk op dat de tijden [1] en [2] nu identiek zijn.
Laten we verdergaan met de analyse van de actie [/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 {
// Ajax-verzoek?
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
// de verzonden waarden worden opgehaald
double a = formulaire.getA();
double b = formulaire.getB();
// het model wordt opgebouwd
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");
}
// de weergave wordt weergegeven
if (isAjax) {
return "vue-02";
} else {
résultats.setVue("vue-02");
return "vue-01";
}
}
- regels 7-17: de resultaten van de vier rekenkundige bewerkingen worden in het model geplaatst;
- regels 22-23: de weergave [vue-01.xml] (regel 22) wordt weergegeven door de weergave [vue-02.xml] (regel 22) erin in te voegen;
Deze invoeging gebeurt als volgt in [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" />
- regel 2: de weergave [vue-02.xml] wordt ingevoegd onder het gebied [resultats];
Hier volgt een voorbeeld van de uitvoering:
![]() |
7.2.9. Javascript uitschakelen met de cultuur [fr-FR]
Met de cultuur [fr-FR] doet zich het volgende probleem voor:
![]() | ![]() |
De waarden die in het Franse formaat zijn ingevoerd, zijn als ongeldig aangemerkt. De server verwacht namelijk reële getallen in het Angelsaksische formaat. De oplossing is vrij complex. We gaan een filter maken dat:
- het verzoek onderschept;
- de komma's in de verzonden waarden [a] en [b] te vervangen door een decimaalteken;
- en vervolgens het nieuwe verzoek doorgeeft aan de actie die het moet verwerken;
Allereerst voegen we een verborgen veld toe aan de weergave [vue-01.xml]:
<form ...>
...
</p>
<!-- verborgen velden -->
<input type="hidden" id="culture" name="culture" th:value="${resultats.culture}"></input>
</form>
- regel 5: de waarde van [fr-FR] of [en-US] wordt in het attribuutveld [name=culture] geplaatst. Aangezien de tag [input] in het formulier staat, wordt de waarde ervan samen met de waarden van [a] en [b] verzonden. We krijgen dan een verzonden tekenreeks in de vorm:
Het is belangrijk om dit punt te begrijpen.
Vervolgens voegen we een filter toe aan de configuratie van de applicatie:
![]() |
Het bestand [Config] wordt als volgt gewijzigd:
@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
...
@Bean
public Filter cultureFilter() {
return new CultureFilter();
}
}
- regel 7: het feit dat de bean [cultureFilter] een type [Filter] retourneert, maakt er een filter van. De bean zelf kan elke willekeurige naam hebben;
De volgende stap is het aanmaken van het filter zelf:
![]() |
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 {
// volgende handler
filterChain.doFilter(new CultureRequestWrapper(request), response);
}
}
- regel 12: we breiden de klasse [OncePerRequestFilter] uit, een Spring-klasse, en wat we moeten doen is de methode [doFilterInternal] van deze klasse herdefiniëren;
- regel 15: de methode [doFilterInternal] ontvangt drie gegevens:
- [HttpServletRequest request]: het te filteren verzoek. Dit kan niet worden gewijzigd,
- [HttpServletResponse response]: het antwoord dat naar de server wordt gestuurd. Het filter kan besluiten dit zelf te doen,
- [FilterChain filterChain]: de filterketen. Zodra de methode [doFilterInternal] haar werk heeft voltooid, moet zij het verzoek doorgeven aan het volgende filter in de filterketen;
- regel 18: er wordt een nieuw verzoek aangemaakt op basis van het ontvangen verzoek [new CultureRequestWrapper(request)] en dit wordt doorgegeven aan het volgende filter. Omdat het oorspronkelijke verzoek [HttpServletRequest request] niet kan worden gewijzigd, wordt er een nieuw verzoek aangemaakt;
De klasse [CultureRequestWrapper] ziet er als volgt uit:
![]() |
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) {
// verzonden waarden a en 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;
}
// andere gevallen
return super.getParameterValues(name);
}
}
- regel 6: de klasse [CultureRequestWrapper] is een uitbreiding van de klasse [HttpServletRequestWrapper] en zal enkele van deze methoden herdefiniëren;
- regels 8-10: de constructor ontvangt de te filteren aanvraag en geeft deze door aan de bovenliggende klasse;
- hierbij moet worden opgemerkt dat de gefilterde aanvraag uiteindelijk als invoerparameter terechtkomt bij een klasse die een servlet wordt genoemd. Met Spring MVC is deze servlet van het type [DispatcherServlet]. Deze klasse beschikt over diverse methoden om de parameters van de verzoek op te halen: [getParameter, getParameterMap, getParameterNames, getParameterValues, ...]. De door de servlet gebruikte methode moet opnieuw worden gedefinieerd. Hiervoor zou men de code van de klasse [DispatcherServlet] moeten lezen. Ik heb dat niet gedaan en heb verschillende methoden opnieuw gedefinieerd. Uiteindelijk is de methode [getParameterValues] opnieuw gedefinieerd;
- regel 13: de methode [getParameterValues] ontvangt als parameter de naam van een van de parameters die door de methode [getParameterNames] worden geretourneerd en moet de array met de waarden daarvan retourneren. We weten namelijk dat een parameter meerdere keren in een verzoek kan voorkomen;
- regel 18: de komma wordt vervangen door een decimaalteken;
Hier volgt een uitvoervoorbeeld:
![]() |
- in [1] worden de waarden van [a,b] in het Franse formaat ingevoerd;
- in [2], de resultaten;
- in [3] heeft de server een pagina teruggestuurd met getallen in het Angelsaksische formaat.
Dit laatste probleem kan met Thymeleaf als volgt worden opgelost in de weergave [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>
Er moeten verschillende wijzigingen worden aangebracht in de regels 3 en 6. Laten we regel 3 eens bekijken:
- we hadden [th:field="*{a}"] geschreven. De parameter [th:field] stelt de attributen [id, name, value] in van de gegenereerde tag HTML [input]. Hier willen we het attribuut [value] zelf beheren. We stellen dus ook de attributen [id, name] zelf in;
- het attribuut [th:value] evalueert een uitdrukking met behulp van de ternaire operator ?. We testen de uitdrukking [${resultats.culture}=='fr-FR' and ${actionModel01.b}!=null]. Als deze waar is, geven we aan het attribuut [value] de waarde van [actionModel01.a], waarbij de decimale punt wordt vervangen door een komma. Als de uitdrukking onwaar is, krijgt het attribuut [value] de waarde van [actionModel01.a] zonder wijzigingen;
- regel 6: we doen hetzelfde voor het veld [b];
Hier volgt een voorbeeld van de uitvoering:
![]() |
- in [1] hebben de getallen in [a,b] de Franse notatie behouden. Dit is niet het geval in [2];
Dit nieuwe probleem kan op dezelfde manier worden opgelost als het vorige. We passen de weergave [vue-03.xml] als volgt aan:
<!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>
Hier volgt een voorbeeld:
![]() | ![]() |
We hebben nu een applicatie die twee culturen correct verwerkt in een omgeving waarin al dan niet JavaScript wordt gebruikt. Hiervoor moest de server-side code aanzienlijk complexer worden gemaakt. Voortaan gaan we er altijd vanuit dat JavaScript in de browser is ingeschakeld. Dit maakt dingen mogelijk die in de pure servermodus onmogelijk zijn.
7.2.10. Beheer van de link [Calculer]
Laten we de link [Calculer] op de hoofdpagina [vue-01.xml] eens bekijken:
![]() | ![]() |
De code van de link [Calculer] in de weergave [vue-01.xml] is als volgt:
<a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
De functie jS [postForm] is in het bestand [local1.js] als volgt gedefinieerd:
// algemene gegevens
var loading;
var formulaire;
var résultats;
var a, b;
function postForm() {
// formulier geldig?
if (!formulaire.validate().form()) {
// ongeldig formulier - voltooid
return;
}
// er worden twee landinstellingen beheerd [fr_FR, en_US]
// de werkelijke waarden [a,b] moeten in alle gevallen in het Angelsaksische formaat worden verzonden
// dit gebeurt via het filter [CultureFilter]
// er wordt handmatig een Ajax-verzoek gedaan
$.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);
}
})
}
- regels 2-5: ter herinnering: deze elementen zijn geïnitialiseerd door de functie [$(document).ready];
- regels 9-12: de validaties jS van het formulier worden uitgevoerd. Als een van de waarden ongeldig is, levert de uitdrukking [formulaire.validate().form()] de waarde false op. In dat geval wordt de validator [submit] van het formulier geannuleerd;
- regels 18-38: er wordt handmatig een Ajax-aanroep gedaan;
- regel 19: de URL als doel van de Ajax-aanroep;
- regels 20-22: een array met headers HTTP die moeten worden toegevoegd aan de standaard headers in het verzoek HTTP. Hier voegen we de header HTTP toe, die de server aangeeft dat er een Ajax-aanroep wordt gedaan;
- regel 23: de gebruikte methode HTTP;
- regel 24: de verzonden gegevens. [formulaire.serialize] genereert de te verzenden string [culture=fr-FR&a=12,7&b=20,89] van het formulier met id [formulaire]. Hier zien we het eerder besproken probleem terug: de waarden [a,b] moeten in het Angelsaksische formaat worden verzonden. We weten dat dit probleem inmiddels is opgelost met de aanmaak van het filter [cultureFilter];
- regel 25: het verwachte gegevenstype in de retouruitvoer. We weten dat de server een HTML-stream zal terugsturen;
- regel 26: de methode die moet worden uitgevoerd wanneer het verzoek start. Hier wordt aangegeven dat de component met id [loading] moet worden weergegeven. Dit is de animatie die wordt getoond tijdens het wachten;
- regel 29: de methode die moet worden uitgevoerd als het Ajax-verzoek succesvol is. De parameter [data] is het volledige antwoord van de server. We weten dat dit een stream is met de waarde HTML;
- regel 30: de component met id [résultats] wordt bijgewerkt met de waarde HTML uit de parameter [data].
- regel 33: het wachtsein wordt in de cache opgeslagen;
- regel 35: functie die wordt uitgevoerd wanneer het antwoord van de server is ontvangen, ongeacht of dit een succes of een fout is;
- regels 35-37: in geval van een fout (de server heeft een antwoord HTTP teruggestuurd met een status die aangeeft dat er een fout aan de serverzijde is opgetreden), wordt het antwoord HTML van de server weergegeven in het veld [resultats];
Hier volgt een uitvoervoorbeeld:
![]() | ![]() |
7.3. Een pagina HTML bijwerken met een feed jSON
In het vorige voorbeeld reageerde de webserver op het Ajax-verzoek HTTP met een feed HTML. In deze feed bevonden zich gegevens met bijbehorende opmaak HTML. We gaan het vorige voorbeeld herhalen, maar ditmaal met jSON-antwoorden (JavaScript Object Notation) die alleen de gegevens bevatten. Het voordeel hiervan is dat er zo minder bytes worden verzonden. We gaan ervan uit dat JavaScript in de browser is ingeschakeld.
7.3.1. De actie [/ajax-04]
De actie [/ajax-04] is identiek aan de actie [/ajax-01], behalve dat de weergave [vue-04.xml] wordt weergegeven in plaats van de weergave [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) {
...
// weergave
return "vue-04";
}
7.3.2. De weergave [vue-04.xml]
![]() |
De weergave [vue-04.xml] neemt de hoofdtekst van de weergave [vue-01.xml] over, met de volgende verschillen:
<!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>
<!-- verborgen velden -->
<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>
- regel 5: de JavaScript-code van de weergave staat nu in het bestand [local4.js];
- regel 16: de tag [form] heeft niet langer de parameters [data-ajax-attr] uit de bibliotheek [Unobtrusive Ajax]. We gaan deze hier niet gebruiken. De tag [form] heeft evenmin de attributen [method] en [action], die aangeven hoe en waar de in het formulier ingevoerde waarden moeten worden verzonden. Dit komt doordat deze tag wordt verzonden door een functie jS (regel 20);
- regels 26-57: het ID-veld [resultats], dat voorheen leeg was, bevat nu de code HTML om de resultaten weer te geven;
- regels 26-34: de koptekst van de resultaten waarin het tijdstip van de berekening wordt weergegeven;
- regels 35-52: de resultaten van de vier rekenkundige bewerkingen;
- regels 53-57: een eventuele foutmelding die door de server wordt verzonden;
De code jS, die wordt uitgevoerd bij het laden van de weergave [vue-04.xm], staat in het bestand [local4.js]. Deze luidt als volgt:
// algemene gegevens
var loading;
var formulaire;
var résultats;
var titre;
var labelHeureCalcul;
var heureCalcul;
var aplusb;
var amoinsb;
var afoisb;
var adivb;
var msgErreur;
// bij het laden van het document
$(document).ready(function() {
// de referenties van de verschillende componenten van de pagina worden opgehaald
loading = $("#loading");
formulaire = $("#formulaire");
résultats = $('#resultaten');
titre=$("#titre");
labelHeureCalcul=$("#labelHeureCalcul");
heureCalcul=$("#heureCalcul");
aplusb=$("#aplusb");
amoinsb=$("#amoinsb");
afoisb=$("#afoisb");
adivb=$("#adivb");
msgErreur=$("#msgErreur");
// bepaalde elementen worden verborgen
résultats.hide();
erreur.hide();
loading.hide();
});
- regels 17-27: de jQuery-referenties van alle elementen op de pagina worden opgehaald;
- regel 29: het resultatengebied wordt verborgen;
- regel 30: evenals het foutvenster;
- regel 31: evenals de geanimeerde wachtafbeelding;
- regels 2-12: de opgehaalde referenties worden globaal gemaakt, zodat andere functies er gebruik van kunnen maken;
7.3.3. De functie jS [postForm]
De koppeling [Calculer] is als volgt:
<p>
<img id="loading" style="display: none" src="/images/loading.gif" />
<a href="javascript:postForm()" th:text="#{action.calculer}">Calculer</a>
</p>
De functie jS [postForm] is in het bestand [local.js] als volgt gedefinieerd:
function postForm() {
// geldig formulier?
if (!formulaire.validate().form()) {
// ongeldig formulier - voltooid
return;
}
// er wordt handmatig een Ajax-verzoek gedaan
$.ajax({
url : '/ajax-05',
headers : {
'Accept: 'application/json'
},
type : 'POST',
data : formulaire.serialize(),
dataType : 'json',
beforeSend : onBegin,
success : onSuccess,
error : onError,
complete : onComplete
})
}
// vóór de Ajax-aanroep
function onBegin() {
...
}
// bij ontvangst van het antwoord van de server
// bij succes
function onSuccess(data) {
...
}
// bij ontvangst van het antwoord van de server
// bij mislukking
function onError(jqXHR) {
...
}
// na [onSuccess, onError]
function onComplete() {
...
}
- regels 3-6: voordat de ingevoerde waarden worden verzonden, worden ze gecontroleerd. Als ze onjuist zijn, wordt de POST van het formulier niet uitgevoerd;
- regel 9: de ingevoerde waarden worden doorgestuurd naar de actie [/ajax-05], die we verderop nader toelichten;
- regels 10-12: een header HTTP om de server te laten weten dat we een antwoord in het formaat jSON verwachten;
- regel 13: de ingevoerde waarden worden verzonden;
- regel 14: serialisatie van de ingevoerde waarden tot een string die klaar is om te worden verzonden in het formaat [a=1,6&b=2,4&culture=fr-FR];
- regel 15: het type van het door de server verzonden antwoord. Dit zal jSON zijn;
- regel 16: de functie die moet worden uitgevoerd vóór de POST;
- regel 17: de functie die moet worden uitgevoerd bij ontvangst van het antwoord van de server, indien dit een succes is. Het 'succes' van een verzoek HTTP wordt afgemeten aan de hand van de status van het antwoord HTTP van de server. Een antwoord [HTTP/1.1 200 OK ] is een succesvol antwoord. Een antwoord [HTTP/1.1 500 Internal Server Error] is een mislukt antwoord. Wat de status van een antwoord HTTP wordt genoemd, is de code [200] of [500]. Een aantal van deze codes houdt verband met ‘succes’, terwijl andere codes verband houden met ‘mislukking’;
- regel 18: de functie die moet worden uitgevoerd bij ontvangst van het antwoord van de server wanneer de status HTTP van dit antwoord een foutstatus is;
- regel 18: de functie die als laatste moet worden uitgevoerd, na de voorgaande functies [onSuccess, onError];
De functie [onBegin] is als volgt:
// vóór de Ajax-aanroep
function onBegin() {
console.log("onBegin");
// de geanimeerde afbeelding wordt weergegeven
loading.show();
// bepaalde elementen van de weergave worden verborgen
entete.hide();
résultats.hide();
erreur.hide();
}
Voordat we de andere functies jS van de Ajax-aanroep bekijken, moeten we weten welk antwoord door de actie [/ajax-05] is verzonden.
7.3.4. De actie [/ajax-05]
De actie [/ajax-05] is als volgt:
@RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
@ResponseBody()
// verwerkt de POST van de weergave [vue-04]
public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale, HttpServletRequest request, HttpSession session) throws InterruptedException {
if(result.hasErrors()){
// afwijkend geval – er wordt niets weergegeven
return null;
}
...
}
- regel 2: het attribuut [ResponseBody] geeft aan dat de actie [/ajax-05] zelf het antwoord aan de client retourneert. Omdat een bibliotheek met de naam jSON tot de afhankelijkheden van het project behoort, configureert Spring Boot dit type acties automatisch zodat ze jSON retourneren. Het is dus de reeks jSON van het type [JsonResults] (regel 4) die naar de client wordt verzonden;
- regel 2: de verzonden waarden [a, b, culture] worden ingekapseld in een type [ActionModel01], waarvan de validatie [@Valid ActionModel01] wordt aangevraagd. Dit is puur voor de vorm. We zijn ervan uitgegaan dat JavaScript is ingeschakeld in de browser van de klant en dat de verzonden waarden dus al aan de kant van de klant zijn gecontroleerd wanneer ze binnenkomen. Toch kunnen we rekening houden met het geval van een ‘wilde’ POST die geen gebruik maakt van onze client jS. In dat geval kan de validatie mislukken;
- regels 5-7: in geval van een fout wordt een lege jSON-stream teruggestuurd;
Laten we verdergaan met de analyse van de actie [/ajax-05]:
@RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
@ResponseBody()
// verwerkt POST uit de weergave [vue-04]
public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale,
HttpServletRequest request, HttpSession session) throws InterruptedException {
...
// de context van de Spring-applicatie
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
// tempo?
Integer tempo = (Integer) session.getAttribute("tempo");
if (tempo != null && tempo > 0) {
Thread.sleep(tempo);
}
...
// het resultaat wordt weergegeven
return résultats;
}
- regel 8: we halen de context [ctx] op uit de Spring-applicatie. Deze hebben we nodig om de berichten uit de bestanden [messages.properties] op te halen aan de hand van een berichtcode en een locale. Dit gebeurt met de volgende syntaxis:
ctx.getMessage(clé_message, tableau_de_paramètres, locale)
- [clé_message]: de sleutel van het gezochte bericht;
- [locale]: de gebruikte locale. Als deze locale bijvoorbeeld [en_US] is, wordt het bestand [messages_en.properties] gebruikt;
- [tableau_de_paramètres]: het verkregen bericht kan worden geconfigureerd zoals in [clé=message {0} {1}]. In dit bericht zijn er twee parameters [{0} {1}]. Als tweede parameter van [ctx.getMessage] moet een array met twee waarden worden opgegeven;
- regels 10-13: als er een vertraging in de sessie is, wordt de huidige thread gedurende die vertraging gestopt;
De actie [/ajax-05] verloopt als volgt:
// het model voor de volgende weergave wordt voorbereid
JsonResults résultats = new JsonResults();
...
}
- regel 2: aanmaken van het sjabloon van de tekenreeks jSON die naar de klant wordt verzonden;
Het sjabloon [JsonResults] ziet er als volgt uit:
![]() |
package istia.st.springmvc.models;
public class JsonResults {
// gegevens
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 en setters
...
}
- regels 6-13: elk veld van de klasse [JsonResult] komt overeen met een veld van dezelfde klasse [id] in de weergave [vue-04.xml]:
De actie [/ajax-05] verloopt als volgt:
// we bereiden het model voor de volgende weergave voor
JsonResults résultats = new JsonResults();
// koptekst
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()));
// er wordt om de beurt een fout gegenereerd
int val = new Random().nextInt(2);
if (val == 0) {
// er wordt een foutmelding teruggestuurd
résultats.setMsgErreur(ctx.getMessage("resultats.erreur",
new Object[] { ctx.getMessage("erreur.aleatoire", null, locale) }, locale));
return résultats;
}
- regel 2: aanmaken van het sjabloon van de string jSON die naar de klant wordt verzonden;
- regels 4-6: de berichten van de koptekst van de resultaten worden aangemaakt;
- regels 8-14: gemiddeld om de twee keer wordt er een foutmelding gegenereerd. In dat geval gaan we niet verder en sturen we de string jSON terug naar de klant (regel 13);
- regel 11: hier zien we een voorbeeld van een geconfigureerd bericht:
erreur.aleatoire=erreur aléatoire
resultats.erreur=Une erreur s''est produite : [{0}]
De actie [/ajax-05] verloopt als volgt:
// de verzonden waarden worden opgehaald
double a = formulaire.getA();
double b = formulaire.getB();
// het model wordt opgebouwd
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");
}
// het resultaat wordt weergegeven
return résultats;
- regels 2-3: de waarden van [a] en [b] worden opgehaald;
- regels 5-12: de vier resultaten worden samengesteld;
- regel 14: de tekenreeks jSON [JsonResults] wordt naar de client verzonden;
Laten we eens kijken wat het resultaat is met de client [Advanced Rest Client]:
![]() |
- in [1-2] wordt een verzoek POST verzonden naar de actie [/ajax-05];
- in [3] worden onjuiste waarden verzonden;
- in [4] heeft de server een lege stream teruggestuurd;
![]() |
- in [1] worden correcte waarden verzonden;
- in [2] wordt het object jSON door de server teruggestuurd, in dit geval met een foutmelding;
![]() |
- in [1] worden correcte waarden verzonden;
- in [2], het object jSON dat door de server wordt teruggestuurd, met hier de vier resultaten;
![]() |
- in [1] worden correcte waarden verzonden;
- in [2] hebben we ervoor gezorgd dat er aan de serverzijde een uitzondering wordt gegenereerd. We zien dat de server nog een object jSON verstuurt. In dit bericht zien we dat de status HTTP van het antwoord [500] is, wat aangeeft dat er een fout aan de serverzijde is opgetreden;
7.3.5. De functie jS [postForm] - 2
Nu we het door de server teruggestuurde object jSON kennen, kunnen we dit in het JavaScript gebruiken. De methode [onSuccess] die wordt uitgevoerd wanneer de server een antwoord verstuurt met de status HTTP [200], is als volgt:
// bij ontvangst van het antwoord van de server
// bij succes
function onSuccess(data) {
console.log("onSuccess");
// het resultatenveld wordt ingevuld
titre.text(data.titre);
labelHeureCalcul.text(data.labelHeureCalcul);
heureCalcul.text(data.heureCalcul);
entete.show();
// resultaten zonder fouten
if (!data.msgErreur) {
aplusb.text(data.aplusb);
amoinsb.text(data.amoinsb);
afoisb.text(data.afoisb);
adivb.text(data.adivb);
résultats.show();
return;
}
// resultaten met fouten
msgErreur.text(data.msgErreur);
erreur.show();
}
- regel 3: de parameter [data] is het object jSON dat door de server wordt teruggestuurd:
![]() |
De methode [onError] die wordt uitgevoerd wanneer de status van het antwoord HTTP [500] is, is als volgt:
// bij ontvangst van het antwoord van de server
// bij mislukking
function onError(jqXHR) {
console.log("onError");
// systeemfout
msgErreur.text(jqXHR.responseText);
erreur.show();
}
- regel 3: het object JQuery [jqXHR] heeft onder andere de volgende eigenschappen:
- responseText: de tekst van het antwoord van de server,
- status: de door de server geretourneerde foutcode,
- statusText: de tekst die bij deze foutcode hoort;
- regel 6: het object [jqXHR.responseText] is het volgende object jSON:
![]() |
7.3.6. Tests
Laten we eens kijken naar enkele schermafbeeldingen van de uitvoering van de webapplicatie:
![]() |
![]() |
![]() |
7.4. Webapplicatie met één pagina
7.4.1. Inleiding
Met Ajax-technologie kunnen applicaties met één pagina worden gebouwd:
- de eerste pagina wordt geladen via een standaardverzoek van de browser;
- de volgende pagina's worden opgehaald via Ajax-verzoeken. Uiteindelijk verandert de browser dus nooit van URL en laadt hij nooit een nieuwe pagina. Dit type applicatie wordt een Single Page Application (APU) genoemd, of in het Engels Single Page Application (SPA).
Hier volgt een eenvoudig voorbeeld van een dergelijke applicatie. De nieuwe applicatie zal twee weergaven hebben:
![]() |
![]() |
- in [1] zorgt de actie [/ajax-06] ervoor dat we de eerste pagina, pagina 1, te zien krijgen;
- in [2] kunnen we via een link naar pagina 2 gaan dankzij een Ajax-aanroep;
- in [3] is URL niet gewijzigd. De weergegeven pagina is pagina 2;
- in [4] kunnen we via een link teruggaan naar pagina 1 dankzij een Ajax-aanroep;
- in [5] is URL niet gewijzigd. De weergegeven pagina is pagina 1.
7.4.2. De actie [/ajax-06]
De code van de actie [/ajax-06] is als volgt:
@RequestMapping(value = "/ajax-06", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax06() {
return "vue-06";
}
- regels 1-4: de actie [/ajax-06] geeft alleen de weergave [vue-06.xml] weer;
7.4.3. De weergave [vue-06.xml]
De weergave [vue-06.xml] is als volgt:
<!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>
- regel 8: de weergave maakt gebruik van een script [local6.js];
- regel 12: de weergave [vue-07.xml] wordt opgenomen in het id-gebied [content] van de weergave [vue-06.xml];
7.4.4. De weergave [vue-07.xml]
De weergave [vue-07.xml] is als volgt:
<!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. De functie jS [gotoPage]
De koppeling [Page 2] van de weergave [vue-07.xml] maakt gebruik van de functie jS [gotoPage] die is gedefinieerd in het volgende bestand [local6.js]:
// algemene gegevens
var content;
function gotoPage(num) {
// er wordt handmatig een Ajax-verzoek gedaan
$.ajax({
url : '/ajax-07',
type : 'POST',
data : 'num=' + num,
dataType : 'html',
beforeSend : function() {
},
success : function(data) {
content.html(data)
},
complete : function() {
},
error : function(jqXHR) {
// systeemfout
content.html(jqXHR.responseText);
}
})
}
// bij het laden van het document
$(document).ready(function() {
// de referenties van de verschillende componenten van de pagina worden opgehaald
content = $("#content");
});
- regel 28: bij het laden van de pagina wordt het ID-veld [content] opgeslagen en tot een globale variabele gemaakt (regel 2);
- regel 4: de functie [gotoPage] ontvangt als parameter het paginanummer (1 of 2) dat in de huidige weergave moet worden getoond;
- regel 7: de URL is het doel van de POST;
- regel 8: de URL uit regel 7 wordt aangeroepen via een POST;
- regel 9: de verzonden string. Er wordt een parameter met de naam [num] verzonden. De waarde ervan is het paginanummer (regel 4) dat in de huidige weergave moet worden getoond;
- regel 10: de server stuurt HTML terug, de parameter van de weer te geven pagina;
- regels 13-15: bij succes (status HTTP gelijk aan 200) wordt de door de server verzonden HTML in het id-veld [content] geplaatst;
- regels 18-20: bij een fout (status HTTP gelijk aan 500) wordt de door de server verzonden HTML in het ID-veld [content] geplaatst;
7.4.6. De actie [/ajax-07]
De actiecode [/ajax-07] is als volgt:
@RequestMapping(value = "/ajax-07", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
public String ajax07(int num) {
// num: paginanummer
switch (num) {
case 1:
return "vue-07";
case 2:
return "vue-08";
default:
return "vue-07";
}
}
- regel 2: de verzonden parameter met de naam [num] wordt opgehaald. Ter herinnering: de parameter op regel 2 moet de naam van de verzonden parameter dragen, in dit geval [num]. [num] is een pagina- of weergavenummer;
- regels 5-6: als [num==1], wordt de weergave [vue-07.xml] teruggestuurd;
- regels 7-8: als [num==2], wordt de weergave [vue-08.xml] geretourneerd;
- regels 9-10: in alle andere gevallen (wat normaal gesproken onmogelijk is) wordt de weergave [vue-07.xml] geretourneerd;
7.4.7. De weergave [vue-08.xml]
De weergave [vue-08.xml] vormt pagina 2 van de applicatie:
<!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. Meerdere HTML-feeds opnemen in één antwoord jSON
7.5.1. Inleiding
We bekijken de volgende applicatie:
![]() |
De pagina [1] bestaat uit vier velden:
- [Zone 1, Zone 3] zijn zones die verschijnen/verdwijnen wanneer je op de knop [Rafraîchir] klikt. We tellen het aantal keren dat elk van deze twee velden [2] verschijnt. Het veld [Zone 1] gebruikt de Franse taal, terwijl het veld [Zone 3] de Engelse taal gebruikt;
- het veld [Zone 2] is altijd aanwezig;
- de zone [Saisies] is permanent aanwezig;
De link [Valider] geeft de volgende pagina weer: [3]:
![]() |
- de link [Retour à la page 1] brengt pagina nr. 1 terug naar de toestand waarin deze zich bevond: [4];
De applicatie bestaat uit één enkele pagina. De eerste pagina wordt door de browser bij de server opgevraagd. De volgende pagina’s worden via Ajax-verzoeken bij de server opgehaald.
7.5.2. De actie [/ajax-09]
![]() |
De actie [/ajax-09] is als volgt:
@RequestMapping(value = "/ajax-09", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax09() {
return "vue-09";
}
Deze actie geeft alleen de weergave [vue-09.xml] weer.
7.5.3. De weergaven XML
![]() |
De weergave [vue-09.xml] is de hoofdpagina van de applicatie:
<!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>
- regel 9: het bestand JS dat in de applicatie wordt gebruikt;
- regel 15: de inhoud van de hoofdpagina;
- regel 16: een geanimeerde afbeelding die wordt weergegeven tijdens het wachten:
- regel 17: gebied voor het weergeven van een eventuele foutmelding;
De weergave [vue-09-page1.xml] is pagina 1 van de applicatie:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h2>Page 1</h2>
<!-- zone 1 -->
<fieldset id="zone1" style="background-color:pink">
<legend>Zone 1</legend>
<span id="zone1-content" th:text="xx">xx</span>
</fieldset>
<!-- zone 2 -->
<fieldset id="zone2" style="background-color:lightgreen">
<legend>Zone 2</legend>
<span>Ce texte reste toujours présent</span>
</fieldset>
<!-- zone 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>
- regels 6-9: het gebied [Zone 1]. De inhoud ervan wordt in de component [id="zone1-content"] geplaatst;
- regels 11-14: het veld [Zone 2], dat niet verandert;
- regels 16-19: het veld [Zone 3]. De inhoud ervan wordt in de component [id="zone3-content"] geplaatst;
- regel 22: de functie JS die het formulier verstuurt;
- regel 25: opname van het invoerveld;
Merk op dat pagina 1 geen [form]-tag heeft. Alles wordt in JavaScript verwerkt.
De weergave [vue-09-saisies.xml] ziet er als volgt uit:
<!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>
- regels 5-8: invoer van een tekenreeks;
- regels 13-16: invoer van een geheel getal;
- regel 14: de functie JS die de ingevoerde waarden verstuurt;
Ook hier valt op dat het invoerveld geen [form]-tag heeft.
In totaal biedt pagina nr. 1 twee functies:
- [Rafraîchir]: hiermee worden de velden 1 en 3 vernieuwd. Deze actie wordt door de server verwerkt, die willekeurig het volgende terugstuurt:
- zone 1 met de bezoekenteller en niets voor zone 3,
- zone 3 met de toegangsteller en niets voor zone 1,
- beide zones met hun bezoekaantallen;
- [Valider]: die pagina 2 weergeeft met de ingevoerde waarden of een foutmelding als de ingevoerde gegevens ongeldig zijn;
We zullen ons eerst richten op de knop [Rafraîchir].
7.5.4. De code JS voor het beheer van de knop [Rafraîchir]
![]() |
De code van het bestand [local9.js] is als volgt:
// globale variabelen
var content;
var loading;
var erreur;
// bij het laden van het document
$(document).ready(function() {
// de referenties van de verschillende componenten van de pagina worden opgehaald
loading = $("#loading");
loading.hide();
erreur = $("#erreur");
erreur.hide();
content = $("#content");
});
- regels 9-13: wanneer de hoofdpagina wordt geladen, worden de referenties van de drie componenten die worden aangeduid met [loading, erreur, content] opgeslagen;
- regels 2-4: de referenties van deze drie componenten worden opgeslagen in globale variabelen. Ze blijven vast omdat de drie betreffende zones altijd aanwezig zijn op de weergegeven pagina, ongeacht het moment. Omdat ze vast blijven, kunnen ze worden berekend in [$(document).ready] en worden gedeeld met de andere functies van het bestand JS;
De functie [postForm] verwerkt de klik op de knop [Rafraîchir]:
function postForm() {
console.log("postForm");
// er wordt handmatig een Ajax-verzoek gedaan
$.ajax({
url : '/ajax-10',
headers : {
'Accept': 'application/json'
},
type : 'POST',
dataType : 'json',
beforeSend : onBegin,
success : onSuccess,
error : onError,
complete : onComplete
})
}
- regels 4-15: de Ajax-aanroep naar de server;
- regel 5: dit is de actie [ajax-10] die de POST zal verwerken;
- regels 6-8: het antwoord zal jSON zijn. De client JS geeft aan dat hij de documenten jSON accepteert;
- regel 9: de actie [ajax-10] wordt aangeroepen met een bewerking POST;
- regel 10: we zullen jSON ontvangen;
- regel 11: de functie die wordt uitgevoerd vóór de Ajax-aanroep;
- regel 12: de functie die wordt uitgevoerd bij ontvangst van het antwoord van de server, wanneer dit succesvol is: [200 OK];
- regel 13: de functie die wordt uitgevoerd bij ontvangst van het antwoord van de server, wanneer dit mislukt is [500 Internal server error, ...];
- regel 14: de functie die wordt uitgevoerd na ontvangst van het antwoord;
De functie [onBegin] is als volgt:
// vóór de Ajax-aanroep
function onBegin() {
console.log("onBegin");
// laadbeeld
loading.show();
}
Deze functie zorgt er alleen voor dat de animatie wordt gestart die het wachten op het antwoord van de server weergeeft.
7.5.5. De actie [/ajax-10]
![]() |
De actie [/ajax-10] is als volgt:
// de sessie
@Autowired
private SessionModel1 session;
// de Thymeleaf/Spring-engine
@Autowired
private SpringTemplateEngine engine;
@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
@ResponseBody()
public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
...
}
- regel 3: de sessie wordt ingevoegd. Deze heeft het volgende type [SessionModel1]:
![]() |
package istia.st.springmvc.models;
import java.io.Serializable;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionModel1 implements Serializable {
private static final long serialVersionUID = 1L;
// twee tellers
private int cpt1 = 0;
private int cpt3 = 0;
// de drie zones
private String zone1 = "xx";
private String zone3 = "zz";
private String saisies;
private boolean zone1Active = true;
private boolean zone3Active = true;
// getters en setters
...
}
De sessie [SessionModel1] slaat de volgende elementen op:
- regel 15: het aantal keren dat [cpt1] wordt weergegeven, waarbij het veld [Zone 1] wordt getoond;
- regel 16: het aantal keren [cpt3] dat het veld [Zone 3] wordt weergegeven;
- regels 18-20: de stromen HTML van de zones [Zone 1], [Zone 3] en [Saisies]. Dit is nodig in de reeks [Page 1] --> [Page 2] --> [Page 1]. Bij de overgang van [Page 2] naar [Page 1] moet [Page 1] worden hersteld, en daarmee ook de drie velden daarvan;
- regels 21-22: twee booleaanse waarden die aangeven of de velden [Zone 1] en [Zone 3] worden weergegeven (zichtbaar zijn);
Het andere element dat in de controller [AjaxController] is ingevoegd, is het volgende:
// de Thymeleaf/Spring-engine
@Autowired
private SpringTemplateEngine engine;
De bean van het type [SpringTemplateEngine] wordt gedefinieerd in het configuratiebestand [Config]:
![]() |
Deze is als volgt gedefinieerd:
@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;
}
- regels 2-10: we kennen de bean van het type [SpringResourceTemplateResolver] waarmee we bepaalde kenmerken van de weergaven kunnen definiëren;
- regels 13-17: met de bean van het type [SpringTemplateEngine] kunnen we de „engine” van de weergaven definiëren, de klasse die verantwoordelijk is voor het genereren van de [Thymeleaf]-antwoorden aan de clients. [Thymeleaf] heeft een standaard „engine“ en een andere wanneer deze wordt gebruikt in een [Spring]-omgeving. Het is deze laatste die we hier gebruiken;
De handtekening van de actie [/ajax-10] is als volgt:
@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
@ResponseBody()
public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
...
}
- regel 1: de actie [/ajax-10] accepteert alleen een POST;
- regel 2: de actie [/ajax-10] geeft zelf het antwoord terug aan de klant. Dit wordt automatisch omgezet in jSON;
- regel 3: het antwoord is van het type [JsonResult10], namelijk:
![]() |
package istia.st.springmvc.models;
public class JsonResult10 {
// gegevens
private String content;
private String zone1;
private String zone3;
private String erreur;
private String saisies;
private boolean zone1Active;
private boolean zone3Active;
public JsonResult10() {
}
// getters en setters
...
}
- regel 6: de inhoud HTML van het veld met de identificatiecode [content];
- regel 7: de inhoud HTML van het veld [Zone 1];
- regel 8: de inhoud HTML van het veld [Zone 3];
- regel 9: de inhoud HTML van het veld [Erreur];
- regel 10: de inhoud HTML van het veld [Saisies];
- regel 11: booleaanse waarde die aangeeft of het veld [Zone 1] moet worden weergegeven;
- regel 12: booleaanse waarde die aangeeft of het veld [Zone 3] moet worden weergegeven;
De actiecode [/ajax-10] is als volgt:
@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
@ResponseBody()
public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
// Thymeleaf-context
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// antwoord
JsonResult10 result = new JsonResult10();
// sessie
session.setZone1(null);
session.setZone3(null);
session.setZone1Active(false);
session.setZone3Active(false);
// er wordt een willekeurig antwoord teruggegeven
int cas = new Random().nextInt(3);
switch (cas) {
case 0:
// zone 1 actief
setZone1(thymeleafContext, result);
return result;
case 1:
// zone 3 actief
setZone3(thymeleafContext, result);
return result;
case 2:
// zones 1 en 3 actief
setZone1(thymeleafContext, result);
setZone3(thymeleafContext, result);
return result;
}
return null;
}
- regel 5: we halen de context [Thymeleaf] op. We zullen later zien waarvoor deze dient;
- regel 7: we maken voorlopig een leeg antwoord aan;
- regels 9-12: we plaatsen de twee velden uit de sessie in [null] en geven aan dat ze niet moeten worden weergegeven. Deze twee velden worden binnenkort gegenereerd, maar het is mogelijk dat slechts één ervan wordt gegenereerd;
- regels 14-29: de twee velden worden gegenereerd;
- regels 17-19: alleen het veld [Zone 1] wordt gegenereerd;
- regels 21-23: alleen het veld [Zone 3] wordt gegenereerd;
- regels 25-28: beide zones [Zone 1] en [Zone 3] worden gegenereerd;
De stroom HTML van het veld [Zone 1] wordt gegenereerd volgens de volgende methode:
private void setZone1(WebContext thymeleafContext, JsonResult10 result) {
// zone 1 actief
// stream 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);
// sessie
session.setCpt1(cpt1);
session.setZone1(zone1);
session.setZone1Active(true);
}
- regel 1: de parameters zijn:
- de context [Thymeleaf] van het type [WebContext],
- het antwoord aan de klant dat momenteel wordt opgebouwd van het type [JsonResult10];
- regel 3: de teller [cpt1] van de sessie, die bijhoudt hoe vaak het veld [Zone 1] wordt weergegeven, wordt verhoogd;
- regel 4: de context [Thymeleaf] van het type [WebContext] gedraagt zich enigszins zoals het Spring-sjabloon [Model]. Om een element aan het model toe te voegen, gebruiken we [WebContext.setVariable]. Hier plaatsen we dus de teller [cpt1] in het model [Thymeleaf]. Hierdoor kan de Thymeleaf-uitdrukking [${cpt1}] worden geëvalueerd
- regel 5: de context [Thymeleaf] heeft een locale. Hierdoor kan deze uitdrukkingen van het type [#{clé_msg}] evalueren. Hier wordt de Thymeleaf-context gekoppeld aan een Franse locale;
- regel 6: dit is de meest interessante instructie. De Thymeleaf-engine verwerkt de weergave [vue-09-zone1.xml] met het model en de locale die zojuist zijn berekend, en in plaats van de resulterende stream HTML naar de client te sturen, geeft hij deze weer als een tekenreeks;
- regels 7-9: de zending HTML van het zojuist berekende veld [Zone 1] wordt opgeslagen in de sessie en in het resultaat dat naar de klant wordt verzonden. Bovendien wordt aangegeven dat het veld [Zone 1] moet worden weergegeven;
- regels 11-13: de informatie over het gebied [Zone 1] wordt in de sessie opgeslagen, zodat het opnieuw kan worden gegenereerd;
Regel 7 verwerkt de volgende weergave [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>
- regel 3: de uitdrukking [#{message.zone}] wordt geëvalueerd met behulp van de locale;
- regel 4: de uitdrukking [${cpt1}] wordt geëvalueerd met behulp van het Thymeleaf-sjabloon;
Het bericht met sleutel [message.zone] is gedefinieerd in de berichtbestanden [messages_fr.properties] en [messages_en.properties]:
![]() |
[messages_fr.properties]
message.zone=Nombre d'accès :
[messages_en.properties]
message.zone=Number of hits:
De stream HTML van het gebied [Zone 3] wordt op een vergelijkbare manier gegenereerd:
private void setZone3(WebContext thymeleafContext, JsonResult10 result) {
// zone 3 actief
// stream 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);
// sessie
session.setCpt3(cpt3);
session.setZone3(zone3);
session.setZone3Active(true);
}
- regel 6: de taalinstelling van het veld [Zone 3] is Engels;
7.5.6. Verwerking van het antwoord van de actie [/ajax-10]
Laten we teruggaan naar de code JS van [local9.js], die het antwoord van de server gaat verwerken:
// bij ontvangst van het antwoord van de server
// bij succes
function onSuccess(data) {
console.log("onSuccess");
// inhoud
if (data.content) {
content.html(data.content);
}
// zone 1
if (data.zone1Active) {
$("#zone1").show();
if (data.zone1) {
$("#zone1-content").html(data.zone1);
}
} else {
$("#zone1").hide();
}
// zone 3 actief?
if (data.zone3Active) {
$("#zone3").show();
if (data.zone3) {
$("#zone3-content").html(data.zone3);
}
} else {
$("#zone3").hide();
}
// invoer?
if (data.saisies) {
$("#saisies").html(data.saisies);
}
// fout?
if (data.erreur) {
erreur.text(data.erreur);
erreur.show();
} else {
erreur.hide();
}
}
Laten we nog eens kijken naar de Java-structuur van het antwoord dat op regel 3 is ontvangen in de variabele [data]:
public class JsonResult10 {
// gegevens
private String content;
private String zone1;
private String zone3;
private String erreur;
private String saisies;
private boolean zone1Active;
private boolean zone3Active;
}
- regels 6-8: als [data.content!=null], dan wordt het veld [id=content] hiermee geïnitialiseerd. Dit veld vertegenwoordigt [Page 1] of [Page 2] in zijn geheel. In dit voorbeeld hebben we [data.content==null] en daarom zal het veld [id=content] niet worden gewijzigd en blijft het [Page 1] weergeven;
- regels 10-17: weergave van [Zone 1] als [data.zone1Active==true]. Als bovendien [data.zone1!=null] geldt, wordt de inhoud van [Zone 1] gewijzigd; anders blijft deze ongewijzigd;
- regels 19-26: hetzelfde geldt voor [Zone 3];
- regels 28-30: als we [data.saisies!=null] hebben, dan wordt het gebied [Saisies] opnieuw gegenereerd. In deze demonstratie hebben we [data.saisies==null] en dus blijft het gebied [Saisies] ongewijzigd;
- regels 32-37: dezelfde redenering geldt voor het veld [Erreur], met de volgende nuances:
- regel 33: [data.erreur] zal een foutmelding in tekstformaat zijn;
- regel 36: als [data.erreur==null], dan wordt het veld [Erreur] verborgen. Dit veld is namelijk mogelijk al bij de vorige aanvraag weergegeven;
In geval van een fout aan de serverzijde (HTTP, status van het type 500 Internal server error), wordt de volgende functie uitgevoerd:
// bij ontvangst van het antwoord van de server
// bij mislukking
function onError(jqXHR) {
console.log("onError");
// systeemfout
erreur.text(jqXHR.responseText);
erreur.show();
}
Om een dergelijke fout te zien, passen we de functie [postForm] als volgt aan:
function postForm() {
console.log("postForm");
// referenties op de huidige pagina ophalen
...
// er wordt handmatig een Ajax-verzoek gedaan
$.ajax({
url : '/ajax-10x',
...
})
}
- regel 7: we voegen een URL toe die niet bestaat;
Dit zijn de resultaten wanneer we op de knop [Rafraîchir] klikken:
![]() |
Het is interessant om te zien dat de foutmelding ook is verzonden in de vorm van een tekenreeks jSON.
De methode die wordt uitgevoerd na ontvangst van het antwoord van de server is de volgende:
// na [onSuccess, onError]
function onComplete() {
console.log("onComplete");
// laadbeeld
loading.hide();
}
We verbergen gewoon de geanimeerde afbeelding die aangeeft dat er gewacht wordt.
7.5.7. Weergave van de pagina [Page 2]
De code HTML van de link [Valider] is als volgt:
<a href="javascript:valider()">Valider</a>
De functie JS [valider] is als volgt:
// validatie van de ingevoerde waarden
function valider() {
// verzonden waarde
var post = JSON3.stringify({
"value1" : $("#text1").val().trim(),
"value2" : $("#text2").val().trim()
});
// we voeren handmatig een Ajax-aanroep uit
$.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
})
}
- regels 4-7: we hebben twee waarden, v1 en v2, die moeten worden gepost: die van de invoercomponenten die worden aangeduid met [#text1] en [#text2]. We gaan iets nieuws doen. We gaan deze twee waarden verzenden in de vorm van een string jSON {"value1":v1,"value2":v2};
- regel 10: de verzonden waarden worden doorgestuurd naar de actie [ajax-11A];
- regel 12: omdat we weten dat we een antwoord jSON zullen ontvangen, geven we aan dat we jSON kunnen ontvangen;
- regel 13: we geven aan de server door dat we de geposte waarde in de vorm van een tekenreeks jSON zullen verzenden;
- regels 15-16: we maken een POST van de te verzenden waarde;
- regel 17: we ontvangen jSON;
7.5.8. De actie [ajax-11A]
De actie [ajax-11A] die de geposte tekenreeks jSON verwerkt, is als volgt:
@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) {
...
}
- regel 1: met ["application/json"] wordt aangegeven dat de actie een document in de vorm jSON verwacht. Dit document is de door de klant verzonden waarde;
- regel 3: de verzonden waarde wordt opgehaald uit het volgende object [PostAjax11A post]:
![]() |
package istia.st.springmvc.models;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.Range;
public class PostAjax11A {
// gegevens
@Size(min = 4, max = 6)
@NotNull
private String value1;
@Range(min = 10, max = 14)
@NotNull
private Integer value2;
// getters en setters
...
}
- de structuur van het object [PostAjax11A] moet de structuur van het verzonden object overnemen {"value1":v1,"value2":v2}. Er zijn dus velden [value1] (regel 13) en [value2] (regel 16) nodig;
- we hebben integriteitsregels ingesteld voor beide velden;
Laten we teruggaan naar de code van de actie [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) {
// Thymeleaf-context
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// antwoord
JsonResult10 result = new JsonResult10();
// geldige post?
if (bindingResult.hasErrors()) {
// pagina 1 wordt met een foutmelding teruggestuurd
result.setZone1Active(session.isZone1Active());
result.setZone3Active(session.isZone3Active());
result.setErreur(getErreursForModel(bindingResult));
return result;
}
...
}
- regel 3: de annotatie [@RequestBody] verwijst naar het door de klant verzonden document. Dit is de waarde die door de klant in jSON is gepost. Deze waarde wordt dus gebruikt om het object [PostAjax11A] samen te stellen;
- regel 3: de annotatie [@Valid] dwingt de validatie van de verzonden waarde af;
- regel 9: als de validatie mislukt:
- regel 13: er wordt een foutmelding teruggestuurd,
- regels 11-12: de velden 1 en 3 worden teruggezet in de toestand waarin ze zich bevonden (al dan niet weergegeven);
De berekening van het foutbericht gebeurt als volgt:
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();
}
Dit is een functie die we al eerder zijn tegengekomen.
De actie [ajax-11A] verloopt als volgt:
@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) {
// Thymeleaf-context
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// antwoord
JsonResult10 result = new JsonResult10();
// geldige post?
if (bindingResult.hasErrors()) {
...
}
// het invoerveld wordt opgeslagen
thymeleafContext.setVariable("value1", post.getValue1());
thymeleafContext.setVariable("value2", post.getValue2());
session.setSaisies(engine.process("vue-09-saisies", thymeleafContext));
// pagina 2 wordt verzonden
result.setContent(engine.process("vue-09-page2", thymeleafContext));
return result;
}
- regels 13-14: de verzonden waarden worden in de Thymeleaf-context geplaatst;
- regel 15: met deze context wordt de weergave [vue-09-saisies] berekend en in de sessie opgeslagen, zodat deze later opnieuw kan worden gegenereerd;
- regel 17: pagina 2 wordt in het resultaat geplaatst dat naar de klant wordt verzonden;
De weergave [vue-09-page2.xml] ziet er als volgt uit:
![]() |
<!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>
- op regel 9 en 13 worden de waarden van [value1, value2] weergegeven die de actie [/ajax-11A] in de Thymeleaf-context heeft geplaatst;
7.5.9. Verwerking van het antwoord van de actie [/ajax-11A]
Aan de clientzijde wordt het antwoord van de actie [/ajax-10] verwerkt door de functie [onSuccess]:
function onSuccess(data) {
console.log("onSuccess");
// inhoud
if (data.content) {
content.html(data.content);
}
// veld 1
if (data.zone1Active) {
$("#zone1").show();
if (data.zone1) {
$("#zone1-content").html(data.zone1);
}
} else {
$("#zone1").hide();
}
// veld 3 actief?
if (data.zone3Active) {
$("#zone3").show();
if (data.zone3) {
$("#zone3-content").html(data.zone3);
}
} else {
$("#zone3").hide();
}
// invoer?
if (data.saisies) {
$("#saisies").html(data.saisies);
}
// fout?
if (data.erreur) {
erreur.text(data.erreur);
erreur.show();
} else {
erreur.hide();
}
}
We hebben deze code al besproken. Laten we de twee gevallen bekijken: een antwoord met of zonder fout:
Met fout
In dit geval heeft de actie [/ajax-11A] een antwoord jSON verzonden in de vorm {"zone1":null, "zone3":null,"invoer":null,"fout":fout,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":null}. Als we de bovenstaande code volgen, zien we dat:
- het veld [content] niet verandert. Dit veld bevatte pagina nr. 1;
- het veld [Erreur] wordt weergegeven;
- de zones [Zone 1], [Zone 3] en [Saisies] blijven ongewijzigd;
Zonder fout
In dit geval heeft de actie [/ajax-11A] een antwoord jSON verzonden in de vorm {"zone1":null, "zone3":null,"invoer":null,"fout":null,"zone1Active":false,"zone3Active":false,"content":content}. Als we de bovenstaande code volgen, zien we dat:
- de zone [content] wordt weergegeven. Deze bevat pagina nr. 2;
Hier volgen drie voorbeelden van uitvoering:
Een geval met een validatiefout:
![]() | ![]() |
Een geval met een fout in POST:
![]() | ![]() |
Dit type fout is anders. Omdat Spring de tekenreeks jSON niet kon converteren naar het type [PostAjax11A], heeft het een reactie HTTP teruggestuurd met [status=400]. De actie [ajax-11A] is niet uitgevoerd;
Een geval zonder fout:
![]() | ![]() |
7.5.10. Terug naar pagina nr. 1
De link [Retour vers la page 1] op pagina nr. 2 is als volgt:
<a href="javascript:retourPage1()">Retour à la page 1</a>
De methode JS [retourPage1] is als volgt:
// terug naar pagina 1
function retourPage1() {
// we voeren handmatig een Ajax-aanroep uit
$.ajax({
url : '/ajax-11B',
headers : {
'Accept': 'application/json',
},
type : 'POST',
dataType : 'json',
beforeSend : onBegin,
success : onSuccess,
error : onError,
complete : onComplete
})
}
Deze methode voert een POST uit, zonder geposte waarde, naar de actie [/ajax-11B].
7.5.11. De actie [/ajax-11B]
De actie [/ajax-11B] is als volgt:
@RequestMapping(value = "/ajax-11B", method = RequestMethod.POST)
@ResponseBody
public JsonResult10 ajax11B(HttpServletRequest request, HttpServletResponse response) {
// Thymeleaf-context
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// antwoord
JsonResult10 result = new JsonResult10();
// we stellen pagina 1 terug naar de oorspronkelijke staat
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;
}
De actie moet pagina nr. 1 met de drie velden [Zone1, Zone3, Erreur] opnieuw genereren:
- regel 9: pagina nr. 1 wordt in het resultaat opgenomen;
- regel 10: het invoerveld wordt in het resultaat opgenomen;
- regel 11: het veld [Zone 1] wordt in het resultaat opgenomen;
- regel 12: het veld [Zone 3] wordt in het resultaat opgenomen;
- regels 13-14: de status van de velden [Zone 1] en [Zone 3] wordt in het resultaat opgenomen;
7.5.12. Verwerking van het antwoord van de actie [/ajax-11B]
Het antwoord van de actie [/ajax-11B] wordt verwerkt door de functie [onSuccess]:
function onSuccess(data) {
console.log("onSuccess");
// inhoud
if (data.content) {
content.html(data.content);
}
// zone 1
if (data.zone1Active) {
$("#zone1").show();
if (data.zone1) {
$("#zone1-content").html(data.zone1);
}
} else {
$("#zone1").hide();
}
// zone 3 actief?
if (data.zone3Active) {
$("#zone3").show();
if (data.zone3) {
$("#zone3-content").html(data.zone3);
}
} else {
$("#zone3").hide();
}
// invoer?
if (data.saisies) {
$("#saisies").html(data.saisies);
}
// fout?
if (data.erreur) {
erreur.text(data.erreur);
erreur.show();
} else {
erreur.hide();
}
}
De actie [/ajax-11B] heeft een antwoord jSON verzonden in de vorm {"zone1":zone1, "zone3":zone3,"invoer":invoer,"fout":null,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":content}. Als we de bovenstaande code volgen, zien we dat:
- het veld [content] is gewijzigd. Het bevatte pagina nr. 2. Het bevat nu pagina nr. 1;
- het gebied [Erreur] is verborgen;
- de zones [Zone 1], [Zone 3] en [Saisies] worden weergegeven zoals ze waren;
7.6. De sessie aan de clientzijde beheren
7.6.1. Inleiding
In de vorige paragraaf hebben we een sessie beheerd met de volgende structuur:
public class SessionModel1 implements Serializable {
// twee tellers
private int cpt1 = 0;
private int cpt3 = 0;
// de drie zones
private String zone1 = "xx";
private String zone3 = "zz";
private String saisies;
private boolean zone1Active = true;
private boolean zone3Active = true;
...
}
Wanneer er zeer veel gebruikers zijn, kan het geheugen dat door de sessies van al deze gebruikers wordt ingenomen, een probleem vormen. De regel is daarom om de omvang ervan tot een minimum te beperken. Het model APU (toepassing met één pagina) maakt het mogelijk om de sessie aan de clientzijde te beheren en een webserver zonder sessie te hebben. De enkele pagina wordt namelijk in eerste instantie door de browser geladen. Samen met deze pagina wordt het bijbehorende JavaScript-bestand geladen. Aangezien de pagina niet opnieuw wordt geladen, blijft dit bestand JS permanent in de browser aanwezig, precies zoals het aanvankelijk is geladen. We kunnen dan de globale variabelen ervan gebruiken om informatie over de verschillende acties van de gebruiker op te slaan. Dat gaan we nu bekijken. We gaan niet alleen de sessie aan de clientzijde beheren, maar ook de applicatie JS herontwerpen om de server zo min mogelijk te belasten.
7.6.2. De actie [/ajax-12]
![]() |
De actie [/ajax-12] is als volgt:
@RequestMapping(value = "/ajax-12", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax12() {
return "vue-12";
}
De weergave [vue-12.xml] ziet er als volgt uit:
![]() |
<!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>
- deze weergave is identiek aan de weergave [vue-09], met als enige verschil het script JS dat in regel 9 wordt gebruikt;
De weergegeven weergave is als volgt:
![]() |
7.6.3. De code JS voor het beheer van de knop [Rafraîchir]
![]() |
De code van het bestand [local12.js] is als volgt:
// globale variabelen
var content;
var loading;
var erreur;
var page1;
var page2;
var value1;
var value2;
var session = {
"cpt1" : 0,
"cpt3" : 0
};
// bij het laden van het document
$(document).ready(function() {
// de referenties van de verschillende componenten van de pagina worden opgehaald
loading = $("#loading");
loading.hide();
erreur = $("#erreur");
erreur.hide();
content = $("#content");
});
- regels 17-21: wanneer de masterpagina wordt geladen, worden de referenties van de drie componenten, geïdentificeerd door [loading, erreur, content], opgeslagen in de globale variabelen van de regels 2-4;
- regels 5-6: om de twee pagina's op te slaan;
- regels 7-8: om de twee waarden op te slaan die via de link [Valider] zijn verzonden;
- regel 9: de sessie. Hiermee worden aan de clientzijde de waarden van de tellers [cpt1, cpt3] opgeslagen;
De functie [postForm] verwerkt de klik op de knop [Rafraîchir]:
function postForm() {
console.log("postForm");
// de sessie wordt opgeslagen
var post = JSON3.stringify(session);
// er wordt handmatig een Ajax-verzoek gedaan
$.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
})
}
De verschillen met de vorige versie zijn als volgt:
- de URL op regel 7 is anders;
- regel 4: er wordt een waarde verzonden, terwijl dat voorheen niet het geval was. Deze waarde is de tekenreeks jSON van de sessie. Het principe is als volgt:
- de client stuurt de sessie naar de server,
- de server wijzigt deze en stuurt deze terug,
- de client slaat de nieuwe sessie op;
- regel 10: er wordt een document verzonden in het formaat jSON (per post);
- regel 13: er moet iets worden verzonden;
- regels 15-20: de functies [beforeSend, error, complete] zijn dezelfde als in de vorige versie. Alleen de functie [success] verandert (regels 16-18);
7.6.4. De actie [/ajax-13]
![]() |
De actie [/ajax-13] is als volgt:
@RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody()
public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request, HttpServletResponse response) {
...
}
- regel 3: de parameter [@RequestBody SessionModel2 session2] haalt de door de client verzonden sessie op. Deze heeft het volgende type [SessionModel2]:
![]() |
package istia.st.springmvc.models;
import java.io.Serializable;
public class SessionModel2 implements Serializable {
private static final long serialVersionUID = 1L;
// twee tellers
private int cpt1 = 0;
private int cpt3 = 0;
// getters en setters
...
}
De sessie [SessionModel2] slaat de volgende elementen op:
- regel 9: het aantal keren dat [cpt1] wordt weergegeven, waarbij het veld [Zone 1] wordt getoond;
- regel 10: het aantal keren [cpt3] waarbij het veld [Zone 3] wordt weergegeven;
Laten we verdergaan met het bestuderen van de code van de actie [/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) {
...
}
- regel 3, het type [JsonResult13] van het antwoord is als volgt:
![]() |
package istia.st.springmvc.models;
public class JsonResult13 {
// gegevens
private String page2;
private String zone1;
private String zone3;
private String erreur;
private String value1;
private Integer value2;
// sessie
private SessionModel2 session;
// getters en setters
...
}
- regel 14: de sessie. De server stuurt deze terug naar de client om te worden opgeslagen;
- regel 6: de inhoud HTML van pagina nr. 2;
- regel 7: de inhoud HTML van het gebied [Zone 1];
- regel 8: de inhoud HTML van het gebied [Zone 3];
- regel 9: de eventuele foutmelding;
- regels 10-11: twee gegevens die door de server worden berekend en op pagina 2 worden weergegeven;
Laten we verdergaan met het bestuderen van de code van de actie [/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) {
// Thymeleaf-context
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// antwoord
JsonResult13 result = new JsonResult13();
result.setSession(session2);
// we geven een willekeurig antwoord
int cas = new Random().nextInt(3);
switch (cas) {
case 0:
// zone 1 actief
setZone1B(thymeleafContext, result);
return result;
case 1:
// zone 3 actief
setZone3B(thymeleafContext, result);
return result;
case 2:
// zones 1 en 3 actief
setZone1B(thymeleafContext, result);
setZone3B(thymeleafContext, result);
return result;
}
return null;
}
- regel 9: de sessie wordt in het resultaat van de actie opgenomen;
De methode [setZone1B] die het veld [Zone 1] activeert, is als volgt:
private void setZone1B(WebContext thymeleafContext, JsonResult13 result) {
// de sessie wordt opgehaald
SessionModel2 session = result.getSession();
// zone 1 actief
// stream 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);
// sessie
session.setCpt1(cpt1);
}
- regel 3: de sessie wordt opgehaald. Deze wordt op regel 12 gewijzigd met de nieuwe teller [cpt1]. Ter herinnering: deze sessie wordt teruggestuurd naar de klant;
- regel 10: het nieuwe veld [Zone 1];
De methode [setZone3B], die het veld [Zone 3] activeert, werkt op dezelfde manier:
private void setZone3B(WebContext thymeleafContext, JsonResult13 result) {
// de sessie wordt opgehaald
SessionModel2 session = result.getSession();
// zone 3 actief
// feed 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);
// sessie
session.setCpt3(cpt3);
}
7.6.5. Verwerking van het antwoord van de actie [/ajax-13]
Aan de clientzijde wordt het antwoord jSON van de actie [/ajax-13] verwerkt door de volgende functie [onSuccess]:
function postForm() {
console.log("postForm");
// de sessie wordt geplaatst
var post = JSON3.stringify(session);
// we voeren handmatig een Ajax-aanroep uit
$.ajax({
...
success : function(data) {
// we slaan de sessie op
session = data.session;
// de twee velden worden bijgewerkt
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();
}
},
...
})
}
- regels 12-17: als de server iets in het veld [zone1] van het antwoord heeft geplaatst, dan moet het veld [Zone 1] opnieuw worden gegenereerd en weergegeven; anders moet het worden verborgen;
- regels 18-23: dezelfde redenering geldt voor het veld [Zone 3];
7.6.6. Weergave van de pagina [Page 2]
De code HTML van de link [Valider] is als volgt:
<a href="javascript:valider()">Valider</a>
De functie JS [valider] is als volgt:
// controle van de ingevoerde waarden
function valider() {
// pagina 1 wordt opgeslagen
page1 = content.html();
// de ingevoerde waarden worden opgeslagen
value1 = $("#text1").val().trim();
value2 = $("#text2").val().trim();
// waarde verzonden
var post = JSON3.stringify({
"value1" : value1,
"value2" : value2,
"pageRequired" : page2 ? false : true
});
// handmatig een Ajax-verzoek uitvoeren
$.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
})
}
- we voeren een POST uit, waardoor we normaal gesproken naar pagina nr. 2 gaan;
- regel 4: we slaan pagina nr. 1 op om er later naar terug te kunnen keren;
- regels 6-7: bij de vorige bewerking worden de ingevoerde waarden niet opgeslagen, alleen de code HTML van de pagina. Daarom slaan we nu de twee waarden op die in het formulier zijn ingevoerd;
- regels 9-13: de twee ingevoerde waarden worden in een tekenreeks jSON geplaatst. Deze tekenreeks wordt verzonden;
- regel 12: een parameter om de server aan te geven of we pagina nr. 2 nodig hebben. We gaan als volgt te werk. We vragen pagina nr. 2 een eerste keer op en slaan deze vervolgens op in de variabele JS [page2]. Daarna vragen we deze niet meer op. We gebruiken de pagina uit de cache. Regel 2: [pageRequired] is gelijk aan [true] als de variabele [page2] leeg is, en aan [false] in het tegenovergestelde geval;
- merk op dat de sessie niet wordt verzonden. Deze slaat namelijk tellers op die de actie [/ajax-14] in regel 20 niet wijzigt;
7.6.7. De actie [/ajax-14]
De actie [/ajax-14] is als volgt:
@RequestMapping(value = "/ajax-14", method = RequestMethod.POST)
@ResponseBody
public JsonResult13 ajax14(@RequestBody @Valid PostAjax14 post, BindingResult bindingResult, Locale locale, HttpServletRequest request, HttpServletResponse response) {
...
}
- regel 3: het antwoord is altijd van het type [JsonResult13];
- regel 3: de verzonden waarde wordt ingekapseld in het volgende type [PostAjax14]:
package istia.st.springmvc.models;
public class PostAjax14 extends PostAjax11A {
// pagina 2
private boolean pageRequired;
// getters en setters
...
}
- regel 3: de klasse [PostAjax14] is een uitbreiding van de klasse [PostAjax11A] uit de vorige versie. Deze heeft dus een structuur van het type [value1, value2, pageRequired];
De actie [/ajax-14] verloopt als volgt:
@RequestMapping(value = "/ajax-14", method = RequestMethod.POST)
@ResponseBody
public JsonResult13 ajax14(@RequestBody @Valid PostAjax14 post, BindingResult bindingResult, Locale locale, HttpServletRequest request, HttpServletResponse response) {
// Thymeleaf-context
WebContext thymeleafContext = new WebContext(request, response, request.getServletContext());
// antwoord
JsonResult13 result = new JsonResult13();
// geldige post?
if (bindingResult.hasErrors()) {
// er wordt een fout teruggestuurd
result.setErreur(getErreursForModel(bindingResult));
return result;
}
// pagina 2 wordt verzonden
result.setValue1(post.getValue1());
result.setValue2(post.getValue2());
// pagina opgevraagd?
if (post.isPageRequired()) {
result.setPage2(engine.process("vue-12-page2", thymeleafContext));
}
return result;
}
- regels 9-13: als de verzonden waarden van [value1, value2] ongeldig zijn, wordt er een foutmelding teruggestuurd;
- regels 15-16: normaal gesproken zou de server een berekening moeten uitvoeren met de verzonden waarden. Hier beperkt hij zich ertoe ze terug te sturen om aan te geven dat hij ze correct heeft ontvangen;
- regels 18-20: pagina nr. 2 wordt alleen teruggestuurd als deze door de klant is opgevraagd. Regel 19: de weergave [vue-12-page2] is nieuw:
![]() |
<!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>
- de code XML bevat geen door Thymeleaf geëvalueerde waarden meer, zoals voorheen het geval was;
- we hebben de plaatsen geïdentificeerd waar de door de server geretourneerde waarden [value1, value2] moeten worden geplaatst. Regel 9: [id='value1'] geeft aan waar [value1] moet worden geplaatst. Regel 13, hetzelfde geldt voor [value2];
7.6.8. Verwerking van het antwoord van actie [/ajax-14]
Het antwoord van de actie [/ajax-14] wordt verwerkt door de volgende functie [success]:
// controle van de ingevoerde waarden
function valider() {
...
// er wordt handmatig een Ajax-verzoek gedaan
$.ajax({
...
success : function(data) {
// fout?
if (data.erreur) {
// foutmelding weergeven
erreur.html(data.erreur);
erreur.show();
} else {
// geen fout
erreur.hide();
// pagina 2
if (page2) {
// we gebruiken de pagina uit de cache
content.html(page2);
} else {
// pagina 2 wordt opgeslagen
page2 = data.page2;
// de pagina wordt weergegeven
content.html(data.page2);
}
// de pagina wordt bijgewerkt met de informatie van de server
$("#value1").text(data.value1);
$("#value2").text(data.value2);
}
},
...
})
}
- regels 9-13: als de server een fout heeft teruggestuurd, wordt deze weergegeven;
- regels 14-29: het geval waarin er geen fout is opgetreden. Dan moet pagina nr. 2 worden weergegeven;
- regel 17: we controleren of pagina nr. 2 al is opgeslagen in de variabele [page2];
- regel 19: in dat geval gebruiken we de variabele [page2] om pagina nr. 2 weer te geven;
- regel 24: anders gebruiken we het veld [data.page2] dat door de server wordt aangeleverd;
- regel 22: we zorgen ervoor dat pagina nr. 2 wordt opgeslagen, zodat deze later niet opnieuw hoeft te worden opgevraagd;
- regels 27-28: op pagina nr. 2 worden de twee door de server verzonden gegevens [value1, value2] weergegeven;
7.6.9. Terug naar pagina nr. 1
De link [Retour vers la page 1] op pagina nr. 2 is als volgt:
<a href="javascript:retourPage1()">Retour à la page 1</a>
De methode JS [retourPage1] is als volgt:
// terug naar pagina 1
function retourPage1() {
// pagina 1 wordt opnieuw gegenereerd
content.html(page1);
// de invoer wordt opnieuw gegenereerd
$("#text1").val(value1);
$("#text2").val(value2);
}
- dit is een actie JS zonder interactie met de server, omdat pagina nr. 1 lokaal is opgeslagen in de variabele [page1];
- regel 4: pagina nr. 1 wordt opnieuw gegenereerd;
- regel 6-7: alleen het gedeelte HTML van pagina nr. 1 was opgeslagen. De invoergegevens niet. Deze moeten dus opnieuw worden gegenereerd;
7.6.10. Conclusie
Door gebruik te maken van de mogelijkheden van het model APU zijn we erin geslaagd de webserver te vereenvoudigen, die nu stateless is (geen sessie) en minder wordt belast:
- we hebben de interactie met de server in de functie JS ([retourPage1]) verwijderd;
- de server genereert pagina nr. 2 slechts één keer;
7.7. Structurering van de JavaScript-code in lagen
7.7.1. Inleiding
De JavaScript-code van de vorige applicatie begint complex te worden. Het is tijd om deze in lagen te structureren. De applicatie blijft hetzelfde als voorheen. We zullen de server niet aanpassen, behalve wat betreft het definiëren van een nieuwe startpagina. We gaan de code JS herontwerpen.
De nieuwe architectuur ziet er als volgt uit:
![]() |
7.7.2. De startpagina
De actie die de applicatie start, is de volgende actie [/ajax-16]:
@RequestMapping(value = "/ajax-16", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String ajax16() {
return "vue-16";
}
Deze geeft de volgende weergave [vue-16.xml] weer:
<!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>
- regels 9-10: de code JS is in twee verschillende bestanden geplaatst:
- [local-ui] implementeert de laag [présentation],
- [local-dao] implementeert de laag [DAO];
![]() |
7.7.3. Implementatie van de laag [DAO]
![]() |
7.7.4. Interface
De laag [DAO] in [local-dao.js] zal de volgende interface presenteren aan de laag [présentation]:
| om pagina 1 bij te werken met de knop [Rafraîchir] |
| om pagina 2 weer te geven met de knop [Valider] |
Javascript kent het begrip 'interface' niet. Ik heb deze term alleen gebruikt om aan te geven dat de laag [présentation] uitsluitend via de twee voorgaande functies met de laag [DAO] communiceert.
7.7.5. Implementatie van de interface
Het raamwerk van de implementatie ziet er als volgt uit:
var session = {
"cpt1" : 0,
"cpt3" : 0
};
// pagina 1 bijwerken
function updatePage1(deferred, sendMeBack) {
...
}
// pagina 2
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
...
}
Het doel van de laag [DAO] is om de details van de HTTP-verzoeken aan de webserver te verbergen voor de laag [présentation]. De sessie maakt deel uit van deze details. Deze wordt daarom voortaan beheerd door de laag [DAO].
7.7.5.1. De functie [updatePage1]
De functie [updatePage1] is de functie die door de laag [présentation] wordt aangeroepen om pagina 1 te verversen. De code ervan is als volgt:
// pagina 1 bijwerken
function updatePage1(deferred, sendMeBack) {
// verzoek HTTP
executePost(deferred, sendMeBack, '/ajax-13', session);
}
- regel 1: de functie [updatePage1] ontvangt twee parameters:
- een object van het type [jQuery.Deferred]. Dit objecttype slaat een status op die drie waarden kan aannemen: ['pending', 'resolved', 'rejected']. Wanneer het in de functie [updatePage1] aankomt, bevindt het zich in de status [pending];
- een object JS dat moet worden teruggestuurd naar de laag [présentation];
Alle verzoeken HTTP worden uitgevoerd door de volgende functie [executePost]:
// verzoek HTTP
function executePost(deferred, sendMeBack, url, post) {
// we voeren handmatig een Ajax-aanroep uit
$.ajax({
headers : {
'Accept: 'application/json',
'Content-Type: 'application/json'
},
url : url,
type : 'POST',
data : JSON3.stringify(post),
dataType : 'json',
success : function(data) {
// we slaan de sessie op
if (data.session) {
session = data.session;
}
// we geven het resultaat weer
deferred.resolve({
"status" : 1,
"data" : data,
"sendMeBack" : sendMeBack
});
},
error : function(jqXHR) {
// de fout weergeven
deferred.resolve({
"status" : 2,
"data" : jqXHR.responseText,
"sendMeBack" : sendMeBack
});
}
});
}
- regel 1: de functie [executePost] voert een Ajax-aanroep van het type POST uit. Deze verwacht vier parameters:
- een object van het type [jQuery.Deferred] in de status [pending];
- een object van het type JS dat moet worden teruggestuurd in de laag [présentation];
- de URL van de POST;
- de waarde die als object JS moet worden gepost;
- regels 5-8: de functie post van jSON (regel 7) en ontvangt van jSON (regel 6);
- regel 11: de te verzenden waarde wordt omgezet in jSON;
- regels 13-24: de functie die wordt uitgevoerd als de Ajax-aanroep slaagt;
- regels 19-23: als de server een sessie heeft teruggestuurd, wordt deze opgeslagen;
- regels 13-18: zetten het object [deferred] om naar de status [resolved] en geven daarbij een resultaat door met de volgende velden:
- [status]: 1 bij succes, 2 bij mislukking,
- [data]: het antwoord jSON van de server,
- [sendMeBack]: de tweede parameter van de functie, een object dat de aanroeper wil ophalen;
- regels 17-31: de functie die wordt uitgevoerd als de Ajax-aanroep mislukt. We doen hetzelfde als eerder, met twee verschillen:
- [status] wordt op 2 gezet om een fout aan te geven;
- [data] is opnieuw het antwoord jSON van de server, maar op een andere manier verkregen;
7.7.5.2. De functie [getPage2]
De functie [getPage2] is als volgt:
// pagina 2
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
// verzoek HTTP
executePost(deferred, sendMeBack, '/ajax-14', {
"value1" : value1,
"value2" : value2,
"pageRequired" : pageRequired,
});
}
- De functie ontvangt de volgende parameters:
- [deferred]: een object van het type [jQuery.Deferred] in de status [pending],
- [sendMeBack]: een object van het type JS dat moet worden teruggestuurd in de laag [présentation],
- [value1]: de eerste invoer op pagina 1,
- [value2]: de tweede invoer op pagina 2,
- [pageRequired]: een booleaanse waarde die de server aangeeft of deze de stream HTML van pagina nr. 2 al dan niet moet verzenden;
- de functie [executePost] wordt aangeroepen om de benodigde aanvraag HTTP uit te voeren;
7.7.6. De laag [présentation]
![]() |
De laag [présentation] wordt geïmplementeerd door het bestand [local-ui.js]. Dit bestand bevat de code uit het bestand [local12.js], aangepast om gebruik te maken van de voorgaande laag [DAO]. Er zijn slechts twee functies gewijzigd: [postForm] en [valider].
7.7.6.1. De functie [postForm]
De functie [postForm] is als volgt:
// pagina 1 bijwerken
function postForm() {
// pagina 1 wordt bijgewerkt
var deferred = $.Deferred();
loading.show();
updatePage1(deferred, {
'afzender: "postForm",
'info: 10
});
// weergave resultaten
deferred.done(postFormDone);
}
- regel 4: er wordt een object [jQuery.Deferred] aangemaakt. Standaard bevindt dit zich in de status [pending];
- regel 5: de laadafbeelding wordt weergegeven
- regels 6-9: de functie [updatePage1] wordt uitgevoerd. Er wordt een fictief object [sendMeBack] doorgegeven, alleen om te laten zien waarvoor dit kan dienen;
- regel 11: de parameter van de functie [deferred.done] is zelf een functie. Dit is de functie die moet worden uitgevoerd wanneer de toestand van het object [deferred] overgaat naar de toestand [resolved]. We hebben zojuist gezien dat de functie DAO [executePost] de status van dit object doorgeeft aan [resolved] bij ontvangst van het antwoord van de server. Dit betekent dat wanneer de functie [postFormDone] wordt uitgevoerd, het antwoord van de server al is ontvangen;
De functie [postFormDone] is als volgt:
function postFormDone(result) {
// einde wachttijd
loading.hide();
// gegevens worden opgehaald
var data = result.data
// voor demo
console.log(JSON3.stringify(result.sendMeBack));
// status wordt geanalyseerd
switch (result.status) {
case 1:
// de twee velden worden bijgewerkt
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:
// foutmelding
erreur.html(data);
break;
}
}
- regel 1: de ontvangen parameter [result] is de parameter die is doorgegeven aan de methode [deferred.resolve] in de functie [executePost], bijvoorbeeld:
// het resultaat wordt weergegeven
deferred.resolve({
"status" : 1,
"data" : data,
"sendMeBack" : sendMeBack
});
- regel 5: het antwoord van de server wordt opgehaald;
- regels 10-24: hier staat de code die in de vorige versie in de functie [onSuccess] van de functie [postForm] stond;
- regels 25-28: hier staat de code die in de vorige versie in de functie [onError] van de functie [postForm] stond;
7.7.6.2. De rol van de parameter [sendMeBack]
Waarvoor dient de parameter [sendMeBack]? Laten we eens kijken naar de aanroepcode van de functie [updatePage1]:
// pagina 1 bijwerken
function postForm() {
// pagina 1 wordt bijgewerkt
var deferred = $.Deferred();
loading.show();
updatePage1(deferred, {
'afzender: "postForm",
'info: 10
});
// weergave resultaten
deferred.done(postFormDone);
}
en de signatuur van de functie [validerDone]:
function postFormDone(result) {
}
Hoe kan de functie [postForm] informatie doorgeven aan de functie [postFormDone]? Deze laatste heeft slechts één parameter: [result]. Deze parameter wordt aangemaakt door de functie [executePost] van de laag [DAO]. Om gegevens door te geven aan de functie [postFormDone], moet de functie [postForm] deze eerst doorgeven aan de functie [updatePage1]. Dit is de rol van de parameter [sendMeBack]. Deze wordt als volgt gebruikt:
function postFormDone(result) {
// einde wachttijd
loading.hide();
// gegevens worden opgehaald
var data = result.data
// voor demo
console.log(JSON3.stringify(result.sendMeBack));
// status wordt geanalyseerd
switch (result.status) {
...
- regel 7, de functie [postFormDone] heeft de parameter [sendMeBack] teruggevonden die aanvankelijk door de functie [postForm] was doorgegeven aan de functie DAO [updatePage1];
7.7.7. De functie [valider]
De functie [valider] is als volgt:
// invoerde gegevens valideren
function valider() {
// pagina 1 wordt opgeslagen
page1 = content.html();
// de ingevoerde waarden worden opgeslagen
value1 = $("#text1").val().trim();
value2 = $("#text2").val().trim();
// geen fout
erreur.hide();
// pagina 2 wordt opgevraagd
var deferred = $.Deferred();
loading.show();
getPage2(deferred, {
''sender': 'bevestigen',
'info' : 20
}, value1, value2, page2 ? false : true);
// weergave resultaten
deferred.done(validerDone);
}
en de functie [validerDone] (regel 18) is als volgt:
function validerDone(result) {
// einde wachttijd
loading.hide();
// gegevens worden opgehaald
var data = result.data
// voor demo
console.log(JSON3.stringify(result.sendMeBack));
// de status wordt geanalyseerd
switch (result.status) {
case 1:
// fout?
if (data.erreur) {
// foutmelding
erreur.html(data.erreur);
erreur.show();
} else {
// geen fout
erreur.hide();
// pagina 2
if (page2) {
// de pagina uit de cache wordt gebruikt
content.html(page2);
} else {
// pagina 2 wordt opgeslagen
page2 = data.page2;
// de pagina wordt weergegeven
content.html(data.page2);
}
// de pagina wordt bijgewerkt met de gegevens van de server
$("#value1").text(data.value1);
$("#value2").text(data.value2);
}
break;
case 2:
// foutmelding
erreur.html(data);
erreur.show();
break;
}
}
- regel 5: het antwoord van de server wordt opgehaald;
- regels 10-32: hier staat de code die in de vorige versie in de functie [onSuccess] van de functie [valider] stond;
- regels 34-38: hier staat de code die in de vorige versie in de functie [onError] van de functie [valider] stond;
7.7.8. Tests
De applicatie blijft werken zoals voorheen en in de Chrome-console zijn de parameters [sendMeBack] van de functies [postForm] en [valider] te zien:
![]() |
7.8. Conclusion
Laten we terugkeren naar het algemene schema van een Spring-applicatie MVC:
![]() |
Dankzij de JavaScript-code die in de pagina's HTML is ingebed en in de browser wordt uitgevoerd, en dankzij het model APU, kunnen we code naar de browser verplaatsen en zo de volgende architectuur realiseren:
![]() |
- hebben we een client-architectuur [2] / server-architectuur [1] waarbij de client en de server communiceren via jSON;
- in [1] levert de Spring-weblaag MVC weergaven, weergavefragmenten en gegevens in jSON;
- in [2]: de JavaScript-code die is ingebed in de weergave die bij het opstarten van de applicatie wordt geladen, kan in lagen worden gestructureerd:
- de laag [présentation] zorgt voor de interacties met de gebruiker,
- de laag [DAO] regelt de toegang tot de gegevens via de webserver [1],
- de laag [métier] bestaat mogelijk niet of neemt bepaalde niet-vertrouwelijke functies van de serverlaag [métier] over om deze te ontlasten;
- de client [2] kan bepaalde weergaven in de cache opslaan om ook hier de server te ontlasten. Hij beheert de sessie;













































































