Back to blog
5 min read

Case Study: Tags input of Apple Reminders app

Table of Contents
Case Study: Tags input of Apple Reminders app
Screenshot of Apple Reminders application

The tags input in Apple Reminders app looks so simple in the first glance. But as I dug into the functionalities, I could see that it has some interesting features —

  • If we type a tag and enter a space, a tag name is made prefixed by a # sign
  • If we type Backspace, the tag is removed
  • If we enter the same tag again, only the existing tag is shown

So, I thought of implementing the same in Svelte.

Handling the input

<script lang="ts">
import type { FormEventHandler } from 'svelte/elements'
let input = ''
let words: string[] = []
const handleInput: FormEventHandler<HTMLInputElement> = (e) => {
const value = e.currentTarget.value
if (value.endsWith(' ') && input.trim().length > 0) {
words = [...words, input.trim()]
input = ''
} else input = value
}
</script>
<span>
{#if words.length > 0}
<span>
{#each words as word}
<span>#{word}</span>
{/each}
</span>
{/if}
<input type="text" placeholder="Tags" bind:value={input} on:input={handleInput} />
</span>

Output —

Now, if we try to add a tag, it works as expected. But if we enter the same tag, it is added again, which is not the expected behavior. This can be fixed as follows —

<script lang="ts">
import type { FormEventHandler } from 'svelte/elements'
let input = ''
let words: string[] = []
$: uniqueWords = [...new Set(words)]
const handleInput: FormEventHandler<HTMLInputElement> = (e) => {
const value = e.currentTarget.value
if (value.endsWith(' ') && input.trim().length > 0) {
words = [...words, input.trim()]
input = ''
} else input = value
}
</script>
<span>
{#if words.length > 0}
{#if uniqueWords.length > 0}
<span class="words">
{#each words as word}
{#each uniqueWords as word}
<span>#{word}</span>
{/each}
</span>
{/if}
<input type="text" placeholder="Tags" bind:value={input} on:input={handleInput} />
</span>

Output —

Handling Backspace

Next, we need to handle the event on entering the Backspace key. This can be done as follows —

<script lang="ts">
import type {
FormEventHandler,
KeyboardEventHandler
} from 'svelte/elements'
let input = ''
let words: string[] = []
let uniqueWords: string[] = []
$: uniqueWords = [...new Set(words)]
const handleInput: FormEventHandler<HTMLInputElement> = (e) => {
const value = e.currentTarget.value
if (value.endsWith(' ') && input.trim().length > 0) {
words = [...words, input.trim()]
input = ''
} else input = value
}
const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (e) => {
if (e.key === 'Backspace' && input.trim().length === 0 && words.length > 0)
words = words.slice(0, -1)
}
</script>
<span>
{#if uniqueWords.length > 0}
<span>
{#each uniqueWords as word}
<span>#{word}</span>
{/each}
</span>
{/if}
<input
type="text"
placeholder="Tags"
bind:value={input}
on:input={handleInput}
on:keydown={handleKeyDown}
/>
</span>

Output —

Handling blur event

Next, say we have entered a tag and clicked outside the input field. The tag should be added. This can be done as follows —

<script lang="ts">
import type {
FormEventHandler,
KeyboardEventHandler,
FocusEventHandler
} from 'svelte/elements'
let input = ''
let words: string[] = []
let uniqueWords: string[] = []
$: uniqueWords = [...new Set(words)]
const handleInput: FormEventHandler<HTMLInputElement> = (e) => {
const value = e.currentTarget.value
if (value.endsWith(' ') && input.trim().length > 0) {
words = [...words, input.trim()]
input = ''
} else input = value
}
const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (e) => {
if (e.key === 'Backspace' && input.trim().length === 0 && words.length > 0)
words = words.slice(0, -1)
}
const handleBlur: FocusEventHandler<HTMLInputElement> = () => {
if (input.trim().length > 0) {
words = [...words, input.trim()]
input = ''
}
}
</script>
<span>
{#if uniqueWords.length > 0}
<span>
{#each uniqueWords as word}
<span>#{word}</span>
{/each}
</span>
{/if}
<input
type="text"
placeholder="Tags"
bind:value={input}
on:input={handleInput}
on:keydown={handleKeyDown}
on:blur={handleBlur}
/>
</span>

Output —

Addition: Removing a tag on clicking it

<script lang="ts">
import type {
FormEventHandler,
KeyboardEventHandler,
FocusEventHandler,
MouseEventHandler
} from 'svelte/elements'
let input = ''
let words: string[] = []
let uniqueWords: string[] = []
$: uniqueWords = [...new Set(words)]
const handleInput: FormEventHandler<HTMLInputElement> = (e) => {
const value = e.currentTarget.value
if (value.endsWith(' ') && input.trim().length > 0) {
words = [...words, input.trim()]
input = ''
} else input = value
}
const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (e) => {
if (e.key === 'Backspace' && input.trim().length === 0 && words.length > 0)
words = words.slice(0, -1)
}
const handleBlur: FocusEventHandler<HTMLInputElement> = () => {
if (input.trim().length > 0) {
words = [...words, input.trim()]
input = ''
}
}
const handlePop: MouseEventHandler<HTMLButtonElement> = (e) => {
words = words.filter((word) => word !== e.currentTarget.textContent?.replace('#', ''))
}
</script>
<span>
{#if uniqueWords.length > 0}
<span>
{#each uniqueWords as word}
<span>#{word}</span>
<button on:click={handlePop}>#{word}</button>
{/each}
</span>
{/if}
<input
type="text"
placeholder="Tags"
bind:value={input}
on:input={handleInput}
on:keydown={handleKeyDown}
on:blur={handleBlur}
/>
</span>

Output —

I found it very interesting about how such a small feature can have so many things going on under the hood. Hope you like it too.



@bhargawanan_b