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}/>
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 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/>
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 tags visualization : 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 call 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 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"];
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 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
Top comments (1)
Very clean implementation! Thanks a lot!