Tutorial: Tags in Svelte

barim

CodeCadim by Brahim Hamdouni

Posted on July 26, 2023

Tutorial: Tags in Svelte

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 keypress. 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}/>
Enter fullscreen mode Exit fullscreen mode

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}/>
Enter fullscreen mode Exit fullscreen mode

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/>
Enter fullscreen mode Exit fullscreen mode

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 -->
Enter fullscreen mode Exit fullscreen mode

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(',','');
Enter fullscreen mode Exit fullscreen mode

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/>
Enter fullscreen mode Exit fullscreen mode

Before continuing, I make sure that the user does not enter anything wrong in the field. For example, a single comma or an Enter 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/>
Enter fullscreen mode Exit fullscreen mode

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/>
Enter fullscreen mode Exit fullscreen mode

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/>
Enter fullscreen mode Exit fullscreen mode

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

{#each tags as t,i}
...
{/each}
Enter fullscreen mode Exit fullscreen mode

#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}
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

The del function just call Splice.

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

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>
Enter fullscreen mode Exit fullscreen mode

Icing on the cake: suggestions!

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

Suggestion in tags input

HTML let us the possibility to add suggestions in a 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"];
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

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

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 a HTML tag (no pun intended 😎).

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

This article was cross-posted from my blog

πŸ’– πŸ’ͺ πŸ™… 🚩
barim
CodeCadim by Brahim Hamdouni

Posted on July 26, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related