Skip to content

7. Ajaxificazione di un'applicazione Spring MVC

7.1. Il ruolo di AJAX in un'applicazione web

Finora, gli esempi didattici che abbiamo studiato avevano la seguente architettura:

Per passare da una vista [View1] a una vista [View2], il browser:

  • invia una richiesta all'applicazione web;
  • riceve la vista [View2] e la visualizza al posto della vista [View1].

Questo è il modello classico:

  • richiesta dal browser;
  • il server web genera una vista in risposta al client;
  • visualizzazione di questa nuova vista da parte del browser.

Da diversi anni ormai esiste un'altra modalità di interazione tra il browser e il server web: AJAX (Asynchronous JavaScript and XML). Ciò comporta interazioni tra la vista visualizzata dal browser e il server web. Il browser continua a fare ciò che sa fare meglio, ovvero visualizzare una vista HTML, ma ora è controllato da JavaScript incorporato nella vista HTML visualizzata. Il diagramma è il seguente:

  • In [1], si verifica un evento sulla pagina visualizzata nel browser (un clic su un pulsante, una modifica del testo, ecc.). Questo evento viene intercettato dal JavaScript (JS) incorporato nella pagina;
  • In [2], il codice JavaScript effettua una richiesta HTTP proprio come avrebbe fatto il browser. La richiesta è asincrona: l'utente può continuare a interagire con la pagina senza essere bloccato in attesa della risposta HTTP. La richiesta segue il flusso di elaborazione standard. Nulla (o quasi) la distingue da una richiesta standard;
  • In [3], viene inviata una risposta al client JS. Piuttosto che una vista HTML completa, in genere viene inviata una vista HTML parziale, un feed XML o JSON (JavaScript Object Notation);
  • In [4], JavaScript recupera questa risposta e la utilizza per aggiornare una regione della pagina HTML visualizzata.

Per l'utente, si verifica un cambiamento nella visualizzazione perché ciò che vede è cambiato. Tuttavia, non c'è un ricaricamento completo della pagina; al contrario, si verifica solo una modifica parziale della pagina visualizzata. Questo contribuisce a rendere la pagina più fluida e interattiva: poiché non c'è un ricaricamento completo della pagina, possiamo gestire eventi che in precedenza non potevano essere gestiti. Ad esempio, offrire all'utente un elenco di opzioni mentre digita i caratteri in un campo di immissione. Ad ogni nuovo carattere digitato, viene inviata una richiesta AJAX al server, che restituisce ulteriori suggerimenti. Senza AJAX, questo tipo di assistenza all'immissione era in precedenza impossibile. Non potevamo ricaricare una nuova pagina ad ogni carattere digitato.

7.2. Aggiornamento di una pagina con un feed HTML

7.2.1. Le viste

Proponiamo di studiare la seguente applicazione:

  • in [1], il tempo di caricamento della pagina;
  • in [2], vengono eseguite le quattro operazioni aritmetiche su due numeri reali A e B;
  • in [3], la risposta del server viene visualizzata in una regione della pagina;
  • in [4], il tempo di calcolo. Questo è diverso dal tempo di caricamento della pagina [5]. Quest'ultimo è uguale a [1], il che indica che l'area [6] non è stata ricaricata. Inoltre, l'URL della pagina [7] non è cambiato.

7.2.2. L'azione [/ajax-01]

  

Il controller [Ajax.java] definisce la seguente azione [/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) {
        // valid tempo?
        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));
            }
        }
        // prepare the view model [view-01]
        ...
}
  • riga 2: l'azione [/ajax-01] accetta un solo parametro [tempo]. Si tratta della durata in millisecondi che il server deve attendere prima di inviare i risultati delle operazioni aritmetiche;
  • riga 4: il parametro [tempo] è facoltativo;
  • righe 5–12: verifichiamo che il valore del parametro [tempo] sia valido;
  • righe 13–15: in tal caso, il valore del timeout viene memorizzato nella sessione. Ciò significa che rimarrà in vigore fino a quando non verrà modificato;

Il codice per l'azione [/ajax-01] prosegue come segue:


    @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) {
        // valid tempo?
...
        // prepare the view model [view-01]
        modèle.addAttribute("actionModel01", new ActionModel01());
...
        // view
        return "vue-01";
}

La classe [ActionModel01] viene utilizzata principalmente per incapsulare i valori inviati dall'azione [/ajax-01]. In questo caso, non viene inviato nulla. Creiamo una classe vuota e la inseriamo nel modello perché la vista [vue-01.xml] la utilizza. La classe [ActionModel01] è la seguente:


package istia.st.springmvc.models;
 
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotNull;
 
public class ActionModel01 {
 
    // posted data
    @NotNull
    @DecimalMin(value = "0.0")
    private Double a;
 
    @NotNull
    @DecimalMin(value = "0.0")
    private Double b;
 
    // getters and setters
    ...
}
  • righe 11 e 15: due numeri reali [a,b] che verranno inviati tramite un modulo;

Torniamo al codice dell'azione:


    @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) {
...
        // prepare the view model [view-01]
        modèle.addAttribute("actionModel01", new ActionModel01());
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
...
        // view
        return "vue-01";
}
  • righe 6-7: aggiungiamo un'istanza di tipo [Risultati] al modello;

Il tipo [Results] inserito nel modello è il seguente:

  

package istia.st.springmvc.models;
 
public class Resultats {
 
    // data
    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 and setters
    ...
}
  • righe 6–9: i risultati delle quattro operazioni aritmetiche sui numeri [a, b];
  • riga 10: il tempo impiegato per caricare inizialmente la pagina;
  • riga 11: l'ora in cui sono state eseguite le quattro operazioni aritmetiche;
  • riga 12: eventuali messaggi di errore;
  • riga 13: la vista da visualizzare, se presente;
  • riga 14: le impostazioni locali della vista, [fr-FR] o [en-US];

Il codice dell'azione [/ajax-01] prosegue come segue:


    @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) {
        ...
        // local
        setLocale(locale, modèle, résultats);
...
}
  • riga 5: il metodo [setLocale] viene utilizzato per impostare la lingua da utilizzare nel modello di visualizzazione, [fr-FR] o [en-US]. Questa lingua è destinata al JavaScript incorporato nella visualizzazione;

Il metodo [setLocale] è il seguente:


    private void setLocale(Locale locale, Model modèle, Resultats résultats) {
        // we only manage fr-FR, en-US locales
        String language = locale.getLanguage();
        String country = null;
        switch (language) {
        case "fr":
            country = "FR";
            break;
        default:
            language = "en";
            country = "US";
            break;
        }
        // culture
        résultats.setCulture(String.format("%s-%s", language, country));
}

Nel modello, la stringa [${results.culture}] sarà uguale a 'fr-FR' o 'en-US'.

Torniamo all'azione [/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) {
...
        // local
        setLocale(locale, modèle, résultats);
        // hour
        résultats.setHeureGet(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        // view
        return "vue-01";
    }
  • riga 7: imposta l'ora dalla richiesta GET nel template;
  • Riga 9: visualizziamo la vista [vue-01.xml]:

7.2.3. La vista [view-01.xml]

La vista [view-01.xml] è la seguente:


<!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>
  • righe 7–12: le librerie jQuery di validazione e internazionalizzazione (culture);
  • riga 15: la libreria [client-validation] creata nella sezione 6.3;
  • riga 14: la libreria JSON utilizzata dalla libreria [client-validation]. È facoltativa se i log di validazione sono stati disabilitati;
  • riga 13: la libreria [Unobtrusive Ajax] di Microsoft. Questa libreria a volte consente di evitare di scrivere codice JavaScript;
  • riga 16: un file JavaScript per le nostre esigenze;
  • righe 17–22: per gestire le impostazioni locali [fr-FR] e [en-US] sul lato client. Abbiamo già incontrato questo codice;
  • riga 27: un messaggio configurato. Li abbiamo studiati nella sezione 5.18;
  • righe 36–38: il modulo su cui torneremo più avanti;
  • Riga 40: l'area del documento in cui JavaScript inserirà la risposta del server;

7.2.4. Il modulo

 

Nella vista [vue-01.xml], il modulo è il seguente:


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

che produce il seguente codice HTML:


<form id="formulaire" name="formulaire" method="post" data-ajax-update="#resultats" data-ajax-complete="afterComplete"     data-ajax-begin="beforeSend" data-ajax-loading-duration="0" data-ajax-mode="replace" data-ajax="true" data-ajax-method="post" data-ajax-loading="#loading" action="/ajax-02.html">
    <table>
        <thead>
            <tr>
                <th>
                    <span>valeur de A</span>
                </th>
                <th>
                    <span>valeur de B</span>
                </th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>
                    <input type="text" data-val="true" data-val-min="Le nombre doit être supérieur ou égal à 0" data-val-number="Format invalide" data-val-min-value="0" data-val-required="Le champ est obligatoire" value="" id="a" name="a" />
                </td>
                <td>
                    <input type="text" data-val="true" data-val-min="Le nombre doit être supérieur ou égal à 0" data-val-number="Format invalide" data-val-min-value="0" data-val-required="Le champ est obligatoire" value="" id="b" name="b" />
                </td>
            </tr>
            <tr>
                <td>
                    <span class="field-validation-valid" data-valmsg-for="a" data-valmsg-replace="true"></span>
 
                </td>
                <td>
                    <span class="field-validation-valid" data-valmsg-for="b" data-valmsg-replace="true"></span>
 
                </td>
            </tr>
        </tbody>
    </table>
    <p>
        <input type="submit" value="Calculer" />
        <img id="loading" style="display: none" src="/images/loading.gif" />
        <a href="javascript:postForm()">Calculer</a>
    </p>
</form>
  • riga 16: il campo [a] è associato ai validatori [required], [number] e [min];
  • riga 19: lo stesso vale per il campo [b];

I vari messaggi si trovano nei file [messages.properties] del progetto:

  

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

Ora, esaminiamo gli attributi del 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">

Possiamo riconoscere gli attributi standard del tag [form]:


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

È immediatamente evidente che, se JavaScript è disabilitato nel browser che visualizza la pagina, il modulo verrà inviato all'URL [/ajax-02.html]. Ora analizziamo gli altri attributi:


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

Gli attributi [data-ajax-xxx] sono gestiti dalla libreria JavaScript [unobtrusive-ajax], che è stata importata dalla vista [vue-01.xml]:


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

Quando sono presenti gli attributi [data-ajax-xxx], il pulsante [submit] del modulo verrà eseguito tramite una chiamata Ajax dalla libreria [unobtrusive-ajax]. I parametri hanno i seguenti significati:

  • [data-ajax="true"]: la presenza di questo attributo fa sì che il [submit] del modulo venga eseguito tramite Ajax;
  • [data-ajax-method="post"]: il metodo del [submit]. L'URL POST sarà quello dell'attributo [action="/ajax-02.html"];
  • [data-ajax-loading="#loading"]: l'ID di un'area da visualizzare in attesa della risposta del server. L'area identificata da [loading] nella vista [vue-01.xml] è la seguente:

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

Si tratta di un'immagine di caricamento animata che verrà visualizzata fino a quando non verrà ricevuta la risposta del server;

  • [data-ajax-loading-duration="0"]: il tempo di attesa in millisecondi prima che venga visualizzata l'area [data-ajax-loading="#loading"]. In questo caso, verrà visualizzata non appena inizia l'attesa;
  • [data-ajax-begin="beforeSend"]: la funzione JavaScript da eseguire prima dell'invio;
  • [data-ajax-complete="afterComplete"] : la funzione JavaScript da eseguire quando la risposta è stata ricevuta;
  • [data-ajax-update="#resultats"]: l'ID dell'area in cui verrà inserito il risultato inviato dal server. La vista [vue-01.xml] contiene la seguente area:

<div id="resultats" />
  • [data-ajax-mode="replace"]: la modalità di inserimento del risultato nell'area precedente. La modalità [replace] farà sì che il risultato "sovrascriva" qualsiasi contenuto precedentemente presente nell'area con l'ID [resultats];

Si noti che il comando JavaScript [submit] verrà eseguito solo se i validatori hanno dichiarato validi i valori testati.

La libreria JavaScript [unobtrusive-ajax] ha due obiettivi:

  • garantire che il modulo si adatti correttamente a entrambe le possibilità: sia che JavaScript sia abilitato o disabilitato nel browser;
  • evitare di scrivere JavaScript. Vedremo che in questo caso non è stato possibile evitarlo.

7.2.5. L'azione [/ajax-02]

Abbiamo visto che i valori inviati sono stati inviati all'azione [/ajax-02]. È la seguente:


@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);
        }
        // prepare the model for the next view
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
        // we set the locale
        setLocale(locale, modèle, résultats);
        // hour
        résultats.setHeurePost(new SimpleDateFormat("hh:mm:ss").format(new Date()));
        ...
}
  • Per ora semplifichiamo le cose: supponiamo che la richiesta POST sia stata effettivamente inviata dal JavaScript nella vista [vue-01.xml]. Rivedremo questa ipotesi più avanti;
  • riga 2: i valori inviati [a,b] vengono inseriti nel modello [ActionModel01];
  • righe 4–7: se l'utente ha impostato un timeout durante una precedente richiesta GET, questo viene recuperato dalla sessione e il timeout viene applicato (riga 6). Lo scopo è consentire all'utente di vedere l'effetto dell'attributo [data-ajax-loading="#loading"] nel modulo;
  • righe 9-10: un attributo [results] viene aggiunto al modello;
  • riga 12: la lingua [fr-FR] o [en-US] viene aggiunta al modello;
  • riga 14: impostiamo il tempo POST nel modello;

Ricordiamo il tipo [Resultats] aggiunto al modello:


public class Resultats {
 
    // data
    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 and setters
...
}

Il codice per l'azione [/ajax-02] prosegue come segue:


@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()));
        // we generate an error every other time
        int val = new Random().nextInt(2);
        if (val == 0) {
            // an error message is returned
            résultats.setErreur("erreur.aleatoire");
            return "vue-03";
        }
...
    }
  • Righe 6–11: In questo esempio, mostriamo come restituire una pagina di errore al client JavaScript. Nella metà dei casi, restituiamo la seguente vista [view-03.xml]:

Nota sulla riga 9: ciò che inseriamo nel modello non è un messaggio, ma una chiave di messaggio:

[messages_fr.properties]


erreur.aleatoire=erreur aléatoire

[messages_fr.properties]


erreur.aleatoire=randomly generated error

Il codice della vista [vue-03.xml] è il seguente:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <body>
        <h4>Résultats</h4>
        <p>
            <strong>
                <span th:text="#{labelHeureCalcul}">Heure de calcul :</span>
                <span id="heureCalcul" th:text="${resultats.heurePost}"></span>
            </strong>
        </p>
        <p style="color: red;">
            <span th:text="#{LabelErreur(#{${resultats.erreur}})}">Une erreur s'est produite :</span>
            <!-- <span id="error" th:text="${resultats.erreur}"></span> -->
        </p>
    </body>
</html>
 
  • riga 12, si noti un messaggio configurato da una chiave di messaggio che è essa stessa calcolata. Abbiamo introdotto questo concetto nella sezione 5.18, pagina 170.

Il codice per l'azione [/ajax-02] prosegue come segue:


@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 {
...
        // retrieve posted values
        double a = formulaire.getA();
        double b = formulaire.getB();
        // we build the model
        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");
        }
        // the view is displayed
        return "vue-02";
    }
  • righe 5–15: le quattro operazioni aritmetiche vengono eseguite sui numeri [a, b] e incapsulate nell'istanza [Resultats] del modello;
  • riga 17: viene restituita la seguente vista [view-02.xml]:

La vista [view-02.xml] è la seguente:


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

Che il risultato sia la vista [vue-02.xml] o la vista [vue-03.xml], questo risultato HTML viene inserito nell'area identificata da [resultats] nella vista [vue-01.xml], grazie all'attributo [data-ajax-update="#resultats"] del modulo.

7.2.6. Invio dei valori inseriti

Qui incontriamo una sfida con i valori inviati. Stiamo lavorando con due impostazioni locali [fr-FR] e [en-US] che rappresentano i numeri reali in modo diverso. Abbiamo affrontato questo problema nella Sezione 6.3, a pagina 190, quando abbiamo dovuto inviare tramite POST numeri reali in due diverse impostazioni locali. Riutilizzeremo gli strumenti utilizzati in quell'occasione. Tuttavia, ci troviamo di fronte a un'ulteriore sfida: non abbiamo accesso al metodo che gestisce l'invio tramite POST dei valori inseriti. Questo è il motivo per cui abbiamo aggiunto i seguenti attributi al tag del modulo:

  • [data-ajax-begin="beforeSend"]: la funzione JavaScript da eseguire prima dell'invio del modulo;
  • [data-ajax-complete="afterComplete"]: la funzione JavaScript da eseguire quando è stata ricevuta la risposta;

Non abbiamo accesso alla funzione JavaScript che invierà i valori inseriti tramite POST, ma possiamo scrivere due funzioni JavaScript:

  • [beforeSend]: una funzione JavaScript eseguita prima del POST;
  • [afterComplete]: una funzione JavaScript eseguita al ricevimento della risposta POST;

Queste due funzioni sono inserite in un file denominato [local1.js]:

  

Il file [local1.js] inizializza l'ambiente JavaScript della vista [vue-01.xml] come segue:


// global data
var loading;
var formulaire;
var résultats;
var a, b;
 
// document loading
$(document).ready(function() {
    // retrieve the references of the page's various components
    loading = $("#loading");
    formulaire = $("#formulaire");
    resultats = $('#resultats');
    a = $("#a");
    b = $("#b");
    // we hide certain elements
    loading.hide();
    // parse the form validators
    $.validator.unobtrusive.parse(formulaire);
    // we manage two locales [fr_FR, en_US]
    // the reals [a,b] are sent by the server in Anglo-Saxon format
    // we put them in French format if necessary
    checkCulture(2);
});
  • riga 22: la funzione [checkCulture] è descritta più avanti;

La funzione JavaScript [beforeSend] sarà la seguente:


function beforeSend(jqXHR, settings) {
    // before POST
    // numbers must be posted in Anglo-Saxon format
    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) {
        // we put the numbers [a,b] in Anglo-Saxon format
        var value1 = a.val().replace(",", ".");
        a.val(value1);
        var value2 = b.val().replace(",", ".");
        b.val(value2);
    }
    if (mode == 2) {
...
    }
}
  • righe 4-6: verifichiamo se la lingua della vista è [fr-FR]. In questo caso, i valori inviati devono essere modificati. Infatti, se l'utente ha inserito [1,6], deve essere inviato il valore [1.6]; altrimenti, il valore [1,6] verrà rifiutato sul lato server. Per farlo, basta sostituire la virgola nei valori inviati con un punto decimale (righe 18–21);
  • ma non possiamo fermarci qui. Quando viene chiamata la funzione [beforeSend], la stringa dei valori inviati [a=val1&b=valB] è già stata costruita. Dobbiamo quindi modificarla. Questo viene fatto utilizzando il secondo parametro della funzione [settings];
  • Riga 7: [settings.data] (settings è un parametro della funzione) rappresenta la stringa inviata. Ricreiamo questa stringa utilizzando l'espressione [form.serialize()]. Questa espressione attraversa il modulo per trovare i valori da inviare e costruisce la stringa POST. Prenderà quindi i nuovi valori di [a,b] con i punti decimali;

Se non facciamo altro, il server invierà la sua risposta, che verrà visualizzata correttamente. Tuttavia, i valori di [a,b] ora hanno i punti decimali, anche se siamo ancora nella locale [fr-FR]. Quindi, se l'utente non se ne accorge e clicca di nuovo su [Calcola], i validatori gli diranno che i valori [a,b] non sono validi. Il che è corretto. È qui che entra in gioco la funzione [afterComplete], eseguita al ricevimento del risultato:


function beforeSend(jqXHR, settings) {
    // before POST
...
}
 
function afterComplete(jqXHR, settings) {
    // after POST
    // numbers must be supplied in French format if necessary
    var culture = Globalize.culture().name;
    if (culture === 'fr-FR') {
        checkCulture(2);
    }
}
 
function checkCulture(mode) {
    if (mode == 1) {
...
    }
    if (mode == 2) {
        // put the numbers in French format
        var value1 = a.val().replace(".", ",");
        a.val(value1);
        var value2 = b.val().replace(".", ",");
        b.val(value2);
    }
}
  • righe 9-12: se la lingua della vista è [fr-FR], converte i numeri [a,b] nel formato francese.

7.2.7. Test

Ecco alcuni screenshot di prova:

  • in [1], la risposta del server;
  • in [2], la risposta del server con un messaggio di errore;
  • in [3], è impostato un timeout di 5 secondi. Ciò significa che il server attenderà 5 secondi prima di inviare la propria risposta. Nel tag [form], abbiamo utilizzato l'attributo [data-ajax-loading='#loading']. Il parametro [loading] è l'identificatore di un'area che viene:
    • visualizzata per l'intera durata dell'attesa;
    • nascosta dopo la ricezione della risposta del server;

Qui, [loading] è l'identificatore di un'immagine animata visibile in [4].

7.2.8. Disabilitazione di JavaScript con le impostazioni locali [en-US]

Cosa succede se disabilitiamo JavaScript nel browser?

Il POST dei valori inseriti avverrà secondo il tag [form], i cui attributi [data-ajax-attr] non verranno utilizzati. Tutto avviene come se avessimo il seguente tag [form]:


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

I valori inseriti verranno quindi inviati all'azione [/ajax-02]. Non saranno stati convalidati sul lato client. Pertanto, interverranno i validatori sul lato server. Questi erano già coinvolti in precedenza, ma su valori che erano già stati convalidati sul lato client e che erano quindi corretti. Ora non è più così.

Modifichiamo l'azione [/ajax-02] come segue:


@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 request?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        ...
    }
  • Riga 4: L'azione [/ajax-02] può ora essere richiamata tramite un POST Ajax o un POST standard. Dobbiamo essere in grado di distinguere tra questi due casi. Lo facciamo utilizzando le intestazioni HTTP inviate dal browser del client;

Quando osserviamo il traffico di rete in Chrome DevTools (Ctrl-Shift-I) con JavaScript abilitato, vediamo che il client invia le seguenti intestazioni durante la richiesta POST:

Come mostrato sopra:

  • è stata inviata un'intestazione [X-Requested-With] [1];
  • un parametro [X-Requested-With] è stato aggiunto ai valori inviati [2];

Questo non avviene nel caso di un POST standard. Abbiamo quindi due opzioni per recuperare le informazioni: recuperarle dalle intestazioni HTTP o dai valori inviati. La riga 4 dell'azione [/ajax-02] ha scelto la prima soluzione.

Continuiamo con il codice per questa azione:


@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 request?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        // tempo?
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
        // prepare the model for the next view
        Resultats résultats = new Resultats();
        modèle.addAttribute("resultats", résultats);
        // we set the locale
        setLocale(locale, modèle, résultats);
        // hour
        String heure = new SimpleDateFormat("hh:mm:ss").format(new Date());
        résultats.setHeurePost(heure);
        résultats.setHeureGet(heure);
        // valid request?
        if (!isAjax && result.hasErrors()) {
            return "vue-01";
        }
...
  • riga 2: il parametro [@Valid ActionModel01 form] attiva i validatori lato server;
  • righe 20–22: se la richiesta non è una richiesta Ajax e la convalida ha dato esito negativo, viene restituita la vista [vue-01.xml] con i messaggi di errore.

Ecco un esempio:

Continuiamo l'analisi dell'azione [/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 request?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
        // valid request?
        if (!isAjax && result.hasErrors()) {
            return "vue-01";
        }
        // we generate an error every other time
        int val = new Random().nextInt(2);
        if (val == 0) {
            // an error message is returned
            résultats.setErreur("erreur.aleatoire");
            if (isAjax) {
                return "vue-03";
            } else {
                résultats.setVue("vue-03");
                return "vue-01";
            }
        }
...
  • riga 14: viene generato un errore casuale;
  • riga 16: in caso di chiamata Ajax, la vista [vue-03.xml] viene restituita e inserita nell'area identificata da [resultats];
  • riga 18: in caso di chiamata non Ajax, la vista da visualizzare viene inserita nel modello [Resultats];
  • riga 19: la vista [vue-01.xml] viene nuovamente renderizzata;

La vista [vue-01.xml] viene modificata come segue:


<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" />
  • Riga 3: La vista [view-03.xml] verrà inserita sotto l'area [results];

Ecco un esempio:

Si noti che i tempi [1] e [2] sono ora identici.

Continuiamo il nostro studio dell'azione [/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 request?
        boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
...
        // retrieve posted values
        double a = formulaire.getA();
        double b = formulaire.getB();
        // we build the model
        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");
        }
        // the view is displayed
        if (isAjax) {
            return "vue-02";
        } else {
            résultats.setVue("vue-02");
            return "vue-01";
        }
}
  • righe 7–17: i risultati delle quattro operazioni aritmetiche vengono inseriti nel template;
  • righe 22-23: la vista [vue-01.xml] (riga 22) viene renderizzata inserendo la vista [vue-02.xml] (riga 22);

Questo inserimento viene effettuato come segue 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" />
  • Riga 2: La vista [vue-02.xml] verrà inserita nell'area [resultats];

Ecco un esempio del risultato:

 

7.2.9. Disattivazione di JavaScript con le impostazioni locali [fr-FR]

Con la lingua [fr-FR], si verifica il seguente problema:

I valori inseriti nel formato francese sono stati dichiarati non validi. Questo perché il server si aspetta numeri reali nel formato anglosassone. La soluzione è piuttosto complessa. Creeremo un filtro che:

  • intercetti la richiesta;
  • sostituirà le virgole nei valori inviati [a] e [b] con punti decimali;
  • quindi trasmetterà la nuova richiesta all'azione che deve elaborarla;

Per prima cosa, aggiungiamo un campo nascosto alla vista [vue-01.xml]:


<form ...>
...
</p>
    <!-- hidden fields -->
    <input type="hidden" id="culture" name="culture" th:value="${resultats.culture}"></input>
</form>
  • riga 5: la cultura [fr-FR] o [en-US] viene inserita nel campo dell'attributo [name=culture]. Poiché il tag [input] si trova nel modulo, il suo valore verrà inviato insieme ai valori di [a] e [b]. Avremo quindi una stringa inviata nel modulo:
culture=fr-FR&a=12,7&b=20,78

È importante comprendere questo punto.

Successivamente, includiamo un filtro nella configurazione dell'applicazione:

  

Il file [Config] viene modificato come segue:


@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
...
    @Bean
    public Filter cultureFilter() {
        return new CultureFilter();
    }
}
  • Riga 7: Il fatto che il bean [cultureFilter] restituisca un tipo [Filter] lo rende un filtro. Il bean stesso può avere qualsiasi nome;

Il passo successivo consiste nel creare il filtro vero e proprio:

  

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 {
        // next handler
        filterChain.doFilter(new CultureRequestWrapper(request), response);
    }
}
  • riga 12: estendiamo la classe [OncePerRequestFilter], che è una classe Spring, e ciò che dobbiamo fare è sovrascrivere il metodo [doFilterInternal] di questa classe;
  • riga 15: il metodo [doFilterInternal] riceve tre informazioni:
    • [HttpServletRequest request]: la richiesta da filtrare. Questa non può essere modificata,
    • [HttpServletResponse response]: la risposta da inviare al server. Il filtro può scegliere di generarla autonomamente,
    • [FilterChain filterChain]: la catena di filtri. Una volta che il metodo [doFilterInternal] ha terminato il suo lavoro, deve passare la richiesta al filtro successivo nella catena di filtri;
  • riga 18: creiamo una nuova richiesta a partire da quella ricevuta [new CultureRequestWrapper(request)] e la passiamo al filtro successivo. Poiché non possiamo modificare la richiesta iniziale [HttpServletRequest request], ne creiamo una nuova;

La classe [CultureRequestWrapper] è la seguente:

  

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) {
        // posted values a and 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;
        }
        // other cases
        return super.getParameterValues(name);
    }
 
}
  • riga 6: la classe [CultureRequestWrapper] estende la classe [HttpServletRequestWrapper] e sovrascriverà alcuni dei suoi metodi;
  • righe 8–10: il costruttore che riceve la richiesta da filtrare e la passa alla classe padre;
  • È importante comprendere che la richiesta filtrata finirà per diventare un parametro di input per una classe chiamata servlet. Con Spring MVC, questa servlet è di tipo [DispatcherServlet]. Questa classe dispone di vari metodi per recuperare i parametri della richiesta: [getParameter, getParameterMap, getParameterNames, getParameterValues, ...]. Il metodo utilizzato dal servlet deve essere ridefinito. Per farlo, bisognerebbe leggere il codice della classe [DispatcherServlet]. Io non l'ho fatto e ho ridefinito vari metodi. Alla fine, è stato il metodo [getParameterValues] a essere ridefinito;
  • riga 13: il metodo [getParameterValues] accetta come parametro il nome di uno dei parametri restituiti dal metodo [getParameterNames] e deve restituire un array dei suoi valori. Infatti, sappiamo che un parametro può comparire più volte in una richiesta;
  • riga 18: la virgola è sostituita da un punto decimale;

Ecco un esempio di esecuzione:

  • in [1], i valori [a,b] sono inseriti nel formato francese;
  • in [2], i risultati;
  • in [3], il server ha restituito una pagina con i numeri in formato anglosassone.

Questo problema può essere risolto con Thymeleaf come segue nella vista [vue-01.xml]


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

Ci sono diverse modifiche da apportare alle righe 3 e 6. Concentriamoci sulla riga 3:

  • avevamo scritto [th:field="*{a}"]. Il parametro [th:field] imposta gli attributi [id, name, value] del tag HTML [input] generato. In questo caso, vogliamo gestire noi stessi l'attributo [value]. Quindi impostiamo anche gli attributi [id, name];
  • l'attributo [th:value] valuta un'espressione utilizzando l'operatore ternario ?. Verifichiamo l'espressione [${resultats.culture}=='fr-FR' and ${actionModel01.b}!=null]. Se è vera, impostiamo l'attributo [value] sul valore di [actionModel01.a], dove il punto decimale è sostituito da una virgola. Se è falsa, impostiamo l'attributo [value] al valore di [actionModel01.a] senza modifiche;
  • Riga 6: Facciamo la stessa cosa per il campo [b];

Ecco un esempio di esecuzione:

  • In [1], i numeri [a,b] mantengono la notazione francese. Questo non è il caso in [2];

Questo nuovo problema viene gestito allo stesso modo del precedente. Modifichiamo la vista [vue-03.xml] come segue:


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

Ecco un esempio:

Ora disponiamo di un'applicazione in grado di gestire correttamente due impostazioni locali in un ambiente che può utilizzare o meno JavaScript. Per ottenere questo risultato, abbiamo dovuto aumentare in modo significativo la complessità del codice lato server. D'ora in poi, daremo sempre per scontato che JavaScript sia abilitato nel browser. Ciò consente di utilizzare funzionalità impossibili in modalità solo server.

Esaminiamo il link [Calcola] nella pagina principale [vue-01.xml]:

Il codice per il link [Calcola] nella vista [vue-01.xml] è il seguente:


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

La funzione JavaScript [postForm] è definita nel file [local1.js] come segue:


// global data
var loading;
var formulaire;
var résultats;
var a, b;
 
function postForm() {
    // valid form?
    if (!formulaire.validate().form()) {
        // invalid form - terminated
        return;
    }
    // we manage two locales [fr_FR, en_US]
    // the real [a,b] must be posted in Anglo-Saxon format in all cases
    // they will be filtered by [CultureFilter]
 
    // make a manual Ajax call
    $.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);
        }
    })
}
  • righe 2–5: Ricordiamo che questi elementi sono stati inizializzati dalla funzione [$(document).ready];
  • righe 9-12: Eseguiamo i validatori JavaScript del modulo. Se uno qualsiasi dei valori non è valido, l'espressione [form.validate().form()] restituisce false. In questo caso, il [submit] del modulo viene annullato;
  • righe 18-38: effettuiamo una chiamata Ajax manuale;
  • riga 19: l'URL di destinazione della chiamata Ajax;
  • righe 20–22: un array di intestazioni HTTP da aggiungere a quelle incluse di default nella richiesta HTTP. Qui, aggiungiamo l'intestazione HTTP che indicherà al server che stiamo effettuando una chiamata Ajax;
  • riga 23: il metodo HTTP utilizzato;
  • riga 24: i dati inviati. [formulaire.serialize] crea la stringa da inviare [culture=fr-FR&a=12,7&b=20,89] dal modulo con ID [formulaire]. Qui incontriamo il problema discusso in precedenza: i valori [a,b] devono essere inviati nel formato anglosassone. Sappiamo che questo problema è stato ora risolto con la creazione del filtro [cultureFilter];
  • riga 25: il tipo di dati di ritorno previsto. Sappiamo che il server restituirà un flusso HTML;
  • riga 26: il metodo da eseguire all'avvio della richiesta. Qui specifichiamo che deve essere visualizzato il componente con id [loading]. Si tratta dell'immagine animata di caricamento;
  • riga 29: il metodo da eseguire se la richiesta Ajax ha esito positivo. Il parametro [data] è la risposta completa dal server. Sappiamo che si tratta di un flusso HTML;
  • riga 30: aggiorniamo il componente con l'ID [results] con l'HTML del parametro [data].
  • riga 33: nascondiamo l'indicatore di caricamento;
  • riga 35: funzione eseguita quando viene ricevuta la risposta del server, indipendentemente dal fatto che si tratti di un esito positivo o di un errore;
  • Righe 35–37: se si verifica un errore (il server ha restituito una risposta HTTP con un codice di stato che indica un errore lato server), la risposta HTML del server viene visualizzata nell'area [results];

Ecco un esempio di esecuzione:

7.3. Aggiornamento di una pagina HTML con un feed JSON

Nell'esempio precedente, il server web ha risposto alla richiesta HTTP Ajax con un flusso HTML. Questo flusso conteneva dati accompagnati da formattazione HTML. Riprenderemo l'esempio precedente, questa volta utilizzando risposte JSON (JavaScript Object Notation) che contengono solo i dati. Il vantaggio è che vengono trasmessi meno byte. Si presume che JavaScript sia abilitato nel browser.

7.3.1. L'azione [/ajax-04]

L'azione [/ajax-04] è identica all'azione [/ajax-01], tranne per il fatto che visualizza la vista [vue-04.xml] invece della vista [vue-01.xml]:


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

7.3.2. La vista [view-04.xml]

 

La vista [view-04.xml] utilizza il corpo della vista [view-01.xml] con le seguenti differenze:


<!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>
            <!-- hidden fields -->
            <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>
  • riga 5: il JavaScript della vista si trova ora nel file [local4.js];
  • riga 16: il tag [form] non ha più i parametri [data-ajax-attr] della libreria [Unobtrusive Ajax]. Non li useremo qui. Il tag [form] non ha più nemmeno gli attributi [method] e [action], che specificano come e dove inviare i valori inseriti nel modulo. Questo perché il modulo verrà inviato da una funzione JavaScript (riga 20);
  • righe 26–57: l'area con l'ID [resultats], che prima era vuota, ora contiene il codice HTML per visualizzare i risultati;
  • righe 26–34: l'intestazione dei risultati in cui viene visualizzato il tempo di calcolo;
  • righe 35–52: i risultati delle quattro operazioni aritmetiche;
  • righe 53–57: eventuali messaggi di errore inviati dal server;

Il codice JavaScript eseguito al caricamento della vista [vue-04.xm] si trova nel file [local4.js]. È il seguente:


// global data
    var loading;
    var formulaire;
    var résultats;
    var titre;
    var labelHeureCalcul;
    var heureCalcul;
    var aplusb;
    var amoinsb;
    var afoisb;
    var adivb;
    var msgErreur;
 
// document loading
$(document).ready(function() {
    // retrieve the references of the page's various components
    loading = $("#loading");
    formulaire = $("#formulaire");
    résultats = $('#résultats');
    titre=$("#titre");
    labelHeureCalcul=$("#labelHeureCalcul");
    heureCalcul=$("#heureCalcul");
    aplusb=$("#aplusb");
    amoinsb=$("#amoinsb");
    afoisb=$("#afoisb");
    adivb=$("#adivb");
    msgErreur=$("#msgErreur");
    // we hide certain elements
    résultats.hide();
    erreur.hide();
    loading.hide();
});
  • righe 17–27: recupera i riferimenti jQuery per tutti gli elementi della pagina;
  • riga 29: l'area dei risultati viene nascosta;
  • riga 30: così come l'area degli errori;
  • riga 31: così come l'immagine di caricamento animata;
  • righe 2–12: i riferimenti recuperati vengono resi globali in modo che altre funzioni possano accedervi;

7.3.3. La funzione jS [postForm]

Il link [Calculate] è il seguente:


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

La funzione JavaScript [postForm] è definita nel file [local.js] come segue:


function postForm() {
    // valid form?
    if (!formulaire.validate().form()) {
        // invalid form - terminated
        return;
    }
    // make a manual Ajax call
    $.ajax({
        url : '/ajax-05',
        headers : {
            'Accept' : 'application/json'
        },
        type : 'POST',
        data : formulaire.serialize(),
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}
 
// before the Ajax call
function onBegin() {
...
}
 
// on receipt of the server response
// in case of success
function onSuccess(data) {
...
}
 
// on receipt of the server response
// in case of failure
function onError(jqXHR) {
...
}
 
// after [onSuccess, onError]
function onComplete() {
...
}
  • righe 3–6: prima di inviare i valori inseriti, li convalidiamo. Se non sono corretti, non inviamo il modulo;
  • riga 9: i valori inseriti vengono inviati all'azione [/ajax-05], che spiegheremo più dettagliatamente in seguito;
  • righe 10–12: un'intestazione HTTP per comunicare al server che ci aspettiamo una risposta in formato JSON;
  • Riga 13: I valori inseriti verranno inviati;
  • riga 14: serializzazione dei valori inseriti in una stringa pronta per essere inviata [a=1,6&b=2,4&culture=fr-FR];
  • riga 15: il tipo di risposta inviata dal server. Sarà JSON;
  • riga 16: la funzione da eseguire prima del POST;
  • riga 17: la funzione da eseguire alla ricezione della risposta del server se questa ha esito positivo. Il "successo" di una richiesta HTTP è determinato dallo stato della risposta HTTP del server. Una risposta [HTTP/1.1 200 OK] è una risposta di successo. Una risposta [HTTP/1.1 500 Internal Server Error] è una risposta di errore. Ciò a cui si fa riferimento come stato di una risposta HTTP è il codice [200] o [500]. Alcuni di questi codici sono associati al "successo", mentre altri sono associati al "fallimento";
  • riga 18: la funzione da eseguire alla ricezione della risposta del server quando lo stato HTTP di tale risposta è uno stato di errore;
  • riga 18: la funzione da eseguire per ultima, dopo le precedenti funzioni [onSuccess, onError];

La funzione [onBegin] è la seguente:


// before the Ajax call
function onBegin() {
    console.log("onBegin");
    // we show the moving image
    loading.show();
    // hide certain elements of the view
    entete.hide();
    résultats.hide();
    erreur.hide();
}

Prima di esplorare le altre funzioni JavaScript della chiamata Ajax, dobbiamo conoscere la risposta inviata dall'azione [/ajax-05].

7.3.4. L'azione [/ajax-05]

L'azione [/ajax-05] è la seguente:


    @RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
    @ResponseBody()
    // processes the POST of view [view-04]
    public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale,    HttpServletRequest request, HttpSession session) throws InterruptedException {
        if(result.hasErrors()){
            // abnormal case - nothing returned
            return null;
        }
        ...
}
  • riga 2: l'attributo [ResponseBody] indica che l'azione [/ajax-05] stessa restituisce la risposta al client. Poiché nelle dipendenze del progetto è inclusa una libreria JSON, Spring Boot configura automaticamente questo tipo di azione per restituire JSON. Pertanto, la stringa JSON di tipo [JsonResults] (riga 4) verrà inviata al client;
  • Riga 2: I valori inviati [a, b, culture] saranno incapsulati in un tipo [ActionModel01], che convalideremo con [@Valid ActionModel01]. Si tratta solo di una formalità. Abbiamo ipotizzato che JavaScript sia abilitato sul browser del client, quindi quando arrivano, i valori inviati sono già stati verificati sul lato client. Tuttavia, possiamo prevedere il caso di una richiesta POST non autorizzata che non utilizza il nostro client JavaScript. In questo caso, la convalida potrebbe fallire;
  • righe 5–7: in caso di errore, restituiamo un oggetto JSON vuoto;

Continuiamo l'esame dell'azione [/ajax-05]:


    @RequestMapping(value = "/ajax-05", method = RequestMethod.POST)
    @ResponseBody()
    // processes the POST of view [view-04]
    public JsonResults ajax05(@Valid ActionModel01 formulaire, BindingResult result, Locale locale,
            HttpServletRequest request, HttpSession session) throws InterruptedException {
...
        // spring application context
        WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
        // tempo?
        Integer tempo = (Integer) session.getAttribute("tempo");
        if (tempo != null && tempo > 0) {
            Thread.sleep(tempo);
        }
    ...
        // we return the result
        return résultats;
}
  • Riga 8: Recuperiamo il contesto [ctx] dall'applicazione Spring. Ne abbiamo bisogno per recuperare i messaggi dai file [messages.properties] in base a una chiave di messaggio e a un'impostazione locale. Questo viene fatto utilizzando la seguente sintassi:

ctx.getMessage(clé_message, tableau_de_paramètres, locale)
    • [chiave_messaggio]: la chiave del messaggio che si sta cercando;
    • [locale]: la lingua utilizzata. Pertanto, se questa lingua è [en_US], verrà utilizzato il file [messages_en.properties];
    • [parameter_array]: il messaggio recuperato può essere parametrizzato come in [key=message {0} {1}]. Questo messaggio contiene due parametri [{0} {1}]. È necessario fornire un array di due valori come secondo parametro di [ctx.getMessage];
  • righe 10-13: se si verifica un timeout nella sessione, il thread corrente viene messo in pausa per tutta la durata del timeout;

L'azione [/ajax-05] prosegue come segue:


        // on prépare le modèle de la prochaine vue
        JsonResults résultats = new JsonResults();
        ...
}
  • riga 2: creazione del modello di stringa JSON inviato al client;

Il modello [JsonResults] è il seguente:

 

package istia.st.springmvc.models;
 
public class JsonResults {
 
    // data
    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 and setters
...
 
}
  • righe 6–13: ogni campo nella classe [JsonResult] corrisponde a un campo con lo stesso [id] nella vista [vue-04.xml]:

L'azione [/ajax-05] prosegue come segue:


        // on prépare le modèle de la prochaine vue
        JsonResults résultats = new JsonResults();
        // entête
        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()));
        // on génère une erreur une fois sur deux
        int val = new Random().nextInt(2);
        if (val == 0) {
            // on renvoie un message d'erreur
            résultats.setMsgErreur(ctx.getMessage("resultats.erreur",
                    new Object[] { ctx.getMessage("erreur.aleatoire", null, locale) }, locale));
            return résultats;
}
  • riga 2: crea il modello di stringa JSON inviato al client;
  • righe 4–6: crea i messaggi per l'intestazione dei risultati;
  • righe 8–14: in media, viene generato un messaggio di errore ogni due tentativi. In questo caso, il processo si interrompe e la stringa JSON viene restituita al client (riga 13);
  • riga 11: ecco un esempio di messaggio parametrizzato:

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

L'azione [/ajax-05] prosegue come segue:


        // on récupère les valeurs postées
        double a = formulaire.getA();
        double b = formulaire.getB();
        // on construit le modèle
        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");
        }
        // on rend le résultat
return résultats;
  • righe 2-3: si recuperano i valori di [a] e [b];
  • righe 5-12: si costruiscono i quattro risultati;
  • riga 14: la stringa JSON [JsonResults] viene inviata al client;

Vediamo come funziona con [Advanced Rest Client]:

  • in [1-2], inviamo una richiesta POST all'azione [/ajax-05];
  • in [3], inviamo valori errati;
  • in [4], il server ha restituito una risposta vuota;
  • In [1], inviamo valori corretti;
  • in [2], l'oggetto JSON restituito dal server, con un messaggio di errore qui;
  • In [1], inviamo valori corretti;
  • in [2], l'oggetto JSON restituito dal server, che mostra i quattro risultati;
  • in [1], inviamo valori corretti;
  • in [2], abbiamo generato un'eccezione lato server. Notiamo che il server invia comunque un oggetto JSON. In questo messaggio, vediamo che lo stato HTTP della risposta è [500], a indicare che si è verificato un errore lato server;

7.3.5. La funzione jS [postForm] - 2

Ora che conosciamo l'oggetto JSON restituito dal server, possiamo utilizzarlo in JavaScript. Il metodo [onSuccess], che viene eseguito quando il server invia una risposta con stato HTTP [200], è il seguente:


// on receipt of the server response
// in case of success
function onSuccess(data) {
    console.log("onSuccess");
    // fill in the results area
    titre.text(data.titre);
    labelHeureCalcul.text(data.labelHeureCalcul);
    heureCalcul.text(data.heureCalcul);
    entete.show();
    // error-free results
    if (!data.msgErreur) {
        aplusb.text(data.aplusb);
        amoinsb.text(data.amoinsb);
        afoisb.text(data.afoisb);
        adivb.text(data.adivb);
        résultats.show();
        return;
    }
    // results with error
    msgErreur.text(data.msgErreur);
    erreur.show();
}
  • riga 3: il parametro [data] è l'oggetto JSON restituito dal server:
 

Il metodo [onError] eseguito quando lo stato della risposta HTTP è [500] è il seguente:


// on receipt of the server response
// in case of failure
function onError(jqXHR) {
    console.log("onError");
    // system error
    msgErreur.text(jqXHR.responseText);
    erreur.show();
}
  • Riga 3: L'oggetto jQuery [jqXHR] ha le seguenti proprietà:
    • responseText: il testo della risposta del server,
    • status: il codice di errore restituito dal server,
    • statusText: il testo associato a questo codice di errore;
  • riga 6: l'oggetto [jqXHR.responseText] è il seguente oggetto JSON:
 

7.3.6. Test

Diamo un'occhiata ad alcuni screenshot dell'applicazione web in funzione:

 
 
 

7.4. Applicazione web a pagina singola

7.4.1. Introduzione

La tecnologia Ajax consente di creare applicazioni a pagina singola:

  • la prima pagina viene caricata tramite una richiesta standard del browser;
  • le pagine successive vengono caricate tramite chiamate Ajax. Di conseguenza, il browser non cambia mai l'URL e non carica mai una nuova pagina. Questo tipo di applicazione è denominata Applicazione a Pagina Singola (SPA).

Ecco un esempio di base di tale applicazione. La nuova applicazione avrà due viste:

  • in [1], l'azione [/ajax-06] fa apparire la prima pagina, la pagina 1;
  • in [2], un link ci permette di passare alla pagina 2 tramite una chiamata Ajax;
  • in [3], l'URL non è cambiato. La pagina visualizzata è la pagina 2;
  • in [4], un link ci permette di tornare alla pagina 1 tramite una chiamata Ajax;
  • in [5], l'URL non è cambiato. La pagina visualizzata è la pagina 1.

7.4.2. L'azione [/ajax-06]

Il codice per l'azione [/ajax-06] è il seguente:


    @RequestMapping(value = "/ajax-06", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String ajax06() {
        return "vue-06";
}
  • Righe 1–4: L'azione [/ajax-06] si limita a visualizzare la vista [vue-06.xml];

7.4.3. La vista [vue-06.xml]

La vista [vue-06.xml] è la seguente:


<!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>
  • riga 8: la vista utilizza uno script [local6.js];
  • riga 12: la vista [vue-07.xml] è inclusa nell'area ID [content] della vista [vue-06.xml];

7.4.4. La vista [vue-07.xml]

La vista [vue-07.xml] è la seguente:


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

7.4.5. La funzione jS [gotoPage]

Il link [Pagina 2] nella vista [vue-07.xml] utilizza la funzione jS [gotoPage] definita nel seguente file [local6.js]:


// global data
var content;
 
function gotoPage(num) {
    // make a manual Ajax call
    $.ajax({
        url : '/ajax-07',
        type : 'POST',
        data : 'num=' + num,
        dataType : 'html',
        beforeSend : function() {
        },
        success : function(data) {
            content.html(data)
        },
        complete : function() {
        },
        error : function(jqXHR) {
            // system error
            content.html(jqXHR.responseText);
        }
    })
}
 
// document loading
$(document).ready(function() {
    // retrieve the references of the page's various components
    content = $("#content");
});
  • riga 28: quando la pagina viene caricata, memorizziamo l'elemento con l'ID [content] e lo rendiamo una variabile globale (riga 2);
  • riga 4: la funzione [gotoPage] riceve come parametro il numero della pagina (1 o 2) da visualizzare nella vista corrente;
  • riga 7: l'URL di destinazione per la richiesta POST;
  • riga 8: l'URL della riga 7 viene richiesto tramite un POST;
  • riga 9: la stringa inviata. Viene inviato un parametro denominato [num]. Il suo valore è il numero di pagina (riga 4) da visualizzare nella vista corrente;
  • riga 10: il server restituirà l'HTML, in particolare l'HTML della pagina da visualizzare;
  • righe 13–15: in caso di esito positivo (stato HTTP 200), l'HTML inviato dal server viene inserito nell'elemento con id [content];
  • righe 18-20: se la richiesta fallisce (stato HTTP 500), l'HTML inviato dal server viene inserito nel campo con id [content];

7.4.6. L'azione [/ajax-07]

Il codice per l'azione [/ajax-07] è il seguente:


@RequestMapping(value = "/ajax-07", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String ajax07(int num) {
        // num : page number
        switch (num) {
        case 1:
            return "vue-07";
        case 2:
            return "vue-08";
        default:
            return "vue-07";
        }
    }
  • riga 2: recuperiamo il parametro inviato denominato [num]. Si noti che il parametro alla riga 2 deve avere lo stesso nome del parametro inviato, in questo caso [num]. [num] è il numero della pagina o della vista;
  • righe 5-6: se [num==1], restituiamo la vista [vue-07.xml];
  • righe 7-8: se [num==2], restituiamo la vista [vue-08.xml];
  • righe 9-10: in tutti gli altri casi (il che è normalmente impossibile), viene restituita la vista [vue-07.xml];

7.4.7. La vista [view-08.xml]

La vista [view-08.xml] costituisce la pagina 2 dell'applicazione:


<!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. Incorporare più flussi HTML in una risposta JSON

7.5.1. Introduzione

Si consideri la seguente applicazione:

La pagina [1] presenta quattro aree:

  • [Zona 1] e [Zona 3] sono zone che appaiono o scompaiono quando si clicca sul pulsante [Aggiorna]. Contiamo il numero di volte in cui ciascuna di queste due zone appare [2]. La zona [Zona 1] utilizza il francese, mentre la zona [Zona 3] utilizza l'inglese;
  • [Zona 2] è sempre presente;
  • la sezione [Voci] è sempre visibile;

Il link [Invia] visualizza la pagina successiva [3]:

  • il link [Torna alla pagina 1] riporta la pagina 1 allo stato precedente [4];

L'applicazione è un'applicazione a pagina singola. La prima pagina viene richiesta al server dal browser. Le pagine successive vengono recuperate dal server tramite chiamate Ajax.

7.5.2. L'azione [/ajax-09]

  

L'azione [/ajax-09] è la seguente:


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

Visualizza semplicemente la vista [vue-09.xml].

7.5.3. Viste XML

  

La vista [vue-09.xml] è la pagina master dell'applicazione:


<!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>
  • riga 9: il file JS utilizzato nell'applicazione;
  • riga 15: il contenuto della pagina master;
  • riga 16: un'immagine di caricamento animata:
  • riga 17: area per visualizzare eventuali errori;

La vista [vue-09-page1.xml] è la pagina 1 dell'applicazione:


<!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>
  • righe 6–9: l'area [Zone 1]. Il suo contenuto è inserito nel componente [id="zone1-content"];
  • righe 11-14: l'area [Zone 2], che non cambia;
  • righe 16-19: l'area [Zone 3]. Il suo contenuto è inserito nel componente [id="zone3-content"];
  • riga 22: la funzione JS che invia il modulo;
  • riga 25: inclusione dell'area di input;

Si noti che la pagina 1 non ha un tag [form]. Tutto sarà gestito in JavaScript.

La vista [vue-09-saisies.xml] è la seguente:


<!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>
  • righe 5-8: inserisci una stringa;
  • righe 13-16: inserisci un numero intero;
  • riga 14: la funzione JS che invia i valori inseriti;

Si noti ancora una volta che il campo di immissione non ha un tag [form].

In totale, la pagina 1 presenta due caratteristiche:

  • [Aggiorna]: che aggiorna le zone 1 e 3. Questa azione è gestita dal server, che restituisce in modo casuale:
    • il campo 1 con il suo contatore di accessi e nulla per il campo 3,
    • la zona 3 con il suo contatore di accessi e nulla per la zona 1,
    • entrambe le zone con i rispettivi contatori di accesso;
  • [Invia]: che visualizza la pagina 2 con i valori inseriti o un messaggio di errore se i dati inseriti non sono validi;

Ci concentreremo innanzitutto sul pulsante [Aggiorna].

7.5.4. Il codice JS per il pulsante [Aggiorna]

  

Il codice nel file [local9.js] è il seguente:


// global variables
var content;
var loading;
var erreur;
 
// document loading
$(document).ready(function() {
    // retrieve the references of the page's various components
    loading = $("#loading");
    loading.hide();
    erreur = $("#erreur");
    erreur.hide();
    content = $("#content");
});
  • righe 9-13: quando viene caricata la pagina master, vengono memorizzati i riferimenti ai tre componenti identificati da [loading, error, content];
  • righe 2-4: i riferimenti a questi tre componenti vengono memorizzati in variabili globali. Rimangono costanti perché le tre aree in questione sono sempre presenti sulla pagina visualizzata, indipendentemente dal momento. Poiché rimangono costanti, possono essere calcolati in [$(document).ready] e condivisi con le altre funzioni nel file JS;

La funzione [postForm] gestisce il clic sul pulsante [Refresh]:


function postForm() {
    console.log("postForm");
    // make a manual Ajax call
    $.ajax({
        url : '/ajax-10',
        headers : {
            'Accept' : 'application/json'
        },
        type : 'POST',
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}
  • righe 4–15: la chiamata Ajax al server;
  • riga 5: l'azione [ajax-10] gestirà il POST;
  • righe 6-8: la risposta sarà in formato JSON. Il client JS indica che accetta documenti JSON;
  • riga 9: l'azione [ajax-10] viene chiamata con un'operazione POST;
  • riga 10: riceveremo JSON;
  • riga 11: la funzione eseguita prima della chiamata Ajax;
  • riga 12: la funzione eseguita alla ricezione della risposta del server, in caso di esito positivo [200 OK];
  • riga 13: la funzione eseguita alla ricezione della risposta del server, in caso di errore [500 Internal Server Error, ...];
  • riga 14: la funzione eseguita dopo aver ricevuto la risposta;

La funzione [onBegin] è la seguente:


// before the Ajax call
function onBegin() {
    console.log("onBegin");
    // waiting image
    loading.show();
}

Visualizza semplicemente l'immagine animata di caricamento mentre si attende la risposta del server.

7.5.5. L'azione [/ajax-10]

  

L'azione [/ajax-10] è la seguente:


// the session
    @Autowired
    private SessionModel1 session;
    // the Thymeleaf / Spring engine
    @Autowired
    private SpringTemplateEngine engine;
 
    @RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
    ...
    }
  • Riga 3: La sessione viene iniettata. Ha il seguente tipo [SessionModel1]:
  

package istia.st.springmvc.models;
 
import java.io.Serializable;
 
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
 
@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionModel1 implements Serializable {
 
    private static final long serialVersionUID = 1L;
    // two meters
    private int cpt1 = 0;
    private int cpt3 = 0;
    // the three zones
    private String zone1 = "xx";
    private String zone3 = "zz";
    private String saisies;
    private boolean zone1Active = true;
    private boolean zone3Active = true;
 
    // getters and setters
    ...
}

La sessione [SessionModel1] memorizza quanto segue:

  • riga 15: il numero di volte [cpt1] in cui viene visualizzata l'area [Zone 1];
  • riga 16: il numero di volte [cpt3] in cui viene visualizzata l'area [Zone 3];
  • righe 18–20: i flussi HTML per le zone [Zone 1], [Zone 3] e [Inputs]. Ciò è necessario nella sequenza [Page 1] --> [Page 2] --> [Page 1]. Quando si passa da [Page 2] a [Page 1], [Page 1] e le sue tre zone devono essere ripristinate;
  • righe 21-22: due valori booleani che indicano se le zone [Zone 1] e [Zone 3] sono visualizzate (visibili);

L'altro elemento inserito nell'[AjaxController] è il seguente:


    // the Thymeleaf / Spring engine
    @Autowired
private SpringTemplateEngine engine;

Il bean [SpringTemplateEngine] è definito nel file di configurazione [Config]:

  

È definito come segue:


    @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;
}
  • righe 2–10: conosciamo bene il bean [SpringResourceTemplateResolver], che ci permette di definire alcune caratteristiche delle viste;
  • righe 13–17: il bean [SpringTemplateEngine] ci permette di definire il “motore” della vista, la classe responsabile della generazione delle risposte [Thymeleaf] ai client. [Thymeleaf] ha un “motore” predefinito e un altro quando viene utilizzato in un ambiente [Spring]. È quest’ultimo che stiamo utilizzando qui;

La firma dell'azione [/ajax-10] è la seguente:


@RequestMapping(value = "/ajax-10", method = RequestMethod.POST)
    @ResponseBody()
    public JsonResult10 ajax10(HttpServletRequest request, HttpServletResponse response) {
    ...
}
  • Riga 1: l'azione [/ajax-10] accetta solo una richiesta POST;
  • riga 2: l'azione [/ajax-10] restituisce la risposta al client stesso. Questa verrà automaticamente convertita in JSON;
  • riga 3: la risposta è di tipo [JsonResult10] come segue:
  

package istia.st.springmvc.models;
 
public class JsonResult10 {
 
    // data
    private String content;
    private String zone1;
    private String zone3;
    private String erreur;
    private String saisies;
    private boolean zone1Active;
    private boolean zone3Active;
 
    public JsonResult10() {
    }
 
    // getters and setters
...
}
  • riga 6: il contenuto HTML dell'area identificata da [content];
  • riga 7: il contenuto HTML dell'area [Zone 1];
  • riga 8: il contenuto HTML dell'area [Zone 3];
  • riga 9: il contenuto HTML dell'area [Errore];
  • riga 10: il contenuto HTML dell'area [Inputs];
  • riga 11: un valore booleano che indica se l'area [Zone 1] deve essere visualizzata;
  • riga 12: un valore booleano che indica se l'area [Zone 3] deve essere visualizzata;

Il codice per l'azione [/ajax-10] è il seguente:


@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());
        // answer
        JsonResult10 result = new JsonResult10();
        // session
        session.setZone1(null);
        session.setZone3(null);
        session.setZone1Active(false);
        session.setZone3Active(false);
        // randomize an answer
        int cas = new Random().nextInt(3);
        switch (cas) {
        case 0:
            // zone 1 active
            setZone1(thymeleafContext, result);
            return result;
        case 1:
            // zone 3 active
            setZone3(thymeleafContext, result);
            return result;
        case 2:
            // active zones 1 and 3
            setZone1(thymeleafContext, result);
            setZone3(thymeleafContext, result);
            return result;
        }
        return null;
    }
  • Riga 5: recuperiamo il contesto [Thymeleaf]. Vedremo più avanti a cosa serve;
  • riga 7: creiamo una risposta vuota per ora;
  • righe 9–12: impostiamo i due campi nella sessione su [null] e specifichiamo che non devono essere visualizzati. Questi due campi verranno generati a breve, ma è possibile che ne venga generato solo uno;
  • righe 14–29: entrambi i campi vengono generati;
  • righe 17–19: viene generata solo la zona [Zone 1];
  • righe 21–23: viene generata solo la zona [Zone 3];
  • righe 25–28: vengono generate sia [Zone 1] che [Zone 3];

Il flusso HTML per la zona [Zone 1] viene generato con il seguente metodo:


    private void setZone1(WebContext thymeleafContext, JsonResult10 result) {
        // zone 1 active
        // flow 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);
        // session
        session.setCpt1(cpt1);
        session.setZone1(zone1);
        session.setZone1Active(true);
}
  • riga 1: i parametri sono:
    • il contesto [Thymeleaf] di tipo [WebContext],
    • la risposta al client attualmente in fase di costruzione di tipo [JsonResult10];
  • riga 3: incrementiamo il contatore di sessione [cpt1], che conta il numero di volte in cui viene visualizzata la zona [Zone 1];
  • riga 4: il contesto [Thymeleaf] di tipo [WebContext] si comporta in modo simile al [Model] in Spring MVC. Per aggiungere un elemento al modello, utilizziamo [WebContext.setVariable]. Qui, inseriamo il contatore [cpt1] nel modello [Thymeleaf]. Ciò consentirà di valutare l'espressione Thymeleaf [${cpt1}]
  • riga 5: il contesto [Thymeleaf] ha una impostazione locale. Ciò gli consente di valutare espressioni del tipo [#{key_msg}]. Qui, associamo il contesto Thymeleaf a un'impostazione locale francese;
  • riga 6: questa è l'istruzione più interessante. Il motore Thymeleaf elaborerà la vista [vue-09-zone1.xml] utilizzando il modello e le impostazioni locali appena calcolati e, invece di inviare l'output HTML risultante al client, lo restituirà come stringa;
  • righe 7–9: l'output HTML per l'area [Zone 1] appena calcolata viene memorizzato nella sessione e nel risultato da inviare al client. Inoltre, specifichiamo che l'area [Zone 1] deve essere visualizzata;
  • righe 11–13: le informazioni relative all'area [Zone 1] vengono memorizzate nella sessione in modo che possano essere rigenerate;

La riga 7 elabora la seguente vista [vue-09-zone1.xml]:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <span th:text="#{message.zone}"></span>
    <span th:text="${cpt1}"></span>
</html>
  • riga 3: l'espressione [#{message.zone}] verrà valutata utilizzando le impostazioni locali;
  • riga 4: l'espressione [${cpt1}] verrà valutata utilizzando il template Thymeleaf;

Il messaggio chiave [message.zone] è definito nei file dei messaggi [messages_fr.properties] e [messages_en.properties]:

  

[messages_fr.properties]


message.zone=Nombre d'accès : 

[messages_en.properties]


message.zone=Number of hits: 

Il flusso HTML per l'area [Zona 3] viene generato con un metodo simile:


    private void setZone3(WebContext thymeleafContext, JsonResult10 result) {
        // zone 3 active
        // flow 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);
        // session
        session.setCpt3(cpt3);
        session.setZone3(zone3);
        session.setZone3Active(true);
}
  • riga 6: la lingua della zona [Zone 3] è l'inglese;

7.5.6. Elaborazione della risposta dall'azione [/ajax-10]

Torniamo al codice JS in [local9.js] che elaborerà la risposta del server:


// on receipt of the server response
// in case of success
function onSuccess(data) {
    console.log("onSuccess");
    // content
    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 active?
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // seized?
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // mistake?
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

Esaminiamo la struttura Java della risposta ricevuta alla riga 3 nella variabile [data]:


public class JsonResult10 {
 
    // data
    private String content;
    private String zone1;
    private String zone3;
    private String erreur;
    private String saisies;
    private boolean zone1Active;
    private boolean zone3Active;
 
}
  • Righe 6–8: Se [data.content] non è nullo, il campo [id=content] viene inizializzato con esso. Questo campo rappresenta [Pagina 1] o [Pagina 2] nella sua interezza. In questo esempio, [data.content == null], quindi la zona [id=content] non verrà modificata e continuerà a visualizzare [Pagina 1];
  • righe 10-17: visualizza [Zone 1] se [data.zone1Active==true]. Se, inoltre, [data.zone1!=null], allora il contenuto di [Zone 1] viene modificato; altrimenti, rimane com'era;
  • righe 19–26: lo stesso vale per [Zone 3];
  • righe 28–30: se [data.saisies!=null], allora la zona [Saisies] viene aggiornata. In questa dimostrazione, [data.saisies==null], quindi la zona [Saisies] rimane invariata;
  • righe 32–37: lo stesso ragionamento si applica al campo [Error], con le seguenti sfumature:
    • riga 33: [data.error] sarà un messaggio di errore in formato testo;
    • riga 36: se [data.error] è nullo, il campo [Error] viene nascosto. Questo perché potrebbe essere stato visualizzato durante la richiesta precedente;

In caso di errore lato server (stato HTTP come 500 Internal Server Error), viene eseguita la seguente funzione:


// on receipt of the server response
// in case of failure
function onError(jqXHR) {
    console.log("onError");
    // system error
    erreur.text(jqXHR.responseText);
    erreur.show();
}

Per visualizzare un errore di questo tipo, modifichiamo la funzione [postForm] come segue:


function postForm() {
    console.log("postForm");
    // retrieve references to the current page
    ...
    // make a manual Ajax call
    $.ajax({
        url : '/ajax-10x',
        ...
    })
}
  • riga 7: inseriamo un URL che non esiste;

Ecco i risultati quando si fa clic sul pulsante [Aggiorna]:

È interessante notare che l'errore è stato inviato anche sotto forma di stringa JSON.

Il metodo eseguito dopo aver ricevuto la risposta del server è il seguente:


// after [onSuccess, onError]
function onComplete() {
    console.log("onComplete");
    // waiting image
    loading.hide();
}

Nascondiamo semplicemente l'immagine animata di caricamento.

7.5.7. Visualizzazione della pagina [Pagina 2]

Il codice HTML per il link [Invia] è il seguente:


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

La funzione JS [validate] è la seguente:


// validation of entered values
function valider() {
    // posted value
    var post = JSON3.stringify({
        "value1" : $("#text1").val().trim(),
        "value2" : $("#text2").val().trim()
    });
    // make a manual Ajax call
    $.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
    })
}
  • righe 4-7: abbiamo due valori, v1 e v2, da inviare: quelli provenienti dai componenti di input identificati da [#text1] e [#text2]. Faremo qualcosa di nuovo. Invieremo questi due valori come stringa JSON {"value1":v1,"value2":v2};
  • riga 10: i valori inviati saranno inviati all'azione [ajax-11A];
  • riga 12: poiché sappiamo che riceveremo una risposta JSON, specifichiamo che possiamo ricevere JSON;
  • riga 13: comunichiamo al server che gli invieremo il valore inviato come stringa JSON;
  • righe 15-16: inviamo il valore da inviare tramite POST;
  • riga 17: riceveremo JSON;

7.5.8. L'azione [ajax-11A]

L'azione [ajax-11A] che elabora la stringa JSON inviata è la seguente:


@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) {
        ...
    }
  • Riga 1: specifichiamo con ["application/json"] che l'azione si aspetta un documento in formato JSON. Questo documento è il valore inviato dal client;
  • riga 3: il valore inviato verrà recuperato nel seguente oggetto [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 {
 
    // data
    @Size(min = 4, max = 6)
    @NotNull
    private String value1;
    @Range(min = 10, max = 14)
    @NotNull
    private Integer value2;
 
    // getters and setters
    ...
}
  • La struttura dell'oggetto [PostAjax11A] deve corrispondere alla struttura dell'oggetto inviato {"value1":v1,"value2":v2}. Pertanto, i campi [value1] (riga 13) e [value2] (riga 16) sono obbligatori;
  • Abbiamo applicato vincoli di integrità su entrambi i campi;

Torniamo al codice dell'azione [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());
        // answer
        JsonResult10 result = new JsonResult10();
        // valid post?
        if (bindingResult.hasErrors()) {
            // page 1 is returned with an error
            result.setZone1Active(session.isZone1Active());
            result.setZone3Active(session.isZone3Active());
            result.setErreur(getErreursForModel(bindingResult));
            return result;
        }
        ...
}
  • riga 3: l'annotazione [@RequestBody] si riferisce al documento inviato dal client. Si tratta del valore inviato dal client in formato JSON. Verrà quindi utilizzato per costruire l'oggetto [PostAjax11A];
  • riga 3: l'annotazione [@Valid] impone la convalida del valore inviato;
  • riga 9: se la convalida fallisce:
    • riga 13: viene restituito un messaggio di errore,
    • righe 11–12: i campi 1 e 3 vengono riportati al loro stato precedente (visualizzati o meno);

Il messaggio di errore viene calcolato come segue:


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

Questa è una funzione che abbiamo già visto in precedenza.

L'azione [ajax-11A] prosegue come segue:


@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());
        // answer
        JsonResult10 result = new JsonResult10();
        // valid post?
        if (bindingResult.hasErrors()) {
    ...
        }
        // the input field is saved
        thymeleafContext.setVariable("value1", post.getValue1());
        thymeleafContext.setVariable("value2", post.getValue2());
        session.setSaisies(engine.process("vue-09-saisies", thymeleafContext));
        // send page 2
        result.setContent(engine.process("vue-09-page2", thymeleafContext));
        return result;
}
  • righe 13-14: i valori inviati vengono inseriti nel contesto Thymeleaf;
  • riga 15: utilizzando questo contesto, calcoliamo la vista [vue-09-saisies] e la memorizziamo nella sessione in modo da poterla rigenerare in seguito;
  • riga 17: la pagina 2 viene inserita nel risultato che verrà inviato al client;

La vista [view-09-page2.xml] è la seguente:

  

<!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>
  • Le righe 9 e 13 mostrano i valori [value1, value2] che l'azione [/ajax-11A] ha inserito nel contesto Thymeleaf;

7.5.9. Elaborazione della risposta dall'azione [/ajax-11A]

Sul lato client, la risposta dell'azione [/ajax-10] viene elaborata dalla funzione [onSuccess]:


function onSuccess(data) {
    console.log("onSuccess");
    // content
    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 active?
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // seized?
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // mistake?
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

Abbiamo già commentato questo codice. Consideriamo i due casi: una risposta con o senza errore:

Con errore

In questo caso, l'azione [/ajax-11A] ha inviato una risposta JSON nella forma {"zone1":null, "zone3":null,"saisies":null,"erreur":erreur,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":null}. Se seguiamo il codice sopra riportato, vediamo che:

  • il campo [content] non cambia. Conteneva la pagina n. 1;
  • il campo [Error] viene visualizzato;
  • i campi [Zone 1], [Zone 3] e [Entries] rimangono invariati;

Nessun errore

In questo caso, l'azione [/ajax-11A] ha inviato una risposta JSON nella forma {"zone1":null, "zone3":null,"saisies":null,"erreur":null,"zone1Active":false,"zone3Active":false,"content":content}. Se seguiamo il codice sopra riportato, vediamo che:

  • il campo [content] viene visualizzato. Contiene la pagina n. 2;

Ecco tre esempi di esecuzione:

Un caso con un errore di convalida:

Un caso con un errore POST:

Questo tipo di errore è diverso. Poiché Spring non è riuscito a convertire la stringa JSON nel tipo [PostAjax11A], ha restituito una risposta HTTP con [status=400]. L'azione [ajax-11A] non è stata eseguita;

Un caso senza errori:

7.5.10. Torna alla pagina 1

Il link [Torna alla pagina 1] nella pagina 2 è il seguente:


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

Il metodo JS [returnPage1] è il seguente:


// back to page 1
function retourPage1() {
    // make a manual Ajax call
    $.ajax({
        url : '/ajax-11B',
        headers : {
            'Accept' : 'application/json',
        },
        type : 'POST',
        dataType : 'json',
        beforeSend : onBegin,
        success : onSuccess,
        error : onError,
        complete : onComplete
    })
}

Invia una richiesta POST, senza dati inviati, all'azione [/ajax-11B].

7.5.11. L'azione [/ajax-11B]

L'azione [/ajax-11B] è la seguente:


    @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());
        // answer
        JsonResult10 result = new JsonResult10();
        // we return page 1 in its original state
        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;
}

L'azione deve ricaricare la pagina n. 1 con le sue tre zone [Zone1, Zone3, Error]:

  • riga 9: la pagina 1 viene aggiunta al risultato;
  • riga 10: la zona di input viene aggiunta al risultato;
  • riga 11: il campo [Zone 1] viene incluso nel risultato;
  • riga 12: la zona [Zone 3] viene aggiunta al risultato;
  • righe 13-14: lo stato delle zone [Zona 1] e [Zona 3] viene aggiunto al risultato;

7.5.12. Elaborazione della risposta dall'azione [/ajax-11B]

La risposta dell'azione [/ajax-11B] viene elaborata dalla funzione [onSuccess]:


function onSuccess(data) {
    console.log("onSuccess");
    // content
    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 active?
    if (data.zone3Active) {
        $("#zone3").show();
        if (data.zone3) {
            $("#zone3-content").html(data.zone3);
        }
    } else {
        $("#zone3").hide();
    }
    // seized?
    if (data.saisies) {
        $("#saisies").html(data.saisies);
    }
    // mistake?
    if (data.erreur) {
        erreur.text(data.erreur);
        erreur.show();
    } else {
        erreur.hide();
    }
}

L'azione [/ajax-11B] ha inviato una risposta JSON nel formato {"zone1":zone1, "zone3":zone3,"saisies":saisies,"erreur":null,"zone1Active":zone1Active,"zone3Active":zone3Active,"content":content}. Se seguiamo il codice sopra riportato, vediamo che:

  • il campo [content] è stato modificato. In precedenza conteneva la pagina n. 2. Ora conterrà la pagina n. 1;
  • il campo [Errore] è nascosto;
  • le zone [Zona 1], [Zona 3] e [Voci] vengono visualizzate come prima;

7.6. Gestione della sessione sul lato client

7.6.1. Introduzione

Nella sezione precedente abbiamo gestito una sessione con la seguente struttura:


public class SessionModel1 implements Serializable {
 
    // two meters
    private int cpt1 = 0;
    private int cpt3 = 0;
    // the three zones
    private String zone1 = "xx";
    private String zone3 = "zz";
    private String saisies;
    private boolean zone1Active = true;
    private boolean zone3Active = true;
...
}

Quando il numero di utenti è elevato, la memoria occupata dalle sessioni di tutti questi utenti può diventare un problema. La regola è quindi quella di ridurre al minimo le dimensioni di questa memoria. Il modello SPV (Single-Page Application) consente di gestire la sessione sul lato client e di avere un server web senza sessioni. Infatti, la singola pagina viene inizialmente caricata dal browser. Insieme ad essa viene caricato anche il file JavaScript associato. Poiché non vi è alcun ricaricamento della pagina, questo file JS rimarrà permanentemente nel browser così come è stato caricato inizialmente. Possiamo quindi utilizzare le sue variabili globali per memorizzare informazioni sulle varie azioni dell'utente. È proprio questo che vedremo ora. Non solo gestiremo la sessione sul lato client, ma riprogetteremo anche l'applicazione JS per ridurre al minimo le richieste al server.

7.6.2. L'azione [/ajax-12]

  

L'azione [/ajax-12] è la seguente:


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

La vista [vue-12.xml] è la seguente:

  

<!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>
  • Questa vista è identica alla vista [vue-09] tranne che per lo script JS utilizzato alla riga 9;

La vista visualizzata è la seguente:

 

7.6.3. Il codice JS per il pulsante [Aggiorna]

  

Il codice nel file [local12.js] è il seguente:


// global variables
var content;
var loading;
var erreur;
var page1;
var page2;
var value1;
var value2;
var session = {
        "cpt1" : 0,
        "cpt3" : 0
    };
 
// document loading
$(document).ready(function() {
    // retrieve the references of the page's various components
    loading = $("#loading");
    loading.hide();
    erreur = $("#erreur");
    erreur.hide();
    content = $("#content");
});
  • righe 17–21: quando viene caricata la pagina master, i riferimenti ai tre componenti identificati da [loading, error, content] vengono memorizzati nelle variabili globali alle righe 2–4;
  • righe 5-6: per memorizzare le due pagine;
  • righe 7-8: per memorizzare i due valori inviati tramite il link [Validate];
  • riga 9: la sessione. Memorizza i valori dei contatori [cpt1, cpt3] sul lato client;

La funzione [postForm] gestisce il clic sul pulsante [Refresh]:


function postForm() {
    console.log("postForm");
    // we post the session
    var post = JSON3.stringify(session);
    // make a manual Ajax call
    $.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
    })
}

Le differenze rispetto alla versione precedente sono le seguenti:

  • l'URL alla riga 7 è diverso;
  • riga 4: viene inviato un valore, mentre in precedenza non ne veniva inviato nessuno. Questo valore è la stringa JSON della sessione. Il principio è il seguente:
    • il client invia la sessione al server,
    • il server la modifica e la rimanda,
    • il client memorizza la nuova sessione;
  • riga 10: inviamo un documento in formato JSON (valore inviato);
  • riga 13: abbiamo qualcosa da inviare;
  • righe 15–20: le funzioni [beforeSend, error, complete] sono le stesse della versione precedente. Cambia solo la funzione [success] (righe 16–18);

7.6.4. L'azione [/ajax-13]

  

L'azione [/ajax-13] è la seguente:


    @RequestMapping(value = "/ajax-13", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    @ResponseBody()
    public JsonResult13 ajax13(@RequestBody SessionModel2 session2, HttpServletRequest request,    HttpServletResponse response) {
    ...
}
  • Riga 3: Il parametro [@RequestBody SessionModel2 session2] recupera la sessione inviata dal client. Questa ha il seguente tipo [SessionModel2]:
  

package istia.st.springmvc.models;
 
import java.io.Serializable;
 
public class SessionModel2 implements Serializable {
 
    private static final long serialVersionUID = 1L;
    // two meters
    private int cpt1 = 0;
    private int cpt3 = 0;
 
    // getters and setters
    ...
}

La sessione [SessionModel2] memorizza quanto segue:

  • riga 9: il numero di volte [cpt1] in cui viene visualizzata l'area [Zone 1];
  • riga 10: il numero di volte [cpt3] in cui viene visualizzata l'area [Zone 3];

Continuiamo ad esaminare il codice dell'azione [/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) {
    ...
}
  • Riga 3: Il tipo [JsonResult13] della risposta è il seguente:
  

package istia.st.springmvc.models;
 
public class JsonResult13 {
 
    // data
    private String page2;
    private String zone1;
    private String zone3;
    private String erreur;
    private String value1;
    private Integer value2;
 
    // session
    private SessionModel2 session;
 
    // getters and setters
    ...
}
  • riga 14: la sessione. Il server la rinvia al client per l'archiviazione;
  • riga 6: il contenuto HTML della pagina 2;
  • riga 7: il contenuto HTML dell'area [Zona 1];
  • riga 8: il contenuto HTML dell'area [Zona 3];
  • riga 9: eventuali messaggi di errore;
  • righe 10–11: due informazioni calcolate dal server e visualizzate dalla pagina n. 2;

Continuiamo ad esaminare il codice per l'azione [/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());
        // answer
        JsonResult13 result = new JsonResult13();
        result.setSession(session2);
        // randomize an answer
        int cas = new Random().nextInt(3);
        switch (cas) {
        case 0:
            // zone 1 active
            setZone1B(thymeleafContext, result);
            return result;
        case 1:
            // zone 3 active
            setZone3B(thymeleafContext, result);
            return result;
        case 2:
            // active zones 1 and 3
            setZone1B(thymeleafContext, result);
            setZone3B(thymeleafContext, result);
            return result;
        }
        return null;
    }
  • Riga 9: La sessione viene inserita nel risultato dell'azione;

Il metodo [setZone1B] che attiva la zona [Zone 1] è il seguente:


    private void setZone1B(WebContext thymeleafContext, JsonResult13 result) {
        // retrieve the session
        SessionModel2 session = result.getSession();
        // zone 1 active
        // flow 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);
        // session
        session.setCpt1(cpt1);
}
  • Riga 3: recuperiamo la sessione. Verrà modificata alla riga 12 con il nuovo contatore [cpt1]. Si noti che questa sessione verrà rinviata al client;
  • riga 10: la nuova zona [Zona 1];

Il metodo [setZone3B] che attiva la zona [Zone 3] è simile:


private void setZone3B(WebContext thymeleafContext, JsonResult13 result) {
        // retrieve the session
        SessionModel2 session = result.getSession();
        // zone 3 active
        // flow 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);
        // session
        session.setCpt3(cpt3);
    }

7.6.5. Elaborazione della risposta dall'azione [/ajax-13]

Sul lato client, la risposta JSON proveniente dall'azione [/ajax-13] viene elaborata dalla seguente funzione [onSuccess]:


function postForm() {
    console.log("postForm");
    // we post the session
    var post = JSON3.stringify(session);
    // make a manual Ajax call
    $.ajax({
    ...
        success : function(data) {
            // save the session
            session = data.session;
            // update both zones
            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();
            }
        },
...
    })
}
  • righe 12–17: se il server ha inserito qualcosa nel campo [zone1] della risposta, l'area [Zone 1] deve essere rigenerata e visualizzata; altrimenti, deve essere nascosta;
  • righe 18-23: la stessa logica si applica all'area [Zone 3];

7.6.6. Visualizzazione della pagina [Page 2]

Il codice HTML per il link [Invia] è il seguente:


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

La funzione JS [validate] è la seguente:


// validation of entered values
function valider() {
    // memorize page 1
    page1 = content.html();
    // store the values entered
    value1 = $("#text1").val().trim();
    value2 = $("#text2").val().trim();
    // posted value
    var post = JSON3.stringify({
        "value1" : value1,
        "value2" : value2,
        "pageRequired" : page2 ? false : true
    });
    // make a manual Ajax call
    $.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
    })
}
  • Invieremo una richiesta POST, che normalmente dovrebbe portarci alla pagina 2;
  • riga 4: salviamo la pagina 1 in modo da poterci tornare in seguito;
  • Righe 6–7: L'operazione precedente non memorizza i valori inseriti, ma solo il codice HTML della pagina. Quindi ora memorizziamo i due valori inseriti nel modulo;
  • righe 9–13: i due valori inseriti vengono inseriti in una stringa JSON. Questo è ciò che verrà inviato;
  • riga 12: un parametro per indicare al server se abbiamo bisogno della pagina n. 2. Procederemo come segue. Richiederemo la pagina n. 2 una volta, poi la salveremo nella variabile JS ` [page2]`. Dopodiché, non la richiederemo più. Useremo la pagina memorizzata nella cache. Riga 2: `[pageRequired]` è `true` se la variabile `[page2]` è vuota, `false` altrimenti;
  • si noti che la sessione non viene inviata. Infatti, memorizza dei contatori che l'azione [/ajax-14] alla riga 20 non modifica;

7.6.7. L'azione [/ajax-14]

L'azione [/ajax-14] è la seguente:


@RequestMapping(value = "/ajax-14", method = RequestMethod.POST)
    @ResponseBody
    public JsonResult13 ajax14(@RequestBody @Valid PostAjax14 post, BindingResult bindingResult, Locale locale,    HttpServletRequest request, HttpServletResponse response) {
        ...
    }
  • riga 3: la risposta è sempre di tipo [JsonResult13];
  • riga 3: il valore inviato è incapsulato nel seguente tipo [PostAjax14]:

package istia.st.springmvc.models;
 
public class PostAjax14 extends PostAjax11A {
 
    // page 2
    private boolean pageRequired;
 
    // getters and setters
    ...
}
  • Riga 3: La classe [PostAjax14] estende la classe [PostAjax11A] della versione precedente. Ha quindi una struttura di [value1, value2, pageRequired];

L'azione [/ajax-14] prosegue come segue:


    @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());
        // answer
        JsonResult13 result = new JsonResult13();
        // valid post?
        if (bindingResult.hasErrors()) {
            // an error is returned
            result.setErreur(getErreursForModel(bindingResult));
            return result;
        }
        // send page 2
        result.setValue1(post.getValue1());
        result.setValue2(post.getValue2());
        // page required?
        if (post.isPageRequired()) {
            result.setPage2(engine.process("vue-12-page2", thymeleafContext));
        }
        return result;
}
  • righe 9–13: se i valori inviati [value1, value2] non sono validi, viene restituito un messaggio di errore;
  • righe 15-16: normalmente, il server dovrebbe eseguire un calcolo utilizzando i valori inviati. Qui, li restituisce semplicemente per indicare che li ha ricevuti;
  • righe 18-20: la pagina n. 2 viene restituita solo se è stata richiesta dal client. Riga 19, la vista [view-12-page2] è nuova:
 

<!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>
  • Il codice XML non contiene più valori valutati da Thymeleaf come in precedenza;
  • abbiamo identificato le aree in cui inserire i valori restituiti [value1, value2] dal server. La riga 9, [id='value1'], indica dove inserire [value1]. La riga 13, lo stesso vale per [value2];

7.6.8. Elaborazione della risposta dall'azione [/ajax-14]

La risposta dell'azione [/ajax-14] viene elaborata dalla seguente funzione [success]:


// validation des valeurs saisies
function valider() {
    ...
    // on fait un appel Ajax à la main
    $.ajax({
        ...
        success : function(data) {
            // erreur ?
            if (data.erreur) {
                // affichage erreur
                erreur.html(data.erreur);
                erreur.show();
            } else {
                // pas d'erreur
                erreur.hide();
                // page 2
                if (page2) {
                    // on utilise la page en cache
                    content.html(page2);
                } else {
                    // on mémorise la page 2
                    page2 = data.page2;
                    // on l'affiche
                    content.html(data.page2);
                }
                // on la met à jour avec les infos du serveur
                $("#value1").text(data.value1);
                $("#value2").text(data.value2);
            }
        },
...
    })
}
  • righe 9–13: se il server ha restituito un errore, visualizzarlo;
  • righe 14–29: il caso in cui non ci sia stato alcun errore. Dobbiamo quindi visualizzare la pagina 2;
  • riga 17: controlliamo se la pagina 2 è già memorizzata nella variabile [page2];
  • riga 19: in questo caso, utilizzare la variabile [page2] per visualizzare la pagina 2;
  • riga 24: altrimenti, usiamo il campo [data.page2] fornito dal server;
  • riga 22: ci assicuriamo di memorizzare la pagina n. 2 in modo da non doverla richiedere nuovamente in seguito;
  • righe 27–28: nella pagina 2, visualizziamo le due informazioni [value1, value2] inviate dal server;

7.6.9. Torna alla pagina 1

Il link [Torna alla pagina 1] nella pagina 2 è il seguente:


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

Il metodo JS [returnPage1] è il seguente:


// back to page 1
function retourPage1() {
    // regenerate page 1
    content.html(page1);
    // regenerate foreclosures
    $("#text1").val(value1);
    $("#text2").val(value2);
}
  • Questa è un'azione JavaScript che non interagisce con il server perché la pagina 1 è stata memorizzata localmente nella variabile [page1];
  • Riga 4: ricarichiamo la pagina 1;
  • righe 6-7: è stata memorizzata nella cache solo la parte HTML della pagina n. 1. Non l'input dell'utente. Dobbiamo quindi ricaricare l'input dell'utente;

7.6.10. Conclusione

Sfruttando le capacità del modello APU, siamo riusciti a semplificare il server web, che ora è stateless (senza sessioni) e meno carico:

  • abbiamo rimosso l'interazione con il server nella funzione JS [returnPage1]);
  • il server genera la pagina 2 una sola volta;

7.7. Strutturazione del codice JavaScript in livelli

7.7.1. Introduzione

Il codice JavaScript dell'applicazione precedente sta iniziando a diventare complesso. È ora di strutturarlo in livelli. L'applicazione rimarrà la stessa di prima. Non apporteremo alcuna modifica al server, tranne che per definire una nuova pagina di destinazione. Riorganizzeremo il codice JS.

La nuova architettura sarà la seguente:

7.7.2. La pagina iniziale

L'azione che avvia l'applicazione è la seguente [/ajax-16]:


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

Visualizza la seguente vista [vue-16.xml]:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>Ajax-12</title>
        <link rel="stylesheet" href="/css/ajax01.css" />
        <script type="text/javascript" src="/js/jquery/jquery-2.1.1.min.js"></script>
        <script type="text/javascript" src="/js/json3.js"></script>
        <script type="text/javascript" src="/js/local16-dao.js"></script>
        <script type="text/javascript" src="/js/local16-ui.js"></script>
    </head>
    <body>
        <h3>Ajax - 16 - Navigation dans une Application à Page Unique</h3>
        <h3>Structuration du code JS</h3>
        <hr />
        <div id="content" th:include="vue-09-page1" />
        <img id="loading" src="/images/loading.gif" />
        <div id="erreur" style="background-color:lightgrey"></div>
    </body>
</html>
  • Righe 9–10: Il codice JS è stato inserito in due file diversi:
    • [local-ui] implementa il livello [presentazione],
    • [local-dao] implementa il livello [DAO];
  

7.7.3. Implementazione del livello [DAO]

7.7.4. Interfaccia

Il livello [DAO] in [local-dao.js] presenterà la seguente interfaccia al livello [presentazione]:


funzione updatePage1(deferred, sendMeBack)
per aggiornare la pagina 1 utilizzando il pulsante [Aggiorna]

funzione getPage2(deferred, sendMeBack, value1, value2, pageRequired)
per visualizzare la pagina 2 con il pulsante [Invia]

JavaScript non prevede il concetto di interfaccia. Ho usato questo termine semplicemente per indicare che il livello [di presentazione] ha concordato di comunicare con il livello [DAO] esclusivamente tramite le due funzioni sopra riportate.

7.7.5. Implementazione dell'interfaccia

Lo scheletro dell'implementazione è il seguente:


var session = {
    "cpt1" : 0,
    "cpt3" : 0
};
 
// update Page 1
function updatePage1(deferred, sendMeBack) {
...
}
 
// page 2
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
...
}

Lo scopo del livello [DAO] è quello di nascondere al livello [presentazione] i dettagli delle richieste HTTP effettuate al server web. La sessione fa parte di questi dettagli. È quindi ora gestita dal livello [DAO].

7.7.5.1. La funzione [updatePage1]

La funzione [updatePage1] è la funzione chiamata dal livello [presentazione] per aggiornare la pagina 1. Il suo codice è il seguente:


// update Page 1
function updatePage1(deferred, sendMeBack) {
    // requête HTTP
    executePost(deferred, sendMeBack, '/ajax-13', session);
}
  • Riga 1: La funzione [updatePage1] riceve due parametri:
    1. un oggetto di tipo [jQuery.Deferred]. Questo tipo di oggetto memorizza uno stato che può assumere tre valori ['pending', 'resolved', 'rejected']. Quando arriva nella funzione [updatePage1], si trova nello stato [pending];
    2. un oggetto JS da restituire al livello [presentation];

Tutte le richieste HTTP vengono effettuate dalla seguente funzione [executePost]:


// requête HTTP
function executePost(deferred, sendMeBack, url, post) {
    // on fait un appel Ajax à la main
    $.ajax({
        headers : {
            'Accept' : 'application/json',
            'Content-Type' : 'application/json'
        },
        url : url,
        type : 'POST',
        data : JSON3.stringify(post),
        dataType : 'json',
        success : function(data) {
            // on mémorise la session
            if (data.session) {
                session = data.session;
            }
            // on rend le résultat
            deferred.resolve({
                "status" : 1,
                "data" : data,
                "sendMeBack" : sendMeBack
            });
        },
        error : function(jqXHR) {
            // on rend l'erreur
            deferred.resolve({
                "status" : 2,
                "data" : jqXHR.responseText,
                "sendMeBack" : sendMeBack
            });
        }
    });
}
  • riga 1: la funzione [executePost] esegue una chiamata Ajax di tipo POST. Richiede quattro parametri:
    1. un oggetto [jQuery.Deferred] nello stato [pending];
    2. un oggetto JS da restituire al livello [presentation];
    3. l'URL POST;
    4. il valore da inviare come oggetto JS;
  • Righe 5–8: la funzione invia JSON (riga 7) e riceve JSON (riga 6);
  • riga 11: il valore da inviare viene convertito in JSON;
  • righe 13–24: la funzione eseguita se la chiamata Ajax ha esito positivo;
  • righe 19–23: se il server ha restituito una sessione, questa viene memorizzata;
  • righe 13–18: imposta l'oggetto [deferred] allo stato [resolved] e passa un risultato con i seguenti campi:
    • [status]: 1 per successo, 2 per fallimento,
    • [data]: la risposta JSON del server,
    • [sendMeBack]: il secondo parametro della funzione, che è un oggetto che il chiamante vuole recuperare;
  • righe 17–31: la funzione eseguita se la chiamata Ajax fallisce. Facciamo la stessa cosa di prima con due differenze:
    • [status] è impostato su 2 per indicare un errore;
    • [data] è di nuovo la risposta JSON del server, ma ottenuta in modo diverso;

7.7.5.2. La funzione [getPage2]

La funzione [getPage2] è la seguente:


// page 2
function getPage2(deferred, sendMeBack, value1, value2, pageRequired) {
    // requête HTTP
    executePost(deferred, sendMeBack, '/ajax-14', {
        "value1" : value1,
        "value2" : value2,
        "pageRequired" : pageRequired,
    });
}
  • La funzione riceve i seguenti parametri:
    1. [deferred]: un oggetto di tipo [jQuery.Deferred] nello stato [pending],
    2. [sendMeBack]: un oggetto JS da restituire al livello [presentation],
    3. [value1]: il primo input della pagina 1,
    4. [value2]: il secondo input della pagina 2,
    5. [pageRequired]: un valore booleano che indica al server se inviare o meno lo stream HTML per la pagina 2;
  • la funzione [executePost] viene chiamata per eseguire la richiesta HTTP necessaria;

7.7.6. Il livello [presentation]

Il livello [presentation] è implementato dal file [local-ui.js]. Questo file riutilizza il codice del file [local12.js], rielaborato per utilizzare il livello [DAO] precedente. Sono cambiate solo due funzioni: [postForm] e [valider].

7.7.6.1. La funzione [postForm]

La funzione [postForm] è la seguente:


// update Page 1
function postForm() {
    // on met à jour la page 1
    var deferred = $.Deferred();
    loading.show();
    updatePage1(deferred, {
        'sender' : "postForm",
        'info' : 10
    });
    // affichage résultats
    deferred.done(postFormDone);
}
  • riga 4: creiamo un oggetto [jQuery.Deferred]. Per impostazione predefinita, si trova nello stato [pending];
  • riga 5: viene visualizzata l'immagine di caricamento
  • righe 6–9: viene eseguita la funzione [updatePage1]. Passiamo un oggetto fittizio [sendMeBack], solo per mostrare a cosa può servire;
  • riga 11: il parametro della funzione [deferred.done] è esso stesso una funzione. Questa è la funzione da eseguire quando lo stato dell'oggetto [deferred] passa a [resolved]. Abbiamo appena visto che la funzione DAO [executePost] imposta lo stato di questo oggetto su [resolved] al ricevimento della risposta del server. Ciò significa che quando viene eseguita la funzione [postFormDone], la risposta del server è stata ricevuta;

La funzione [postFormDone] è la seguente:


function postFormDone(result) {
    // end waiting
    loading.hide();
    // data recovery
    var data = result.data
    // for demo
    console.log(JSON3.stringify(result.sendMeBack));
    // status analysis
    switch (result.status) {
    case 1:
        // update both zones
        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:
        // error display
        erreur.html(data);
        break;
    }
}
  • Riga 1: Il parametro [result] ricevuto è il parametro passato al metodo [deferred.resolve] nella funzione [executePost], ad esempio:

            // on rend le résultat
            deferred.resolve({
                "status" : 1,
                "data" : data,
                "sendMeBack" : sendMeBack
});
  • riga 5: recuperiamo la risposta dal server;
  • righe 10–24: questo è il codice che, nella versione precedente, si trovava nella funzione [onSuccess] della funzione [postForm];
  • righe 25–28: questo è il codice che in precedenza si trovava nella funzione [onError] della funzione [postForm];

7.7.6.2. Il ruolo del parametro [sendMeBack]

A cosa serve il parametro [sendMeBack]? Diamo un'occhiata al codice che chiama la funzione [updatePage1]:


// update Page 1
function postForm() {
    // on met à jour la page 1
    var deferred = $.Deferred();
    loading.show();
    updatePage1(deferred, {
        'sender' : "postForm",
        'info' : 10
    });
    // affichage résultats
    deferred.done(postFormDone);
}

e la firma della funzione [validerDone]:


function postFormDone(result) {
}

In che modo la funzione [postForm] può passare informazioni alla funzione [postFormDone]? Quest'ultima ha un solo parametro, [result]. Questo viene creato dalla funzione [executePost] nel livello [DAO]. Per passare informazioni alla funzione [postFormDone], la funzione [postForm] deve prima passarle alla funzione [updatePage1]. Questo è il ruolo del parametro [sendMeBack]. Viene utilizzato come segue:


function postFormDone(result) {
    // end waiting
    loading.hide();
    // data recovery
    var data = result.data
    // for demo
    console.log(JSON3.stringify(result.sendMeBack));
    // status analysis
    switch (result.status) {
...
  • alla riga 7, la funzione [postFormDone] ha recuperato il parametro [sendMeBack] inizialmente passato alla funzione DAO [updatePage1] dalla funzione [postForm];

7.7.7. La funzione [valider]

La funzione [valider] è la seguente:


// validation valeurs saisies
function valider() {
    // on mémorise la page 1
    page1 = content.html();
    // on mémorise les valeurs saisies
    value1 = $("#text1").val().trim();
    value2 = $("#text2").val().trim();
    // pas d'erreur
    erreur.hide();
    // on demande la page 2
    var deferred = $.Deferred();
    loading.show();
    getPage2(deferred, {
        'sender' : 'valider',
        'info' : 20
    }, value1, value2, page2 ? false : true);
    // affichage résultats
    deferred.done(validerDone);
}

e la funzione [validerDone] (riga 18) come segue:


function validerDone(result) {
    // end waiting
    loading.hide();
    // data recovery
    var data = result.data
    // for demo
    console.log(JSON3.stringify(result.sendMeBack));
    // status analysis
    switch (result.status) {
    case 1:
        // mistake?
        if (data.erreur) {
            // error display
            erreur.html(data.erreur);
            erreur.show();
        } else {
            // no error
            erreur.hide();
            // page 2
            if (page2) {
                // use the cached page
                content.html(page2);
            } else {
                // memorize page 2
                page2 = data.page2;
                // we display it
                content.html(data.page2);
            }
            // we update it with server info
            $("#value1").text(data.value1);
            $("#value2").text(data.value2);
        }
        break;
    case 2:
        // error display
        erreur.html(data);
        erreur.show();
        break;
    }
}
  • riga 5: recuperiamo la risposta dal server;
  • righe 10–32: questo è il codice che, nella versione precedente, si trovava nella funzione [onSuccess] della funzione [validate];
  • righe 34–38: questo è il codice che in precedenza si trovava nella funzione [onError] della funzione [validate];

7.7.8. Test

L'applicazione continua a funzionare come prima e, nella console di Chrome, è possibile vedere i parametri [sendMeBack] delle funzioni [postForm] e [validate]:

 

7.8. Conclusione

Torniamo all'architettura generale di un'applicazione Spring MVC:

Grazie al JavaScript incorporato nelle pagine HTML ed eseguito nel browser, e grazie al modello APU, possiamo trasferire il codice al browser e ottenere la seguente architettura:

  • abbiamo un'architettura client [2] / server [1] in cui il client e il server comunicano tramite JSON;
  • in [1], il livello web Spring MVC fornisce viste, frammenti di vista e dati in formato JSON;
  • in [2]: il codice JavaScript incorporato nella vista caricata all'avvio dell'applicazione può essere strutturato in livelli:
    • il livello [presentazione] gestisce le interazioni dell'utente,
    • il livello [DAO] gestisce l'accesso ai dati tramite il server web [1],
    • il livello [business] potrebbe non esistere o potrebbe assumere alcune funzionalità non riservate dal livello [business] del server per alleggerire il carico del server;
  • il client [2] può memorizzare nella cache determinate viste per alleggerire ulteriormente il carico del server. Gestisce la sessione;