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

Sujets avancés