Skip to content

6. Convalida JavaScript lato client

Nel capitolo precedente abbiamo esaminato la convalida lato server. Torniamo all'architettura di un'applicazione Spring MVC:

DB

Finora, le pagine inviate al client non contenevano alcun JavaScript. Esploreremo ora questa tecnologia, che inizialmente ci consentirà di eseguire la validazione lato client. Il principio è il seguente:

  • JavaScript invia i valori al server web;
  • e quindi, prima di questo POST, può verificare la validità dei dati e impedire l'invio se i dati non sono validi;

Useremo il modulo che abbiamo convalidato sul lato server. Ora offriremo la possibilità di convalidarlo sia sul lato client che sul lato server.

Nota: si tratta di un argomento complesso. I lettori non interessati a questo argomento possono passare direttamente al paragrafo 7.

6.1. Caratteristiche del progetto

Presentiamo alcune schermate del progetto per illustrarne le caratteristiche. La pagina iniziale è accessibile tramite l'URL [http://localhost:8080/js01.html]

 

Le validazioni sono state implementate su entrambi i fronti: client e server. Poiché la richiesta POST viene inviata solo se i valori sono stati ritenuti validi sul lato client, le validazioni sul lato server hanno sempre esito positivo. Abbiamo quindi fornito un link per disabilitare le validazioni sul lato client. In questa modalità, il comportamento è lo stesso di quello che abbiamo già studiato. Ecco un esempio:

123
  • in [1], i valori inseriti;
  • in [2], i messaggi di errore relativi alle voci;
  • in [3], un riepilogo degli errori, con le seguenti informazioni per ciascuno:
    • il nome del campo convalidato,
    • il codice di errore,
    • il messaggio predefinito per quel codice di errore;

Ora, abilitiamo la convalida lato client:

  • in [1], i valori inseriti. Si noti che gli inserimenti errati hanno uno stile specifico;
  • in [2], i messaggi di errore associati ai campi non corretti. Sono identici a quelli generati dal server;
  • in [3-4], non c'è nulla perché finché ci sono voci errate, la richiesta POST al server non viene inviata;

6.2. Convalida lato server

6.2.1. Configurazione

Iniziamo creando un nuovo progetto Maven [springmvc-validation-client]:

Sviluppiamo il progetto come segue:

  

La classe [Config] configura il progetto. È identica a quella dei progetti precedenti:


package istia.st.springmvc.config;
 
 
import java.util.Locale;
 
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.thymeleaf.spring4.SpringTemplateEngine;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;
 
@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("i18n/messages");
        return messageSource;
    }
 
    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("lang");
        return localeChangeInterceptor;
    }
 
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }
 
    @Bean
    public CookieLocaleResolver localeResolver() {
        CookieLocaleResolver localeResolver = new CookieLocaleResolver();
        localeResolver.setCookieName("lang");
        localeResolver.setDefaultLocale(new Locale("fr"));
        return localeResolver;
    }
 
    @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;
    }
 
}

La classe [Main] è la classe eseguibile del progetto:


package istia.st.springmvc.main;
 
import istia.st.springmvc.config.Config;
 
import java.util.Arrays;
 
import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext;
 
public class Main {
    public static void main(String[] args) {
        // launch the application
        ApplicationContext context = SpringApplication.run(Config.class, args);
        // displays the list of beans found by Spring
        System.out.println("Liste des beans Spring");
        String[] beanNames = context.getBeanDefinitionNames();
        Arrays.sort(beanNames);
        for (String beanName : beanNames) {
            System.out.println(beanName);
        }
    }
}
  • Riga 13: Spring Boot viene avviato con il file di configurazione [Config];
  • righe 15–20: In questo esempio, mostriamo come visualizzare l'elenco degli oggetti gestiti da Spring. Questo può essere utile se avete l'impressione che Spring non stia gestendo uno dei vostri componenti. È un modo per verificarlo. È anche un modo per verificare l'autoconfigurazione eseguita da Spring Boot. Sulla console, vedrete un elenco simile al seguente:
Liste des beans Spring
basicErrorController
beanNameHandlerMapping
beanNameViewResolver
config
defaultServletHandlerMapping
defaultTemplateResolver
defaultViewResolver
dispatcherServlet
dispatcherServletRegistration
embeddedServletContainerCustomizerBeanPostProcessor
error
errorAttributes
faviconHandlerMapping
faviconRequestHandler
handlerExceptionResolver
hiddenHttpMethodFilter
http.mappers.CONFIGURATION_PROPERTIES
httpRequestHandlerAdapter
jacksonObjectMapper
jsController
layoutDialect
localeChangeInterceptor
localeResolver
mappingJackson2HttpMessageConverter
mbeanExporter
mbeanServer
messageConverters
messageSource
multipart.CONFIGURATION_PROPERTIES
multipartConfigElement
multipartResolver
mvcContentNegotiationManager
mvcConversionService
mvcUriComponentsContributor
mvcValidator
objectNamingStrategy
org.springframework.boot.autoconfigure.AutoConfigurationPackages
org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonObjectMapperAutoConfiguration
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration$Empty
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration$DefaultTemplateResolverConfiguration
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration$ThymeleafViewResolverConfiguration
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration$ThymeleafWebLayoutConfiguration
org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration
org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration$DispatcherServletConfiguration
org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration
org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration$EmbeddedTomcat
org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration
org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration
org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration
org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration$ObjectMappers
org.springframework.boot.autoconfigure.web.MultipartAutoConfiguration
org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration
org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration
org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter
org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter$FaviconConfiguration
org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor
org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.store
org.springframework.context.annotation.ConfigurationClassPostProcessor.enhancedConfigurationProcessor
org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor
org.springframework.context.annotation.MBeanExportConfiguration
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalRequiredAnnotationProcessor
org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration
propertySourcesPlaceholderConfigurer
requestContextListener
requestMappingHandlerAdapter
requestMappingHandlerMapping
resourceHandlerMapping
serverProperties
simpleControllerHandlerAdapter
spring.mvc.CONFIGURATION_PROPERTIES
spring.resources.CONFIGURATION_PROPERTIES
templateEngine
templateResolver
thymeleafResourceResolver
thymeleafViewResolver
tomcatEmbeddedServletContainerFactory
viewControllerHandlerMapping
viewResolver

Abbiamo evidenziato gli oggetti definiti nella classe [Config].

6.2.2. Il modello del modulo

Continuiamo ad esplorare il progetto:

  

La classe [Form01] è quella che riceverà i valori inviati. È la seguente:


package istia.st.springmvc.models;
 
import java.util.Date;
 
import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
 
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;
import org.springframework.format.annotation.DateTimeFormat;
 
public class Form01 {
 
    // posted values
    @NotNull
    @AssertFalse
    private Boolean assertFalse;
 
    @NotNull
    @AssertTrue
    private Boolean assertTrue;
 
    @NotNull
    @Future
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dateInFuture;
 
    @NotNull
    @Past
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dateInPast;
 
    @NotNull
    @Max(value = 100)
    private Integer intMax100;
 
    @NotNull
    @Min(value = 10)
    private Integer intMin10;
 
    @NotNull
    @NotBlank
    private String strNotEmpty;
 
    @NotNull
    @Size(min = 4, max = 6)
    private String strBetween4and6;
 
    @NotNull
    @Pattern(regexp = "^\\d{2}:\\d{2}:\\d{2}$")
    private String hhmmss;
 
    @NotNull
    @Email
    @NotBlank
    private String email;
 
    @NotNull
    @Length(max = 4, min = 4)
    private String str4;
 
    @Range(min = 10, max = 14)
    @NotNull
    private Integer int1014;
 
    @NotNull
    @DecimalMax(value = "3.4")
    @DecimalMin(value = "2.3")
    private Double double1;
 
    @NotNull
    private Double double2;
 
    @NotNull
    private Double double3;
 
    @URL
    @NotBlank
    private String url;
 
    // customer validation
    private boolean clientValidation = true;
    // local
    private String lang;
    ...
}

Vediamo validatori che abbiamo già incontrato in precedenza. Introdurremo anche il concetto di validazione personalizzata. Si tratta di un tipo di validazione che non può essere gestita da un validatore predefinito. In questo caso, richiederemo che [double1+double2] sia compreso nell'intervallo [10,13].

6.2.3. Il controller

Il controller [JsController] è il seguente:

  

package istia.st.springmvc.controllers;
 
import istia.st.springmvc.models.Form01;
...
 
@Controller
public class JsController {
 
    @RequestMapping(value = "/js01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String js01(Form01 formulaire, Locale locale, Model model) {
        setModel(formulaire, model, locale, null);
        return "vue-01";
    }
...
 
    // preparing the view-01 model
    private void setModel(Form01 formulaire, Model model, Locale locale, String message) {
...
    }
}
  • riga 9, l'azione [/js01];
  • riga 10: viene istanziato un oggetto di tipo [Form01] e inserito automaticamente nel modello, associato alla chiave [form01];
  • riga 10: la locale e il modello vengono inseriti nei parametri;
  • riga 11: con queste informazioni, il modello viene preparato;
  • riga 12: viene visualizzata la vista [vue-01.xml];

Il metodo [setModel] è il seguente:


    // preparing the view-01 model
    private void setModel(Form01 formulaire, Model model, Locale locale, String message) {
        // we only manage fr-FR, en-US locales
        String language = locale.getLanguage();
        String country = null;
        if (language.equals("fr")) {
            country = "FR";
            formulaire.setLang("fr_FR");
        }
        if (language.equals("en")) {
            country = "US";
            formulaire.setLang("en_US");
        }
        model.addAttribute("locale", String.format("%s-%s", language, country));
        // any message
        if (message != null) {
            model.addAttribute("message", message);
        }
}
  • Lo scopo del metodo [setModel] è quello di aggiungere al modello le seguenti informazioni:
    • informazioni sulla lingua,
    • il messaggio passato come ultimo parametro;
  • riga 14: inseriamo le informazioni relative alle impostazioni locali (lingua, paese) nel modello;
  • righe 16–18: qualsiasi messaggio passato come parametro viene inserito nella locale;
  • righe 8, 12: le informazioni relative alla lingua vengono memorizzate anche nel modulo [Form01]. JavaScript utilizzerà queste informazioni;

I valori inseriti nel modulo [vue-01.xml] verranno inviati alla seguente azione [/js02]:


    @RequestMapping(value = "/js02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String js02(@Valid Form01 formulaire, BindingResult result, RedirectAttributes redirectAttributes, Locale locale, Model model) {
        Form01Validator validator = new Form01Validator(10, 13);
        validator.validate(formulaire, result);
        ...
}
  • Riga 2: L'annotazione [@Valid Form01 form] garantisce che i valori inviati vengano sottoposti ai validatori della classe [Form01]. Sappiamo che esiste una validazione specifica [double1+double2] nell'intervallo [10,13]. Quando arriviamo alla riga 3, questa validazione non è ancora stata eseguita;
  • riga 3: creiamo il seguente oggetto [Form01Validator]:
  

package istia.st.springmvc.validators;
 
import istia.st.springmvc.models.Form01;
 
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
 
public class Form01Validator implements Validator {
 
    // validation interval
    private double min;
    private double max;
 
    // manufacturer
    public Form01Validator(double min, double max) {
        this.min = min;
        this.max = max;
    }
 
    @Override
    public boolean supports(Class<?> classe) {
        return Form01.class.equals(classe);
    }
 
    @Override
    public void validate(Object form, Errors errors) {
        // validated object
        Form01 form01 = (Form01) form;
        // the value of [double1]
        Double double1 = form01.getDouble1();
        if (double1 == null) {
            return;
        }
        // the value of [double2]
        Double double2 = form01.getDouble2();
        if (double2 == null) {
            return;
        }
        // [double1+double2]
        double somme = double1 + double2;
        // validation
        if (somme < min || somme > max) {
            errors.rejectValue("double2", "form01.double2", new Double[] { min, max }, null);
        }
    }
 
}
  • riga 8: per implementare una validazione specifica, creiamo una classe che implementa l'interfaccia Spring [Validator]. Questa interfaccia ha due metodi: [supports] alla riga 21 e [validate] alla riga 26;
  • righe 21–23: il metodo [supports] accetta un oggetto di tipo [Class]. Deve restituire true per indicare che supporta questa classe, false in caso contrario;
  • riga 22: specifichiamo che la classe [Form01Validator] convalida solo oggetti di tipo [Form01];
  • righe 15–18: ricordiamo che vogliamo implementare il vincolo [double1+double2] all'interno dell'intervallo [10,13]. Anziché limitarci a questo intervallo, verificheremo il vincolo [double1+double2] all'interno dell'intervallo [min, max]. Questo è il motivo per cui abbiamo un costruttore con questi due parametri;
  • riga 26: il metodo [validate] viene chiamato con un'istanza dell'oggetto validato — in questo caso, un'istanza di [Form01] — e con la collezione degli errori attualmente noti [Errors errors]. Se la validazione eseguita dal metodo [validate] fallisce, deve creare un nuovo elemento nella collezione [Errors errors];
  • riga 43: la convalida ha dato esito negativo. Aggiungiamo un elemento alla raccolta [Errors errors] utilizzando il metodo [Errors.rejectValue], i cui parametri sono i seguenti:
    • parametro 1: solitamente il nome del campo con l'errore. Qui abbiamo testato i campi [double1, double2]. Possiamo utilizzare uno qualsiasi dei due,
    • il messaggio di errore associato, o più precisamente la sua chiave nei file di messaggi esternalizzati:

[messages_fr.properties]


form01.double2=[double2+double1] doit être dans l''intervalle [{0},{1}]

[messages_en.properties]


form01.double2=[double2+double1] must be in [{0},{1}

Qui abbiamo messaggi parametrizzati da {0} e {1}. Pertanto, per questo messaggio devono essere forniti due valori. Questo è ciò che fa il terzo parametro del metodo [Errors.rejectValue].

    • Il quarto parametro è un messaggio predefinito per l'errore;

Torniamo all'azione [/js02]:


    @RequestMapping(value = "/js02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String js02(@Valid Form01 formulaire, BindingResult result, RedirectAttributes redirectAttributes, Locale locale, Model model) {
        Form01Validator validator = new Form01Validator(10, 13);
        validator.validate(formulaire, result);
        if (result.hasErrors()) {
            StringBuffer buffer = new StringBuffer();
            for (ObjectError error : result.getAllErrors()) {
                buffer.append(String.format("[name=%s,code=%s,message=%s]", error.getObjectName(), error.getCode(), error.getDefaultMessage()));
            }
            setModel(formulaire, model, locale, buffer.toString());
            return "vue-01";
        } else {
            redirectAttributes.addFlashAttribute("form01", formulaire);
            return "redirect:/js01.html";
        }
}
  • riga 4: il validatore [Form01Validator] viene eseguito con i seguenti parametri:
    • parametro 1: l'oggetto da convalidare,
    • parametro 2: l'elenco degli errori per questo oggetto. Si tratta dell'oggetto [BindingResult result] passato come parametro all'azione. Se la convalida fallisce, questo oggetto avrà un errore in più;
  • riga 5: verifichiamo se sono presenti errori di convalida;
  • righe 7–10: si esegue un'iterazione sull'elenco degli errori per memorizzare per ciascuno di essi:
    • il nome dell'oggetto convalidato,
    • il suo codice di errore,
    • il suo messaggio di errore predefinito;
  • riga 10: utilizzando queste informazioni, costruiamo il modello di visualizzazione [vue-01.xml]. Questa volta, c'è un messaggio: la versione concatenata e abbreviata dei vari messaggi di errore;
  • righe 12–15: se tutti i valori inviati sono validi, reindirizziamo il client all'azione [/js01] impostando i valori inviati come attributi Flash;

6.2.4. La vista

La vista [view-01.xml] è complessa. Ne presenteremo solo una piccola parte:


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form01.css" />
        <script type="text/javascript" src="/js/jquery/jquery-1.10.2.min.js"></script>
        ...
    </head>
    <body>
        <!-- title -->
        <h3>
            <span th:text="#{form01.title}"></span>
            <span th:text="${locale}"></span>
        </h3>
        <!-- menu -->
        <p>
...
        </p>
        <!-- form -->
        <form action="/someURL" th:action="@{/js02.html}" method="post" th:object="${form01}" name="form" id="form">
            <table>
                <thead>
                    <tr>
                        <th class="col1" th:text="#{form01.col1}">Contrainte</th>
                        <th class="col2" th:text="#{form01.col2}">Saisie</th>
                        <th class="col3" th:text="#{form01.col3}">Validation client</th>
                        <th class="col4" th:text="#{form01.col4}">Validation serveur</th>
                    </tr>
                </thead>
                <tbody>
                    <!-- required -->
                    <tr>
                        <td class="col1">required</td>
                        <td class="col2">
                            <input type="text" th:field="*{strNotEmpty}" data-val="true" th:attr="data-val-required=#{NotNull}" />
                        </td>
                        <td class="col3">
                            <span class="field-validation-valid" data-valmsg-for="strNotEmpty" data-valmsg-replace="true"></span>
                        </td>
                        <td class="col4">
                            <span th:if="${#fields.hasErrors('strNotEmpty')}" th:errors="*{strNotEmpty}" class="error">Donnée erronée</span>
                        </td>
                    </tr>
...
                </tbody>
            </table>
            <p>
            <!-- validation button -->
            <input type="submit" th:value="#{form01.valider}" value="Valider" onclick="javascript:postForm01()" />
            </p>
        </form>
        <!-- server-side validator message -->
        <br/>
        <fieldset class="fieldset">
            <legend>
                <span th:text="#{server.error.message}"></span>
            </legend>
            <span th:text="${message}" class="error"></span>
        </fieldset>
    </body>
</html>

Questa pagina utilizza una serie di messaggi presenti nei file di messaggi esternalizzati:

[messages_fr.properties]


form01.title=Formulaire - Validations côté client - locale=
form01.col1=Contrainte
form01.col2=Saisie
form01.col3=Validation client
form01.col4=Validation serveur
form01.valider=Valider
server.error.message=Erreurs détectées par les validateurs côté serveur

[messages_en.properties]


form01.title=Form - Client side validation - locale=
form01.col1=Constraint
form01.col2=Input
form01.col3=Client validation
form01.col4=Server validation
form01.valider=Validate
server.error.message=Errors detected by the validators on the server side

Torniamo al codice della pagina:

  • riga 8: un gran numero di importazioni di librerie JavaScript che possiamo ignorare in questa sede;
  • riga 14: visualizza le impostazioni locali impostate nel template dal server;
  • riga 59: visualizza il messaggio impostato nel template dal server;

Il codice nelle righe 33–44 è nuovo. Esaminiamolo:


<!-- required -->
<tr>
  <td class="col1">required</td>
  <td class="col2">
    <input type="text" th:field="*{strNotEmpty}" data-val="true" th:attr="data-val-required=#{NotNull}" />
  </td>
  <td class="col3">
    <span class="field-validation-valid" data-valmsg-for="strNotEmpty" data-valmsg-replace="true"></span>
  </td>
  <td class="col4">
    <span th:if="${#fields.hasErrors('strNotEmpty')}" th:errors="*{strNotEmpty}" class="error">Donnée erronée</span>
  </td>
</tr>

L'approccio più semplice potrebbe essere quello di esaminare il codice HTML generato da questo segmento Thymeleaf:


<!-- required -->
<tr>
  <td class="col1">required</td>
  <td class="col2">
    <input type="text" data-val="true" data-val-required="Le champ est obligatoire" id="strNotEmpty" name="strNotEmpty" value="" />
  </td>
  <td class="col3">
    <span class="field-validation-valid" data-valmsg-for="strNotEmpty" data-valmsg-replace="true"></span>
  </td>
  <td class="col4">
 
  </td>
</tr>

Utilizzeremo una libreria di validazione lato client chiamata [jquery.validate]. Tutti gli attributi [data-x] sono destinati a questa libreria. Quando la validazione lato client è disabilitata, questi attributi non verranno utilizzati. Quindi, per ora, non è necessario comprenderli. Possiamo semplicemente concentrarci sulla seguente riga di Thymeleaf:


<input type="text" th:field="*{strNotEmpty}" data-val="true" th:attr="data-val-required=#{NotNull}" />

che genera la seguente riga HTML:


<input type="text" data-val="true" data-val-required="Le champ est obligatoire" id="strNotEmpty" name="strNotEmpty" value="" />

Come accennato in precedenza, si verifica un problema nella generazione dell'attributo [data-val-required="Questo campo è obbligatorio"]. Ciò è dovuto al fatto che il valore associato all'attributo proviene da file di messaggi esterni. Siamo quindi costretti a utilizzare un'espressione Thymeleaf per ottenerlo. L'espressione è la seguente: [th:attr="data-val-required=#{NotNull}"]. Questa espressione viene valutata e il suo valore viene inserito così com'è nel tag HTML generato. Si chiama [th:attr] perché viene utilizzata per generare attributi non predefiniti in Thymeleaf. Abbiamo incontrato attributi predefiniti [th:text, th:value, th:class, ...] ma non esiste un attributo [th:data-val-required].

6.2.5. Il foglio di stile

Sopra, vediamo classi CSS come [class="field-validation-valid"]. Alcune di queste classi sono utilizzate dalla libreria di validazione JavaScript. Sono definite nel seguente file [form01.css]:

  

@CHARSET "UTF-8";
 
/*styles perso*/
body {
    background-image: url("/images/standard.jpg");
}
 
.col1 {
    background: lightblue;
}
 
.col2 {
    background: Cornsilk;
}
 
.col3 {
    background: AliceBlue;
}
 
.col4 {
    background: Lavender;
}
 
.error {
    color: red;
}
 
.fieldset{
    background: Lavender;
}
/* Styles for validation helpers
-----------------------------------------------------------*/
.field-validation-error {
    color: #f00;
}
 
.field-validation-valid {
    display: none;
}
 
.input-validation-error {
    border: 1px solid #f00;
    background-color: #fee;
}
 
.validation-summary-errors {
    font-weight: bold;
    color: #f00;
}
 
.validation-summary-valid {
    display: none;
}

6.3. Convalida lato client

6.3.1. Nozioni di base su jQuery e JavaScript

La convalida lato client viene eseguita utilizzando JavaScript. Utilizzeremo il framework jQuery, che fornisce molte funzioni che semplificano lo sviluppo in JavaScript. Tratteremo le nozioni di base di jQuery necessarie per comprendere gli script in questo capitolo e in quelli successivi.

Creiamo un file HTML statico [JQuery-01.html] e lo inseriamo in una cartella [static / views]:

 

Questo file avrà il seguente contenuto:


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>JQuery-01</title>
  <script type="text/javascript" src="/js/jquery-1.11.1.min.js"></script>
</head>
<body>
  <h3>Rudiments de JQuery</h3>
  <div id="element1">
    Elément 1
  </div>
</body>
</html>
  • riga 6: importazione di jQuery;
  • righe 10–12: un elemento della pagina con l'ID [element1]. Lavoreremo con questo elemento.

Dobbiamo scaricare il file [jquery-1.11.1.min.js]. Puoi trovare l'ultima versione di jQuery all'URL [http://jquery.com/download/]:

Image

Inseriremo il file scaricato nella cartella [static / js]:

  

Una volta fatto ciò, apri la vista statica [jQuery-01.html] in Chrome [1-2]:

In Google Chrome, premi [Ctrl-Shift-I] per aprire gli strumenti di sviluppo [3]. La scheda [Console] [4] ti permette di eseguire codice JavaScript. Di seguito, ti forniamo i comandi JavaScript da digitare e ti spieghiamo a cosa servono.

JS
risultato
$("#element1")
: restituisce l'insieme di tutti gli elementi con l'ID [element1], quindi normalmente un insieme di 0 o 1 elemento poiché non è possibile avere due ID identici in una pagina HTML.
$("#element1").text("blabla")
: imposta il testo [blabla] per tutti gli elementi della raccolta. Questo modifica il contenuto visualizzato sulla pagina
$("#element1").hide()
nasconde gli elementi della collezione. Il testo [blabla] non viene più visualizzato.
$("#element1")
: visualizza nuovamente la collezione. Questo ci permette di vedere che l'elemento con l'ID [element1] ha l'attributo CSS style='display: none;', il che fa sì che l'elemento sia nascosto.
$("#element1").show()
: visualizza gli elementi della collezione. Il testo [blabla] appare nuovamente. L'attributo CSS style='display: block;' è responsabile di questa visualizzazione.
$("#element1").attr('style','color: red')
: imposta un attributo su tutti gli elementi della collezione. L'attributo in questo caso è [style] e il suo valore è [color: red]. Il testo [blabla] diventa rosso.
Tabella
Dizionario

Si noti che l'URL del browser non è cambiato durante tutte queste operazioni. Non c'è stata alcuna comunicazione con il server web. Tutto avviene all'interno del browser. Ora, visualizziamo il codice sorgente della pagina:


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>JQuery-01</title>
  <script type="text/javascript" src="/js/jquery-1.11.1.min.js"></script>
</head>
<body>
  <h3>Rudiments de JQuery</h3>
  <div id="element1">
    Elément 1
  </div>
</body>
</html>

Questo è il testo originale. Non riflette le modifiche apportate all'elemento nelle righe 10–12. È importante tenerlo presente durante il debug di JavaScript. In questi casi, spesso non è necessario visualizzare il codice sorgente della pagina visualizzata.

Ora sappiamo abbastanza per comprendere gli script JavaScript che seguono.

6.3.2. Librerie di validazione JS

Useremo le librerie dell'ecosistema jQuery. Ci sono un sacco di progetti che ruotano attorno a jQuery, che a loro volta danno vita a delle librerie. Useremo la libreria di validazione [jquery.validate.unobstrusive] creata da Microsoft e donata alla jQuery Foundation. D'ora in poi la chiameremo libreria di validazione MS o, più semplicemente, libreria MS. Per averla, ti serve un ambiente Microsoft Visual Studio. Non ho trovato nessun altro modo per ottenerla. Puoi usare una versione gratuita come [Visual Studio Community] [http://www.visualstudio.com/en-us/news/vs2013-community-vs.aspx] (dicembre 2014). I lettori che non sono interessati a seguire i passaggi qui sotto possono recuperare questa libreria e quelle da cui dipende dagli esempi forniti sul sito web di questo documento.

Creare un progetto console con Visual Studio [1-4]:

12
34
  • in [5], il progetto console;
  • in [6-7]: aggiungeremo pacchetti [NuGet] al progetto. [NuGet] è una funzionalità di Visual Studio che consente di scaricare librerie sotto forma di DLL e librerie JavaScript.
  • Nei passaggi [9-10], effettuare una ricerca utilizzando la parola chiave [jQuery];
  • In [11-13], scaricare le librerie JavaScript necessarie per la convalida lato client nell'ordine indicato;
  • In [14], scarica anche la libreria [Microsoft jQuery Unobtrusive Ajax], che useremo tra poco;
  • In [15-16], cerca i pacchetti utilizzando la parola chiave [globalize];
  • in [17], scaricare la libreria [jQuery.Validation.Globalize];

Questi vari download hanno installato una serie di librerie JavaScript nella cartella [Scripts] del progetto [18]. Non tutte sono utili. Ogni file è disponibile in due versioni:

  • [js]: la versione leggibile della libreria;
  • [min.js]: la versione non leggibile, cosiddetta "minimizzata", della libreria. Non è realmente illeggibile — è testo — ma non è comprensibile. Questa è la versione da utilizzare in produzione perché il file è più piccolo della corrispondente versione [js] e quindi migliora la velocità di comunicazione client/server;

Le versioni [min.map] non sono essenziali. Nella cartella [cultures] è possibile conservare solo le culture gestite dall'applicazione.

Utilizzando Esplora risorse, copiare questi file nella cartella [static/js/jquery] del progetto [springmvc-validation-client] e conservare solo i file utili [20]:

In [21], mantenere solo due impostazioni locali:

  • [fr-FR]: francese (Francia);
  • [en-US]: inglese (Stati Uniti);

6.3.3. Importazione delle librerie JS di validazione

Per poter essere utilizzate, queste librerie devono essere importate dalla vista [vue-01.xml]:


<head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form01.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/client-validation.js"></script>
        <script type="text/javascript" src="/js/local.js"></script>
        <script th:inline="javascript">
            /*<![CDATA[*/
                    var culture = [[${locale}]];
                    Globalize.culture(culture);
                    /*]]>*/
        </script>
    </head>
  • riga 11: importazione di un file JavaScript di cui non abbiamo ancora parlato;
  • righe 13–18: uno script JavaScript interpretato da Thymelaf. Gestisce le impostazioni locali sul lato client;

6.3.4. Gestione delle impostazioni locali lato client

La localizzazione lato client è gestita dal seguente script JavaScript:


<script th:inline="javascript">
            /*<![CDATA[*/
                    var culture = [[${locale}]];
                    Globalize.culture(culture);
                    /*]]>*/
</script>
  • Righe 3-4: codice JavaScript contenente l'espressione Thymeleaf [[${locale}]]. Si noti la sintassi specifica di questa espressione, poiché è scritta in JavaScript. L'espressione [[${locale}]] verrà sostituita dal valore della chiave [locale] nel modello di visualizzazione;

Il risultato nell'output HTML generato da queste righe è il seguente:


<script>
            /*<![CDATA[*/
                    var culture = 'en-US';
                    Globalize.culture(culture);
                    /*]]>*/
</script>

Le righe 3-4 impostano la cultura lato client. Ne supportiamo solo due: [fr-FR] e [en-US]. Ecco perché abbiamo importato solo due file di cultura:


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

La cultura da utilizzare sul lato client viene impostata sul lato server. Torniamo al codice lato server:


    @RequestMapping(value = "/js01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String js01(Form01 formulaire, Locale locale, Model model) {
        setModel(formulaire, model, locale, null);
        return "vue-01";
    }
 
    // preparing the view-01 model
    private void setModel(Form01 formulaire, Model model, Locale locale, String message) {
        // we only manage fr-FR, en-US locales
        String language = locale.getLanguage();
        String country = null;
        if (language.equals("fr")) {
            country = "FR";
            formulaire.setLang("fr_FR");
        }
        if (language.equals("en")) {
            country = "US";
            formulaire.setLang("en_US");
        }
        model.addAttribute("locale", String.format("%s-%s", language, country));
...
}
  • Riga 20: La lingua [fr-FR] o [en-US] è impostata nel modello di visualizzazione [vue-01.xml] (riga 4). Si noti una potenziale fonte di complicazioni. Mentre una lingua francese è indicata come [fr-FR] sul lato client, è indicata come [fr_FR] sul lato server. Questo è il motivo per cui, alle righe 14 e 18, viene memorizzata in questa forma nell'oggetto [Form01] che riceve i valori inviati;

Si noti il seguente punto importante. Lo script


<script>
            /*<![CDATA[*/
                    var culture = 'en-US';
                    Globalize.culture(culture);
                    /*]]>*/
</script>

modifica la cultura del client in base alle impostazioni locali inviate dal server. Ciò non internazionalizza i messaggi visualizzati sulla pagina. Modifica solo il modo in cui vengono interpretate alcune informazioni che dipendono dalla cultura di un paese. Con la cultura [fr_FR], il numero reale [12.78] è valido, mentre non lo è con la cultura [en-US]. È quindi necessario scrivere [12.78]. Allo stesso modo, la data [12/01/2014] è una data valida nella cultura [fr-FR], mentre nella cultura [en-US] è necessario scrivere [01/12/2014]. I file nella cartella [jquery / globalize] gestiscono questo tipo di problemi:

  

L'internazionalizzazione dei messaggi di errore viene gestita esclusivamente sul lato server. Vedremo che la pagina HTML/JS riporta i messaggi di errore corrispondenti alle impostazioni locali gestite dal server: in francese per le impostazioni locali [fr_FR] e in inglese per quelle [en_US].

6.3.5. I file dei messaggi

La vista [vue-01.xml] utilizza i seguenti messaggi internazionalizzati:

  

[messages_fr.properties]


NotNull=Le champ est obligatoire
NotEmpty=La donnée ne peut être vide
NotBlank=La donnée ne peut être vide
typeMismatch=Format invalide
Future.form01.dateInFuture=La date doit être postérieure ou égale à celle d''aujourd'hui
Past.form01.dateInPast=La date doit être antérieure ou égale à celle d''aujourd'hui
Min.form01.intMin10=La valeur doit être supérieure ou égale à 10
Max.form01.intMax100=La valeur doit être inférieure ou égale à 100
Size.form01.strBetween4and6=La chaîne doit avoir entre 4 et 6 caractères
Length.form01.str4=La chaîne doit avoir quatre caractères exactement
Email.form01.email=Adresse mail invalide
URL.form01.url=URL invalide
Range.form01.int1014=La valeur doit être dans l''intervalle [10,14]
AssertTrue=Seule la valeur True est acceptée
AssertFalse=Seule la valeur False est acceptée
Pattern.form01.hhmmss=Tapez l''heure sous la forme hh:mm:ss
form01.hhmmss.pattern=^\\d{2}:\\d{2}:\\d{2}$
DateInvalide.form01=Date invalide
form01.str4.pattern=^.{4,4}$
form01.int1014.max=14
form01.int1014.min=10
form01.strBetween4and6.pattern=^.{4,6}$
form01.intMax100.value=100
form01.intMin10.value=10
form01.double1.min=2.3
form01.double1.max=3.4
Range.form01.double1=La valeur doit être dans l'intervalle [2,3-3,4]
form01.title=Formulaire - Validations côté client - locale=
form01.col1=Contrainte
form01.col2=Saisie
form01.col3=Validation client
form01.col4=Validation serveur
form01.valider=Valider
form01.double2=[double2+double1] doit être dans l''intervalle [{0},{1}]
form01.double3=[double3+double1] doit être dans l''intervalle [{0},{1}]
locale.fr=Français
locale.en=English
client.validation.true=Activer la validation client
client.validation.false=Inhiber la validation client
DecimalMin.form01.double1=Le nombre doit être supérieur ou égal à 2,3
DecimalMax.form01.double1=Le nombre doit être inférieur ou égal à 3,4
server.error.message=Erreurs détectées par les validateurs côté serveur

[messages_en.properties]


NotNull=Field is required
NotEmpty=Field can''t be empty
NotBlank=Field can''t be empty
typeMismatch=Invalid format
Future.form01.dateInFuture=Date must be greater or equal to today''s date
Past.form01.dateInPast=Date must be lower or equal today''s date
Min.form01.intMin10=Value must be higher or equal to 10
Max.form01.intMax100=Value must be lower or equal to 100
Size.form01.strBetween4and6=String must have between 4 and 6 characters
Length.form01.str4=String must be exactly 4 characters long
Email.form01.email=Invalid mail address
URL.form01.url=Invalid URL
Range.form01.int1014=Value must be in [10,14]
AssertTrue=Only value True is allowed
AssertFalse=Only value False is allowed
Pattern.form01.hhmmss=Time must follow the format hh:mm:ss
form01.hhmmss.pattern=^\\d{2}:\\d{2}:\\d{2}$
DateInvalide.form01=Invalid Date
form01.str4.pattern=^.{4,4}$
form01.int1014.max=14
form01.int1014.min=10
form01.strBetween4and6.pattern=^.{4,6}$
form01.intMax100.value=100
form01.intMin10.value=10
form01.double1.min=2.3
form01.double1.max=3.4
Range.form01.double1=Value must be in [2.3,3.4]
form01.title=Form - Client side validation - locale=
form01.col1=Constraint
form01.col2=Input
form01.col3=Client validation
form01.col4=Server validation
form01.valider=Validate
form01.double2=[double2+double1] must be in [{0},{1}]
form01.double3=[double3+double1] must be in [{0},{1}]
locale.fr=Français
locale.en=English
client.validation.true=Activate client validation
client.validation.false=Inhibate client validation
DecimalMin.form01.double1=Value must be greater or equal to 2.3
DecimalMax.form01.double1=Value must be lower or equal to 3.4
server.error.message=Errors detected by the validators on the server side

Il file [messages.properties] è una copia del file dei messaggi in inglese. In definitiva, qualsiasi locale diverso da [fr] utilizzerà i messaggi in inglese. Si noti che il file [messages_fr.properties] viene utilizzato per tutti i locali [fr_XX], come [fr_CA] o [fr_FR].

La vista [vue-01.xml] utilizza le chiavi di questi messaggi. Se si desidera conoscere il valore associato a queste chiavi, fare riferimento a questa sezione per trovarlo.

6.3.6. Modifica della lingua

La vista [vue-01.xml] contiene quattro link:


<body>
        <!-- title -->
        <h3>
            <span th:text="#{form01.title}"></span>
            <span th:text="${locale}"></span>
        </h3>
        <!-- menu -->
        <p>
            <a id="locale_fr" href="javascript:setLocale('fr_FR')">
                <span th:text="#{locale.fr}"></span>
            </a>
            <a id="locale_en" href="javascript:setLocale('en_US')">
                <span style="margin-left:30px" th:text="#{locale.en}"></span>
            </a>
            <a id="clientValidationTrue" href="javascript:setClientValidation(true)">
                <span style="margin-left:30px" th:text="#{client.validation.true}"></span>
            </a>
            <a id="clientValidationFalse" href="javascript:setClientValidation(false)">
                <span style="margin-left:30px" th:text="#{client.validation.false}"></span>
            </a>
        </p>
        <!-- form -->
        <form action="/someURL" th:action="@{/js02.html}" method="post" th:object="${form01}" name="form" id="form">
            ...

alcuni dei quali sono riportati di seguito [1]:

Esaminiamo i due link che consentono di cambiare la lingua in francese o inglese:


            <a id="locale_fr" href="javascript:setLocale('fr_FR')">
                <span th:text="#{locale.fr}"></span>
            </a>
            <a id="locale_en" href="javascript:setLocale('en_US')">
                <span style="margin-left:30px" th:text="#{locale.en}"></span>
</a>

Cliccando su questi link si avvia l'esecuzione di uno script JavaScript contenuto nel file [local.js] [2]. In entrambi i casi, viene chiamata una funzione JavaScript [setLocale]:


// local
function setLocale(locale) {
    // update the locale
    lang.val(locale);
    // we submit the form - this doesn't trigger the client validators - that's why we haven't inhibited client-side validation
    document.form.submit();
}

Per comprendere la riga 4 sono necessarie alcune informazioni di base. La vista [vue-01.xml] include un campo nascosto denominato [lang]:


<input type="hidden" th:field="*{lang}" th:value="*{lang}" value="true" />

che corrisponde a un campo [lang] in [Form01]:


    // locale
    private String lang;

I campi nascosti sono utili quando si desidera arricchire i valori inviati. JavaScript consente di assegnare loro un valore, e questo valore viene inviato come un normale input dell'utente. Il codice HTML generato da Thymeleaf è il seguente:


<input type="hidden" value="en_US" id="lang" name="lang" />

Il valore del parametro [value] è quello del campo [Form01.lang] al momento della generazione dell'HTML. È importante notare l'identificatore JavaScript del nodo [id="lang"]. Questo identificatore viene utilizzato dalla seguente funzione []:


// global variables
var lang;
 
// document ready
$(document).ready(function() {
    // global references
    lang = $("#lang");
});
 
// local
function setLocale(locale) {
    // update the locale
    lang.val(locale);
    // the form is submitted - for some reason this does not trigger the client's validators
    // that's why we didn't inhibit validation
    document.form.submit();
}
  • righe 5-8: la funzione JavaScript [$(document).ready(f)] è una funzione che viene eseguita quando il browser ha caricato l'intero documento inviato dal server. Il suo parametro è una funzione. Usiamo la funzione JavaScript [$(document).ready(f)] per inizializzare l'ambiente JavaScript del documento caricato;
  • riga 7: l'espressione [$("#lang")] è un'espressione jQuery. Il suo valore è un riferimento al nodo DOM con l'attributo [id='lang'];
  • riga 2: le variabili dichiarate al di fuori di una funzione sono globali per tutte le funzioni. In questo caso, ciò significa che la variabile [lang] inizializzata in [$(document).ready()] è disponibile anche nella funzione [setLocale] alla riga 11;
  • riga 13: modifica l'attributo [value] del nodo identificato da [lang]. Se lang è [xx_XX], il tag HTML per il nodo diventa:

<input type="hidden" value="xx_XX" id="lang" name="lang" />

JavaScript consente di modificare i valori degli elementi DOM (Document Object Model).

  • Riga 16: [document] si riferisce al DOM. [document.form] si riferisce al primo modulo trovato in questo documento. Un documento HTML può avere più tag <form> e quindi più moduli. Qui ne abbiamo solo uno. [document.form.submit] invia questo modulo come se l'utente avesse cliccato su un pulsante con l'attributo [type='submit']. A quale azione vengono inviati i valori del modulo? Per scoprirlo, guarda il tag [form] in [vue-01.xml]:

        <!-- form -->
        <form action="/someURL" th:action="@{/js02.html}" method="post" th:object="${form01}" name="form" id="form">

L'azione che riceverà i valori inviati è quella designata dall'attributo [th:action]. Si tratterà quindi dell'azione [/js02.html]. Ricorda che in questo nome, il suffisso [.html] verrà rimosso e, in definitiva, verrà eseguita l'azione [/js02]. È importante comprendere che il nuovo valore [xx_XX] del nodo [lang] verrà inviato nel modulo [lang=xx_XX]. Tuttavia, abbiamo configurato la nostra applicazione per intercettare il parametro [lang] e interpretarlo come un cambiamento di lingua. Pertanto, sul lato server, la lingua diventerà [xx_XX]. Esaminiamo l'azione [/js02] che verrà eseguita:


    @RequestMapping(value = "/js02", method = RequestMethod.POST, produces = "text/html; charset=UTF-8")
    public String js02(@Valid Form01 formulaire, BindingResult result, RedirectAttributes redirectAttributes, Locale locale, Model model) {
        Form01Validator validator = new Form01Validator(10, 13);
        validator.validate(formulaire, result);
        if (result.hasErrors()) {
            StringBuffer buffer = new StringBuffer();
            for (ObjectError error : result.getAllErrors()) {
                buffer.append(String.format("[name=%s,code=%s,message=%s]", error.getObjectName(), error.getCode(),
                        error.getDefaultMessage()));
            }
            setModel(formulaire, model, locale, buffer.toString());
            return "vue-01";
        } else {
            redirectAttributes.addFlashAttribute("form01", formulaire);
            return "redirect:/js01.html";
        }
    }
 
    // preparing the view-01 model
    private void setModel(Form01 formulaire, Model model, Locale locale, String message) {
        // we only manage fr-FR, en-US locales
        String language = locale.getLanguage();
        String country = null;
        if (language.equals("fr")) {
            country = "FR";
            formulaire.setLang("fr_FR");
        }
        if (language.equals("en")) {
            country = "US";
            formulaire.setLang("en_US");
        }
        model.addAttribute("locale", String.format("%s-%s", language, country));
        ...
}
  • riga 2: l'azione [/js02] riceverà la nuova impostazione locale [xx_XX] incapsulata nel parametro [Locale locale]:
  • righe 5-12: se uno qualsiasi dei valori inviati non è valido, verrà visualizzata la vista [vue-01.xml] con messaggi di errore utilizzando la nuova impostazione locale [xx_XX]. Inoltre, la riga 11 imposta la variabile [locale=xx-XX] nel modello. Sul lato client, questo valore verrà utilizzato per aggiornare l'impostazione locale del client. Abbiamo descritto questo processo;
  • Righe 14–15: se tutti i valori inviati sono validi, si verifica un reindirizzamento all'azione seguente [/js01]:

    @RequestMapping(value = "/js01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
    public String js01(Form01 formulaire, Locale locale, Model model) {
        setModel(formulaire, model, locale, null);
        return "vue-01";
}
  • riga 2: viene inserita la nuova impostazione locale [xx_XX];
  • riga 3: il metodo [setModel] imposterà quindi la lingua del client su [xx-XX];

Ora esaminiamo l'influenza delle impostazioni locali nella vista [view-01.xml]. Per il momento non abbiamo mostrato l'intera vista perché conta oltre 300 righe. Tuttavia, la maggior parte delle righe consiste in una sequenza simile alla seguente:


<!-- required -->
<tr>
  <td class="col1">required</td>
  <td class="col2">
    <input type="text" th:field="*{strNotEmpty}" data-val="true" th:attr="data-val-required=#{NotNull}" />
  </td>
    <td class="col3">
      <span class="field-validation-valid" data-valmsg-for="strNotEmpty" data-valmsg-replace="true"></span>
    </td>
  <td class="col4">
      <span th:if="${#fields.hasErrors('strNotEmpty')}" th:errors="*{strNotEmpty}" class="error">Donnée erronée</span>
  </td>
</tr>

Questo codice visualizza il seguente frammento [1]:

Il messaggio di errore [2] deriva dall'attributo [th:attr="data-val-required=#{NotNull}"] alla riga 5. [#{NotNull}] è un messaggio localizzato. A seconda delle impostazioni locali sul lato server, la riga 5 genera il tag:


<input type="text" data-val="true" data-val-required="Field is required" id="strNotEmpty" name="strNotEmpty" />

oppure il tag:


<input type="text" data-val="true" data-val-required="Le champ est obligatoire" id="strNotEmpty" name="strNotEmpty" />

Gli attributi [data-x] sono utilizzati dalla libreria JavaScript di validazione.

Infine, si noti che entrambi i link per il cambio di lingua:

  • innescano una richiesta POST per i valori inseriti;
  • modificano le impostazioni locali sia sul lato server che su quello client;
  • generano una pagina HTML che include messaggi di errore destinati alla libreria di validazione JavaScript e assicurano che tali messaggi siano nella lingua delle impostazioni locali selezionate;

6.3.7. Invio dei valori inseriti

Esaminiamo il pulsante [Validate], che invia i valori inseriti nella vista [vue-01.xml]. Il suo codice HTML è il seguente:


<!-- validation button -->
<input type="submit" value="Valider" onclick="javascript:postForm01()" />

Se JavaScript è abilitato nel browser, cliccando sul pulsante verrà avviata l'esecuzione del metodo [postForm01]. Se questa funzione restituisce il valore booleano [False], l'invio non avrà luogo. Se restituisce qualsiasi altro valore, l'invio avrà luogo. Questa funzione si trova nel file [local.js]:

 

Viene importata dalla vista [vue-01.xml] tramite la riga 6 riportata di seguito:


    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form01.css" />
...
        <script type="text/javascript" src="/js/local.js"></script>
</head>

In questo file troviamo il seguente codice:


// global variables
var formulaire;
var clientValidation;
var double1;
var double2;
var double3;
...
$(document).ready(function() {
    // global references
    formulaire = $("#form");
    clientValidation = $("#clientValidation");
    double1 = $("#double1");
    double2 = $("#double2");
    double3 = $("#double3");
...
});
....
// post form
function postForm01() {
...
}
  • righe 8–16: la funzione JavaScript [$(document).ready(f)] è una funzione che viene eseguita quando il browser ha caricato l'intero documento inviato dal server. Il suo parametro è una funzione. Utilizziamo la funzione JavaScript [$(document).ready(f)] per inizializzare l'ambiente JavaScript del documento caricato;
  • Righe 10–14: per comprendere queste righe, è necessario esaminare sia il codice Thymeleaf che il codice HTML generato;

Il codice Thymeleaf pertinente è il seguente:


<form action="/someURL" th:action="@{/js02.html}" method="post" th:object="${form01}" name="form" id="form">
...
<input type="text" th:field="*{double1}" th:value="*{double1}" ... />
...
<input type="text" th:field="*{double2}" th:value="*{double2}" />
...
<input type="text" th:field="*{double3}" th:value="*{double3}" ... />
...
<input type="hidden" th:field="*{clientValidation}" th:value="*{clientValidation}" value="true" />

che genera il seguente codice HTML:


<form action="/js02.html" method="post" name="form" id="form">
...
<input type="text" id="double1" name="double1" .../>
....
<input type="text" value="" id="double2" name="double2" />
...
<input value="" id="double3" name="double3" .../>
...
<input type="hidden" value="false" id="clientValidation" name="clientValidation" /> 

Ogni attributo [th:field='x'] genera due attributi HTML: [name='x'] e [id='x']. L'attributo [name] è il nome dei valori inviati. Pertanto, la presenza degli attributi [name='x'] e [value='y'] per un tag HTML <input type='text'> inserirà la stringa x=y nei valori inviati name1=val1&name2=val2&... L'attributo [id='x'] è utilizzato da JavaScript. Serve a identificare un elemento del DOM (Document Object Model). Il documento HTML caricato viene infatti trasformato in un albero JavaScript chiamato DOM, dove ogni nodo è identificato dal proprio attributo [id].

Torniamo al codice della funzione [$(document).ready()]:


// global variables
var formulaire;
var clientValidation;
var double1;
var double2;
var double3;
...
$(document).ready(function() {
    // global references
    formulaire = $("#form");
    clientValidation = $("#clientValidation");
    double1 = $("#double1");
    double2 = $("#double2");
    double3 = $("#double3");
...
});
....
// post form
function postForm01() {
...
}
  • riga 10: l'espressione [$("#form")] è un'espressione jQuery. Il suo valore è un riferimento al nodo DOM con l'attributo [id='form'];
  • righe 10–14: recuperiamo i riferimenti a cinque nodi DOM;
  • righe 2–6: le variabili dichiarate al di fuori di una funzione sono globali rispetto alle funzioni. In questo caso, ciò significa che le variabili [form, clientValidation, double1, double2, double3] inizializzate in [$(document).ready()] saranno disponibili anche nella funzione [postForm01] alla riga 19;

Ora, esaminiamo la funzione [postForm01]:


// post formulaire
function postForm01() {
    // mode de validation côté client
    var validationActive = clientValidation.val() === "true";
    if (validationActive) {
        // on efface les erreurs du serveur
        clearServerErrors();
        // validation du formulaire
        if (!formulaire.validate().form()) {
            // pas de submit
            return false;
        }
    }
    // réels au format anglo-saxon
    var value1 = double1.val().replace(",", ".");
    double1.val(value1);
    var value2 = double2.val().replace(",", ".");
    double2.val(value2);
    var value3 = double3.val().replace(",", ".");
    double3.val(value3);
    // on laisse le submit se faire
    return true;
}

Ricorda che questa funzione JavaScript viene eseguita prima dell'invio del modulo. Se restituisce [false] (riga 11), il modulo non verrà inviato. Se restituisce qualsiasi altro valore (riga 22), il modulo verrà inviato.

  • Il codice importante è quello delle righe 4–12;
  • riga 4: recuperiamo il valore del campo nascosto [clientValidation]. Questo valore è 'true' se la convalida lato client deve essere abilitata, 'false' in caso contrario;
  • riga 6: in caso di convalida lato client, cancelliamo eventuali messaggi di errore del server che potrebbero essere presenti perché l'utente ha appena cambiato le impostazioni locali;
  • riga 9: ricorda che la variabile [form] rappresenta il nodo del tag HTML <form>, ovvero il modulo. Questo modulo contiene validatori JavaScript che non abbiamo ancora trattato e che saranno discussi nelle sezioni seguenti. L'espressione [form.validate().form()] forza l'esecuzione di tutti i validatori JavaScript presenti nel modulo. Il suo valore è [true] se tutti i valori testati sono validi, [false] in caso contrario;
  • riga 11: il valore viene impostato su [false] se almeno uno dei valori testati non è valido. Ciò impedirà l'invio del modulo al server;
  • righe 15–20: gli identificatori [double1, double2, double3] rappresentano i tre numeri reali nel modulo. A seconda delle impostazioni locali, il valore inserito varia. Con le impostazioni locali [fr-FR], scriviamo [10,37], mentre con le impostazioni locali [en-US], scriviamo [10.37]. Questo copre l'input. Con le impostazioni locali [fr-FR], il valore inviato per [double1] apparirà come [double1=10,37]. Una volta raggiunto il server, il valore [10,37] verrà rifiutato perché il server si aspetta [10.37], il formato predefinito per i numeri reali in Java. Pertanto, le righe 15–20 sostituiscono la virgola con un punto nel valore inserito per questi numeri;
  • riga 15: l'espressione [double1.val()] restituisce la stringa inserita per il nodo [double1]. L'espressione [double1.val().replace(",", ".")] sostituisce le virgole in questa stringa con punti. Il risultato è una stringa [value1];
  • riga 16: l'istruzione [double1.val(value1)] assegna questo valore [value1] al nodo [double1].

Tecnicamente, se l'utente ha inserito [10,37] per il numero reale [double1], dopo le istruzioni precedenti il nodo [double1] ha il valore [10.37] e il valore che verrà inviato sarà [param1=val1&double1=10.37&param2=val2], un valore che sarà accettato dal server;

  • Riga 22: Impostiamo il valore su [true] in modo che venga eseguito il [submit] del modulo;

Si noti che la funzione JavaScript [postForm01]:

  • esegue tutti i validatori JavaScript del modulo se la validazione lato client è abilitata e impedisce l'invio del modulo al server se uno qualsiasi dei valori inseriti è stato dichiarato non valido;
  • consente di cliccare sul pulsante [submit] sia perché la convalida lato client non è abilitata, sia perché è abilitata e tutti i valori inseriti sono validi;

Rimane quindi l'istruzione alla riga [3]:


    // on efface les erreurs du serveur
clearServerErrors();

Lo scopo della funzione [clearServerErrors] è quello di cancellare i messaggi nella colonna 4 della vista [vue-01.xml]:

Nello screenshot qui sopra, abbiamo cliccato sul link [English]. Abbiamo visto che questo ha innescato un POST dei valori inseriti senza attivare i validatori JavaScript. Al ritorno del POST, la colonna [Server Validation] si riempie di eventuali messaggi di errore. Se ora clicchiamo sul pulsante [Validate] [2] con i validatori JavaScript abilitati [3], allora la colonna [Client Validation] [4] si riempirà di messaggi. Se non facciamo nulla, i messaggi presenti nella colonna [Convalida server] rimarranno, il che causerà confusione poiché, nel caso di errori rilevati dai validatori JavaScript, il server non viene interpellato. Per evitare ciò, cancelliamo la colonna [Convalida server] nella funzione [postForm01]. La funzione [clearServerErrors()] esegue questa operazione:


function clearServerErrors() {
    // delete server error msgs
    $(".error").each(function(index) {
        $(this).text("");
    });
}

Una caratteristica distintiva dei messaggi di errore è che hanno tutti la classe [error]. Ad esempio, per la prima riga della tabella in [vue-01.html]:


<span th:if="${#fields.hasErrors('strNotEmpty')}" th:errors="*{strNotEmpty}" class="error">Donnée erronée</span>

E questi sono gli unici nodi nel DOM con questa classe. Usiamo questa proprietà nella funzione [clearServerErrors]:


function clearServerErrors() {
    // delete server error msgs
    $(".error").each(function(index) {
        $(this).text("");
    });
}
  • riga 3: l'espressione [$(".error")] restituisce la collezione di nodi DOM con la classe [error];
  • riga 3: l'espressione [$(".error").each(function(index){f}] esegue la funzione [f] per ogni nodo della collezione. Riceve un parametro [index] — che qui non viene utilizzato — che rappresenta l'indice del nodo nella collezione;
  • riga 4: l'espressione [$(this)] si riferisce al nodo corrente nell'iterazione. Si tratta di un tag span HTML. L'espressione [$(this).text("")] assegna la stringa vuota al testo visualizzato dal tag span;

Esamineremo ora vari validatori JavaScript.

6.3.8. Validatore [required]

Esaminiamo il primo elemento del modulo:

La riga [1] è generata dalla seguente sequenza nella vista [vue-01.xml]:


<!-- required -->
<tr>
  <td class="col1">required</td>
  <td class="col2">
    <input type="text" th:field="*{strNotEmpty}" data-val="true" th:attr="data-val-required=#{NotNull}" />
  </td>
  <td class="col3">
    <span class="field-validation-valid" data-valmsg-for="strNotEmpty" data-valmsg-replace="true"></span>
  </td>
  <td class="col4">
    <span th:if="${#fields.hasErrors('strNotEmpty')}" th:errors="*{strNotEmpty}" class="error">Donnée erronée</span>
  </td>
</tr>

Queste righe si riferiscono al campo [strNotEmpty] nel modulo [Form01]:


    @NotNull
    @NotBlank
private String strNotEmpty;

I vincoli [1-2] garantiscono che il campo [strNotEmpty] sia una stringa valida [NotNull], non sia vuoto e non sia composto esclusivamente da spazi [NotBlank]. Vogliamo replicare questo vincolo sul lato client utilizzando JavaScript.

Esaminiamo le righe 5 e 8. La riga 11 non pone alcun problema. Visualizza il messaggio di errore associato al campo [strNotEmpty]. Cominciamo con la riga 5:


<input type="text" th:field="*{strNotEmpty}" data-val="true" th:attr="data-val-required=#{NotNull}" />

Da questo codice, Thymeleaf genererà il seguente tag:


<input type="text" data-val="true" data-val-required="Field is required" id="strNotEmpty" name="strNotEmpty" value="x" />
  • L'attributo [data-val='true'] è utilizzato dalle librerie di validazione jQuery. La sua presenza indica che il valore del nodo è soggetto a validazione;
  • L'attributo [data-val-X='msg'] fornisce due informazioni. [X] è il nome del validatore e [msg] è il messaggio di errore associato a un valore non valido del nodo su cui viene applicato il validatore. Si tratta semplicemente di un'informazione. Non comporta la visualizzazione del messaggio di errore;
  • [required] è un validatore riconosciuto dalla libreria di validazione [jquery.validate.unobstrusive] di Microsoft. Non è necessario definirlo. In futuro, però, non sarà sempre così;
  • gli attributi [data-x] vengono ignorati da HTML5. Sono utili solo se c'è del JavaScript che li utilizza;

Esaminiamo ora la riga 8:


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

Viene utilizzato per visualizzare il messaggio di errore del validatore [required]. In caso di errore, la libreria JavaScript di validazione sostituirà dinamicamente la riga HTML nella tabella con il seguente codice:


<tr>
    <td class="col1">required</td>
    <td class="col2">
        <input type="text" data-val="true" data-val-required="Le champ est obligatoire" id="strNotEmpty" name="strNotEmpty" value="" aria-required="true" aria-invalid="true" aria-describedby="strNotEmpty-error" class="input-validation-error">
    </td>
    <td class="col3">
        <span class="field-validation-error" data-valmsg-for="strNotEmpty" data-valmsg-replace="true">
            <span id="strNotEmpty-error" class="">Le champ est obligatoire</span>
        </span>
    </td>
    <td class="col4">
        <span class="error"></span>
    </td>
</tr>
</tr>
  • riga 4: la classe del nodo [strNotEmpty] è cambiata. È diventata [input-validation-error], il che fa sì che il campo non valido venga colorato di rosso;
  • riga 7: la classe dello [span] è cambiata. È diventata [field-validation-error], il che farà visualizzare il testo dello [span] in rosso;
  • riga 8: lo [span], che prima era vuoto, ora contiene il testo [Questo campo è obbligatorio]. Questo testo proviene dall'attributo [data-val-required="Questo campo è obbligatorio"] alla riga 4;
  • riga 7: per visualizzare il messaggio di errore per il nodo [strNotEmpty] alla riga 4, utilizzare gli attributi [data-valmsg-for="strNotEmpty"] e [data-valmsg-replace="true"] alla riga 7;

6.3.9. Validatore [assertfalse]

La riga [1] è generata dalla seguente sequenza nella vista [vue-01.xml]:


<!-- required, assertfalse -->
<tr>
    <td class="col1">required, assertfalse</td>
    <td class="col2">
        <input type="radio" th:field="*{assertFalse}" value="true" data-val="true"
            th:attr="data-val-required=#{NotNull},data-val-assertfalse=#{AssertFalse}" />
        <label th:for="${#ids.prev('assertFalse')}">true</label>
        <input type="radio" th:field="*{assertFalse}" value="false" data-val="true"
            th:attr="data-val-required=#{NotNull},data-val-assertfalse=#{AssertFalse}" />
        <label th:for="${#ids.prev('assertFalse')}">false</label>
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="assertFalse" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('assertFalse')}" th:errors="*{assertFalse}" class="error">Donnée erronée</span>
    </td>
</tr>

Queste righe si riferiscono al campo [assertFalse] nel modulo [Form01]:


    @NotNull
    @AssertFalse
private Boolean assertFalse;

Vogliamo replicare questo vincolo sul lato client utilizzando JavaScript. Le righe 12–17 sono ora standard:

  • righe 12–14: visualizzano, in caso di errore sul campo [assertFalse], il messaggio contenuto nell'attributo [data-val-assertfalse] alla riga 6 o quello contenuto nell'attributo [data-val-required] sulla stessa riga. Si noti che questi messaggi sono localizzati, ovvero nella lingua precedentemente selezionata dall'utente o in francese se non è stata effettuata alcuna scelta;
  • Righe 5–10: visualizzano i pulsanti di opzione con validatori JavaScript che si attivano non appena l'utente clicca su uno di essi.

Entrambi i pulsanti sono costruiti allo stesso modo. Esaminiamo il primo:


<input type="radio" th:field="*{assertFalse}" value="true" data-val="true" th:attr="data-val-required=#{NotNull},data-val-assertfalse=#{AssertFalse}" />

Una volta elaborata da Thymeleaf, questa riga diventa la seguente:


<input type="radio" value="true" data-val="true" data-val-required="Le champ est obligatoire" data-val-assertfalse="Seule la valeur False est acceptée" id="assertFalse1" name="assertFalse" />

Abbiamo dei validatori [data-val="true"]. Ce ne sono due. Un validatore denominato [required] [data-val-required="Questo campo è obbligatorio"] e un altro denominato [assertfalse] [data-val-assertfalse="È accettato solo il valore False"]. Ricorda che il valore dell'attributo [data-val-X] corrisponde al messaggio di errore per il validatore X.

Abbiamo già visto il validatore [required]. La novità qui è che possiamo associare più validatori a un singolo valore di input. Mentre il validatore [required] è riconosciuto dalla libreria di validazione MS (Microsoft), questo non è il caso del validatore [assertFalse]. Impareremo quindi come creare un nuovo validatore. Ne creeremo diversi, che saranno inseriti in un file denominato [client-validation.js]:

  

Questo file, come gli altri, viene importato dalla vista [vue-01.xml] (riga 6 qui sotto):


    <head>
        <title>Spring 4 MVC</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" href="/css/form01.css" />
        ...
        <script type="text/javascript" src="/js/client-validation.js"></script>
...
</head>

Per aggiungere il validatore [assertfalse] è sufficiente creare le seguenti due funzioni JavaScript:


// -------------- assertfalse
$.validator.addMethod("assertfalse", function(value, element, param) {
    return value === "false";
});
 
$.validator.unobtrusive.adapters.add("assertfalse", [], function(options) {
    options.rules["assertfalse"] = options.params;
    options.messages["assertfalse"] = options.message.replace("''", "'");
});

Ad essere sincero, non sono un esperto di JavaScript; è un linguaggio che mi sembra ancora piuttosto misterioso. I suoi fondamenti sono semplici, ma le librerie che vi si basano sono spesso molto complesse. Per scrivere il codice qui sopra, ho tratto ispirazione da alcuni esempi trovati online. È stato il link [http://jsfiddle.net/LDDrk/] a indicarmi la strada da seguire. Se esiste ancora, invito i lettori a dargli un'occhiata, poiché è completo e include un esempio funzionante. Mostra come creare un nuovo validatore e mi ha permesso di creare tutti quelli presenti in questo capitolo. Torniamo al codice:

  • righe 2–4: definiscono il nuovo validatore. La funzione [$.validator.addMethod] accetta il nome del validatore come primo parametro e una funzione che definisce il validatore come secondo parametro;
  • riga 2: la funzione ha tre parametri:
    • [value]: il valore da convalidare. La funzione deve restituire [true] se il valore è valido, [false] in caso contrario;
    • [element]: l'elemento HTML a cui appartiene il valore da convalidare,
    • [param]: un oggetto contenente i valori associati ai parametri di un validatore. Non abbiamo ancora introdotto questo concetto. Qui, il validatore [assertFalse] non ha parametri. Possiamo determinare se il valore [value] è valido senza informazioni aggiuntive. Sarebbe diverso se dovessimo verificare che il valore [value] fosse un numero reale nell'intervallo [min, max]. In quel caso, avremmo bisogno di conoscere [min] e [max]. Questi due valori sono chiamati parametri del validatore;
  • righe 6–9: una funzione richiesta dalla libreria di validazione MS. La funzione [$.validator.unobtrusive.adapters.add] si aspetta, come primo parametro, il nome del validatore; come secondo parametro, l'array dei parametri del validatore; e come terzo parametro, una funzione;
  • il validatore [assertFalse] non ha parametri. Ecco perché il secondo parametro è un array vuoto;
  • la funzione ha un solo parametro, un oggetto [options] che contiene informazioni sull'elemento da validare e per il quale devono essere definite due nuove proprietà, [rules] e [messages];
    • riga 7: definiamo le regole [rules] per il validatore [assertFalse]. Queste regole sono i parametri del validatore [assertFalse], gli stessi del parametro [param] alla riga 2. Questi parametri si trovano in [options.params];
    • riga 8: definisce il messaggio di errore per il validatore [assertFalse]. Questo si trova in [options.message]. Riscontriamo il seguente problema con i messaggi di errore. Nei file dei messaggi, troveremo il seguente messaggio:

Range.form01.int1014=La valeur doit être dans l''intervalle [10,14]

Il doppio apostrofo è necessario per Thymeleaf. Thymeleaf lo interpreta come un apostrofo singolo. Se si utilizza un apostrofo singolo, Thymeleaf non lo visualizza. Ora questi messaggi fungeranno anche da messaggi di errore per la libreria di validazione MS. Tuttavia, JavaScript visualizzerà entrambi gli apostrofi. Alla riga 8, sostituiamo quindi il doppio apostrofo nel messaggio di errore con uno singolo.

Per avere un'idea più chiara di ciò che sta accadendo, possiamo aggiungere del codice di logging JavaScript:


// logs
var logs = {
    assertfalse : true
}
 
 
// -------------- assertfalse
$.validator.addMethod("assertfalse", function(value, element, param) {
    // logs
    if (logs.assertfalse) {
        console.log(jSON.stringify({
            "[assertfalse] value" : value
        }));
        console.log("[assertfalse] element");
        console.log(element);
        console.log(jSON.stringify({
            "[assertfalse] param" : param
        }));
    }
    // test validity
    return value === "false";
});
 
$.validator.unobtrusive.adapters.add("assertfalse", [], function(options) {
    // logs
    if (logs.assertfalse) {
        console.log(jSON.stringify({
            "[assertfalse] options.params" : options.params
        }));
        console.log(jSON.stringify({
            "[assertfalse] options.message" : options.message
        }));
        console.log(jSON.stringify({
            "[assertfalse] options.messages" : options.messages
        }));
    }
    // code
    options.rules["assertfalse"] = options.params;
    options.messages["assertfalse"] = options.message.replace("''", "'");
});

Questo codice utilizza la libreria JSON3 [http://bestiejs.github.io/json3/]. Se abilitiamo la registrazione (riga 3), otteniamo il seguente output nella console:

Al caricamento iniziale della pagina, vengono visualizzati i seguenti log:

 

È stata eseguita la funzione jS [$.validator.unobtrusive.adapters.add]. Ne ricaviamo quanto segue:

  • [options.params] è un oggetto vuoto perché il validatore [assertFalse] non ha parametri;
  • [options.message] è il messaggio di errore che abbiamo creato per il validatore [assertFalse] nell'attributo [data-val-assertFalse];
  • [options.messages] è un oggetto contenente gli altri messaggi di errore per l'elemento validato. Qui troviamo il messaggio di errore che abbiamo inserito nell'attributo [data-val-required];

Ora inseriamo un valore errato nel campo [assertFalse] e validiamo:

 

Otterremo quindi i seguenti log:

Qui vediamo quanto segue:

  • il valore sottoposto a test è [true] (riga 118);
  • l'elemento HTML sottoposto al test è il pulsante di opzione con l'ID [assertFalse1] (riga 122);
  • il validatore [assertFalse] non ha parametri (riga 123);

Ecco fatto. Cosa possiamo trarre da tutto questo?

Per un validatore jS X, dobbiamo definire:

  • nel tag HTML da validare, l'attributo [data-val-X='msg'], che definisce sia il validatore XJS che il suo messaggio di errore;
  • due funzioni JavaScript da inserire nel file [client-validation.js]:
    • [$.validator.addMethod("X", function(value, element, param)],
    • [$.validator.unobtrusive.adapters.add("X", [param1, param2], function(options)];

Andando avanti, partiremo da quanto fatto per questo primo validatore e presenteremo semplicemente le novità.

6.3.10. Validatore [asserttrue]

Questo validatore è, ovviamente, analogo al validatore [assertFalse].

La riga [1] è generata dalla seguente sequenza nella vista [vue-01.xml]:


<!-- required, asserttrue -->
<tr>
    <td class="col1">asserttrue</td>
    <td class="col2">
        <select th:field="*{assertTrue}" data-val="true" th:attr="data-val-asserttrue=#{AssertTrue}">
            <option value="true">True</option>
            <option value="false">False</option>
        </select>
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="assertTrue" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('assertTrue')}" th:errors="*{assertTrue}" class="error">Donnée erronée</span>
    </td>
</tr>

Queste righe si riferiscono al campo [assertTrue] nel modulo [Form01]:


    @NotNull
    @AssertTrue
private Boolean assertTrue;

Non c'è nulla di nuovo nelle righe 1–16. Utilizzano un validatore [assertTrue] che deve essere definito nel file [client-validation.js]:


// -------------- asserttrue
$.validator.addMethod("asserttrue", function(value, element, param) {
    return value === "true";
});
 
$.validator.unobtrusive.adapters.add("asserttrue", [], function(options) {
    options.rules["asserttrue"] = options.params;
    options.messages["asserttrue"] = options.message.replace("''", "'");
});

6.3.11. Validatori [date] e [past]

La riga [1] è generata dalla seguente sequenza nella vista [vue-01.xml]:


<!-- required, date, past -->
<tr>
    <td class="col1">required, date, past</td>
    <td class="col2">
        <input type="date" th:field="*{dateInPast}" th:value="*{dateInPast}" data-val="true"
            th:attr="data-val-required=#{NotNull},data-val-date=#{DateInvalide.form01},data-val-past=#{Past.form01.dateInPast}" />
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="dateInPast" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('dateInPast')}" th:errors="*{dateInPast}" class="error">Donnée erronée</span>
    </td>
</tr>

Queste righe si riferiscono al campo [dateInPast] nel modulo [Form01]:


    @NotNull
    @Past
    @DateTimeFormat(pattern = "yyyy-MM-dd")
private Date dateInPast;

La riga relativa ai validatori di data è la seguente:


<input type="date" th:field="*{dateInPast}" th:value="*{dateInPast}" data-val="true"             th:attr="data-val-required=#{NotNull},data-val-date=#{DateInvalide.form01},data-val-past=#{Past.form01.dateInPast}" />

Esistono tre validatori [data-val-X]: required, date e past. Dobbiamo definire le funzioni associate a questi due nuovi validatori in [client-validation.js]:


logs.date = true;
// -------------- date
$.validator.addMethod("date", function(value, element, param) {
    // validity
    var valide = Globalize.parseDate(value, "yyyy-MM-dd") != null;
    // logs
    if (logs.date) {
        console.log(jSON.stringify({
            "[date] value" : value,
            "[date] valide" : valide
        }));
    }
    // result
    return valide;
});
 
$.validator.unobtrusive.adapters.add("date", [], function(options) {
    options.rules["date"] = options.params;
    options.messages["date"] = options.message.replace("''", "'");
});

e


logs.past = true;
// -------------- past
$.validator.addMethod("past", function(value, element, param) {
    // validity
    var valide = value <= new Date().toISOString().substring(0, 10);
    // logs
    if (logs.past) {
        console.log(jSON.stringify({
            "[past] value" : value,
            "[past] valide" : valide
        }));
    }
    // result
    return valide;
});
 
$.validator.unobtrusive.adapters.add("past", [], function(options) {
    options.rules["past"] = options.params;
    options.messages["past"] = options.message.replace("''", "'");
});

Prima di spiegare il codice, diamo un'occhiata ai log quando si inserisce una data successiva a quella odierna:

 

La prima cosa da notare è che la data da convalidare arriva come stringa nel formato [yyyy-mm-dd]. Questo spiega le righe seguenti:


var valide = Globalize.parseDate(value, "yyyy-MM-dd") != null;

La libreria [globalize.js] mette a disposizione la funzione [Globalize.parseDate] sopra riportata. Il primo parametro è la data sotto forma di stringa, mentre il secondo è il suo formato. Il risultato è un puntatore nullo se la data non è valida, oppure la data risultante in caso contrario.

La validità del validatore [past] viene verificata dal seguente codice:


var valide = value <= new Date().toISOString().substring(0, 10);

Ecco la valutazione dell'espressione [new Date().toISOString().substring(0, 10)] in una console:

  

Per essere valida, la stringa [value] deve precedere la stringa [new Date().toISOString().substring(0, 10)] in ordine alfabetico.

Si noti che la versione di Chrome utilizzata fornisce la data nel formato [yyyy-mm-dd]. Per un browser in cui ciò non avviene, l'utente dovrebbe ricevere istruzioni esplicite sull'uso di questo formato di immissione.

6.3.12. Validatore [futuro]

La riga [1] è generata dalla seguente sequenza nella vista [vue-01.xml]:


<!-- required, date, future -->
<tr>
    <td class="col1">required, date, future</td>
    <td class="col2">
        <input type="date" th:field="*{dateInFuture}" th:value="*{dateInFuture}" data-val="true" th:attr="data-val-required=#{NotNull},data-val-date=#{DateInvalide.form01},data-val-future=#{Future.form01.dateInFuture}" />
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="dateInFuture" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('dateInFuture')}" th:errors="*{dateInFuture}" class="error">Donnée erronée</span>
    </td>
</tr>

Queste righe si riferiscono al campo [dateInFuture] nel modulo [Form01]:


    @NotNull
    @Future
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dateInFuture;
  • alla riga 5 compare un nuovo validatore [data-val-future];

Questo validatore è, ovviamente, molto simile al validatore [past]. Le due funzioni da aggiungere a [client-validation.js] sono le seguenti:


// -------------- future
$.validator.addMethod("future", function(value, element, param) {
    var now = new Date().toISOString().substring(0, 10);
    return value > now;
});
 
$.validator.unobtrusive.adapters.add("future", [], function(options) {
    options.rules["future"] = options.params;
    options.messages["future"] = options.message.replace("''", "'");
});

6.3.13. Validatori [int] e [max]

La riga [1] è generata dalla seguente sequenza nella vista [vue-01.xml]:


<!-- required, int, max(100) -->
<tr>
    <td class="col1">required, int, max(100)</td>
    <td class="col2">
        <input type="text" th:field="*{intMax100}" th:value="*{intMax100}" data-val="true"             th:attr="data-val-required=#{NotNull},data-val-int=#{typeMismatch},data-val-max=#{Max.form01.intMax100},data-val-max-value=#{form01.intMax100.value}" />
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="intMax100" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('intMax100')}" th:errors="*{intMax100}" class="error">Donnée erronée</span>
    </td>
</tr>

Queste righe si riferiscono al campo [intMax100] nel modulo [Form01]:


    @NotNull
    @Max(value = 100)
    private Integer intMax100;

La riga 5 contiene due nuovi validatori: [int] e [max]. Il secondo ha un solo parametro: il valore massimo. Esaminiamo il codice HTML generato dalla riga 5:


<!-- required, int, max(100) -->
<tr>
    <td class="col1">required, int, max(100)</td>
    <td class="col2">
        <input type="text" data-val="true" data-val-int="Format invalide" data-val-max-value="100" data-val-required="Le champ est obligatoire" data-val-max="La valeur doit être inférieure ou égale à 100" value="" id="intMax100" name="intMax100" />
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="intMax100" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
 
    </td>
</tr>

Esaminiamo il significato dei vari attributi [data-X]:

  • [data-val="true"] indica che all'elemento HTML sono associati dei validatori;
  • [data-val-required] introduce il validatore [required] con il relativo messaggio;
  • [data-val-int] introduce il validatore [int] con il relativo messaggio;
  • [data-val-max] introduce il validatore [max] con il relativo messaggio;
  • [data-val-max-value="100"] introduce un parametro denominato [value] per il validatore [max]. [100] è il valore di questo parametro. È la prima volta che incontriamo il concetto di parametri dei validatori.

Il file [client-validation.js] è stato arricchito con il seguente validatore [int]:


logs.int = true;
// -------------- int
$.validator.addMethod("int", function(value, element, param) {
    // validity
    valide = /^\s*[-\+]?\s*\d+\s*$/.test(value);
    // logs
    if (logs.int) {
        console.log(jSON.stringify({
            "[int] value" : value,
            "[int] valide" : valide,
        }));
    }
    // result
    return valide;
});
 
$.validator.unobtrusive.adapters.add("int", [], function(options) {
    options.rules["int"] = options.params;
    options.messages["int"] = options.message.replace("''", "'");
});
  • Riga 5: viene utilizzata un'espressione regolare per verificare che la stringa [value] sia effettivamente un numero intero. Questo numero intero può essere con segno;

Ecco alcuni esempi di log:

1
2
3
{"[int] value":"x","[int] valide":false}
{"[int] value":"11","[int] valide":true}
{"[int] value":"11x","[int] valide":false}

Il validatore [max] viene aggiunto come segue in [client-validation.js]


// -------------- max to be used in conjunction with [int] or [number]
logs.max = true;
$.validator.addMethod("max", function(value, element, param) {
    // logs
    if (logs.max) {
        console.log(jSON.stringify({
            "[max] value" : value,
            "[max] param" : param
        }));
    }
    // validity
    var val = Globalize.parseFloat(value);
    if (isNaN(val)) {
        // logs
        if (logs.max) {
            console.log(jSON.stringify({
                "[max] valide" : true
            }));
        }
        // result
        return true;
    }
    var max = Globalize.parseFloat(param.value);
    var valide = val <= max;
    // logs
    if (logs.max) {
        console.log(jSON.stringify({
            "[max] valide" : valide
        }));
    }
    // result
    return valide;
});
 
$.validator.unobtrusive.adapters.add("max", [ "value" ], function(options) {
    options.rules["max"] = options.params;
    options.messages["max"] = options.message.replace("''", "'");
});

Affronteremo ora il caso del parametro [value] del validatore [max] introdotto dall'attributo [data-val-max-value="100"].

  • alla riga 35, il parametro [value] viene passato come secondo argomento alla funzione [$.validator.unobtrusive.adapters.add];
  • alla riga 3, l'oggetto [param] non sarà più vuoto, ma conterrà {"value":100};

Per comprendere il codice nelle righe 3–33, è necessario sapere che quando sono presenti più validatori sullo stesso elemento HTML:

  • l'ordine in cui i validatori vengono eseguiti è sconosciuto;
  • l'esecuzione dei validatori si interrompe non appena un validatore dichiara l'elemento non valido. È quindi il messaggio di errore di quel validatore ad essere associato all'elemento non valido;

Esaminiamo il codice:

  • riga 12: verifichiamo di avere un numero. Se il validatore [int] è stato eseguito prima del validatore [max], ciò è necessariamente vero poiché un valore non valido interrompe l'esecuzione dei validatori;
  • righe 13–22: se non abbiamo un numero, ciò significa che il validatore [int] non è stato ancora eseguito. Indichiamo quindi che il valore testato è valido per consentire al validatore [int] di svolgere il proprio lavoro e dichiarare l'elemento non valido con il proprio messaggio di errore;
  • righe 23–24: calcola la validità di [value];

Ecco alcuni log:

Valore inserito
log
x

{"[max] value":"x","[max] param":{"value":"100"}}
{"[max] valid":true}
{"[int] valore":"x","[int] valido":false}
111

{"[max] valore":"111","[max] param":{"valore":"100"}}
{"[max] valid":false}
111x

{"[max] value":"111x","[max] param":{"value":"100"}}
{"[max] valid":true}
{"[int] valore":"111x","[int] valido":false}

6.3.14. [min] validatore

La riga [1] è generata dalla seguente sequenza nella vista [vue-01.xml]:


<!-- required, int, min(10) -->
<tr>
    <td class="col1">required, int, min(10)</td>
    <td class="col2">
        <input type="text" th:field="*{intMin10}" th:value="*{intMin10}" data-val="true"             th:attr="data-val-required=#{NotNull},data-val-int=#{typeMismatch},data-val-min=#{Min.form01.intMin10},data-val-min-value=#{form01.intMin10.value}" />
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="intMin10" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('intMin10')}" th:errors="*{intMin10}" class="error">Donnée erronée</span>
    </td>
</tr>

Queste righe riguardano il campo [intMin10] nel modulo [Form01]:


    @NotNull
    @Min(value = 10)
private Integer intMin10;

La riga 5 introduce un nuovo validatore [min] [data-val-int=#{typeMismatch}] con un parametro [value] [data-val-min-value=#{form01.intMin10.value}"]. È simile al validatore [max]. Aggiungi il seguente codice a [client-validation.js]:


logs.min = true;
//-------------- min to be used in conjunction with [int] or [number]
$.validator.addMethod("min", function(value, element, param) {
    // logs
    if (logs.min) {
        console.log(jSON.stringify({
            "[min] value" : value,
            "[min] param" : param
        }));
    }
    // validity
    var val = Globalize.parseFloat(value);
    if (isNaN(val)) {
        // logs
        if (logs.min) {
            console.log(jSON.stringify({
                "[min] valide" : true
            }));
        }
        // result
        return true;
    }
    var min = Globalize.parseFloat(param.value);
    var valide = val >= min;
    // logs
    if (logs.min) {
        console.log(jSON.stringify({
            "[min] valide" : valide
        }));
    }
    // result
    return valide;
});
 
$.validator.unobtrusive.adapters.add("min", [ "value" ], function(options) {
    options.rules["min"] = options.params;
    options.messages["min"] = options.message.replace("''", "'");
});

Ecco alcuni log di esecuzione:

Valore inserito
log
x

{"[min] value":"x","[min] param":{"value":"10"}}
{"[min] valido":true}
{"[int] valore":"x","[int] valido":false}
11

{"[min] valore":"11","[min] param":{"valore":"10"}}
{"[min] valid":true}
{"[int] valore":"11","[int] valido":true}
8x

{"[min] value":"8x","[min] param":{"value":"10"}}
{"[min] valid":true}
{"[int] valore":"8x","[int] valido":false}

6.3.15. [regex] Validatore

La riga [1] è generata dalla seguente sequenza nella vista [vue-01.xml]:


<!-- required, regex -->
<tr>
    <td class="col1">required, regex</td>
    <td class="col2">
        <input type="text" th:field="*{strBetween4and6}" th:value="*{strBetween4and6}" data-val="true"    th:attr="data-val-required=#{NotNull},data-val-regex=#{Size.form01.strBetween4and6}, data-val-regex-pattern=#{form01.strBetween4and6.pattern}" />
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="strBetween4and6" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('strBetween4and6')}" th:errors="*{strBetween4and6}" class="error">Donnée erronée</span>
    </td>
</tr>

Queste righe si riferiscono al campo [strBetween4and6] nel modulo [Form01]:


    @NotNull
    @Size(min = 4, max = 6)
    private String strBetween4and6;

La riga 5 genera il seguente codice HTML:


<input type="text" data-val="true" data-val-required="Le champ est obligatoire" data-val-regex="La chaîne doit avoir entre 4 et 6 caractères" data-val-regex-pattern="^.{4,6}$" value="" id="strBetween4and6" name="strBetween4and6" />

Questo tag introduce il validatore [regex] [data-val-regex="La stringa deve avere tra 4 e 6 caratteri"] con il suo parametro [pattern] [data-val-regex-pattern="^.{4,6}$"]. Il parametro [pattern] è l'espressione regolare rispetto alla quale deve essere verificato il valore da validare. In questo caso, l'espressione regolare verifica che la stringa contenga da 4 a 6 caratteri di qualsiasi tipo. Il validatore [regex] è predefinito nella libreria di validazione MS. Pertanto, non è necessario aggiungere nulla al file [client-validation.js].

6.3.16. Validatore [email]

La riga [1] è generata dalla seguente sequenza nella vista [vue-01.xml]:


<!-- required, email -->
<tr>
    <td class="col1">required, email</td>
    <td class="col2">
        <input type="text" th:field="*{email}" th:value="*{email}" data-val="true"             th:attr="data-val-required=#{NotNull},data-val-email=#{Email.form01.email}" />
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="email" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="error">Donnée erronée</span>
    </td>
</tr>

Queste righe si riferiscono al campo [email] nel modulo [Form01]:


    @NotNull
    @Email
    @NotBlank
    private String email;

La riga 5 genera la seguente riga HTML:


<input type="text" data-val="true" data-val-required="Le champ est obligatoire" data-val-email="Adresse mail invalide"    value="" id="email" name="email" />

Questo tag introduce il validatore [email] [data-val-email="Indirizzo email non valido"]. Il validatore [email] è predefinito nella libreria di validazione MS. Pertanto, non è necessario aggiungere nulla al file [client-validation.js].

6.3.17. Validatore [range]

La riga [1] è generata dalla seguente sequenza nella vista [vue-01.xml]:


<!-- required, int, range (10,14) -->
<tr>
    <td class="col1">required, int, range (10,14)</td>
    <td class="col2">
        <input type="text" th:field="*{int1014}" th:value="*{int1014}" data-val="true"            th:attr="data-val-required=#{NotNull},data-val-int=#{typeMismatch}, data-val-range=#{Range.form01.int1014},data-val-range-max=#{form01.int1014.max},data-val-range-min=#{form01.int1014.min}" />
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="int1014" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('int1014')}" th:errors="*{int1014}" class="error">Donnée erronée</span>
    </td>
</tr>

Queste righe si applicano al campo [int1014] nel modulo [Form01]:


    @Range(min = 10, max = 14)
    @NotNull
    private Integer int1014;

La riga 5 genera la seguente riga HTML:


<input type="text" data-val="true" data-val-range-max="14" data-val-range="La valeur doit être dans l&#39;&#39;intervalle [10,14]" data-val-int="Format invalide" data-val-required="Le champ est obligatoire" data-val-range-min="10" value="" id="int1014" name="int1014" />

Questo tag introduce un nuovo validatore [range] [data-val-range="Il valore deve rientrare nell&#39;intervallo [10,14]"] che ha due parametri: [min] [data-val-range-min="10"] e [max] [data-val-range-max="14"].

Nel file [client-validation.js], definiamo il validatore [range] come segue:


// -------------- range to be used in conjunction with [int] or [number]
logs.range=true
$.validator.addMethod("range", function(value, element, param) {
    // logs
    if (logs.range) {
        console.log(jSON.stringify({
            "[range] value" : value,
            "[range] param" : param
        }));
    }
    // validity
    var val = Globalize.parseFloat(value);
    if (isNaN(val)) {
        // logs
        if (logs.min) {
            console.log(jSON.stringify({
                "[range] valide" : true
            }));
        }
        // completed
        return true;
    }
    var min = Globalize.parseFloat(param.min);
    var max = Globalize.parseFloat(param.max);    
    var valide = val >= min && val <= max;
    // logs
    if (logs.range) {
        console.log(jSON.stringify({
            "[range] valide" : valide
        }));
    }
    // completed
    return valide;
});
 
$.validator.unobtrusive.adapters.add("range", [ "min", "max" ], function(options) {
    options.rules["range"] = options.params;
    options.messages["range"] = options.message.replace("''", "'");
});

È molto simile ai validatori [min] e [max] di cui abbiamo già parlato.

Ecco alcuni esempi di registri:

Valore inserito
registrazioni
x

{"[range] value":"x","[range] param":{"min":"10","max":"14"}}
{"[int] valore":"x","[int] valido":false}
8

{"[range] value":"8","[range] param":{"min":"10","max":"14"}}
{"[range] valid":false}
11

{"[range] valid":true}
{"[int] value":"11","[int] valid":true}

6.3.18. [numero] Validatore

La riga [1] è generata dalla seguente sequenza nella vista [vue-01.xml]:


<!-- double1 : required, number, range (2.3,3.4) -->
<tr>
    <td class="col1">double1 : required, number, range (2.3,3.4)</td>
    <td class="col2">
        <input type="text" th:field="*{double1}" th:value="*{double1}" data-val="true"
            th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-range=#{Range.form01.double1},data-val-range-max=#{form01.double1.max},data-val-range-min=#{form01.double1.min}" />
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="double1" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('double1')}" th:errors="*{double1}" class="error">Donnée erronée</span>
    </td>
</tr>

Queste righe riguardano il campo [double1] del modulo [Form01]:


    @NotNull
    @DecimalMax(value = "3.4")
    @DecimalMin(value = "2.3")
private Double double1;

La riga 5 genera la seguente riga HTML:


<input type="text" data-val="true" data-val-number="Format invalide" data-val-range-max="3.4"     data-val-range="La valeur doit être dans l&#39;intervalle [2,3-3,4]" data-val-required="Le champ est obligatoire" data-val-range-min="2.3" value="" id="double1" name="double1" />

Il tag introduce un nuovo validatore [number] con l'attributo [data-val-number="Formato non valido"]. Questo validatore è definito come segue nel file [client-validation.js]:


// -------------- number
logs.number = true;
$.validator.addMethod("number", function(value, element, param) {
    var valide = !isNaN(Globalize.parseFloat(value));
    // logs
    if (logs.number) {
        console.log(jSON.stringify({
            "[number] value" : value,
            "[number] valide" : valide
        }));
    }
    // result
    return valide;
});
 
$.validator.unobtrusive.adapters.add("number", [], function(options) {
    options.rules["number"] = options.params;
    options.messages["number"] = options.message.replace("''", "'");
});

Ecco alcuni esempi di log:

Valore inserito
log
x
 {"[numero] valore":"x","[numero] valido":false}
-2,5

{"[numero] valore":"-2,5","[numero] valido":true}
{"[intervallo] valore":"-2,5","[intervallo] param":{"min":"2,3","max":"3,4"}}
{"[intervallo] valido":false}
2,5

{"[number] value":"+2.5","[number] valid":true}
{"[range] value":"+2.5","[range] param":{"min":"2.3","max":"3.4"}}
{"[intervallo] valido":true}
+2,5

{"[number] value":"+2.5","[number] valid":true}
{"[range] value":"+2.5","[range] param":{"min":"2.3","max":"3.4"}}
{"[intervallo] valido":true}

Sappiamo che i numeri reali dipendono dalla cultura. Sopra, ci troviamo nella cultura [fr-FR]. Quando inseriamo [2.5] (notazione anglosassone), il numero viene accettato. Ciò è dovuto a [Globalize.parseFloat], che accetta entrambe le notazioni:

Globalize.parseFloat("3.3")
3.3
Globalize.parseFloat("3,3")
3.3

Passiamo all'inglese e inseriamo [+2,5] e [+2.5]. I log sono i seguenti:

Valore immesso
log
x
 {"[numero] valore":"x","[numero] valido":false}
2.5

{"[numero] valore":"+2.5","[numero] valido":true}
{"[intervallo] valore":"+2,5","[intervallo] param":{"min":"2,3","max":"3,4"}}
{"[intervallo] valido":false}
+2,5

{"[number] value":"+2.5","[number] valid ":true}
{"[range] value":"+2.5","[range] param":{"min":"2.3","max":"3.4"}}
{"[intervallo] valido":true}

C'è un problema con [2,5]. È stato dichiarato come un numero float valido, ma dovrebbe essere scritto come [2.5]. Ciò è dovuto a [Globalize.parseFloat]:

Globalize.parseFloat("2,5")
25

Nell'esempio sopra riportato, [Globalize.parseFloat] ignora la virgola e interpreta il numero come 25. Nella cultura [en-US], un numero reale può includere un punto decimale e delle virgole, che a volte vengono utilizzate per separare le migliaia.

Ecco come possiamo migliorare la situazione:


// -------------- number
logs.number = true;
$.validator.addMethod("number", function(value, element, param) {
    // we manage [fr-FR] and [en-US] cultures only
    var pattern_fr_FR = /^\s*[-+]?[0-9]*\,?[0-9]+\s*$/;
    var pattern_en_US = /^\s*[-+]?[0-9]*\.?[0-9]+\s*$/;
    var culture = Globalize.culture().name;
    // validity test
    var valide;
    if (culture === "fr-FR") {
        valide = pattern_fr_FR.test(value);
    } else if (culture === "en-US") {
        valide = pattern_en_US.test(value);
    } else {
        valide = !isNaN(Globalize.parseFloat(value));
    }
    // logs
    if (logs.number) {
        console.log(jSON.stringify({
            "[number] value" : value,
            "[number] culture" : culture,
            "[number] valide" : valide
        }));
    }
    // result
    return valide;
});
  • riga 5: l'espressione regolare per un numero reale nella locale [fr-FR];
  • riga 6: l'espressione regolare per un numero reale nella locale [en-US];
  • riga 7: il nome della cultura corrente. Nel nostro esempio, sarà una delle due culture sopra indicate;
  • righe 9–16: il controllo di validità per il valore inserito;
  • riga 15: abbiamo previsto il caso in cui la locale non sia né [fr-FR] né [en-US];

I log ora mostrano quanto segue:

Cultura [fr-FR]

Valore inserito
registri
x

 {"[numero] valore":"x","[numero] cultura":"fr-FR","[numero] valido":false}
-2,5

{"[number] value":"-2.5","[number] culture":"fr-FR","[number] valid":true}
{"[range] value":"-2.5","[range] param":{"min":"2.3","max":"3.4"}}
{"[intervallo] valido":false}
2,5

{"[number] value":"+2.5","[number] culture":"fr-FR","[number] valid":true}
{"[range] value":"+2.5","[range] param":{"min":"2.3","max":"3.4"}}
{"[range] valid":true}
+2,5

{"[number] value":"+2.5","[number] culture":"fr-FR","[number] valid":false}

Lingua [en-US]

Valore inserito
registri
x

{"[numero] valore":"x","[numero] lingua":"en-US","[numero] valido":false}
2.5

{"[numero] valore":"+2,5","[numero] cultura":"en-US","[numero] valido":false}
+2,5

{"[number] value":"+2.5","[number] culture":"en-US","[number] valid":true}
{"[intervallo] valore":"+2.5","[intervallo] param":{"min":"2.3","max":"3.4"}}
{"[range] valid":true}

6.3.19. Validatore [custom3]

La riga [1] è generata dalla seguente sequenza nella vista [vue-01.xml]:


<!-- double3 : required, number, custom3 -->
<tr>
    <td class="col1">double3 : required, number, custom3</td>
    <td class="col2">
        <input type="text" th:field="*{double3}" th:value="*{double3}" data-val="true"            th:attr="data-val-required=#{NotNull},data-val-number=#{typeMismatch},data-val-custom3=${custom3.message},data-val-custom3-field=${custom3.otherFieldName},data-val-custom3-max=${custom3.max},data-val-custom3-min=${custom3.min}" />
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="double3" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('double3')}" th:errors="*{double3}" class="error">Donnée erronée</span>
    </td>
</tr>

Queste righe riguardano il campo [double3] nel modulo [Form01]:


    @NotNull
    private Double double3;

Qui vogliamo esaminare un validatore che convalidi non solo un valore inserito, ma anche una relazione tra due valori inseriti. In questo caso, vogliamo che [double1+double3] rientri nell'intervallo [10,13].

La riga 5 genera la seguente riga HTML:


<input type="text" data-val="true" data-val-custom3-min="10.0" data-val-number="Invalid format"
    data-val-custom3="[double3+double1] must be in [10,13]" data-val-custom3-max="13.0" data-val-custom3-field="double1"    data-val-required="Field is required" value="" id="double3" name="double3" />

Questa riga introduce il nuovo validatore [custom3] dichiarato dall'attributo [data-val-custom3="[double3+double1] deve essere compreso tra [10,13]"]. Questo validatore ha i seguenti parametri:

  • [field] dichiarato dall'attributo [data-val-custom3-field="double1"]. Questo parametro indica il campo il cui valore viene utilizzato per calcolare la validità di [double3];
  • [min] dichiarato dall'attributo [data-val-custom3-min="10.0"]. Questo parametro è il minimo dell'intervallo [min, max] entro il quale deve rientrare [double1+double3];
  • [max] dichiarato dall'attributo [data-val-custom3-max="13.0"]. Questo parametro è il massimo dell'intervallo [min, max] entro il quale deve rientrare [double1+double3];

Questo validatore viene gestito come segue in [client-validation.js]:


// -------------- custom3 used in conjunction with [number]
logs.custom3 = true;
$.validator.addMethod("custom3", function(value1, element, param) {
    // second value
    var value2 = $("#" + param.field).val();
    // logs
    if (logs.custom3) {
        console.log(jSON.stringify({
            "[custom3] value1" : value1,
            "[custom3] param" : param,
            "[custom3] value2" : value2            
        }))
    }
    // first value
    var valeur1 = Globalize.parseFloat(value1);
    if (isNaN(valeur1)) {
        // let the [number] validator do the work
        if (logs.custom3) {
            console.log(jSON.stringify({
                "[custom3] valide" : true
            }))
        }
        return true;
    }
    // second value
    var valeur2 = Globalize.parseFloat(value2);
    if (isNaN(valeur2)) {
        // we cannot calculate the validity
        if (logs.custom3) {
            console.log(jSON.stringify({
                "[custom3] valide" : false
            }))
        }
        return false;
    }
    // validity calculation
    var min = Globalize.parseFloat(param.min);
    var max = Globalize.parseFloat(param.max);
    var somme = valeur1 + valeur2;
    var valide = somme >= min && somme <= max;
    // logs
    if (logs.custom3) {
        console.log(jSON.stringify({
            "[custom3] valide" : valide
        }))
    }
    // result
    return valide;
});
 
$.validator.unobtrusive.adapters.add("custom3", [ "field", "max", "min" ], function(options) {
    options.rules["custom3"] = options.params;
    options.messages["custom3"] = options.message.replace("''", "'");
});

Ecco alcuni esempi di log:

Valori inseriti
[double1,double3]
log
[x,1]

{"[custom3] value1":"1","[custom3] param":{"field":"double1","max":"13.0","min":"10.0"},"[custom3] value2":"x"}
{"[custom3] valid":false}
[1,x]

{"[number] value":"x","[number] culture":"en-US","[number] valid":false}
[1,20]

{"[custom3] valore1":"20","[custom3] param":{"campo":"double1","max":"13.0","min":"10.0"},"[custom3] valore2":"1"}
{"[custom3] valid":false}
[1,10]

{"[number] value":"10","[number] culture":"en-US","[number] valid":true}
{"[custom3] valore1":"10","[custom3] param":{"campo":"double1","max":"13.0","min":"10.0"},"[custom3] valore2":"1"}
{"[custom3] valid":true}

6.3.20. [url] Validatore

La riga [1] è generata dalla seguente sequenza nella vista [vue-01.xml]:


<!-- required, url -->
<tr>
    <td class="col1">required, url</td>
    <td class="col2">
        <input type="text" th:field="*{url}" th:value="*{url}" data-val="true"            th:attr="data-val-required=#{NotNull},data-val-url=#{URL.form01.url}" />
    </td>
    <td class="col3">
        <span class="field-validation-valid" data-valmsg-for="url" data-valmsg-replace="true"></span>
    </td>
    <td class="col4">
        <span th:if="${#fields.hasErrors('url')}" th:errors="*{url}" class="error">Donnée erronée</span>
    </td>
</tr>

Queste righe si riferiscono al campo [url] nel modulo [Form01]:


    @URL
    @NotBlank
    private String url;

La riga 5 genera la seguente riga HTML:


<input type="text" data-val="true" data-val-url="Invalid URL" data-val-required="Field is required" value="" id="url"    name="url" />

Introduce il validatore [url] con l'attributo [data-val-url]. Questo validatore è predefinito nella libreria di validazione jQuery. Non è necessario aggiungere nulla a [client-validation.js].

6.3.21. Abilitazione/disabilitazione della convalida lato client

Finché la convalida lato client è abilitata, la convalida lato server non viene mai eseguita perché i valori inviati raggiungono il server solo se sono stati convalidati sul lato client. Per vedere la convalida lato server in azione, è necessario disabilitare la convalida lato client. La vista [vue-01.xml] fornisce due collegamenti per gestire questa abilitazione/disabilitazione:


<a id="clientValidationTrue" href="javascript:setClientValidation(true)">
    <span style="margin-left:30px" th:text="#{client.validation.true}"></span>
</a>
<a id="clientValidationFalse" href="javascript:setClientValidation(false)">
    <span style="margin-left:30px" th:text="#{client.validation.false}"></span>
</a>

Questi due link non sono visibili contemporaneamente:

Il codice HTML di questi link è il seguente:


<a id="clientValidationTrue" href="javascript:setClientValidation(true)">
    <span style="margin-left:30px">Activer la validation client</span>
</a>
<a id="clientValidationFalse" href="javascript:setClientValidation(false)">
    <span style="margin-left:30px">Inhiber la validation client</span>
</a>

Lo script JavaScript [setClientValidation] è definito nel file [local.js] (vedi sopra). Nella funzione [$(document).ready] di questo file vengono utilizzati i link di convalida:


// document ready
$(document).ready(function() {
    // global references
...
    activateValidationTrue = $("#clientValidationTrue");
    activateValidationFalse = $("#clientValidationFalse");
    clientValidation = $("#clientValidation");
...
    // validation links
    // clientValidation is a hidden field set by the server
    var validate = clientValidation.val();
    setClientValidation2(validate === "true");
});
  • riga 5: un riferimento al link di attivazione della convalida lato client;
  • riga 6: un riferimento al link di disattivazione della convalida lato client;
  • riga 7: un riferimento a un campo nascosto del modulo che memorizza l'ultimo stato di attivazione come valore booleano [true: convalida lato client abilitata, false: convalida lato client disabilitata]. Questo campo si trova nella vista [vue-01.xml] nella seguente forma:

<input type="hidden" th:field="*{clientValidation}" th:value="*{clientValidation}" value="true" />

e corrisponde al campo [clientValidation] nel modulo [Form01]:


// validation client
private boolean clientValidation = true;
  • Riga 11: Recupera il valore del campo nascosto;
  • Riga 12: Richiamiamo la seguente funzione [setClientValidation2]:

function setClientValidation2(activate) {
    // liens
    if (activate) {
        // la validation client est active
        activateValidationTrue.hide();
        activateValidationFalse.show();
        // on parse les validateurs du formulaire
        $.validator.unobtrusive.parse(formulaire);
    } else {
        // la validation client est inactive
        activateValidationFalse.hide();
        activateValidationTrue.show();
        // on désactive les validateurs du formulaire
        formulaire.data('validator', null);
    }
}
  • riga 1: il parametro [activate] viene impostato su [true] se la convalida lato client deve essere abilitata, altrimenti su false;
  • righe 5-6: il link di disattivazione viene visualizzato, mentre quello di attivazione viene nascosto;
  • riga 8: affinché la convalida lato client funzioni, il documento deve essere analizzato (parsato) per cercare i validatori [data-val-X]. Il parametro della funzione [$.validator.unobtrusive.parse] è l'ID JavaScript del modulo da analizzare;
  • righe 11-12: il link di attivazione viene visualizzato, quello di disattivazione viene nascosto;
  • riga 14: i validatori del modulo sono disabilitati. Da questo momento in poi, è come se nel modulo non ci fossero validatori JavaScript;

Qual è lo scopo di questa funzione [setClientValidation2]? Viene utilizzata per gestire le richieste POST. Poiché il campo [clientValidation] è un campo nascosto, viene inviato e restituito con il modulo rispedito dal server. Utilizziamo quindi il suo valore per ripristinare la validazione lato client allo stato in cui si trovava prima del POST. Questo perché non vi è alcuno stato JavaScript conservato tra le richieste. Il server deve quindi passare a quella vista le informazioni necessarie per inizializzare il JavaScript della nuova vista. Questo viene solitamente fatto nella funzione [$(document).ready].

Torniamo alla funzione [setClientValidation], che gestisce i clic sui link per abilitare/disabilitare la convalida lato client:


// validation côté client
function setClientValidation(activate) {
    // on gère l'activation / désactivation de la validation client
    setClientValidation2(activate);
    // on mémorise le choix de l'utilisateur dans le champ caché
    clientValidation.val(activate ? "true" : "false");
    // ajustements supplémentaires
    if (activate) {
        // la validation client est active
        // on efface tous les messages d'erreur du serveur
        clearServerErrors();
        // on valide le formulaire
        formulaire.validate().form();
    } else {
        // la validation client est inactive
        // on efface tous les messages d'erreur du client
        clearClientErrors();
    }
}
  • riga 4: utilizziamo la funzione [setClientValidation2] che abbiamo appena visto;
  • riga 6: memorizziamo la selezione dell'utente nel campo nascosto per recuperarla quando verrà restituita la successiva richiesta POST;
  • riga 11: se la convalida lato client è abilitata, cancelliamo i messaggi di errore dalla colonna [server] della vista. Abbiamo descritto la funzione [clearServerErrors] nella sezione 6.3.7;
  • riga 13: i validatori JavaScript vengono eseguiti per visualizzare eventuali messaggi di errore nella colonna [client] della vista;
  • riga 17: se la convalida lato client è disabilitata, cancelliamo i messaggi di errore dalla colonna [client] della vista. Esaminiamo il codice HTML di un elemento errato nella console degli sviluppatori di Chrome:

<td class="col2">
    <input type="text" data-val="true" data-val-int="Format invalide" data-val-max-value="100" data-val-required="Le champ est obligatoire" data-val-max="La valeur doit être inférieure ou égale à 100" value="" id="intMax100" name="intMax100" aria-required="true"        class="input-validation-error" aria-describedby="intMax100-error">
</td>
<td class="col3">
    <span class="field-validation-error" data-valmsg-for="intMax100" data-valmsg-replace="true">
        <span id="intMax100-error" class="">Le champ est obligatoire</span>
    </span>
</td>
  • Nella riga 2, vediamo che nella colonna 2 della tabella, l'elemento errato ha lo stile [class="input-validation-error"] ;
  • alla riga 5, si nota che nella colonna 3 della tabella il messaggio di errore ha lo stile [class="field-validation-error"] ;

Questo vale per tutti gli elementi non validi. Utilizziamo queste due informazioni nella seguente funzione [clearClientErrors]:


// clear client errors
function clearClientErrors() {
    // erase client error messages
    $(".field-validation-error").each(function(index) {
        $(this).text("");
    });
    // change the CSS class of erroneous entries
    $(".input-validation-error").each(function(index) {
        $(this).removeClass("input-validation-error");
    });
}
  • righe 4-6: cerchiamo tutti gli elementi DOM con la classe [field-validation-error] e cancelliamo il testo che visualizzano. In questo modo vengono cancellati i messaggi di errore;
  • righe 8-10: cerchiamo tutti gli elementi DOM con la classe [input-validation-error] e rimuoviamo tale classe da essi. Questo ripristina lo stile originale dell'elemento che era stato evidenziato in rosso;