Skip to content

6. Validación con 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

Hasta ahora, las páginas enviadas al cliente no contenían JavaScript. Ahora abordaremos 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 del lado del servidor. Ahora vamos a ofrecer la posibilidad de validarlo tanto del lado del cliente como del lado del servidor.

Nota: el tema es complejo. El lector que no esté interesado en este tema puede pasar directamente al párrafo 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: del cliente y del servidor. Como el POST solo se ejecuta si los valores se han considerado válidos del lado del cliente, las validaciones del lado del servidor siempre se realizan con éxito. Por lo tanto, se ha incluido 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. A continuación, un ejemplo:

123
  • en [1], los valores ingresados;
  • 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 ingresados. Se puede observar que los datos ingresados incorrectamente 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, no se envía el POST al servidor;

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 ser útil si en algún momento nos parece que Spring no está gestionando alguno de nuestros componentes. Es una forma de verificarlo. También es una forma de verificar 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

Sigamos 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 exigir 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 inserta automáticamente en la plantilla, asociado a la clave [form01];
  • línea 10: la configuración regional y la plantilla se insertan 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 manejan 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 mensaje, si lo hay
        if (message != null) {
            model.addAttribute("message", message);
        }
}
  • El objetivo del método [setModel] es incluir en la plantilla:
    • información sobre la configuración regional,
    • el mensaje pasado como último parámetro;
  • línea 14: se incluye en la plantilla información sobre la configuración regional (idioma, país);
  • líneas 16-18: se inserta en la configuración regional el mensaje que se haya 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 ingresados 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 aú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 de 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: establecemos 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 conocidos actualmente, [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 falló. Se agrega un elemento a la colección [Errors errors] mediante el método [Errors.rejectValue], cuyos parámetros son los siguientes:
    • parámetro 1: por lo general, el nombre del campo con error. Aquí se han probado los campos [double1, double2]. Se puede indicar 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. Esta es el 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 almacenar, para cada uno de ellos:
    • el nombre del objeto validado,
    • su código de error,
    • su mensaje de error predeterminado;
  • línea 10: con esta información, se construye la plantilla de la vista [vue-01.xml]. En esta ocasión, hay un mensaje, que es la versión concatenada y abreviada 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 presentaremos una pequeña parte de ella:


<!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 varios 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 de JavaScript que podemos ignorar aquí;
  • línea 14: muestra la configuración regional que el servidor ha colocado en la plantilla;
  • línea 59: muestra el mensaje que el servidor ha insertado en la plantilla;

El código de las líneas 33 a 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 observar 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, del 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 del lado del cliente, estos atributos no se utilizarán. Por lo tanto, por ahora, no es necesario entenderlos. Simplemente podemos centrarnos 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, existe una dificultad para 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 de 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 como [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 JavaScript de validación. 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. Utilizaremos el marco jQuery, que ofrece numerosas funciones que facilitan el desarrollo en JavaScript. A continuación, presentamos los conceptos básicos de jQuery que es necesario conocer para comprender los scripts de este capítulo y 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 con el ID [element1]. Vamos a trabajar con este elemento.

Necesitamos descargar el archivo [jquery-1.11.1.min.js]. Encontraremos la última versión de jQuery en URL [http://jquery.com/download/]:

Image

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

  

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

Con Google Chrome, ejecuta [Ctrl-Maj-I] para que aparezcan las herramientas de desarrollo [3]. La pestaña [Console] [4] permite ejecutar código JavaScript. A continuación, te presentamos algunos comandos de JavaScript que puedes escribir, junto con una explicación de cada uno.

JS
resultado
$("#element1")
: devuelve la colección de todos los elementos con el identificador [element1]; por lo tanto, normalmente será una colección de 0 o 1 elemento, ya que no pueden existir dos identificadores 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 con el 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]. El atributo CSS style='display: block;' es el que garantiza esta visualización.
$("#element1").attr('style','color: red')
: asigna un atributo a todos los elementos de la colección. El atributo aquí es [style] y su valor es [color: red]. El texto [blabla] se vuelve rojo.
Tableau
Dictionnaire

Cabe señalar que el URL del navegador no ha cambiado durante todas estas operaciones. No ha habido intercambio alguno 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 modificaciones que hemos realizado en el elemento de las líneas 10 a 12. Es importante recordar esto al depurar JavaScript. Por lo tanto, a menudo es innecesario ver el código fuente de la página que se muestra.

Ya sabemos lo suficiente para entender los scripts jS que vendrán a continuación.

6.3.2. Las bibliotecas de validación jS

Vamos a utilizar bibliotecas del ecosistema jQuery. En torno a jQuery giran varios proyectos que, a su vez, dan lugar a bibliotecas. Utilizaremos la biblioteca de validación [jquery.validate.unobstrusive] creada por Microsoft y donada a la fundación jQuery. En adelante, nos referiremos a ella como la biblioteca de validación MS o, más simplemente, como la biblioteca MS. Para obtenerla, se necesita un entorno de Microsoft Visual Studio. No he encontrado otra forma de conseguirla. Se puede utilizar una versión gratuita como [Visual Studio Community] o [http://www.visualstudio.com/en-us/news/vs2013-community-vs.aspx] (diciembre de 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 agregar los 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], descarga también la biblioteca [Microsoft jQuery Unobtrusive Ajax] que usaremos próximamente;
  • en [15-16], busca 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]: la versión no legible, conocida como «minificada» de la biblioteca. En realidad no es ilegible. Es texto. Pero no es comprensible. Es la versión que se debe usar en producción, ya que este archivo es más pequeño que la versión correspondiente [js] y, por lo tanto, mejora la velocidad de las comunicaciones entre el cliente y el servidor;

Las versiones [min.map] no son imprescindibles. En la carpeta [cultures], solo se pueden conservar las configuraciones de idioma que maneja la aplicación.

Con el Explorador de Windows, se copian estos archivos a la carpeta [static / js / jquery] del proyecto [springmvc-validation-client] y solo se conservan los archivos útiles [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. Este 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}]]. Observe la sintaxis particular de esta expresión. Esto se debe a que está escrita en JavaScript. La expresión [[${locale}]] será reemplazada 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 y 4 establecen la configuración regional del lado del cliente. Solo se manejan dos: [fr-FR] y [en-US]. Por eso 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á del lado del cliente se establece del 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 manejan 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 20: la configuración regional [fr-FR] o [en-US] se inserta 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 denota como [fr-FR] del lado del cliente, se denota como [fr_FR] del lado del servidor. Por esta razón, 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 enviada por el servidor. Esto no internacionaliza los mensajes que muestra 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] manejan este tipo de problemas:

  

La internacionalización de los mensajes de error se maneja únicamente del lado del servidor. Veremos que la página HTML / jS incluye mensajes de error correspondientes a la configuración regional que maneja 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 distinta de [fr] utilizará mensajes en inglés. Cabe recordar que el archivo [messages_fr.properties] se utiliza para cualquier configuración regional de [fr_XX], como [fr_CA] o [fr_FR].

La vista [vue-01.xml] utiliza las claves de estos mensajes. Si el lector desea conocer el valor asociado a estas claves, se le invita 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 que se encuentra en el archivo [local.js] [2]. En ambos casos, se invoca 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 entender la línea 4 es necesario un preámbulo. La vista [vue-01.xml] incluye un campo oculto llamado [lang]:


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

que corresponde a un campo [lang] en [Form01]:


    // local
    private String lang;

Los campos ocultos son útiles cuando se desea enriquecer los valores enviados. El 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 de 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 [id='lang'] de DOM;
  • línea 2: las variables declaradas fuera de una función son globales para todas 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 igual a [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 de DOM (Modelo de Objetos de Documento).

  • Línea 16: [document] hace referencia a 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. En este caso, 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 revisar 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]. Cabe recordar que, en este nombre, se eliminará el sufijo [.html] y, al final, se ejecutará la acción [/js02]. Lo importante es comprender que el nuevo valor [xx_XX] del nodo [lang] se enviará en el formato [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, del lado del servidor, la configuración regional pasará a ser [xx_XX]. Veamos la acción [/js02] que se 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 admiten 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 no son vá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. Del 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 redirige 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 configuración regional 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. Sin embargo, 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 procesados por la biblioteca de validación jS.

En resumen, cabe destacar que los dos enlaces para cambiar la configuración regional:

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

6.3.7. El POST de los valores ingresados

Analicemos el botón [Valider], que envía los valores ingresados 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");
...
});
....
// envío del 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 entender estas líneas, hay que revisar 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. Por lo tanto, 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'], por su parte, es utilizado por JavaScript. Sirve para identificar un elemento del DOM (Modelo de Objetos de Documento). De hecho, el documento HTML cargado se transforma en un árbol de JavaScript llamado 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");
...
});
....
// envío de formulario
function postForm01() {
...
}
  • línea 10: la expresión [$("#form")] es una expresión jQuery. Su valor es una referencia al nodo de 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 todas las funciones. En este caso, esto significa que las variables [formulaire, clientValidation , double1, double2, double3] inicializadas en [$(document).ready()] también estarán disponibles en la función [postForm01] de la línea 19;

Ahora, analicemos la función [postForm01]:


// envío de 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 envío
            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 permite que se realice el envío
    return true;
}

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

  • El código importante se encuentra en las líneas 4 a 12;
  • línea 4: se recupera el valor del campo oculto [clientValidation]. Este valor es «true» si se debe activar la validación del lado del cliente, «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 debido a que 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 el tema de los siguientes párrafos. 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, y [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 se envíe al servidor;
  • líneas 15-20: los identificadores [double1, double2, double3] representan los tres números reales del formulario. Dependiendo de la configuración regional, el valor ingresado 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 se refiere al ingreso de datos. Con la configuración regional [fr-FR], el valor publicado para [double1] se verá así: [double1=10,37]. Al llegar al servidor, el valor [10,37] será rechazado porque este espera [10.37], el formato predeterminado para los números reales en Java. Por lo tanto, en las líneas 15 a 20, se reemplaza la coma por el punto en el valor ingresado para estos números;
  • línea 15: la expresión [double1.val()] genera la cadena de caracteres ingresada para el nodo [double1]. La expresión [double1.val().replace(",", ".")] reemplaza 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 ingresó [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 ingresados 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 ingresados 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 hizo clic en el enlace [English]. Observamos que esto generaba un POST de los valores ingresados sin que se activaran los validadores jS. Al regresar de la POST, la columna [Server Validation] se llena con los posibles mensajes de error. Si ahora hacemos 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 en la columna [Server Validation] se mantendrán, lo que generará confusión, ya que, en caso de que los validadores jS detecten errores, no se solicitará al servidor. Para evitar esto, 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 característica de los mensajes de error es que todos pertenecen a 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 de 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í, el cual 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 que muestra la etiqueta <span>;

Ahora analizaremos 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 de [1-2] establecen que el campo [strNotEmpty] debe ser una cadena existente ([NotNull]), no estar vacía y no estar compuesta únicamente por espacios ([NotBlank]). Queremos reproducir esta restricción del lado del cliente con JavaScript.

Analicemos las líneas 5 y 8. La línea 11 no presenta ningún problema. Muestra el mensaje de error relacionado con el campo [strNotEmpty]. Comencemos 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, y [msg] es el mensaje de error asociado a un valor inválido del nodo sobre el que se aplica el validador. Esto es solo información. No provoca que se muestre el 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 aprovecharlas;

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 reemplazará 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 con error se coloree de rojo;
  • línea 7: la clase del nodo [span] ha cambiado. Ahora es [field-validation-error], lo que hará que el texto del nodo [span] se muestre en rojo;
  • línea 8: el [span], que antes estaba vacío, ahora contiene el texto [Le champ est obligatoire]. Este texto proviene 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 del lado del cliente con JavaScript. Las líneas 12 a 17 son ahora las habituales:

  • líneas 12-14: muestran, en caso de error en el campo [assertFalse], el mensaje contenido en el atributo [data-val-assertfalse] de la línea 6 o el contenido en 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 seleccionado ninguno;
  • líneas 5-10: muestran los botones de opción con validadores de JavaScript que se activan tan pronto como el usuario hace clic en uno de ellos.

Ambos botones están construidos de la misma manera. Analizaremos 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 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.

Ya hemos visto el validador [required]. La novedad aquí es que se pueden asociar varios validadores a un valor ingresado. Si bien el validador [required] es reconocido por la biblioteca de validación MS (Microsoft), no es el caso del validador [assertFalse]. Por lo tanto, aprenderemos a crear un nuevo validador. Crearemos varios y los colocaremos en un archivo [client-validation.js]:

  

Este archivo, al igual que los demás, se importa mediante 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] consiste simplemente en la creación de las siguientes dos funciones jS:


// -------------- 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 construyen sobre 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 mostró el camino a seguir. Si aún existe, invito al lector a revisarlo, ya que es 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, y [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] fuera un número real dentro del intervalo [min, max]. En ese caso, necesitaríamos conocer [min] y [max]. A estos dos valores se les llama 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 cual 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 bien, estos mensajes también servirán como mensajes de error para la biblioteca de validación MS. Sin embargo, el JavaScript mostrará los dos apóstrofos. En la línea 8, reemplazamos el doble apóstrofo del mensaje de error por uno solo.

Para ver un poco qué está pasando, podemos agregar código de registro de 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 salidas en la consola:

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

 

Se ejecutó la función jS [$.validator.unobtrusive.adapters.add]. Se observa lo siguiente:

  • [options.params] es un objeto vacío porque el validador [assertFalse] no tiene parámetros;
  • [options.message] es el mensaje de error que se generó 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 colocamos en el atributo [data-val-required];

Ahora asignemos un valor incorrecto al campo [assertFalse] y validemos:

 

Entonces obtenemos los siguientes registros:

En ellos se observa lo siguiente:

  • el valor que se está probando es [true] (línea 118);
  • el elemento HTML que se probó es el botón de radio con el ID [assertFalse1] (línea 122);
  • el validador [assertFalse] no tiene ningún parámetro (línea 123);

Eso es todo. ¿Qué podemos aprender 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 simplemente presentaremos las novedades.

6.3.10. Validador [asserttrue]

Este validador es, evidentemente, similar 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 a 16. Estas utilizan un validador [asserrtrue] que debe definirse 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 a partir de 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 se encuentran 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 ingresa una fecha posterior a la de hoy:

 

Lo primero que hay que destacar es que la fecha 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] mencionada anteriormente. El primer parámetro es la fecha como cadena de caracteres y el segundo, su formato. El resultado es un puntero null si la fecha no es válida; de lo contrario, es la fecha resultante.

La validez del validador [past] se verifica 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 la versión de Chrome utilizada muestra la fecha en el formato [yyyy-mm-dd]. En un navegador donde esto 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]:


<!-- requerido, 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;
  • en la 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 agregar 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 a partir de la siguiente secuencia de la vista [vue-01.xml]:


<!-- obligatorio, entero, máximo (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 validadores nuevos: [int] y [max]. Este último tiene un parámetro: el valor máximo. Analicemos el código HTML generado por la línea 5:


<!-- obligatorio, entero, máximo (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] presenta 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 llamado [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;
// -------------- entero
$.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] represente efectivamente un número 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 agrega de la siguiente manera en [client-validation.js]


// -------------- máximo, debe usarse 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("''", "'");
});

Ahora abordaremos 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 entender el código de las líneas 3 a 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 tan pronto como uno de ellos declara que el elemento es 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 verifica que se trate de un número. Si el validador [int] se ejecutó 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. Entonces se indica que el valor comprobado es válido para permitir que el validador [int] haga su trabajo y declare el elemento inválido con su propio mensaje de error;
  • líneas 23-24: calcula la validez de [value];

A continuación se muestran algunos registros:

Valor ingresado
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, entero, mínimo (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}"]. Este es un caso similar al del validador [max]. Se agrega el siguiente código en [client-validation.js]:


logs.min = true;
//-------------- min debe usarse 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 ingresado
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, expresión regular -->
<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 cumplir el valor a validar. En este caso, la expresión regular verifica 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 agregar al 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 agregar al 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, entero, 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 usarse 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] que ya hemos analizado.

A continuación se muestran algunos ejemplos de registros:

Valor ingresado
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 ingresado
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 el formato [fr-FR]. Al ingresar [2.5] (notación anglosajona), el número se acepta. Esto se debe a que [Globalize.parseFloat] acepta ambas notaciones:

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

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

Valor ingresado
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 ser [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 de [en-US], un número real puede incluir un punto decimal y comas, que a veces se usan para separar los miles.

Se puede mejorar 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 mencionadas anteriormente;
  • líneas 9-16: la verificación de validez del valor ingresado;
  • línea 15: se ha previsto el caso en que la cultura no sea ni [fr-FR] ni [en-US];

Los registros ahora muestran lo siguiente:

Configuración regional [fr-FR]

Valor ingresado
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 ingresado
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 analizar un validador que ya no valida un valor ingresado, sino una relación entre dos valores ingresados. En este caso, queremos que [double1+double3] esté dentro del intervalo de [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 valor máximo del intervalo [min, max] en el que debe encontrarse [double1+double3];

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


// -------------- custom3 utilizado junto 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 ingresados
[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" />

Esta 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 agregar 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 han sido declarados 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] está definido 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 lado 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 se debe activar la validación del lado del cliente; de lo contrario, es false;
  • 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, es necesario analizar el documento en busca de los 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; el enlace de desactivación está oculto;
  • línea 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 regresa con el formulario devuelto por el servidor. Entonces se utiliza su valor para restablecer la validación del lado del cliente tal 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 permita inicializar el jS de esta. Esto se hace habitualmente 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 lado del cliente
    setClientValidation2(activate);
    // se guarda la elección del usuario en el campo oculto
    clientValidation.val(activate ? "true" : "false");
    // ajustes adicionales
    if (activate) {
        // la validación del lado 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 guarda la elección del usuario en el campo oculto para recuperarla al regresar de la siguiente llamada a POST;
  • línea 11: si la validación del lado 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 párrafo 6.3.7;
  • línea 13: se ejecutan los validadores jS para mostrar 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. Analicemos en la consola de desarrollo de Chrome el código HTML de un elemento con error:

<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 con error 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 se aplica a todos los elementos con error. Estas dos informaciones se utilizan 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 quita esa clase. De esta manera, el elemento con error que se había resaltado en rojo recupera su estilo original;