
Imaginez-vous dans l'une de ces deux situations :
- Vous avec un formulaire avec un file input qui permet à votre utilisateur de sélectionner des fichiers. Mais, vous voulez lui donner la possibilité de désélectionner certains fichiers qui ne lui plaisent pas, et sélectionner d'autres à volonté.
- Vous voulez lui permettre de soumettre beaucoup de fichiers mais chaque fichier avec des informations additives. Par exemple, imaginez le site d'un dessinateur qui fait des portraits des gens. L'utilisateur peut avoir besoin d'envoyer plusieurs images à la fois, tout en fournissant des détails pour chaque image : la couleur à utiliser, ne pas dessiner son point de beauté, ...
Malheureusement pour vous, la manipulation du file input avec du code javascript est très limité. Une fois que des fichiers ont été sélectionnés dans un formulaire, il est difficile (mais pas impossible) de les modifier, les supprimer ou même d'en ajouter.
Dans ce blog post, je vais partager avec vous une solution (un workaround). Elle ne sera peut-être pas la meilleure du monde, mais elle fonctionne, et avec la flexibilité de React, c'est suffisamment facile. Retenez que, bien que j'utilise React dans ce post, la même solution peut être adaptée et appliqué à du Vanilla Js.
Prérequis
Pour suivre ce post et comprendre le code vous devez avoir comme prérequis : une connaissance suffisante sur le fonctionnement du file input en HTML, une connaissance suffisante en javascript et une connaissance suffisante en React.
Nous allons coder un petit système qui permet à un utilisateur de sélectionner des images, supprimer des images qu'il n'aime pas, ajouter d'autres au besoin, avant de tout envoyer à un serveur.
Comment ça va fonctionner ?
En général ce que l'ont fait, c'est qu'on met un file input dans un formulaire. On donne à ce formulaire un attribut enctype avec comme valeur multipart/form-data. Ensuite, quand l'utilisateur soumet le formulaire, des bits sont envoyés au serveur sans que nous n'ayons à nous occuper de quoi que ce soit.
Mais dans notre cas, nous avons besoin de plus de contrôle sur la manière dont nos fichiers sont stockés afin de savoir supprimer, modifier, ajouter des informations supplémentaires, ...
Ce que nous ferons, c'est que nous allons attacher un écouteur d'évènement au file input. Chaque fois qu'une image sera sélectionnée, nous la stockerons dans une state. Cette state nous permettra d'ajouter d'autres images plus tard et même d'en supprimer (avec setState).
A la soumission du formulaire, nous allons nous-même gérer l'envoi au serveur avec du code Javascript. Comme les fichiers sont toujours envoyés en binaire, on ne pourra pas soumettre du json. On utilisera une FormData.
A la réception, côté serveur, tout sera comme si ça avait été soumis avec un formulaire normalement. Et c'est tout ! A ce niveau, vous pouvez implémenter la solution vous-même. Mais si vous préférez voir du code, poursuivez la lecture.
Code
Comme d'habitude, pour sélectionner des fichiers, on aura besoin d'un input de type file. Cet input, nous allons lui donner un attribut multiple pour spécifier que la sélection de plusieurs fichiers est autorisée. Nous allons aussi lui donner l'attribut accept pour spécifier le type de fichier que nous voulons sélectionner (image, vidéo, audio, ...).
jsxNous allons ensuite attacher un écouteur d'évènement au file input. Chaque fois que l'utilisateur sélectionnera un fichier (ou des fichiers) une fonction sera appelée. Vous vous souvenez, je vous ai dit que nous devons stocker ces fichiers quelque part ? C'est dans cette fonction que nous allons le faire. Deux options de stockage s'offrent à nous :
- Si nous avons besoin d'une sauvegarde temporaire qui ne dure que le temps d'une session, nous utilisons une state. Dans ce cas, dès que l'utilisateur rafraîchit la page, les fichiers sélectionnés disparaissent.
- Si nous avons besoin d'une sauvegarde plus pérenne, nous utilisons une base de donnée indexedDB. Elle nous permet de stocker des objets Javascript complexes, comme les File, sans compromettre leur intégrité. Dans ce cas, même si l'utilisateur rafraichit la page, les fichiers seront toujours là.
Par souci de simplicité, dans notre exemple, nous allons utiliser les states. Pour permettre une accès plus globale aux fichiers sélectionnés, nous allons mettre le states dans un contexte. En code voici ce que cela donne :
jsxComme vous le voyez dans la fonction handeSelection, chaque fois que l'utilisateur sélectionne des fichiers, nous les récupérons et nous les gardons dans une variable files. Cette variable aura le type FileList (lisez la doc MDN pour en savoir plus). Une FileList c'est juste une liste d'objets File. Un objet File représente un seul fichier.
Mais attention, il peut arriver que l'utilisateur clique sur la file input mais ne choisisse aucun fichier. Dans ce cas, au lieu d'avoir un objet de type FileList, on recevra null. D'où le return qui nous permet de ne pas gaspiller de ressource en poursuivant avec la logique de l'algorithme.
Maintenant, nous pouvons stocker les fichiers quelque part (une state, dans notre cas). Mais avant cela, nous allons installer un petit package sympa qui permet de générer des identifiants uniques. Tapez simplement cette commande dans votre terminal (vous pouvez aussi utiliser yarn, pnpm, ...) :
plaintextEn fait, avant de stocker les fichiers, il est important de leur donner des identifiants uniques afin de faciliter leur manipulation. Supposons que l'utilisateur a sélectionné 10 fichiers et veut en supprimer un, comment savoir celui qu'il veut supprimer ? Notre code va ressembler à ceci :
jsxQue venons-nous de faire ? Nous avons loopé sur la FileList pour obtenir chaque File. Nous avons ensuite créé un object avec deux propriétés :
- id : qui est l'identifiant
- file : qui est le vrai fichier (le binaire qui sera envoyé au serveur).
Tous ces objets sont stockés dans la variable filesWithIds. Au final, nous mettons à jour notre state en ajoutant les fichiers sélectionnés.
Attention, pour que les fichiers soient stockés dans la state, il faut que ce composant soit contenu dans le context provider. Autrement, cela ne fonctionnera pas.
A ce niveau, tout est bon. Chaque fois que l'utilisateur sélectionnera de nouveaux fichiers, ils seront stockés dans la state. Pour lui donner la possibilité de supprimer des fichiers, c'est simple.
Comme dans notre exemple ce sont des images, nous pouvons faire quelque comme ça :
jsxNous affichons toutes nos images avec un petit bouton de suppression. Pour obtenir l'URL à passer à l'attribut src de l'élément img, nous créons une blob url (lire la documentation MDN pour en savoir plus). Pour supprimer un fichier, nous n'avons qu'à récupérer notre state (qui est une liste de fichiers) et la filtrer : seuls les fichiers qui ont une id différente l'id que nous essayons de supprimer peuvent être conservés.
Et alors, pour soumettre ? Nous allons prendre notre state, nous allons récupérer les vrais fichiers (en laissant les identifiants), nous en faisons une liste, nous l'ajoutons à une FormnData, nous ajoutons des données additives et nous envoyons le tout au serveur.
typescriptConclusion
Merci d'avoir lu jusqu'à la fin. Si vous avez une meilleure façon, votre façon, de vous y prendre, merci de m'en faire part. Je suis conscient que ce code aura besoin d'optimisations selon vos besoins et aussi la quantité de fichiers que vous avez.
PS : Mon anniversaire le 16 Juin. Merci de poster ma photo que voici 😁😁😁 :


