Tuto: Tags in Svelte

Tuesday, July 25, 2023 · 6 minutes · 1,125 words

Tags input

I want to code a simple tags input component in Svelte like the animation above.

  • An input text where I can type words.
  • As soon as I type a comma or Enter, the input turns into a “tag”.
  • The tag appears next to the input text with a small cross to delete it.

I can retrieve the list of tags in an easy-to-use data structure: for example, an array of string.

I’ll use the Svelte Repl for this tutorial.

Exploring possibilities

In Svelte, on:keydown triggers a function on every key press. So I create a small piece of code to highlight this behavior:

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

keydown call pressed function passing an event object as parameter. I’m only interested in the key information inside this object as it contains the key pressed by the user.

Each press on the keyboard, with the cursor in the text field, will cause a display in the console.

So, I can monitor the input waiting for a comma by doing a test on key:

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

We’ve got something, now let’s use that.

Into the heart of the matter

When you press comma, that’s when you have a tag to add: so I take the content of the input field and I add it to my list.

To see what I’m doing, I add a line with ‘{tags}’ which will display the list of saved tags:

<script>
let tags = [];                      // save tags here
let value = "";                     // input value
function pressed(ev){
    if(ev.key === ',') {            // comma ?
        tags = [...tags, value];    // add to the list
        value = "";                 // and clean input
    }
};
</script>
{tags}
<input on:keydown={pressed} bind:value/>

Small problem: the comma remains in the field despite the cleaning.

The keydown intercepts the event before the key is taken into account by the input field. So our processing is fired before the comma is added in the value variable.

Instead of keydown, we will rather use keyup, which will trigger our function after input is taken care. So the comma will be embedded in the value of the field.

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

Now the comma no longer stays in the field, but is part of the ‘value’.

I can easily remove it before adding to the list:

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

So now my code looks like :

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

Before continuing, I make sure that the user does not enter anything wrong in the field. For example, a single comma or the Enter key with no value should have no effect, which is not the case with the current code.

So, once I remove the comma, if I have nothing left, it means I have nothing to do:

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

I don’t like this multi-nested level. I rather reverse the tests and exit immediately if the conditions are not met. Thus, I end up with the “good code” aligned to the left.

Demonstration:

<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/>

Oh, I forgot I wanted to use also Enter as a tag separator :

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

Time to take care of the visualization of the tags : I iterate on every saved tags with the #each command:

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

#each takes every item inside the tags array and makes it available in the t variable with its index in i.

Let’s use this to display each tag with an X next to it. The X is used to remove the tag.

It’s not a real X, it’s an utf8 code that looks way more stylish 😎.

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

Ok, the X is not actionable yet, so I add an anchor around it with a call to a del function with the index of the tag to remove.

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

The del function just calls Splice.

function del(idx){
    tags.splice(idx,1); // <-- remove the element at index `idx`
    tags = tags;        // <-- force Svelte reactivity
}

The line with tags = tags is used to force Svelte to refresh. This is one of the rare cases where I need to explicitly indicate to Svelte that de data model has changed.

I surround each tag with a ‘span’ element and add a little touch of CSS to make it look more taggy:

{#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>

Icing on the cake: suggestions!

It will be nice if I can have suggestions as I type the first characters.

HTML let us the possibility to add suggestions in an input field, from a predefined list: datalist contains the suggestions list, and we use it with the list property inside the input field.

I define a new tagsugg variable with all my suggestions:

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

In a real project, this list can be generated from all previously saved tags in the system.

From this list, I construct the HTML datalist:

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

Then I can reference it inside the input field with the list property:

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

That’s it (for the moment) !

I now have an input text field that generates tags with a suggestion list !

A very useful evolution is to transform this code to a web component, so it can be used like an HTML tag (no pun intended 😎).

You can get the complete code with comments on this Repl Svelte page

tuto svelte