Tuto: Saisie de tags en Svelte

mardi 25 juillet 2023 · 6 minutes · 1,159 mots

Saisie de tags

L’idée est de coder une saisie de “tags” comme dans l’animation ci-dessus.

  • Un champ texte, dans lequel je rentre des mots.
  • Dès que je tape une virgule ou Entrée, la saisie se transforme en “tag”.
  • Le tag apparaît à côté du champ avec une petite croix pour le supprimer.

Je récupère la liste des tags dans une structure de données facile d’utilisation : par exemple, un tableau de string.

Pour ce code, je me mets dans le Repl de Svelte.

Exploration des possibles

Je sais que dans Svelte, on:keydown déclenche une fonction à chaque appuie sur le clavier. Je crée donc un petit bout de code pour mettre en évidence ce comportement :

<script>
function pressed(ev){
    console.info(ev.key);
};
</script>
<input on:keydown={pressed}/>

keydown appelle ma fonction pressed en lui passant un objet de type event qui contient plein d’infos. Je m’intéresse seulement à key qui contient la touche pressée.

Chaque appuie sur le clavier, avec le curseur dans le champ texte, va provoquer un affichage dans la console.

Donc, je surveille la saisie d’une virgule en faisant un test sur key :

<script>
function pressed(ev){
    if(ev.key === ',') {
        console.info("VIRGULE!!!")
    }
};
</script>
<input on:keydown={pressed}/>

Rentrons dans le vif du sujet

Quand on appuie sur la virgule, c’est là qu’on a un tag à ajouter : donc je prends le contenu du champ et je l’ajoute à ma liste, un tableau vide au départ.

Pour voir ce que je fais, j’ajoute un ‘{tags}’ qui affichera la liste des tags enregistrés :

<script>
let tags = [];                      // ici je stock les tags
let value = "";                     // la valeur du champ
function pressed(ev){
    if(ev.key === ',') {            // virgule ?
        tags = [...tags, value];    // on ajoute au tableau
        value = "";                 // ne pas oublier de nettoyer
    }
};
</script>
{tags}
<input on:keydown={pressed} bind:value/>

Petit problème : la virgule reste dans le champ malgré le nettoyage.

Le keydown intercepte l’évènement avant que la touche ne soit prise en compte par le champ. Ainsi, notre traitement se déclenche avant que la virgule soit ajoutée dans la ‘value’.

Au lieu de keydown, on va plutôt prendre keyup, qui déclenchera notre traitement après la fin de l’appui. Ainsi, on pourra embarquer la virgule dans la valeur du champ.

<script>
let tags = [];
let value = "";
function pressed(ev){
    if(ev.key === ',') {
        tags = [...tags, value];
        value = "";
    }
};
</script>
{tags}
<input on:keyup={pressed} bind:value/> <!-- keyup à la place de keydown -->

Maintenant, la virgule ne reste plus dans le champ, mais fait partie de la ‘value’.

Je retire la virgule avant de l’ajouter à notre liste de tags avec la ligne :

value = value.replace(',','');

Et cela nous donne :

<script>
let tags = [];
let value = "";
function pressed(ev){
    if(ev.key === ',') {
        value = value.replace(',',''); // <-- ici on vire la virgule
        tags = [...tags, value];
        value = "";
    }
};
</script>
{tags}
<input on:keyup={pressed} bind:value/>

Avant de continuer, je m’assure un minimum que l’utilisateur ne rentre pas n’importe quoi dans le champ. Par exemple, une simple virgule ou la touche Entrée sans aucune valeur, ne devrait avoir aucun effet, ce qui n’est pas le cas avec le code actuel.

Donc, une fois que j’ai retiré la virgule, s’il ne me reste rien, c’est que je n’ai rien à faire :

<script>
let tags = [];
let value = "";
function pressed(ev){
    if(ev.key === ',') {
        value = value.replace(',','');
        if(value !== "") {              // <-- champ non vide ?
            tags = [...tags, value];    // <-- on traite...
            value = "";
        }
    }
};
</script>
{tags}
<input on:keyup={pressed} bind:value/>

Cette imbrication d’imbrication ne me plait pas. Je préfère inverser les tests et sortir tout de suite si les conditions ne sont pas remplies. Ainsi, je me retrouve avec le “bon code” bien aligné à gauche.

Démonstration :

<script>
let tags = [];
let value = "";
function pressed(ev){
    if(ev.key !== ',') return;
    value = value.replace(',','');
    if(value === "") return;
    tags = [...tags, value];
    value = "";
};
</script>
{tags}
<input on:keyup={pressed} bind:value/>

Pour peaufiner, on va aussi prendre en compte l’appui de la touche Entrée comme déclencheur, en plus de la virgule.

<script>
let tags = [];
let value = "";
function pressed(ev){
    if(ev.key !== ',' && ev.key !== 'Enter') return; // <-- Enter aussi
    value = value.replace(',','');
    tags = [...tags, value];
    value = "";
};
</script>
{tags}
<input on:keyup={pressed} bind:value/>

À présent, je vais afficher les tags plus proprement. J’itère sur chaque tag à l’aide de #each :

{#each tags as t,i}
...
{/each}

L’instruction #each va parcourir la liste tags et me mettre à disposition chaque valeur dans t et son index dans i.

Dans la boucle, j’affiche chaque tag avec un ‘X’ pour pouvoir le supprimer. J’ai pris le code utf8 correspondant à un X plus stylé 😎.

{#each tags as t,i}
    {t} ⨉
{/each}

Pour faire la suppression, je crée un lien autour du X qui va appeler une fonction del avec, en paramètre, l’index du tag à retirer.

{#each tags as t,i}
    {t} <a href="#del" on:click={()=>del(i)}></a>
{/each}

La fonction del se contente d’appeler Splice.

function del(idx){
    tags.splice(idx,1); // <-- supprime un élément en idx
    tags = tags;        // <-- force la réactivité de Svelte
}

Le tags = tags sert à déclencher le rafraîchissement de Svelte parce qu’il ne détecte pas le changement induit par Splice.

J’entoure chaque tag d’un élément span et j’ajoute une petite touche de CSS pour donner une apparence plus tag :

{#each tags as t,i}
<span class="tag">
    {t} <a href="#del" on:click={()=>del(i)}></a>
</span>
{/each}
<input on:keyup={pressed} bind:value/>

<style>
.tag {font-size: 0.8rem; margin-right:0.33rem; padding:0.15rem 0.25rem; border-radius:1rem; background-color: #5AD; color: white;}
.tag a {text-decoration: none; color: inherit;}
</style>

Cerise sur le gâteau : les suggestions !

Et si je pouvais gérer une liste de suggestion quand je commence à taper dans le champ ? Ce serait top !

Saisie de tags avec suggestion

En HTML, j’ai la possibilité de suggérer des valeurs à saisir dans un champ, à partir d’une liste pré-définie : datalist pour définir les valeurs de suggestion possibles et la propriété list pour y faire référence.

Je commence d’abord par créer ma liste de suggestion à l’aide d’une nouvelle variable tagsugg :

let tagsugg = ["tag1", "tag2", "tag3"];

Dans la réalité, cette liste pourra être générée à la volée, par exemple en reprenant tous les tags précédemment enregistrés.

Je vais utiliser cette liste pour construire datalist dans le HTML :

<datalist id="tag_suggestion">
{#each tagsugg as ts}
    <option>{ts}</option>
{/each}
</datalist>

Et ensuite, je fais référence à datalist dans mon champ de saisie grâce à la propriété list :

<input list="tag_suggestion" on:keyup={pressed} bind:value/>

Et c’est tout (pour le moment) !

J’ai maintenant un champ de saisie de texte qui génère des tags et le tout avec une liste de suggestion !

Une évolution intéressante serait de transformer le code en un composant web pour pouvoir le réutiliser comme une balise HTML.

Retrouvez l’intégralité de ce code, commenté (en anglais), sur le Repl Svelte

tuto svelte