Envoyer des formulaires avec JavaScript
=======================================

Les formulaires HTML peuvent envoyer des requêtes HTTP de façon déclarative. Il est aussi possible d'utiliser les formulaires afin de préparer des requêtes HTTP qu'on enverra avec JavaScript, par exemple avec `XMLHttpRequest` ou `Fetch`. Dans cet article, nous verrons en quoi consiste cette approche.

Un formulaire parfois détourné
------------------------------

Avec les applications progressives et/ou basées sur un *framework*, il est fréquent d'utiliser les formulaires HTML afin d'envoyer des données sans charger un nouveau document lorsque les données de la réponse sont reçues. Voyons d'abord pourquoi une nouvelle approche est nécessaire.

### Maîtriser l'interface générale

Lorsqu'on utilise un envoi de formulaire HTML standard, comme décrit dans l'article précédent, le navigateur charge l'URL où les données ont été envoyées et navigue donc vers une autre page, avec un chargement complet. Éviter un tel chargement permet une meilleure expérience en évitant des requêtes sur le réseau ainsi que des problèmes visuels de clignotement à l'affichage (*flickering*).

Une interface utilisateur moderne utilisera généralement des formulaires HTML pour récupérer des données saisies par la personne, pas nécessairement pour les envoyer. Lorsque la personne soumet le formulaire, l'application prend le contrôle et transmet les données en arrière-plan, de façon asynchrone, mettant uniquement à jour les éléments de l'interface qui le nécessitent.

L'envoi de données arbitraires de façon asynchrone est généralement désigné par l'acronyme AJAX, qui signifie ***Asynchronous JavaScript And XML*** en anglais (qu'on pourrait traduire par « JavaScript et XML asynchrones »).

### En quoi est-ce différent ?

L'objet `XMLHttpRequest` (souvent abrégé en XHR) fourni par le DOM permet de construire des requêtes HTTP, de les envoyer et d'en utiliser le résultat. À l'origine, `XMLHttpRequest` fut conçu pour échanger des données au format XML, mais il permet désormais aussi d'échanger des données JSON. Toutefois, ni XML ni JSON ne sont des formats appropriés pour l'encodage des données de formulaire dans une requête HTTP. Les données de formulaire, décrite avec le type (`application/x-www-form-urlencoded`), prennent la forme d'une liste de paires clé/valeur encodées en URL. Pour la transmission de données binaires, la requête HTTP utilise le type `multipart/form-data`.

**Note :** Désormais, c'est l'API *Fetch* qui est utilisée à la place de XHR, en raison de ses avantages. La plupart du code présenté dans cet article pourrait être réécrit pour utiliser *Fetch* à la place de XHR.

Si vous contrôlez la partie cliente (celle exécutée dans le navigateur) et la partie serveur, vous pouvez échanger du JSON et du XML et les traiter comme bon vous semble. Cependant, si vous utilisez un service tiers, vous devez envoyer les données dans un format bien défini.

Comment donc envoyer de telles données ? Nous allons voir différentes techniques par la suite.

Envoyer les données d'un formulaire
-----------------------------------

Il existe trois méthodes pour envoyer les données d'un formulaire :

* Construire un objet `XMLHttpRequest` manuellement
* Utiliser un objet `FormData` autonome
* Utiliser un objet `FormData` rattaché à un élément `<form>`

Voyons chacune en détails par la suite.

### Construire une requête XHR manuellement

`XMLHttpRequest` une façon fiable pour construire des requêtes HTTP. Pour envoyer des données de formulaire avec `XMLHttpRequest`, on prépare les données en les encodant en URL et on respecte les contraintes propres aux requêtes d'envoi des données de formulaires. Prenons un exemple.

#### HTML


```html
<button>Cliquez ici !</button>
```


#### JavaScript


```js
const btn = document.querySelector("button");

function sendData(data) {
  console.log("Envoi des données en cours");

  const XHR = new XMLHttpRequest();

  const urlEncodedDataPairs = [];

  // On transforme l'objet des données en un tableau
  // de paires clé/valeur encodées en URL.
  for (const [name, value] of Object.entries(data)) {
    urlEncodedDataPairs.push(
      `${encodeURIComponent(name)}=${encodeURIComponent(value)}`,
    );
  }

  // On combine les paires en une seule chaîne de caractères
  // et on remplace les espaces encodés par le caractère +
  // afin de correspondre au comportement des navigateurs
  // pour les envois de formulaires.
  const urlEncodedData = urlEncodedDataPairs.join("&").replace(/%20/g, "+");

  // On définit ce qui se produit lorsque
  // les données sont bien envoyées
  XHR.addEventListener("load", (event) => {
    alert("Les données ont été envoyées et la réponse chargée.");
  });

  // On définit ce qui se produit en cas
  // d'erreur
  XHR.addEventListener("error", (event) => {
    alert("Une erreur est survenue.");
  });

  // On prépare la requête
  XHR.open("POST", "https://example.com/cors.php");

  // On ajoute l'en-tête HTTP nécessaire pour le format
  // des données de formulaires
  XHR.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

  // Pour finir, on envoie les données.
  XHR.send(urlEncodedData);
}

btn.addEventListener("click", () => {
  sendData({ test: "ok" });
});
```


#### Résultat

**Note :** Cette utilisation de `XMLHttpRequest` est sujette aux règles de même origine (*same-origin policy*). Si vous souhaitez effectuer des requêtes entre différentes origines, vous devrez paramétrer le contrôle d'accès CORS.

### Utiliser `XMLHttpRequest` et `FormData`

Construire une requête HTTP manuellement peut s'avérer laborieux. Heureusement, la spécification de l'API `XMLHttpRequest` fournit une méthode pour gérer les requêtes transmettant les données d'un formulaire avec l'objet `FormData`.

On peut utiliser un objet `FormData` pour construire des données de formulaire à transmettre ou pour obtenir les données provenant d'un formulaire afin de gérer leur envoi.

L'article Utiliser les objets `FormData` couvre ce sujet en particulier, mais voici deux exemples :

#### Construire un objet `FormData` autonome

##### HTML


```html
<button>Cliquez ici !</button>
```


##### JavaScript


```js
const btn = document.querySelector("button");

function sendData(data) {
  const XHR = new XMLHttpRequest();
  const FD = new FormData();

  // On inscrit les données dans l'objet FormData
  for (const [name, value] of Object.entries(data)) {
    FD.append(name, value);
  }

  // On définit ce qui se produit lorsque
  // les données sont bien envoyées
  XHR.addEventListener("load", (event) => {
    alert("Les données ont été envoyées et la réponse chargée.");
  });

  // On définit ce qui se produit en cas
  // d'erreur
  XHR.addEventListener("error", (event) => {
    alert("Une erreur est survenue.");
  });

  // On prépare la requête
  XHR.open("POST", "https://example.com/cors.php");

  // On envoie l'objet FormData : les en-têtes HTTP sont
  // paramétrés automatiquement
  XHR.send(FD);
}

btn.addEventListener("click", () => {
  sendData({ test: "ok" });
});
```


##### Résultat

#### Utiliser un objet `FormData` couplé à un élément `<form>`

Il est aussi possible de rattacher un objet `FormData` à un élément `<form>`. On obtient ainsi un objet `FormData` qui représente les données contenues dans le formulaire.

##### HTML


```html
<form id="monFormulaire">
  <label for="monNom">Indiquez votre nom :</label>
  <input id="monNom" name="name" value="Dominique" />
  <input type="submit" value="Envoyer !" />
</form>
```


##### JavaScript

C'est le code JavaScript qui intercepte le formulaire :


```js
window.addEventListener("load", () => {
  function sendData() {
    const XHR = new XMLHttpRequest();

    // on crée l'objet FormData en le rattachant
    // à l'élément de formulaire
    const FD = new FormData(form);

    // On définit ce qui se produit lorsque
    // les données sont bien envoyées
    XHR.addEventListener("load", (event) => {
      alert(event.target.responseText);
    });

    // On définit ce qui se produit en cas
    // d'erreur
    XHR.addEventListener("error", (event) => {
      alert("Une erreur est survenue.");
    });

    // On prépare la requête
    XHR.open("POST", "https://example.com/cors.php");

    // On envoie les données avec ce qui a été
    // fourni dans le formulaire
    XHR.send(FD);
  }

  // On récupère une référence au formulaire HTML
  const form = document.getElementById("monFormulaire");

  // On ajoute un gestionnaire d'évènement 'submit'
  form.addEventListener("submit", (event) => {
    event.preventDefault();

    sendData();
  });
});
```


##### Résultat

Il est possible d'aller encore plus loin en utilisant la propriété `elements` afin d'obtenir la liste complète des éléments de données du formulaire pour les gérer individuellement. Pour en savoir plus à ce sujet, consultez l'exemple Accéder aux contrôles d'un formulaire.

Gérer les données binaires
--------------------------

Si vous utilisez un objet `FormData` pour un formulaire qui inclut des contrôles `<input type="file">`, les données seront traitées automatiquement. En revanche, si on les traite manuellement, il y aura un travail supplémentaire à accomplir.

Il existe de nombreuses sources fournissant des données binaires, comme `FileReader`, `Canvas`, et WebRTC. Toutefois, certains navigateurs historiques ne peuvent pas accéder aux données binaires ou nécessitent des contournements compliqués. Pour en savoir plus sur l'API `FileReader`, voir Utiliser les fichiers depuis les applications web.

La méthode la plus simple pour envoyer des données binaires à l'aide de `FormData` est d'utiliser la méthode `append()` illustrée avant. Refaire tout à la main sera plus compliqué.

Dans l'exemple qui suit, on utilise l'API `FileReader` afin d'accéder aux données binaires, puis on construit les données composites du formulaire manuellement.

### HTML


```html
<form id="leFormulaire">
  <p>
    <label for="leTexte">Données texte :</label>
    <input id="leTexte" name="monTexte" value="Des données texte" type="text" />
  </p>
  <p>
    <label for="leFichier">Données fichier :</label>
    <input id="leFichier" name="monFichier" type="file" />
  </p>
  <button>Envoyer !</button>
</form>
```


Comme on peut le voir, le fragment HTML reprend un formulaire classique. La logique intéressante a lieu dans le code JavaScript.

### JavaScript


```js
// On souhaite accéder aux nœuds du DOM,
// on initialise donc le script au chargement
// de la page
window.addEventListener("load", () => {
  // On utilisera ces variables pour stocker
  // les données du formulaire
  const text = document.getElementById("leTexte");
  const file = {
    dom: document.getElementById("leFichier"),
    binary: null,
  };

  // On utilise l'API FileReader pour lire le contenu
  // du fichier
  const reader = new FileReader();

  // FileReader est asynchrone, on stocke le résultat
  // lorsque la lecture du fichier est terminée.
  reader.addEventListener("load", () => {
    file.binary = reader.result;
  });

  // Au chargement de la page, si un fichier est
  // déjà sélectionné, on le lit.
  if (file.dom.files[0]) {
    reader.readAsBinaryString(file.dom.files[0]);
  }

  // Si ce n'est pas le cas, on attend que la personne
  // sélectionne un fichier.
  file.dom.addEventListener("change", () => {
    if (reader.readyState === FileReader.LOADING) {
      reader.abort();
    }

    reader.readAsBinaryString(file.dom.files[0]);
  });

  // sendData est la fonction principale
  function sendData() {
    // S'il y a un fichier sélectionné, on attend qu'il ait été lu
    // Sinon, on retarde l'exécution de la fonction
    if (!file.binary && file.dom.files.length > 0) {
      setTimeout(sendData, 10);
      return;
    }

    // Pour construire la requête de formulaire multi-parties
    // il nous faut une instance XMLHttpRequest
    const XHR = new XMLHttpRequest();

    // Il nous faut un séparateur pour définir chaque partie
    // de la requête
    const boundary = "blob";

    // On stocke le corps de la requête dans une chaîne de
    // caractères
    let data = "";

    // Si un fichier a été sélectionné :
    if (file.dom.files[0]) {
      // On commence un nouveau fragment dans le corps de
      // la requête
      data += `--${boundary}\r\n`;

      // On le décrit comme données de formulaire
      data +=
        "content-disposition: form-data; " +
        // On indique le nom des données du formulaire
        `name="${file.dom.name}"; ` +
        // On fournit le nom effectif du fichier
        `filename="${file.dom.files[0].name}"\r\n`;
      // Ainsi que le type MIME du fichier
      data += `Content-Type: ${file.dom.files[0].type}\r\n`;

      // Il y a un saut de ligne entre les métadonnées
      // et les données
      data += "\r\n";

      // On concatène les données binaires au corps de la
      // requête
      data += file.binary + "\r\n";
    }

    // Pour les données texte, c'est plus simple :
    // on commence un nouveau fragment dans le corps
    // de la requête.
    data += `--${boundary}\r\n`;

    // On indique qu'il s'agit de données de formulaire et on
    // précise un nom
    data += `content-disposition: form-data; name="${text.name}"\r\n`;
    // Il y a un saut de ligne entre les métadonnées
    // et les données
    data += "\r\n";

    // On ajoute les données texte au corps de la requête
    data += text.value + "\r\n";

    // Et lorsque c'est terminé, on ferme le corps de la requête
    data += `--${boundary}--`;

    // On définit ce qui se produit en cas de réussite
    XHR.addEventListener("load", (event) => {
      alert("Les données ont bien été envoyées et la réponse chargée.");
    });

    // On définit ce qui se passe en cas d'échec
    XHR.addEventListener("error", (event) => {
      alert("Une erreur est survenue.");
    });

    // On prépare la requête
    XHR.open("POST", "https://example.com/cors.php");

    // On ajoute l'en-tête HTTP pour le format des données
    XHR.setRequestHeader(
      "Content-Type",
      `multipart/form-data; boundary=${boundary}`,
    );

    // On envoie les données
    XHR.send(data);
  }

  // On récupère l'élément du formulaire
  const form = document.getElementById("leFormulaire");

  // On ajoute un gestionnaire d'évènement 'submit'
  form.addEventListener("submit", (event) => {
    event.preventDefault();
    sendData();
  });
});
```

### Résultat

Conclusion
----------

Selon le navigateur et le type de données que vous avez à gérer, envoyer les données d'un formulaire avec JavaScript pourra s'avérer simple ou délicat. Cela passera généralement par l'utilisation d'un objet `FormData`.

Voir aussi
----------

### Parcours d'apprentissage

* Un premier formulaire HTML
* Comment structurer un formulaire HTML
* Les contrôles de formulaire natifs
* Les champs de saisie HTML5
* Les autres contrôles de formulaire
* Les pseudo-classes relatives à l'interface utilisateur
* Mettre en forme les formulaires HTML
* La validation des données d'un formulaire
* Envoyer les données d'un formulaire
### Sujets avancés

* **Envoyer des formulaires avec JavaScript**
* Construire des contrôles de formulaire spécifiques
* Les formulaires HTML dans les navigateurs historiques
* Mise en forme avancée des formulaires HTML
* Tableau de compatibilité des propriétés pour les contrôles de formulaire