Validation des contraintes
==========================

La création de formulaires web a toujours été une tâche complexe. Bien que le balisage du formulaire en lui-même soit plutôt simple, c'est la vérification de la validité et de la cohérence des valeurs de chaque champ qui s'avère difficile. Informer l'utilisatrice ou l'utilisateur à propos de la validité (ou de l'invalidité) des champs est parfois un casse-tête. HTML5 introduit de nouveaux mécanismes pour les formulaires : de nouveaux types sémantiques pour `<input>` et *la validation des contraintes* pour simplifier la vérification du contenu d'un formulaire côté client. Les contraintes de base usuelles peuvent être vérifiées sans recourir à JavaScript à l'aide de nouveaux attributs. Des contraintes plus complexes peuvent être testées à l'aide de l'API *Constraint Validation*.

Pour une introduction à ces concepts avec des exemples, voir le tutoriel sur la validation des formulaires.

**Note :** La validation des contraintes HTML ne signifie pas qu'il n'est plus nécessaire de vérifier *côté serveur*. Même si cela réduit les risques d'envoi de formulaires invalides, des acteurs malveillants pourraient passer outre ces vérifications côté client. Aussi, assurez-vous de toujours valider les contraintes de saisie côté serveur, en étant cohérent avec ce qui est fait côté client.

Contraintes intrinsèques et contraintes de base
-----------------------------------------------

En HTML, les contraintes de base peuvent être déclarées de deux façons :

* En choisissant la valeur sémantique la plus appropriée pour l'attribut `type` de l'élément `<input>`. Ainsi, choisir le type `email` créera automatiquement une contrainte vérifiant que la valeur est une adresse électronique valide.
* En définissant des valeurs pour les attributs relatifs à la validation qui permettent de décrire des contraintes simplement, sans avoir besoin de JavaScript.
### Types de champs

Les contraintes intrinsèques portées par l'attribut `type` sont :

| Type de champ | Description de la contrainte | Violation correspondante |
| --- | --- | --- |
| `<input type="URL">` | La valeur doit être une URL absolue, telle que définie dans le standard évolutif URL. | `TypeMismatch` (en-US) |
| `<input type="email">` | La valeur doit être une adresse électronique avec une syntaxe valide (généralement au format `nom@domaine.tld` ou `nom@domaine`). | `TypeMismatch` (en-US) |

Pour ces deux types de champ, si l'attribut `multiple` est utilisé, plusieurs valeurs peuvent être passées dans le champ sous la forme d'une liste séparée par des virgules. Si au moins une des valeurs ne respecte pas les conditions décrites ici, la violation de contrainte `TypeMismatch` est déclenchée.

On notera que la plupart des types de champ n'ont pas de contraintes intrinsèques : soit il n'y a pas de contrainte particulière, soit le navigateur applique un algorithme de transformation pour que les valeurs incorrectes utilisent une valeur par défaut correcte.

### Attributs relatifs à la validation

En complément de l'attribut `type` mentionné ci-avant, les attributs suivants permettent de décrire des contraintes basiques :

| Attribut | Types de champ prenant en charge cet attribut | Valeurs possibles | Description de la contrainte | Violation correspondante |
| --- | --- | --- | --- | --- |
| `pattern` | `text`, `search`, `url`, `tel`, `email`, `password` | Une expression rationnelle JavaScript (compilée avec les marqueurs `global`, `ignoreCase`, et `multiline` *désactivés*). | La valeur doit correspondre au motif décrit par l'expression. | `patternMismatch` (en-US) |
| `min` | `range`, `number` | Un nombre valide | La valeur du champ doit être supérieure ou égale à la valeur de l'attribut. | `rangeUnderflow (en-US)` |
| `date`, `month`, `week` | Une date valide |
| `datetime-local`, `time` | Un horodatage valide |
| `max` | `range`, `number` | Un nombre valide | La valeur du champ doit être inférieure ou égale à la valeur de l'attribut. | `rangeOverflow (en-US)` |
| `date`, `month`, `week` | Une date valide |
| `datetime-local`, `time` | Un horodatage valide |
| `required` | `text`, `search`, `url`, `tel`, `email`, `password`, `date`, `datetime-local`, `month`, `week`, `time`, `number`, `checkbox`, `radio`, `file`. Également utilisable sur les éléments `<select>` et `<textarea>`. | *Aucune valeur*, il s'agit d'un attribut booléen : sa présence indique que la valeur est requise et son absence indique que la valeur est facultative. | Si l'attribut est présent, il doit y avoir une valeur. | `valueMissing (en-US)` |
| `step` | `date` | Un nombre entier (qui exprime des jours) | À moins que l'attribut vaille `any`, la valeur devra être **min** + un multiple entier de l'incrément. | `stepMismatch (en-US)` |
| `month` | Un nombre entier de mois |
| `week` | Un nombre entier de semaines |
| `datetime-local`, `time` | Un nombre entier de secondes |
| `range`, `number` | Un entier |
| `minlength` | `text`, `search`, `url`, `tel`, `email`, `password`. Également disponible sur l'élément `<textarea>`. | Une longueur entière | Le nombre de caractères, exprimé en points de code, ne doit pas être inférieur à la valeur de l'attribut si ce dernier n'est pas vide. Pour l'élément `<textarea>`, les passages à la ligne sont normalisés en un seul caractère (contrairement aux paires CRLF). | `tooShort (en-US)` |
| `maxlength` | `text`, `search`, `url`, `tel`, `email`, `password`. Également disponible sur l'élément `<textarea>`. | Une longueur entière | Le nombre de caractères, exprimé en points de code, ne doit pas dépasser la valeur de l'attribut. | `tooLong (en-US)` |

Processus de validation des contraintes
---------------------------------------

En complément de la validation native effectuée par le navigateur, on peut manipuler la validation des contraintes en JavaScript à l'aide de l'API *Constraint Validation*, sur un élément du formulaire ou sur le formulaire (`<form>`). La validation des contraintes a lieu quand :

* On appelle la méthode `checkValidity()` ou `reportValidity()` depuis une instance d'une interface du DOM correspondant à un élément de formulaire, (`HTMLInputElement`, `HTMLSelectElement`, `HTMLButtonElement`, `HTMLOutputElement` (en-US) ou `HTMLTextAreaElement` (en-US)). Dans ce cas, seules les contraintes de l'élément correspondant sont évaluées et permettent au script d'obtenir l'état de validité. La méthode `checkValidity()` renvoie un booléen qui indique si la valeur de l'élément respecte les contraintes (c'est généralement ce qui est fait par l'agent utilisateur pour déterminer quelle pseudo-classe CSS s'applique entre `:valid` et `:invalid`). La méthode `reportValidity()` renvoie quant à elle le détail des contraintes qui ne sont pas respectées.
* On appelle la méthode `checkValidity()` ou `reportValidity()` de l'objet `HTMLFormElement` correspondant au formulaire.
* On envoie le formulaire.

On qualifie parfois un appel à `checkValidity()` de validation *statique* des contraintes, en opposition à `reportValidity()` ou à l'envoi du formulaire qui constituent une validation *interactive*.

**Note :**

* Si l'attribut `novalidate` est placé sur l'élément `<form>`, la validation interactive des contraintes n'a pas lieu.
* Appeler la méthode `submit()` d'un objet `HTMLFormElement` ne déclenchera pas de validation des contraintes. Autrement dit, cette méthode envoie les données du formulaire au serveur, même si elles ne respectent pas les contraintes. Pour passer par la validation, on pourra appeler la méthode `click()` du bouton d'envoi.

Implémenter des contraintes complexes à l'aide de l'API
-------------------------------------------------------

Grâce à JavaScript et à l'API *Constraint Validation*, on peut implémenter des contraintes plus complexes, qui portent par exemple sur plusieurs champs à la fois ou qui impliquent des calculs avancés.

Le principe consiste à déclencher une fonction JavaScript lorsqu'un évènement d'un champ de formulaire a lieu (par exemple `change`) et de calculer à ce moment si la contrainte est respectée ou non puis d'utiliser `field.setCustomValidity()` pour fournir le résultat de la validation : une chaîne vide indiquera que la contrainte est respectée et n'importe quelle autre chaîne indiquera une erreur et c'est alors cette chaîne de caractères qui sera affichée à l'utilisatrice ou à l'utilisateur.

### Exemple de contrainte portant sur plusieurs champs : validation du code postal

Le format utilisé pour les codes postaux varie d'un pays à l'autre. Certains pays autorisent un préfixe avec le code du pays (comme `D-` en Allemagne, `F-` en France, etc.), d'autres ont des codes postaux avec un nombre précis de chiffres et d'autres encore, comme au Royaume-Uni, ont des structures plus complexes, où on peut avoir des lettres à certaines positions.

**Note :** Ce qui suit ne constitue pas une bibliothèque exhaustive de validation des codes postaux, il ne s'agit que d'un exemple.

Pour cet exemple, nous allons ajouter un script de vérification pour ce formulaire :


```html
<form>
  <label for="ZIP">Code postal : </label>
  <input type="text" id="ZIP" />
  <label for="Country">Pays : </label>
  <select id="Country">
    <option value="ch">Suisse</option>
    <option value="fr">France</option>
    <option value="de">Allemagne</option>
    <option value="nl">Pays-Bas</option>
  </select>
  <input type="submit" value="Valider" />
</form>
```


Ce fragment HTML affiche le formulaire suivant :

Pour commencer, on écrit une fonction qui vérifie la contrainte :


```js
function checkZIP() {
  // Pour chaque pays, on définit le motif que doit suivre le code
  const constraints = {
    ch: [
      "^(CH-)?\\d{4}$",
      "Les codes postaux suisses ont 4 chiffres : par exemple CH-1950 ou 1950",
    ],
    fr: [
      "^(F-)?\\d{5}$",
      "Les codes postaux français ont 5 chiffres : par exemple F-75012 ou 75012",
    ],
    de: [
      "^(D-)?\\d{5}$",
      "Les codes postaux allemands ont 5 chiffres : par exemple D-12345 ou 12345",
    ],
    nl: [
      "^(NL-)?\\d{4}\\s\*([A-RT-Z][A-Z]|S[BCE-RT-Z])$",
      "Les codes postaux néerlandais ont 4 chiffres, suivi par deux lettres sauf SA, SD et SS",
    ],
  };

  // On récupère l'identifiant du pays
  const country = document.getElementById("Country").value;

  // On récupère le champ du code postal
  const ZIPField = document.getElementById("ZIP");

  // On construit le validateur pour la contrainte
  const constraint = new RegExp(constraints[country][0], "");
  console.log(constraint);

  // On vérifie la valeur par rapport à la contrainte !
  if (constraint.test(ZIPField.value)) {
    // Le code postal respecte la contrainte, on communique ce résultat via l'API
    ZIPField.setCustomValidity("");
  } else {
    // Le code postal ne respecte pas la contrainte, on envoie un message
    // via l'API pour fournir des informations sur le format attendu
    ZIPField.setCustomValidity(constraints[country][1]);
  }
}
```


Ensuite, on ajoute des gestionnaires d'évènements pour l'évènement `change` du champ `<select>` et pour l'évènement `input` de l'élément `<input>` :


```js
window.onload = () => {
  const countrySelect = document.getElementById("Country");
  const zipInput = document.getElementById("ZIP");
  countrySelect.addEventListener("change", checkZIP);
  zipInput.addEventListener("input", checkZIP);
};
```

### Limiter la taille d'un fichier avant son envoi

Une autre contrainte fréquemment rencontrée consiste à appliquer une limite sur la taille d'un fichier à téléverser. Pour vérifier cela côté client avant d'envoyer le fichier, nous allons combiner l'API *Constraint Validation* (notamment la méthode `field.setCustomValidity()`), avec une autre API, l'API *File*.

Voici le fragment HTML utilisé pour l'exemple :


```html
<label for="FS">Veuillez choisir un fichier qui ne dépasse pas 75ko : </label>
<input type="file" id="FS" />
```


Cela donnera le formulaire suivant :

Pour le code JavaScript, on lit le fichier sélectionné avec la méthode `File.size()` pour obtenir sa taille et on compare cette valeur avec la limite (ici codée en dur), puis on appelle l'API de validation pour indiquer au navigateur si la contrainte est respectée :


```js
function checkFileSize() {
  const FS = document.getElementById("FS");
  const files = FS.files;

  // S'il y a (au moins) un fichier sélectionné
  if (files.length > 0) {
    if (files[0].size > 75 \* 1024) {
      // La contrainte n'est pas respectée
      FS.setCustomValidity("Le fichier sélectionné ne doit pas dépasser 75ko.");
      return;
    }
  }
  // La contrainte spécifique est bien respectée
  FS.setCustomValidity("");
}
```


Pour finir, on attache cette méthode au gestionnaire d'évènement correspondant :


```js
window.onload = () => {
  const fsInput = document.getElementById("FS");
  fsInput.addEventListener("change", checkFileSize);
};
```

Mise en forme visuelle pour la validation des contraintes
---------------------------------------------------------

En plus de définir des contraintes, lors du développement, on voudra contrôler la façon dont les contraintes sont communiquées aux utilisatrices et utilisateurs : quels messages sont utilisés et quelle mise en forme est appliquée pour les champs valides/invalides.

### Contrôler l'aspect des éléments

L'aspect des éléments peut être personnalisé grâce aux pseudo-classes CSS suivantes.

#### `:required` et `:optional`

Les pseudo-classes `:required` et `:optional` permettent d'écrire des sélecteurs pour cibler les éléments qui ont ou non l'attribut `required`.

#### `:placeholder-shown`

Voir `:placeholder-shown`.

#### `:valid` et `:invalid`

Les pseudo-classes `:valid` et `:invalid` sont utilisées pour représenter des éléments `<input>` dont le contenu est valide (respectivement invalide) par rapport au type de champ. Ces classes permettent de mettre en forme les éléments de formulaire valides ou invalides afin d'en faciliter l'identification.

### Contrôler le texte utilisé pour la validation des contraintes

Plusieurs outils peuvent vous aider à contrôler le texte utilisé pour indiquer une erreur de validation :

* La méthode `setCustomValidity(message)` pour les éléments suivants :
    + `<fieldset>`. Note : fournir un message d'invalidité personnalisé pour les éléments `<fieldset>` n'empêchera pas l'envoi du formulaire dans la plupart des navigateurs.
    + `<input>`
    + `<output>`
    + `<select>`
    + Les boutons d'envoi (créés avec un élément `<button>` de type `submit` ou avec un élément `<input>` de type `submit`. Les autres types de bouton ne contribuent pas à la validation des contraintes.)
    + `<textarea>`
* L'interface `ValidityState` (en-US) décrit l'objet renvoyé par la propriété `validity` des types d'éléments listés ci-avant. Elle représente différentes façons selon lesquelles une valeur saisie peut être invalide. Avec la méthode précédente, elle permet d'expliquer la raison pour laquelle la valeur d'un champ est invalide.