Skip to content

6. Validación Javascript del lado del cliente

En el capítulo anterior nos centramos en la validación del lado del servidor. Volvamos a la arquitectura de una aplicación Spring MVC:

BD

Por el momento, las páginas enviadas al cliente no contenían Javascript. Ahora abordamos esta tecnología que nos permitirá, en un primer momento, realizar validaciones del lado del cliente. El principio es el siguiente:

  • es el Javascript el que envía los valores al servidor web;
  • y, por lo tanto, antes de este POST, puede verificar la validez de los datos e impedir el POST si estos son inválidos;

Vamos a utilizar el formulario que hemos validado en el lado del servidor. Ahora vamos a ofrecer la posibilidad de validarlo tanto en el lado del cliente como en el lado del servidor.

Nota: el tema es complejo. El lector que no esté interesado en este tema puede pasar directamente al apartado 7.

6.1. Las funcionalidades del proyecto

Presentamos algunas vistas del proyecto para mostrar sus funcionalidades. La página inicial se obtiene con el URL [http://localhost:8080/js01.html]

 

Las validaciones se han implementado en ambos lados: cliente y servidor. Como el POST solo se ejecuta si los valores se han considerado válidos en el lado del cliente, las validaciones en el lado del servidor siempre se realizan con éxito. Por lo tanto, se ha proporcionado un enlace para desactivar las validaciones del lado del cliente. Cuando se está en este modo, se vuelve al modo de funcionamiento que ya hemos estudiado. He aquí un ejemplo:

123
  • en [1], los valores introducidos;
  • en [2], los mensajes de error relacionados con las entradas;
  • en [3], un resumen de los errores con, para cada uno de ellos:
    • el nombre del campo validado,
    • el código de error,
    • el mensaje predeterminado de ese código de error;

Ahora, habilitemos la validación del lado del cliente:

  • en [1], los valores introducidos. Se puede observar que las entradas erróneas tienen un estilo particular;
  • en [2], los mensajes de error asociados a las entradas erróneas. Son idénticos a los generados por el servidor;
  • en [3-4], ya no hay nada, ya que mientras haya entradas erróneas, el POST al servidor no se produce;

6.2. Validación del lado del servidor

6.2.1. Configuración

Comenzamos creando un nuevo proyecto Maven [springmvc-validation-client]:

Desarrollamos el proyecto de la siguiente manera:

  

La clase [Config] configura el proyecto. Es idéntica a la de los proyectos anteriores:


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 clase [Main] es la clase ejecutable del proyecto:


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) {
        // se inicia la aplicación
        ApplicationContext context = SpringApplication.run(Config.class, args);
        // se muestra la lista de beans encontrados por Spring
        System.out.println("Liste des beans Spring");
        String[] beanNames = context.getBeanDefinitionNames();
        Arrays.sort(beanNames);
        for (String beanName : beanNames) {
            System.out.println(beanName);
        }
    }
}
  • línea 13, Spring Boot se inicia con el archivo de configuración [Config];
  • líneas 15-20: a modo de ejemplo, mostramos cómo visualizar la lista de objetos gestionados por Spring. Esto puede resultar útil si en ocasiones tenemos la impresión de que Spring no gestiona alguno de nuestros componentes. Es una forma de verificarlo. También es una forma de comprobar la autoconfiguración realizada por Spring Boot. En la consola, obtenemos una lista similar a la siguiente:
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

Hemos resaltado los objetos definidos en la clase [Config].

6.2.2. El modelo del formulario

Continuemos explorando el proyecto:

  

La clase [Form01] es la clase que recibirá los valores enviados. Es la siguiente:


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 {

    // valores enviados
    @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;
    
    // validación del cliente
    private boolean clientValidation = true;
    // local
    private String lang;
    ...
}

Encontramos validadores que ya hemos visto. Además, vamos a introducir el concepto de validación específica. Se trata de una validación que no se puede formalizar con un validador predefinido. Aquí vamos a pedir que [double1+double2] esté dentro del intervalo [10,13].

6.2.3. El controlador

El controlador [JsController] es el siguiente:

  

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";
    }
...

    // preparación del modelo de la vista vista-01
    private void setModel(Form01 formulaire, Model model, Locale locale, String message) {
...
    }
}
  • línea 9, la acción [/js01];
  • línea 10: se instancia un objeto de tipo [Form01] y se coloca automáticamente en la plantilla, asociado a la clave [form01];
  • línea 10: la configuración regional y la plantilla se introducen en los parámetros;
  • línea 11: con esta información, se prepara la plantilla;
  • línea 12: se muestra la vista [vue-01.xml];

El método [setModel] es el siguiente:


    // preparación del modelo de la vista vista-01
    private void setModel(Form01 formulaire, Model model, Locale locale, String message) {
        // solo se gestionan las configuraciones locales fr-FR y en-US
        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));
        // el posible mensaje
        if (message != null) {
            model.addAttribute("message", message);
        }
}
  • el objetivo del método [setModel] es introducir en la plantilla:
    • información sobre la configuración regional,
    • el mensaje pasado como último parámetro;
  • línea 14: se introduce en la plantilla información sobre la configuración regional (idioma, país);
  • líneas 16-18: se introduce en la configuración regional el posible mensaje pasado como parámetro;
  • líneas 8 y 12: la información sobre la configuración regional también se almacena en el formulario [Form01]. El Javascript utilizará esta información;

Los valores introducidos en el formulario [vue-01.xml] se enviarán a la siguiente acción [/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);
        ...
}
  • línea 2: la anotación [@Valid Form01 formulaire] hace que los valores enviados se sometan a los validadores de la clase [Form01]. Sabemos que existe una validación específica [double1+double2] en el intervalo [10,13]. Al llegar a la línea 3, esta validación no se ha realizado;
  • línea 3: se crea el siguiente objeto [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 {

    // el intervalo de validación
    private double min;
    private double max;

    // creador
    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) {
        // objeto validado
        Form01 form01 = (Form01) form;
        // el valor de [double1]
        Double double1 = form01.getDouble1();
        if (double1 == null) {
            return;
        }
        // el valor de [double2]
        Double double2 = form01.getDouble2();
        if (double2 == null) {
            return;
        }
        // [double1+double2]
        double somme = double1 + double2;
        // validación
        if (somme < min || somme > max) {
            errors.rejectValue("double2", "form01.double2", new Double[] { min, max }, null);
        }
    }

}
  • línea 8: para implementar una validación específica, creamos una clase que implementa la interfaz Spring [Validator]. Esta interfaz tiene dos métodos: [supports] en la línea 21 y [validate] en la línea 26;
  • líneas 21-23: el método [supports] recibe un objeto de tipo [Class]. Debe devolver «true» para indicar que admite esta clase, y «false» en caso contrario;
  • línea 22: indicamos que la clase [Form01Validator] solo valida objetos de tipo [Form01];
  • líneas 15-18: recordemos que queremos implementar la restricción [double1+double2] en el intervalo [10,13]. En lugar de limitarnos a este intervalo, vamos a verificar la restricción [double1+double2] en el intervalo [min, max]. Por eso tenemos un constructor con estos dos parámetros;
  • línea 26: se invoca el método [validate] con una instancia del objeto validado, es decir, en este caso una instancia de [Form01], y con la colección de errores actualmente conocidos [Errors errors]. Si la validación realizada por el método [validate] falla, debe crear un nuevo elemento en la colección [Errors errors];
  • línea 43: la validación ha fallado. Se añade un elemento a la colección [Errors errors] con el método [Errors.rejectValue], cuyos parámetros son los siguientes:
    • parámetro 1: normalmente el nombre del campo erróneo. Aquí se han comprobado los campos [double1, double2]. Se puede introducir cualquiera de los dos,
    • el mensaje de error asociado o, más exactamente, su clave en los archivos de mensajes externalizados:

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

Aquí tenemos mensajes configurados con {0} y {1}. Por lo tanto, hay que proporcionar dos valores a este mensaje. Eso es lo que hace el tercer parámetro del método [Errors.rejectValue].

    • El cuarto parámetro es un mensaje predeterminado para el error;

Volvamos a la acción [/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";
        }
}
  • línea 4: el validador [Form01Validator] se ejecuta con los siguientes parámetros:
    • parámetro 1: el objeto que se está validando,
    • parámetro 2: la lista de errores de este objeto. Se trata del objeto [BindingResult result] pasado como parámetro de la acción. Si la validación falla, este objeto tendrá un error más;
  • línea 5: se comprueba si hay errores de validación;
  • líneas 7-10: se recorre la lista de errores para memorizar, para cada uno de ellos:
    • el nombre del objeto validado,
    • su código de error,
    • su mensaje de error por defecto;
  • línea 10: con esta información, se construye la plantilla de la vista [vue-01.xml]. En esta ocasión, hay un mensaje, el version, que es una concatenación y un resumen de los distintos mensajes de error;
  • líneas 12-15: si todos los valores enviados son válidos, se redirige al cliente a la acción [/js01] colocando los valores enviados en el atributo Flash;

6.2.4. La vista

La vista [vue-01.xml] es compleja. Solo vamos a presentar una pequeña 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>
        <!-- título -->
        <h3>
            <span th:text="#{form01.title}"></span>
            <span th:text="${locale}"></span>
        </h3>
        <!-- menú -->
        <p>
...
        </p>
        <!-- formulario -->
        <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>
                    <!-- obligatorio -->
                    <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>
            <!-- botón de validación -->
            <input type="submit" th:value="#{form01.valider}" value="Valider" onclick="javascript:postForm01()" />
            </p>
        </form>
        <!-- mensaje de los validadores del lado del servidor -->
        <br/>
        <fieldset class="fieldset">
            <legend>
                <span th:text="#{server.error.message}"></span>
            </legend>
            <span th:text="${message}" class="error"></span>
        </fieldset>
    </body>
</html>

Esta página utiliza una serie de mensajes que se encuentran en los archivos de mensajes externalizados:

[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

Volvamos al código de la página:

  • línea 8: una gran cantidad de importaciones de bibliotecas Javascript que podemos ignorar aquí;
  • línea 14: muestra la configuración regional introducida en la plantilla por el servidor;
  • línea 59: muestra el mensaje introducido en la plantilla por el servidor;

El código de las líneas 33-44 es nuevo. Analicémoslo:


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

Quizás lo más sencillo sea examinar el código HTML generado por este segmento de Thymeleaf:


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

Vamos a utilizar, en el lado del cliente, una biblioteca de validación llamada [jquery.validate]. Todos los atributos [data-x] son para ella. Cuando se desactive la validación en el lado del cliente, estos atributos no se utilizarán. Por lo tanto, por ahora, no es necesario entenderlos. Podemos centrarnos simplemente en la siguiente línea de Thymeleaf:


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

que genera la siguiente línea HTML:


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

En el ejemplo anterior, surge una dificultad a la hora de generar el atributo [data-val-required="Le champ est obligatoire"]. De hecho, el valor asociado al atributo proviene de los archivos de mensajes externalizados. Por lo tanto, nos vemos obligados a utilizar una expresión Thymeleaf para obtenerlo. Se trata de la siguiente expresión: [th:attr="data-val-required=#{NotNull}"]. Esta expresión se evalúa y su valor se inserta tal cual en la etiqueta HTML generada. Se llama [th:attr] porque se utiliza para generar atributos no predefinidos en Thymeleaf. Hemos encontrado atributos predefinidos [th:text, th:value, th:class, ...], pero no existe ningún atributo [th:data-val-required].

6.2.5. La hoja de estilo

Arriba, encontramos clases CSS como [class="field-validation-valid"]. Algunas de estas clases son utilizadas por la biblioteca de validación Javascript. Están definidas en el siguiente archivo [form01.css]:

  

@CHARSET "UTF-8";

/*estilos personalizados*/
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;
}
/* Estilos para los ayudantes de validación
-----------------------------------------------------------*/
.field-validation-error {
    color: #f00;
}

.field-validation-valid {
    display: none;
}

.input-validation-error {
    border: 1px solid #f00;
    background-color: #tarifa;
}

.validation-summary-errors {
    font-weight: bold;
    color: #f00;
}

.validation-summary-valid {
    display: none;
}

6.3. Validación del lado del cliente

6.3.1. Conceptos básicos de jQuery y Javascript

La validación del lado del cliente se realiza con Javascript. Nos valeremos del framework jQuery, que ofrece numerosas funciones que facilitan el desarrollo de Javascript. Presentamos los conceptos básicos de jQuery que hay que conocer para comprender los scripts de este capítulo y de los siguientes.

Creamos un archivo estático HTML [JQuery-01.html] que colocamos en una carpeta [static / vues]:

 

Este archivo tendrá el siguiente contenido:


<!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>
  • línea 6: importación de jQuery;
  • líneas 10-12: un elemento de la página id [element1]. Vamos a jugar con este elemento.

Tenemos que descargar el archivo [jquery-1.11.1.min.js]. Lo encontraremos en el último version de jQuery al URL [http://jquery.com/download/]:

Image

Colocaremos el archivo descargado en la carpeta [static / js]:

  

Una vez hecho esto, se solicita la vista estática [jQuery-01.html] con Chrome [1-2]:

Con Google Chrome, ejecute [Ctrl-Maj-I] para que aparezcan las herramientas de desarrollo [3]. La pestaña [Console] [4] permite ejecutar código Javascript. A continuación, indicamos los comandos Javascript que hay que escribir y ofrecemos una explicación.

JS
resultado
$("#element1")
: devuelve la colección de todos los elementos de id [element1], por lo que normalmente es una colección de 0 o 1 elemento, ya que no se pueden tener dos id idénticos en una página HTML.
$("#element1").text("blabla")
: asigna el texto [blabla] a todos los elementos de la colección. Esto tiene como efecto cambiar el contenido que muestra la página
$("#element1").hide()
oculta los elementos de la colección. El texto [blabla] ya no se muestra.
$("#element1")
: vuelve a mostrar la colección. Esto nos permite ver que el elemento de id [element1] tiene el atributo CSS style='display : none;', lo que hace que el elemento quede oculto.
$("#element1").show()
: muestra los elementos de la colección. Vuelve a aparecer el texto [blabla]. Es el atributo CSS style='display : block;' el que garantiza esta visualización.
$("#element1").attr('style','color: red')
: establece un atributo para todos los elementos de la colección. El atributo es aquí [style] y su valor [color: red]. El texto [blabla] se vuelve rojo.
Tableau
Dictionnaire

Cabe destacar que el URL del navegador no ha cambiado durante todas estas operaciones. No ha habido intercambio de datos con el servidor web. Todo ocurre dentro del navegador. Ahora, veamos el código fuente de la página:


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

Este es el texto inicial. No refleja en absoluto las manipulaciones que hemos realizado en el elemento de las líneas 10-12. Es importante recordarlo cuando se realiza la depuración de Javascript. Por lo tanto, a menudo es inútil visualizar el código fuente de la página mostrada.

Sabemos lo suficiente para comprender los scripts jS que vendrán a continuación.

6.3.2. Las bibliotecas de validación

Vamos a utilizar bibliotecas del ecosistema jQuery. En torno a jQuery gravitan varios proyectos que, a su vez, dan lugar a bibliotecas. Vamos a utilizar la biblioteca de validación [jquery.validate.unobstrusive] creada por Microsoft y cedida a la fundación jQuery. En adelante la denominaremos biblioteca de validación MS o, más sencillamente, biblioteca MS. Para obtenerla, se necesita un entorno Microsoft Visual Studio. No he visto cómo conseguirla de otra manera. Se puede utilizar una version gratuita del tipo [Visual Studio Community] [http://www.visualstudio.com/en-us/news/vs2013-community-vs.aspx] (dic. 2014). El lector que no esté interesado en seguir los pasos que se indican a continuación puede descargar esta biblioteca y aquellas en las que se basa en los ejemplos que se encuentran en el sitio web de este documento.

Creamos un proyecto de consola con Visual Studio [1-4]:

12
34
  • en [5], el proyecto de consola;
  • en [6-7]: vamos a añadir paquetes [NuGet] al proyecto. [NuGet] es una función de Visual Studio que permite descargar bibliotecas en formato DLL, así como bibliotecas jS.
  • en [9-10], realice una búsqueda con la palabra clave [jQuery];
  • en [11-13], descargue en el orden indicado las bibliotecas jS necesarias para la validación del lado del cliente;
  • en [14], descargue también la biblioteca [Microsoft jQuery Unobtrusive Ajax] que vamos a utilizar próximamente;
  • en [15-16], busque paquetes con la palabra clave [globalize];
  • en [17], descargue la biblioteca [jQuery.Validation.Globalize];

Estas diversas descargas han instalado varias bibliotecas jS en la carpeta [Scripts] del proyecto [18]. No todas son útiles. Cada archivo viene en dos copias:

  • [js]: la versión legible de la biblioteca;
  • [min.js]: el version ilegible, denominado «minificado» (minified), de la biblioteca. No es realmente ilegible. Es texto. Pero no es comprensible. Es el version el que se debe utilizar en producción, ya que este archivo es más pequeño que el version correspondiente [js] y, por lo tanto, mejora la velocidad de los intercambios cliente/servidor;

Los archivos version y [min.map] no son imprescindibles. En la carpeta [cultures], solo es necesario conservar los cultivos gestionados por la aplicación.

Con el Explorador de Windows, se copian estos archivos en la carpeta [static / js / jquery] del proyecto [springmvc-validation-client] y solo se conservan los archivos útiles de [20]:

En [21], solo se conservan dos configuraciones regionales:

  • [fr-FR]: el francés de Francia;
  • [en-US]: el inglés de USA;

6.3.3. Importación de las bibliotecas de validación jS

Para poder utilizarlas, estas bibliotecas deben importarse mediante la 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>
  • línea 11: la importación de un archivo jS del que aún no hemos hablado;
  • líneas 13-18: un script jS interpretado por Thymelaf. Gestiona la configuración regional del lado del cliente;

6.3.4. Gestión de la configuración regional del lado del cliente

La localización del lado del cliente se realiza mediante el siguiente script jS:


<script th:inline="javascript">
            /*<![CDATA[*/
                    var culture = [[${locale}]];
                    Globalize.culture(culture);
                    /*]]>*/
</script>
  • líneas 3-4: código jS en el que se encuentra la expresión Thymeleaf [[${locale}]]. Obsérvese la sintaxis particular de esta expresión. Esto se debe a que se encuentra en javascript. La expresión [[${locale}]] será sustituida por el valor de la clave [locale] del modelo de la vista;

El resultado en el flujo HTML generado a partir de estas líneas es el siguiente:


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

Las líneas 3-4 establecen la configuración regional del lado del cliente. Solo se gestionan dos, [fr-FR] y [en-US]. Por este motivo, solo hemos importado dos archivos de configuración regional:


        <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 configuración regional que se utilizará en el lado del cliente se establece en el lado del servidor. Volvamos al código del lado del servidor:


    @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";
    }

    // preparación de la plantilla de la vista vista-01
    private void setModel(Form01 formulaire, Model model, Locale locale, String message) {
        // solo se gestionan las locales fr-FR, en-US
        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));
...
}
  • línea 20: la configuración regional [fr-FR] o [en-US] se introduce en la plantilla de la vista [vue-01.xml] (línea 4). Cabe señalar una fuente de complicaciones. Mientras que una configuración regional francesa se indica como [fr-FR] en el lado del cliente, en el lado del servidor se indica como [fr_FR]. Por este motivo, en las líneas 14 y 18, se almacena de esta forma en el objeto [Form01 formulaire], que recibe los valores enviados;

Cabe destacar el siguiente punto importante. El script


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

cambia la configuración regional del cliente a partir de la configuración regional transmitida por el servidor. Esto no internacionaliza los mensajes mostrados por la página. Solo cambia la forma de interpretar cierta información que depende de la configuración regional de un país. Con la configuración regional [fr_FR], el número real [12,78] es válido, mientras que no lo es con la configuración regional [en-US]. Por lo tanto, hay que escribir [12.78]. Del mismo modo, la fecha [12/01/2014] es válida en la configuración regional [fr-FR], mientras que en la configuración regional [en-US] hay que escribir [01/12/2014]. Los archivos de la carpeta [jquery / globalize] gestionan este tipo de problemas:

  

La internacionalización de los mensajes de error se gestiona únicamente en el lado del servidor. Veremos que la página HTML / jS lleva consigo mensajes de error correspondientes a la configuración regional gestionada por el servidor: en francés para la configuración regional [fr_FR] y en inglés para la configuración regional [en_US].

6.3.5. Los archivos de mensajes

La vista [vue-01.xml] utiliza los siguientes mensajes internacionalizados:

  

[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

El archivo [messages.properties] es una copia del archivo de mensajes en inglés. En definitiva, cualquier configuración regional diferente de [fr] utilizará mensajes en inglés. Recordamos que el archivo [messages_fr.properties] se utiliza para cualquier configuración regional [fr_XX], como [fr_CA] o [fr_FR].

La vista [vue-01.xml] utiliza las claves de estos mensajes. Si desea conocer el valor asociado a estas claves, se invita al lector a volver a este párrafo para descubrirlo.

6.3.6. Cambio de configuración regional

La vista [vue-01.xml] presenta cuatro enlaces:


<body>
        <!-- título -->
        <h3>
            <span th:text="#{form01.title}"></span>
            <span th:text="${locale}"></span>
        </h3>
        <!-- menú -->
        <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>
        <!-- formulario -->
        <form action="/someURL" th:action="@{/js02.html}" method="post" th:object="${form01}" name="form" id="form">
            ...

algunos de los cuales se muestran a continuación [1]:

Veamos los dos enlaces que permiten cambiar la configuración regional a francés o inglés:


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

Al hacer clic en estos enlaces se ejecuta un script jS presente en el archivo [local.js] [2]. En ambos casos, se llama a una función jS [setLocale]:


// configuración regional
function setLocale(locale) {
    // se actualiza la configuración regional
    lang.val(locale);
    // se envía el formulario; esto no activa los validadores del cliente; por eso no se ha desactivado la validación del lado del cliente
    document.form.submit();
}

Para comprender la línea 4 es necesario un preámbulo. La vista [vue-01.xml] incluye un campo oculto denominado [lang]:


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

que se corresponde con un campo [lang] en [Form01]:


    // local
    private String lang;

Los campos ocultos son útiles cuando se desea enriquecer los valores enviados. El campo javascript permite asignarles un valor y este valor se envía como una entrada normal realizada por el usuario. El código HTML generado por Thymeleaf es el siguiente:


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

El valor del parámetro [value] es el del campo [Form01.lang] en el momento de la generación del HTML. Lo que es importante destacar es el identificador jS del nodo [id="lang"]. Este identificador es utilizado por la siguiente función []:


// variables globales
var lang;

// documento listo
$(document).ready(function() {
    // referencias globales
    lang = $("#lang");
});

// local
function setLocale(locale) {
    // se actualiza la configuración regional
    lang.val(locale);
    // se envía el formulario; por alguna razón desconocida, esto no activa los validadores del cliente
    // por eso no se ha desactivado la validación
    document.form.submit();
}
  • líneas 5-8: la función jS [$(document).ready(f)] es una función que se ejecuta cuando el navegador ha cargado la totalidad del documento enviado por el servidor. Su parámetro es una función. Se utiliza la función jS [$(document).ready(f)] para inicializar el entorno jS del documento cargado;
  • línea 7: la expresión [$("#lang")] es una expresión jQuery. Su valor es una referencia al nodo del atributo DOM de [id='lang'];
  • línea 2: las variables declaradas fuera de una función son globales para las funciones. En este caso, esto significa que la variable [lang] inicializada en [$(document).ready()] también es conocida en la función [setLocale] de la línea 11;
  • línea 13: modifica el atributo [value] del nodo identificado por [lang]. Si lang es [xx_XX], entonces la etiqueta HTML del nodo pasa a ser:

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

El javascript permite modificar el valor de los elementos del DOM (Modelo de objetos de documento).

  • Línea 16: [document] hace referencia al DOM. [document.form] hace referencia al primer formulario encontrado en este documento. Un documento HTML puede tener varias etiquetas <form> y, por lo tanto, varios formularios. Aquí solo tenemos uno. [document.form.submit] envía este formulario como si el usuario hubiera hecho clic en un botón con el atributo [type='submit']. ¿A qué acción se envían los valores del formulario? Para saberlo, hay que fijarse en la etiqueta [form] del formulario en [vue-01.xml]:

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

La acción que recibirá los valores enviados es la designada por el atributo [th:action]. Por lo tanto, será la acción [/js02.html]. Recordemos que, en este nombre, se eliminará el sufijo [.html] y, finalmente, se ejecutará la acción [/js02]. Lo importante es comprender que el nuevo valor [xx_XX] del nodo [lang] se enviará en forma de [lang=xx_XX]. Sin embargo, hemos configurado nuestra aplicación para interceptar el parámetro [lang] e interpretarlo como un cambio de configuración regional. Por lo tanto, en el lado del servidor, la configuración regional pasará a ser [xx_XX]. Veamos la acción [/js02] que se va a ejecutar:


    @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";
        }
    }

    // preparación de la plantilla de la vista vista-01
    private void setModel(Form01 formulaire, Model model, Locale locale, String message) {
        // solo se gestionan las configuraciones regionales fr-FR y en-US
        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));
        ...
}
  • línea 2: la acción [/js02] recibirá la nueva configuración regional [xx_XX] encapsulada en el parámetro [Locale locale]:
  • líneas 5-12: si algunos de los valores enviados son inválidos, se mostrará la vista [vue-01.xml] con mensajes de error que utilizan la nueva configuración regional [xx_XX]. Además, la línea 11 hace que la variable [locale=xx-XX] se incluya en la plantilla. En el lado del cliente, este valor se utilizará para actualizar la configuración regional del lado del cliente. Ya hemos descrito este proceso;
  • líneas 14-15: si todos los valores enviados son válidos, se produce una redirección a la siguiente acción [/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";
}
  • línea 2, se inyecta la nueva configuración regional [xx_XX];
  • línea 3: el método [setModel] establecerá entonces la cultura del cliente en [xx-XX];

Ahora veamos la influencia de la configuración regional en la vista [vue-01.xml]. Por el momento no la hemos presentado en su totalidad, ya que cuenta con más de 300 líneas. No obstante, la mayor parte de las líneas consiste en la repetición de una secuencia similar a la siguiente:


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

Este código muestra el siguiente fragmento [1]:

El mensaje de error [2] proviene del atributo [th:attr="data-val-required=#{NotNull}"] de la línea 5. [#{NotNull}] es un mensaje localizado. Según la configuración regional del servidor, la línea 5 genera la etiqueta:


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

o bien la etiqueta:


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

Los atributos [data-x] son utilizados por la biblioteca de validación jS.

En definitiva, cabe destacar que los dos enlaces de cambio de configuración regional:

  • provocan un POST de los valores introducidos;
  • cambian la configuración regional tanto en el lado del servidor como en el del cliente;
  • generan una página HTML que incluye los mensajes de error destinados a la biblioteca de validación jS, y que dichos mensajes están en el idioma de la configuración regional seleccionada;

6.3.7. El POST de los valores introducidos

Analicemos el botón [Valider] que envía los valores introducidos de la vista [vue-01.xml]. Su código HTML es el siguiente:


<!-- botón de validación -->
<input type="submit" value="Valider" onclick="javascript:postForm01()" />

Si Javascript está activo en el navegador, al hacer clic en el botón se activará la ejecución del método [postForm01]. Si esta función devuelve el valor booleano [False], entonces submit no se ejecutará. Si devuelve cualquier otro valor, entonces sí se ejecutará. Esta función se encuentra en el archivo [local.js]:

 

Se importa mediante la vista [vue-01.xml] en la línea 6 que se muestra a continuación:


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

En este archivo se encuentra el siguiente código:


// variables globales
var formulaire;
var clientValidation;
var double1;
var double2;
var double3;
...
$(document).ready(function() {
    // referencias globales
    formulaire = $("#form");
    clientValidation = $("#clientValidation");
    double1 = $("#double1");
    double2 = $("#double2");
    double3 = $("#double3");
...
});
....
// post formulario
function postForm01() {
...
}
  • líneas 8-16: la función jS [$(document).ready(f)] es una función que se ejecuta cuando el navegador ha cargado la totalidad del documento enviado por el servidor. Su parámetro es una función. Se utiliza la función jS [$(document).ready(f)] para inicializar el entorno jS del documento cargado;
  • líneas 10-14: para comprender estas líneas, hay que examinar tanto el código Thymeleaf como el código HTML generado;

El código Thymeleaf en cuestión es el siguiente:


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

que genera el siguiente código 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" /> 

Cada atributo [th:field='x'] genera dos atributos: HTML, [name='x'] y [id='x']. El atributo [name] es el nombre de los valores enviados. Así, la presencia de los atributos [name='x'] y [value='y'] para una etiqueta HTML <input type='text'> incluirá la cadena x=y en los valores enviados name1=val1&name2=val2&... El atributo [id='x'] es utilizado por el Javascript. Sirve para identificar un elemento del DOM (Modelo de Objetos de Documento). El documento HTML cargado se transforma, de hecho, en un árbol Javascript denominado DOM, donde cada nodo se identifica mediante su atributo [id].

Volvamos al código de la función [$(document).ready()]:


// variables globales
var formulaire;
var clientValidation;
var double1;
var double2;
var double3;
...
$(document).ready(function() {
    // referencias globales
    formulaire = $("#form");
    clientValidation = $("#clientValidation");
    double1 = $("#double1");
    double2 = $("#double2");
    double3 = $("#double3");
...
});
....
// post formulario
function postForm01() {
...
}
  • línea 10: la expresión [$("#form")] es una expresión jQuery. Su valor es una referencia al nodo del DOM con el atributo [id='form '];
  • líneas 10-14: se recuperan las referencias a cinco nodos de DOM;
  • líneas 2-6: las variables declaradas fuera de una función son globales para las funciones. En este caso, esto significa que las variables [formulaire, clientValidation , double1, double2, double3] inicializadas en [$(document).ready()] también serán conocidas en la función [postForm01] de la línea 19;

Ahora, analicemos la función [postForm01]:


// post formulario
function postForm01() {
    // modo de validación del lado del cliente
    var validationActive = clientValidation.val() === "true";
    if (validationActive) {
        // se borran los errores del servidor
        clearServerErrors();
        // validación del formulario
        if (!formulaire.validate().form()) {
            // sin submit
            return false;
        }
    }
    // valores reales en formato anglosajón
    var value1 = double1.val().replace(",", ".");
    double1.val(value1);
    var value2 = double2.val().replace(",", ".");
    double2.val(value2);
    var value3 = double3.val().replace(",", ".");
    double3.val(value3);
    // se deja que submit se ejecute
    return true;
}

Recordemos que esta función jS se ejecuta antes que la [submit] del formulario. Si devuelve el valor booleano [false] (línea 11), entonces submit no se ejecutará. Si devuelve cualquier otro valor (línea 22), entonces sí se ejecutará.

  • El código importante es el de las líneas 4-12;
  • línea 4: se recupera el valor del campo oculto [clientValidation]. Este valor es «true» si se debe activar la validación del cliente, y «false» en caso contrario;
  • línea 6: en caso de validación del lado del cliente, se borran los mensajes de error del servidor que puedan estar presentes porque el usuario acaba de cambiar la configuración regional;
  • línea 9: recordemos que la variable [formulaire] representa el nodo de la etiqueta HTML <form>, es decir, el formulario. Este contiene validadores jS que aún no hemos presentado y que serán objeto de los párrafos siguientes. La expresión [formulaire.validate().form()] fuerza la ejecución de todos los validadores jS presentes en el formulario. Su valor es [true] si todos los valores comprobados son válidos, [false] en caso contrario;
  • línea 11: se devuelve el valor [false] si al menos uno de los valores comprobados no es válido. Esto impedirá que el [submit] del formulario llegue al servidor;
  • líneas 15-20: los identificadores [double1, double2, double3] representan los tres números reales del formulario. Según la configuración regional, el valor introducido es diferente. Con la configuración regional [fr-FR], se escribe [10,37], mientras que con la configuración regional [en-US] se escribe [10.37]. Esto es para la introducción de datos. Con la configuración [fr-FR], el valor enviado para [double1] se verá como [double1=10,37]. Al llegar al servidor, el valor [10,37] será rechazado porque este espera [10.37], el formato predeterminado de los números reales en Java. Por lo tanto, en las líneas 15-20, se sustituye la coma por el punto en el valor introducido para estos números;
  • línea 15: la expresión [double1.val()] devuelve la cadena de caracteres introducida para el nodo [double1]. La expresión [double1.val().replace(",", ".")] sustituye en esta cadena las comas por puntos. El resultado es una cadena [value1];
  • línea 16: la instrucción [double1.val(value1)] asigna este valor [value1] al nodo [double1].

Técnicamente, si el usuario ha introducido [10,37] en lugar del valor real [double1], tras las instrucciones anteriores, el nodo [double1] tendrá el valor [10.37] y el valor que se enviará será [param1=val1&double1=10.37&param2=val2], valor que será aceptado por el servidor;

  • línea 22: se devuelve el valor [true] para que se ejecute el [submit] del formulario;

Cabe destacar que la función jS [postForm01]:

  • ejecuta todos los validadores jS del formulario si la validación del lado del cliente está activada e impide que el [submit] del formulario se envíe al servidor si alguno de los valores introducidos ha sido declarado inválido;
  • permite que se ejecute el [submit], ya sea porque la validación del lado del cliente no está activada, o porque está activada y todos los valores introducidos son válidos;

Queda la instrucción de la línea [3]:


    // se borran los errores del servidor
clearServerErrors();

La función [clearServerErrors] tiene como objetivo borrar los mensajes presentes en la columna 4 de la vista [vue-01.xml]:

En la captura de pantalla anterior, se ha hecho clic en el enlace [English]. Hemos visto que esto provocaba un POST de los valores introducidos sin que se activaran los validadores jS. Al volver de la POST, la columna [Server Validation] se rellena con los posibles mensajes de error. Si ahora se hace clic en el botón [Validate] [2] con los validadores jS activados [3], entonces la columna [Client Validation] [4] se llenará de mensajes. Si no se hace nada, los que estaban presentes en la columna [Server Validation] permanecerán ahí, lo que creará confusión, ya que en caso de que los validadores jS detecten errores, no se solicitará al servidor. Para evitarlo, se borra la columna [Server Validation] en la función [postForm01]. Es la función [] la que realiza esta tarea:


function clearServerErrors() {
    // se borran los mensajes de error del servidor
    $(".error").each(function(index) {
        $(this).text("");
    });
}

Una particularidad de los mensajes de error es que todos tienen la clase [error]. Por ejemplo, para la primera línea de la tabla en [vue-01.html]:


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

Y estos son los únicos nodos de DOM que tienen esta clase. Utilizamos esta propiedad en la función [clearServerErrors]:


function clearServerErrors() {
    // se borran los mensajes de error del servidor
    $(".error").each(function(index) {
        $(this).text("");
    });
}
  • línea 3: la expresión [$(".error")] devuelve la colección de nodos del DOM que tienen la clase [error];
  • línea 3: la expresión [$(".error").each(function(index){f}] ejecuta la función [f] para cada uno de los nodos de la colección. Recibe un parámetro [index] que no se utiliza aquí, que es el número del nodo en la colección;
  • línea 4: la expresión [$(this)] designa el nodo actual en la iteración. Este es una etiqueta HTML <span>. La expresión [$(this).text("")] asigna la cadena vacía al texto mostrado por la etiqueta <span>;

Ahora vamos a examinar diferentes validadores jS.

6.3.8. Validador [required]

Analicemos el primer elemento del formulario:

La línea [1] se genera a partir de la siguiente secuencia de la vista [vue-01.xml]:


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

Estas líneas se refieren al campo [strNotEmpty] del formulario [Form01]:


    @NotNull
    @NotBlank
private String strNotEmpty;

Las restricciones [1-2] hacen que el campo [strNotEmpty] deba ser una cadena existente [NotNull], no vacía y que no esté compuesta únicamente por espacios [NotBlank]. Queremos reproducir esta restricción en el lado del cliente con Javascript.

Analicemos las líneas 5 y 8. La línea 11 no plantea ningún problema. Muestra el mensaje de error relacionado con el campo [strNotEmpty]. Empecemos por la línea 5:


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

A partir de este código, Thymeleaf generará la siguiente etiqueta:


<input type="text" data-val="true" data-val-required="Field is required" id="strNotEmpty" name="strNotEmpty" value="x" />
  • el atributo [data-val='true'] es utilizado por las bibliotecas de validación jQuery. Su presencia indica que el valor del nodo está sujeto a validación;
  • el atributo [data-val-X='msg'] proporciona dos datos. [X] es el nombre del validador, [msg] es el mensaje de error asociado a un valor no válido del nodo sobre el que se aplica el validador. Se trata solo de una información. Esto no provoca la visualización del mensaje de error;
  • [required] es un validador reconocido por la biblioteca de validación [jquery.validate.unobstrusive] de Microsoft. No es necesario definirlo. Esto no siempre será así en lo sucesivo;
  • las etiquetas [data-x] son ignoradas por HTML5. Solo son útiles si hay javascript para explotarlas;

Veamos ahora la línea 8:


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

Sirve para mostrar el mensaje de error del validador [required]. Si hay un error, la biblioteca de validación jS sustituirá dinámicamente la línea HTML de la tabla por el siguiente código:


<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>
  • línea 4: la clase del nodo [strNotEmpty] ha cambiado. Ahora es [input-validation-error], lo que hace que el campo erróneo se coloree en rojo;
  • línea 7: la clase del [span] ha cambiado. Ahora es [field-validation-error], lo que hará que el texto del [span] se muestre en rojo;
  • línea 8: el nodo [span], que antes estaba vacío, ahora contiene el texto [Le champ est obligatoire]. Este texto procede de la etiqueta [data-val-required="Le champ est obligatoire"] de la línea 4;
  • línea 7: para mostrar el mensaje de error del nodo [strNotEmpty] de la línea 4, hay que utilizar en la línea 7 los atributos [data-valmsg-for="strNotEmpty"] y [data-valmsg-replace="true"];

6.3.9. Validador [assertfalse]

La línea [1] se genera a partir de la siguiente secuencia de la vista [vue-01.xml]:


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

Estas líneas se refieren al campo [assertFalse] del formulario [Form01]:


    @NotNull
    @AssertFalse
private Boolean assertFalse;

Queremos reproducir esta restricción en el lado del cliente con Javascript. Las líneas 12-17 son ahora las habituales:

  • líneas 12-14: muestran, en caso de error en el campo [assertFalse], el mensaje transportado por el atributo [data-val-assertfalse] de la línea 6 o el transportado por el atributo [data-val-required] de la misma línea. Cabe recordar que estos mensajes están localizados, es decir, en el idioma elegido previamente por el usuario o en francés si no ha realizado ninguna selección;
  • líneas 5-10: muestran los botones de radio con los validadores js, que se activan en cuanto el usuario hace clic en uno de ellos.

Los dos botones están construidos de la misma manera. Vamos a examinar el primero:


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

Una vez procesada por Thymeleaf, esta línea se convierte en la siguiente:


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

Tenemos los validadores [data-val="true"]. Hay dos. Un validador llamado [required] [data-val-required="Le champ est obligatoire"] y otro llamado [assertfalse] [data-val-assertfalse="Seule la valeur False est acceptée"]. Recordemos que el valor del atributo [data-val-X] es el mensaje de error del validador X.

Hemos visto el validador [required]. La novedad aquí es que se pueden asociar varios validadores a un valor introducido. Si el validador [required] es conocido por la biblioteca de validación MS (Microsoft), no es el caso del validador [assertFalse]. Por lo tanto, vamos a aprender a crear un nuevo validador. Vamos a crear varios y se colocarán en un archivo [client-validation.js]:

  

Este archivo, al igual que los demás, es importado por la vista [vue-01.xml] (línea 6 a continuación):


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

La incorporación del validador [assertfalse] se resume en la creación de las dos funciones jS siguientes:


// -------------- 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("''", "'");
});

Sinceramente, no soy un especialista en javascript, un lenguaje que para mí sigue siendo un misterio. Sus fundamentos son sencillos, pero las bibliotecas que se basan en ellos suelen ser muy complejas. Para escribir las líneas de código anteriores, me inspiré en códigos que encontré en Internet. Fue el enlace [http://jsfiddle.net/LDDrk/] el que me indicó el camino a seguir. Si aún existe, invito al lector a que lo consulte, ya que es muy completo e incluye un ejemplo funcional. Muestra cómo crear un nuevo validador y me permitió crear todos los de este capítulo. Volvamos al código:

  • líneas 2-4: definen el nuevo validador. La función [$.validator.addMethod] espera como primer parámetro el nombre del validador y, como segundo parámetro, una función que lo defina;
  • línea 2: la función tiene tres parámetros:
    • [value]: el valor a validar. La función debe devolver [true] si el valor es válido, [false] en caso contrario;
    • [element]: elemento HTML al que pertenece el valor a validar,
    • [param]: un objeto que contiene los valores asociados a los parámetros de un validador. Aún no hemos introducido este concepto. En este caso, el validador [assertFalse] no tiene parámetros. Se puede determinar si el valor [value] es válido sin necesidad de información adicional. No sería lo mismo si tuviéramos que verificar que el valor [value] es un número real en el intervalo [min, max]. En ese caso, necesitaríamos conocer [min] y [max]. A estos dos valores se les denomina parámetros del validador;
  • líneas 6-9: una función necesaria para la biblioteca de validación MS. La función [$.validator.unobtrusive.adapters.add] espera como primer parámetro el nombre del validador, como segundo parámetro la matriz de parámetros del validador y como tercer parámetro una función;
  • el validador [assertFalse] no tiene parámetros. Por eso, el segundo parámetro es una matriz vacía;
  • la función solo tiene un parámetro, un objeto [options] que contiene información sobre el elemento que se va a validar y para el que hay que definir dos nuevas propiedades [rules] y [messages];
    • línea 7: se definen las reglas [rules] para el validador [assertFalse]. Estas reglas son los parámetros del validador [assertFalse], los mismos que los del parámetro [param] de la línea 2. Estos parámetros se encuentran en [options.params];
    • línea 8: definen el mensaje de error del validador [assertFalse]. Este se encuentra en [options.message]. Tenemos la siguiente dificultad con los mensajes de error. En los archivos de mensajes, encontraremos el siguiente mensaje:

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

El doble apóstrofo es necesario para Thymeleaf. Este lo interpreta como un apóstrofo simple. Si se pone un apóstrofo simple, Thymeleaf no lo muestra. Ahora estos mensajes también servirán como mensajes de error para la biblioteca de validación MS. Sin embargo, javascript mostrará los dos apóstrofos. En la línea 8, sustituimos el doble apóstrofo del mensaje de error por uno solo.

Para ver un poco lo que ocurre, podemos añadir código de registro jS:


// registros
var logs = {
    assertfalse : true
}


// -------------- assertfalse
$.validator.addMethod("assertfalse", function(value, element, param) {
    // registros
    if (logs.assertfalse) {
        console.log(jSON.stringify({
            "[assertfalse] value" : value
        }));
        console.log("[assertfalse] element");
        console.log(element);
        console.log(jSON.stringify({
            "[assertfalse] param" : param
        }));
    }
    // prueba de validez
    return value === "false";
});

$.validator.unobtrusive.adapters.add("assertfalse", [], function(options) {
    // registros
    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
        }));
    }
    // código
    options.rules["assertfalse"] = options.params;
    options.messages["assertfalse"] = options.message.replace("''", "'");
});

Este código utiliza la biblioteca jSON JSON3 [http://bestiejs.github.io/json3/]. Si activamos los registros (línea 3), obtenemos las siguientes entradas en la consola:

Al cargar la página por primera vez, aparecen los siguientes registros:

 

Se ha ejecutado la función jS [$.validator.unobtrusive.adapters.add]. Se obtiene la siguiente información:

  • [options.params] es un objeto vacío porque el validador [assertFalse] no tiene parámetros;
  • [options.message] es el mensaje de error que hemos creado para el validador [assertFalse] en el atributo [data-val-assertFalse];
  • [options.messages] es un objeto que contiene los demás mensajes de error del elemento validado. Aquí encontramos el mensaje de error que hemos puesto en el atributo [data-val-required];

Ahora asignemos un valor erróneo al campo [assertFalse] y validemos:

 

A continuación, obtenemos los siguientes registros:

En ellos vemos lo siguiente:

  • el valor comprobado es [true] (línea 118);
  • el elemento HTML comprobado es el botón de radio de id [assertFalse1] (línea 122);
  • El validador [assertFalse] no tiene parámetros (línea 123);

Eso es todo. ¿Qué podemos sacar en claro de todo esto?

Para un validador X jS, debemos definir:

  • en la etiqueta HTML que se va a validar, el atributo [data-val-X='msg'], que define tanto el validador X como su mensaje de error;
  • dos funciones jS que deben incluirse en el archivo [client-validation.js]:
    • [$.validator.addMethod("X", function(value, element, param)],
    • [$.validator.unobtrusive.adapters.add("X", [param1, param2], function(options)] ;

A continuación, nos basaremos en lo que se ha hecho para este primer validador y nos limitaremos a presentar las novedades.

6.3.10. Validador [asserttrue]

Este validador es, evidentemente, análogo al validador [assertFalse].

La línea [1] se genera a partir de la siguiente secuencia de la vista [vue-01.xml]:


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

Estas líneas se refieren al campo [assertTrue] del formulario [Form01]:


    @NotNull
    @AssertTrue
private Boolean assertTrue;

No hay nada nuevo en las líneas 1-16. Utilizan un validador [asserrtrue] que hay que definir en el archivo [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. Validadores [date] y [past]

La línea [1] se genera mediante la siguiente secuencia de la vista [vue-01.xml]:


<!-- obligatorio, fecha, pasado -->
<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>

Estas líneas se refieren al campo [dateInPast] del formulario [Form01]:


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

La línea de los validadores de fecha es la siguiente:


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

En ella hay tres validadores [data-val-X]: required, date, past. Debemos definir en [client-validation.js] las funciones asociadas a estos dos nuevos validadores:


logs.date = true;
// -------------- fecha
$.validator.addMethod("date", function(value, element, param) {
    // validez
    var valide = Globalize.parseDate(value, "yyyy-MM-dd") != null;
    // registros
    if (logs.date) {
        console.log(jSON.stringify({
            "[date] value" : value,
            "[date] valide" : valide
        }));
    }
    // resultado
    return valide;
});

$.validator.unobtrusive.adapters.add("date", [], function(options) {
    options.rules["date"] = options.params;
    options.messages["date"] = options.message.replace("''", "'");
});

y


logs.past = true;
// -------------- pasado
$.validator.addMethod("past", function(value, element, param) {
    // validez
    var valide = value <= new Date().toISOString().substring(0, 10);
    // registros
    if (logs.past) {
        console.log(jSON.stringify({
            "[past] value" : value,
            "[past] valide" : valide
        }));
    }
    // resultado
    return valide;
});

$.validator.unobtrusive.adapters.add("past", [], function(options) {
    options.rules["past"] = options.params;
    options.messages["past"] = options.message.replace("''", "'");
});

Antes de explicar el código, veamos los registros cuando se introduce una fecha posterior a la de hoy:

 

Lo primero que hay que destacar es que la fecha que se va a validar llega como una cadena de caracteres con el formato [aaaa-mm-jj]. Esto explica las siguientes líneas:


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

La biblioteca [globalize.js] proporciona la función [Globalize.parseDate] anterior. El primer parámetro es la fecha como cadena de caracteres y el segundo, su formato. El resultado es un puntero nulo si la fecha no es válida; de lo contrario, es la fecha resultante.

La validez del validador [past] se comprueba mediante el siguiente código:


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

A continuación se muestra en una consola la evaluación de la expresión [new Date().toISOString().substring(0, 10)]:

  

La cadena de caracteres [value] debe preceder alfabéticamente a la cadena [new Date().toISOString().substring(0, 10)] para ser válida.

Cabe señalar que el version de Chrome utilizado proporciona la fecha en el formato [yyyy-mm-dd]. En un navegador en el que no fuera así, habría que indicar explícitamente al usuario que utilice este formato de entrada.

6.3.12. Validador [future]

La línea [1] se genera a partir de la siguiente secuencia de la vista [vue-01.xml]:


<!-- obligatorio, fecha, futuro -->
<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>

Estas líneas se refieren al campo [dateInFuture] del formulario [Form01]:


    @NotNull
    @Future
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date dateInFuture;
  • línea 5, aparece un nuevo validador [data-val-future];

Este validador es, por supuesto, muy similar al validador [past]. Las dos funciones que hay que añadir en [client-validation.js] son las siguientes:


// -------------- futuro
$.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. Validadores [int] y [max]

La línea [1] se genera mediante la siguiente secuencia de la vista [vue-01.xml]:


<!-- obligatorio, int, máx(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>

Estas líneas se refieren al campo [intMax100] del formulario [Form01]:


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

En la línea 5 hay dos nuevos validadores: [int] y [max]. Este último tiene un parámetro: el valor máximo. Examinemos el código HTML generado por la línea 5:


<!-- obligatorio, int, máx(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>

Recordemos el significado de los diferentes atributos de [data-X]:

  • [data-val="true"] indica que hay validadores asociados al elemento HTML;
  • [data-val-required] introduce el validador [required] con su mensaje;
  • [data-val-int] introduce el validador [int] con su mensaje;
  • [data-val-max] introduce el validador [max] con su mensaje;
  • [data-val-max-value="100"] introduce un parámetro denominado [value] para el validador [max]. [100] es el valor de este parámetro. Es la primera vez que nos encontramos con el concepto de parámetros de un validador.

El archivo [client-validation.js] se amplía con el siguiente validador [int]:


logs.int = true;
// -------------- int
$.validator.addMethod("int", function(value, element, param) {
    // validez
    valide = /^\s*[-\+]?\s*\d+\s*$/.test(value);
    // registros
    if (logs.int) {
        console.log(jSON.stringify({
            "[int] value" : value,
            "[int] valide" : valide,
        }));
    }
    // resultado
    return valide;
});

$.validator.unobtrusive.adapters.add("int", [], function(options) {
    options.rules["int"] = options.params;
    options.messages["int"] = options.message.replace("''", "'");
});
  • línea 5: se utiliza una expresión regular para verificar que la cadena [value] representa efectivamente un entero. Este puede ser con signo;

A continuación se muestran algunos ejemplos de registros:

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

El validador [max] se añade de la siguiente manera en [client-validation.js]


// -------------- max para usar junto con [int] o [number]
logs.max = true;
$.validator.addMethod("max", function(value, element, param) {
    // registros
    if (logs.max) {
        console.log(jSON.stringify({
            "[max] value" : value,
            "[max] param" : param
        }));
    }
    // validez
    var val = Globalize.parseFloat(value);
    if (isNaN(val)) {
        // registros
        if (logs.max) {
            console.log(jSON.stringify({
                "[max] valide" : true
            }));
        }
        // resultado
        return true;
    }
    var max = Globalize.parseFloat(param.value);
    var valide = val <= max;
    // registros
    if (logs.max) {
        console.log(jSON.stringify({
            "[max] valide" : valide
        }));
    }
    // resultado
    return valide;
});

$.validator.unobtrusive.adapters.add("max", [ "value" ], function(options) {
    options.rules["max"] = options.params;
    options.messages["max"] = options.message.replace("''", "'");
});

Vamos a tratar ahora el caso del parámetro [value] del validador [max] introducido por el atributo [data-val-max-value="100"].

  • En la línea 35, el parámetro [value] se integra en el segundo parámetro de la función [$.validator.unobtrusive.adapters.add];
  • línea 3, el objeto [param] ya no estará vacío, sino que contendrá {"value":100};

Para comprender el código de las líneas 3-33, hay que saber que cuando hay varios validadores en un mismo elemento HTML:

  • no se conoce el orden de ejecución de los validadores;
  • la ejecución de los validadores se detiene en cuanto un validador declara el elemento inválido. Es entonces el mensaje de error de este último el que se asocia al elemento inválido;

Analicemos el código:

  • línea 12: se comprueba que se trata de un número. Si el validador [int] se ha ejecutado antes que el validador [max], esto es necesariamente cierto, ya que un valor inválido detiene la ejecución de los validadores;
  • líneas 13-22: si no tenemos un número, eso significa que el validador [int] aún no se ha ejecutado. A continuación, se indica que el valor comprobado es válido para permitir que el validador [int] haga su trabajo y declare el elemento no válido con su propio mensaje de error;
  • líneas 23-24: calcula la validez de [value];

A continuación se muestran algunos registros:

Valor introducido
registros
x

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

{"[max] value":"111","[max] param":{"value":"100"}}
{"[max] valide":false}
111x

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

6.3.14. Validador [min]

La línea [1] se genera a partir de la siguiente secuencia de la vista [vue-01.xml]:


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

Estas líneas se refieren al campo [intMin10] del formulario [Form01]:


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

La línea 5 introduce un nuevo validador [min] [data-val-int=#{typeMismatch}] con un parámetro [value] [data-val-min-value=#{form01.intMin10.value}"]. Se trata de un caso similar al del validador [max]. Se añade en [client-validation.js] el siguiente código:


logs.min = true;
//-------------- min para usar junto con [int] o [number]
$.validator.addMethod("min", function(value, element, param) {
    // registros
    if (logs.min) {
        console.log(jSON.stringify({
            "[min] value" : value,
            "[min] param" : param
        }));
    }
    // validez
    var val = Globalize.parseFloat(value);
    if (isNaN(val)) {
        // registros
        if (logs.min) {
            console.log(jSON.stringify({
                "[min] valide" : true
            }));
        }
        // resultado
        return true;
    }
    var min = Globalize.parseFloat(param.value);
    var valide = val >= min;
    // registros
    if (logs.min) {
        console.log(jSON.stringify({
            "[min] valide" : valide
        }));
    }
    // resultado
    return valide;
});

$.validator.unobtrusive.adapters.add("min", [ "value" ], function(options) {
    options.rules["min"] = options.params;
    options.messages["min"] = options.message.replace("''", "'");
});

A continuación se muestran algunos registros de ejecución:

Valor introducido
registros
x

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

{"[min] value":"11","[min] param":{"value":"10"}}
{"[min] valide":true}
{"[int] value":"11","[int] valide":true}
8x

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

6.3.15. Validador [regex]

La línea [1] se genera a partir de la siguiente secuencia de la vista [vue-01.xml]:


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

Estas líneas se refieren al campo [strBetween4and6] del formulario [Form01]:


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

La línea 5 genera el siguiente 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" />

Esta etiqueta introduce el validador [regex] [data-val-regex="La chaîne doit avoir entre 4 et 6 caractères"] con su parámetro [pattern] [data-val-regex-pattern="^.{4,6}$"]. El parámetro [pattern] es la expresión regular que debe verificar el valor a validar. En este caso, la expresión regular comprueba que la cadena tenga entre 4 y 6 caracteres cualesquiera. El validador [regex] está predefinido en la biblioteca de validación MS. Por lo tanto, no hay nada que añadir en el archivo [client-validation.js].

6.3.16. Validador [email]

La línea [1] se genera a partir de la siguiente secuencia de la vista [vue-01.xml]:


<!-- obligatorio, correo electrónico -->
<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>

Estas líneas se refieren al campo [email] del formulario [Form01]:


    @NotNull
    @Email
    @NotBlank
    private String email;

La línea 5 genera la siguiente línea 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" />

Esta etiqueta introduce el validador [email] [data-val-email="Adresse mail invalide"]. El validador [email] está predefinido en la biblioteca de validación MS. Por lo tanto, no hay nada que añadir en el archivo [client-validation.js].

6.3.17. Validador [range]

La línea [1] se genera a partir de la siguiente secuencia de la vista [vue-01.xml]:


<!-- obligatorio, int, rango (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>

Estas líneas se refieren al campo [int1014] del formulario [Form01]:


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

La línea 5 genera la siguiente línea 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" />

Esta etiqueta introduce un nuevo validador [range] [data-val-range="La valeur doit être dans l&#39;&#39;intervalle [10,14]"] que tiene dos parámetros [min] [data-val-range-min="10"] y [max] [data-val-range-max="14"].

En el archivo [client-validation.js], definimos el validador [range] de la siguiente manera:


// -------------- rango que debe utilizarse junto con [int] o [number]
logs.range=true
$.validator.addMethod("range", function(value, element, param) {
    // registros
    if (logs.range) {
        console.log(jSON.stringify({
            "[range] value" : value,
            "[range] param" : param
        }));
    }
    // validez
    var val = Globalize.parseFloat(value);
    if (isNaN(val)) {
        // registros
        if (logs.min) {
            console.log(jSON.stringify({
                "[range] valide" : true
            }));
        }
        // finalizado
        return true;
    }
    var min = Globalize.parseFloat(param.min);
    var max = Globalize.parseFloat(param.max);    
    var valide = val >= min && val <= max;
    // registros
    if (logs.range) {
        console.log(jSON.stringify({
            "[range] valide" : valide
        }));
    }
    // finalizado
    return valide;
});

$.validator.unobtrusive.adapters.add("range", [ "min", "max" ], function(options) {
    options.rules["range"] = options.params;
    options.messages["range"] = options.message.replace("''", "'");
});

Es muy similar a los validadores [min] y [max] ya estudiados.

A continuación se muestran algunos ejemplos de registros:

Valor introducido
registros
x

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

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

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

6.3.18. Validador [number]

La línea [1] se genera a partir de la siguiente secuencia de la vista [vue-01.xml]:


<!-- doble1: obligatorio, número, rango (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>

Estas líneas se refieren al campo [double1] del formulario [Form01]:


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

La línea 5 genera la siguiente línea 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" />

La etiqueta introduce un nuevo validador [number] con el atributo [data-val-number="Format invalide"]. Este validador se define de la siguiente manera en el archivo [client-validation.js]:


// -------------- número
logs.number = true;
$.validator.addMethod("number", function(value, element, param) {
    var valide = !isNaN(Globalize.parseFloat(value));
    // registros
    if (logs.number) {
        console.log(jSON.stringify({
            "[number] value" : value,
            "[number] valide" : valide
        }));
    }
    // resultado
    return valide;
});

$.validator.unobtrusive.adapters.add("number", [], function(options) {
    options.rules["number"] = options.params;
    options.messages["number"] = options.message.replace("''", "'");
});

A continuación se muestran algunos ejemplos de registros:

Valor introducido
registros
x
 {"[number] value":"x","[number] valide":false}
-2,5
{"[number] value":"-2,5","[number] valide":true}
{"[range] value":"-2,5","[range] param":{"min":"2.3","max":"3.4"}}
{"[range] valide":false}
2,5
{"[number] value":"+2,5","[number] valide":true}
{"[range] value":"+2,5","[range] param":{"min":"2.3","max":"3.4"}}
{"[range] valide":true}
+2.5
{"[number] value":"+2.5","[number] valide":true}
{"[range] value":"+2.5","[range] param":{"min":"2.3","max":"3.4"}}
{"[range] valide":true}

Sabemos que los números reales dependen de la cultura. En el ejemplo anterior, estamos en la cultura [fr-FR]. Al introducir [2.5] (notación anglosajona), el número se acepta. La culpa es de [Globalize.parseFloat], que acepta ambas notaciones:

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

Pasemos al inglés e introduzcamos [+2,5] y [+2.5]. Los registros son los siguientes:

Valor introducido
registros
x
 {"[number] value":"x","[number] valide":false}
2,5
{"[number] value":"+2,5","[number] valide":true}
{"[range] value":"+2,5","[range] param":{"min":"2.3","max":"3.4"}}
{"[range] valide":false}
+2.5
{"[number] value":"+2.5","[number] valide":true}
{"[range] value":"+2.5","[range] param":{"min":"2.3","max":"3.4"}}
{"[range] valide":true}

Hay un problema con [2,5]. Se ha declarado como un valor válido, cuando en realidad debería escribirse [2.5]. La culpa es de [Globalize.parseFloat]:

Globalize.parseFloat("2,5")
25

En el ejemplo anterior, [Globalize.parseFloat] ignora la coma y considera que el número es 25. En la cultura [en-US], un número real puede incluir un punto decimal y comas, que a veces se utilizan para separar los miles.

Se puede mejorar el resultado de la siguiente manera:


// -------------- número
logs.number = true;
$.validator.addMethod("number", function(value, element, param) {
    // solo se gestionan los cultivos [fr-FR] y [en-US]
    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;
    // prueba de validez
    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));
    }
    // registros
    if (logs.number) {
        console.log(jSON.stringify({
            "[number] value" : value,
            "[number] culture" : culture,
            "[number] valide" : valide
        }));
    }
    // resultado
    return valide;
});
  • línea 5: la expresión regular de un número real para la cultura [fr-FR];
  • línea 6: la expresión regular de un número real para la cultura [en-US];
  • línea 7: el nombre de la cultura actual. En nuestro ejemplo, será una de las dos culturas anteriores;
  • líneas 9-16: la comprobación de validez del valor introducido;
  • línea 15: se ha previsto el caso en el que la cultura no sea ni [fr-FR] ni [en-US];

Los registros muestran ahora lo siguiente:

Configuración [fr-FR]

Valor introducido
registros
x

 {"[number] value":"x","[number] culture":"fr-FR","[number] valide":false}
-2,5

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

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

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

Cultivo [en-US]

Valor introducido
registros
x

{"[number] value":"x","[number] culture":"en-US","[number] valide":false}
2,5

{"[number] value":"+2,5","[number] culture":"en-US","[number] valide":false}
+2.5

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

6.3.19. Validador [custom3]

La línea [1] se genera a partir de la siguiente secuencia de la vista [vue-01.xml]:


<!-- double3: obligatorio, número, 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>

Estas líneas se refieren al campo [double3] del formulario [Form01]:


    @NotNull
    private Double double3;

Aquí queremos estudiar un validador que ya no valida un valor introducido, sino una relación entre dos valores introducidos. En este caso, queremos que [double1+double3] esté dentro del intervalo [10,13].

La línea 5 genera la siguiente línea 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" />

Esta línea introduce el nuevo validador [custom3] declarado por el atributo [data-val-custom3="[double3+double1] must be in [10,13]"]. Este validador tiene los siguientes parámetros:

  • [field] declarado por el atributo [data-val-custom3-field="double1"]. Este parámetro designa el campo cuyo valor interviene en el cálculo de la validez de [double3];
  • [min] declarado por el atributo [data-val-custom3-min="10.0"]. Este parámetro es el mínimo del intervalo [min, max] en el que debe encontrarse [double1+double3];
  • [max] declarado por el atributo [data-val-custom3-max="13.0"]. Este parámetro es el máximo del intervalo [min, max] en el que debe encontrarse [double1+double3];

Este validador se gestiona de la siguiente manera en [client-validation.js]:


// -------------- custom3 utilizado conjuntamente con [number]
logs.custom3 = true;
$.validator.addMethod("custom3", function(value1, element, param) {
    // segundo valor
    var value2 = $("#" + param.field).val();
    // registros
    if (logs.custom3) {
        console.log(jSON.stringify({
            "[custom3] value1" : value1,
            "[custom3] param" : param,
            "[custom3] value2" : value2            
        }))
    }
    // primer valor
    var valeur1 = Globalize.parseFloat(value1);
    if (isNaN(valeur1)) {
        // dejamos que el validador [number] haga el trabajo
        if (logs.custom3) {
            console.log(jSON.stringify({
                "[custom3] valide" : true
            }))
        }
        return true;
    }
    // segundo valor
    var valeur2 = Globalize.parseFloat(value2);
    if (isNaN(valeur2)) {
        // no se puede realizar el cálculo de validez
        if (logs.custom3) {
            console.log(jSON.stringify({
                "[custom3] valide" : false
            }))
        }
        return false;
    }
    // cálculo de validez
    var min = Globalize.parseFloat(param.min);
    var max = Globalize.parseFloat(param.max);
    var somme = valeur1 + valeur2;
    var valide = somme >= min && somme <= max;
    // registros
    if (logs.custom3) {
        console.log(jSON.stringify({
            "[custom3] valide" : valide
        }))
    }
    // resultado
    return valide;
});

$.validator.unobtrusive.adapters.add("custom3", [ "field", "max", "min" ], function(options) {
    options.rules["custom3"] = options.params;
    options.messages["custom3"] = options.message.replace("''", "'");
});

A continuación se muestran algunos ejemplos de registros:

Valores introducidos
[double1,double3]
registros
[x,1]

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

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

{"[custom3] value1":"20","[custom3] param":{"field":"double1","max":"13.0","min":"10.0"},"[custom3] value2":"1"}
{"[custom3] valide":false}
[1,10]

{"[number] value":"10","[number] culture":"en-US","[number] valide":true}
{"[custom3] value1":"10","[custom3] param":{"field":"double1","max":"13.0","min":"10.0"},"[custom3] value2":"1"}
{"[custom3] valide":true}

6.3.20. Validador [url]

La línea [1] se genera a partir de la siguiente secuencia de la vista [vue-01.xml]:


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

Estas líneas se refieren al campo [url] del formulario [Form01]:


    @URL
    @NotBlank
    private String url;

La línea 5 genera la siguiente línea HTML:


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

Introduce el validador [url] con el atributo [data-val-url]. Este validador está predefinido en la biblioteca de validación jQuery. No hay nada que añadir en [client-validation.js].

6.3.21. Activación/desactivación de la validación del lado del cliente

Mientras la validación del lado del cliente esté activa, nunca se verá la validación del lado del servidor, ya que los valores enviados solo llegan al servidor si se han declarado válidos en el lado del cliente. Para ver cómo funciona la validación del lado del servidor, es necesario desactivar la validación del lado del cliente. La vista [vue-01.xml] ofrece dos enlaces para gestionar esta activación/desactivación:


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

Estos dos enlaces no son visibles al mismo tiempo:

La traducción HTML de estos enlaces es la siguiente:


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

El script jS [setClientValidation] se define en el archivo [local.js] (véase más arriba). En la función [$(document).ready] de este archivo, se utilizan los enlaces de validación:


// documento listo
$(document).ready(function() {
    // referencias globales
...
    activateValidationTrue = $("#clientValidationTrue");
    activateValidationFalse = $("#clientValidationFalse");
    clientValidation = $("#clientValidation");
...
    // enlaces de validación
    // clientValidation es un campo oculto establecido por el servidor
    var validate = clientValidation.val();
    setClientValidation2(validate === "true");
});
  • línea 5: una referencia al enlace de activación de la validación del lado del cliente;
  • línea 6: una referencia al enlace de desactivación de la validación del lado del cliente;
  • línea 7: una referencia a un campo oculto del formulario que almacena el último estado de la activación en forma de un valor booleano [true : validation client activée, false : validation client désactivée]. Este campo se encuentra en la vista [vue-01.xml] con el siguiente formato:

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

y corresponde al campo [clientValidation] del formulario [Form01]:


// validación del cliente
private boolean clientValidation = true;
  • línea 11: se recupera el valor del campo oculto;
  • línea 12: se llama a la siguiente función [setClientValidation2]:

function setClientValidation2(activate) {
    // enlaces
    if (activate) {
        // la validación del cliente está activa
        activateValidationTrue.hide();
        activateValidationFalse.show();
        // se analizan los validadores del formulario
        $.validator.unobtrusive.parse(formulaire);
    } else {
        // la validación del cliente está desactivada
        activateValidationFalse.hide();
        activateValidationTrue.show();
        // se desactivan los validadores del formulario
        formulaire.data('validator', null);
    }
}
  • línea 1: el parámetro [activate] toma el valor [true] si hay que activar la validación del lado del cliente; false en caso contrario;
  • líneas 5-6: se muestra el enlace de desactivación y se oculta el enlace de activación;
  • línea 8: para que la validación del lado del cliente funcione, hay que analizar el documento en busca de validadores [data-val-X]. El parámetro de la función [$.validator.unobtrusive.parse] es el identificador jS del formulario que se va a analizar;
  • líneas 11-12: se muestra el enlace de activación y se oculta el enlace de desactivación;
  • líneas 14: los validadores del formulario están desactivados. A partir de ahora, es como si no hubiera validadores jS en el formulario;

¿Para qué sirve esta función [setClientValidation2]? Sirve para gestionar los POST. Como el campo [clientValidation] es un campo oculto, se envía y vuelve con el formulario devuelto por el servidor. A continuación, se utiliza su valor para restablecer la validación del lado del cliente tal y como estaba antes del POST. De hecho, no hay memoria jS entre las solicitudes. Por lo tanto, el servidor debe transmitir en la nueva vista la información que permite inicializar el jS de esta. Esto se suele hacer en la función [$(document).ready].

Volvamos a la función [setClientValidation], que gestiona el clic en los enlaces de activación/desactivación de la validación del lado del cliente:


// validación del lado del cliente
function setClientValidation(activate) {
    // se gestiona la activación/desactivación de la validación del cliente
    setClientValidation2(activate);
    // se almacena la elección del usuario en el campo oculto
    clientValidation.val(activate ? "true" : "false");
    // ajustes adicionales
    if (activate) {
        // la validación del cliente está activa
        // se borran todos los mensajes de error del servidor
        clearServerErrors();
        // se valida el formulario
        formulaire.validate().form();
    } else {
        // la validación del cliente está desactivada
        // se borran todos los mensajes de error del cliente
        clearClientErrors();
    }
}
  • línea 4: se utiliza la función [setClientValidation2] que acabamos de ver;
  • línea 6: se almacena la elección del usuario en el campo oculto para recuperarla al volver de la siguiente POST;
  • línea 11: si la validación del cliente está activa, se borran los mensajes de error de la columna [serveur] de la vista. Hemos descrito la función [clearServerErrors] en el apartado 6.3.7;
  • línea 13: se ejecutan los validadores jS para mostrar los posibles mensajes de error en la columna [client] de la vista;
  • línea 17: si la validación del cliente está desactivada, se borran los mensajes de error de la columna [client] de la vista. Examinemos en la consola de desarrollo de Chrome el código HTML de un elemento erróneo:

<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>
  • En la línea 2, vemos que, en la columna 2 de la tabla, el elemento erróneo tiene el estilo [class="input-validation-error"];
  • en la línea 5, vemos que en la columna 3 de la tabla, el mensaje de error tiene el estilo [class="field-validation-error"];

Esto es así para todos los elementos erróneos. Se utilizan estas dos informaciones en la siguiente función [clearClientErrors]:


// borrar errores del cliente
function clearClientErrors() {
    // se borran los mensajes de error del cliente
    $(".field-validation-error").each(function(index) {
        $(this).text("");
    });
    // se cambia la clase CSS de las entradas erróneas
    $(".input-validation-error").each(function(index) {
        $(this).removeClass("input-validation-error");
    });
}
  • líneas 4-6: se buscan todos los elementos de DOM que tengan la clase [field-validation-error] y se borra el texto que muestran. Así es como se borran los mensajes de error;
  • líneas 8-10: se buscan todos los elementos del DOM que tengan la clase [input-validation-error] y se les elimina dicha clase. De este modo, el elemento erróneo que se había resaltado en rojo recupera su estilo original;