JavaScript Development Space

Create a Tags Input Field With Autocomplete in React

Add to your RSS feed9 September 20247 min read
Create a Tags Input Field With Autocomplete in React

In this tutorial, we are going to create a tags input component with autocomplete using React JS without using any external packages. This guide demonstrates how to manage state, dynamically filter suggestions, and efficiently handle user interactions such as adding and removing tags. Perfect for developers looking to implement tag inputs with minimal dependencies and flexible design.

Project Setup

We will use Vite for a fast and simple project build. Vite offers instant module hot reloading, improved performance, and quicker build times compared to traditional bundlers.

npm create vite@latest react-tags-autocomplete -- --template react-ts

then

cd react-tags-autocomplete npm install npm run dev

Clear the application

Remove file App.css, clean App.tsx (remove everything).

App.tsx

js
1 function App() {
2 return <></>;
3 }
4
5 export default App;

Let's create components folder inside the src folder.

Create a tags input component

Create a TextInput.tsx file in the components folder

ts
1 import { ChangeEvent, useState } from 'react';
2
3 const TextInput = () => {
4 const [tags, setTags] = useState<string[]>([]);
5 const handleKeydown = (e: ChangeEvent<HTMLInputElement> & KeyboardEvent) => {
6 if (e.key !== 'Enter') {
7 return;
8 }
9 const value = e.target.value;
10 if (!value.trim()) {
11 return;
12 }
13 setTags([...tags, value]);
14 e.target.value = '';
15 };
16
17 const removeTag = (idx) => {
18 setTags(tags.filter((el, i) => i !== idx));
19 };
20 return (
21 <div className='text-input-container'>
22 {tags.map((tag, i) => {
23 return (
24 <div className='tag-item' key={tag + i}>
25 <span className='text'>{tag}</span>
26 <span className='close' onClick={() => removeTag(i)}>
27 &times;
28 </span>
29 </div>
30 );
31 })}
32 <input
33 type='text'
34 placeholder='Type something...'
35 className='text-input'
36 onKeyDown={handleKeydown}
37 />
38 </div>
39 );
40 };
41 export default TextInput;

Explanation

  • The input field captures user text, filtering the predefined suggestions (suggestions array).
  • The filtered suggestions are shown as a dropdown. Clicking on a suggestion or pressing Enter adds it to the list of tags.
  • The added tags are displayed with an option to remove them.

Styling the Component

Remove everything from index.css file, and put the next styles

css
1 * {
2 margin: 0;
3 padding: 0;
4 }
5
6 html,
7 body {
8 height: 100%;
9 width: 100%;
10 }
11
12 body {
13 display: flex;
14 justify-content: center;
15 align-items: center;
16 font-family: 'Courier New', monospace;
17 }
18
19 label {
20 margin-bottom: 4px;
21 display: block;
22 font-size: 1.125rem;
23 line-height: 1.75rem;
24 }
25
26 #root {
27 display: flex;
28 flex-direction: column;
29 justify-content: center;
30 max-width: 540px;
31 margin-left: auto;
32 margin-right: auto;
33 padding-left: 1rem;
34 padding-right: 1rem;
35 margin-top: calc(1.5rem);
36 margin-bottom: calc(1.5rem);
37 color: #333333;
38 }
39
40 .text-input__wrapper {
41 border: 1px solid black;
42 padding: 0.5rem;
43 border-radius: 3px;
44 width: min(80vw, 600px);
45 margin-top: 1em;
46 display: flex;
47 align-items: center;
48 flex-wrap: wrap;
49 gap: 0.5em;
50 }
51 .tag-item {
52 background-color: rgb(218, 216, 216);
53 display: inline-block;
54 padding: 0.5em 0.75em;
55 border-radius: 20px;
56 }
57 .tag-item .close {
58 width: 20px;
59 height: 20px;
60 background-color: rgb(48, 48, 48);
61 color: #fff;
62 border-radius: 50%;
63 display: inline-flex;
64 justify-content: center;
65 align-items: center;
66 margin-left: 0.5em;
67 font-size: 18px;
68 }
69 .text-input {
70 padding-left: 1rem;
71 padding-right: 1rem;
72 padding-top: 0.625rem;
73 padding-bottom: 0.625rem;
74 font-size: 1.125rem;
75 line-height: 1.75rem;
76 background-color: #ffffff;
77 position: relative;
78 flex-grow: 1;
79 outline: none;
80 border: none;
81 outline: none;
82 }

Run

npm run dev Tags input

We got an almost finished component for adding tags, but it has one small bug. Right now, we can add duplicate tags in the input. To fix this, we need to modify the handleKeydown() function.

ts
1 const handleKeydown = (e: ChangeEvent<HTMLInputElement> & KeyboardEvent) => {
2 if (e.key !== 'Enter') {
3 return;
4 }
5 const value = e.target.value;
6 if (!value.trim()) {
7 return;
8 }
9
10 setTags((tags: string[]) => {
11 if (tags.some((tag) => tag.toLowerCase() === value.toLowerCase())) {
12 return [...tags];
13 } else {
14 return [...tags, value];
15 }
16 });
17 e.target.value = '';
18 };

Now only unique tags will be added.

Create Autocomplete Component

Create AutoComplete.tsx file inside the components directory.

ts
1 import React, { ChangeEvent, useState } from 'react';
2
3 type AutoCompleteProps = {
4 possibleValues: string[];
5 handleKeydown: () => void;
6 setTags: (values: string[]) => void;
7 };
8
9 function Autocomplete({ possibleValues, handleKeydown, setTags }: AutoCompleteProps) {
10 const [inputValue, setInputValue] = useState('');
11 const [suggestions, setSuggestions] = useState<string[]>([]);
12
13 const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
14 const value = event.target.value;
15
16 setInputValue(value);
17
18 if (value.length > 0) {
19 const filteredSuggestions = possibleValues.filter((suggestion) =>
20 suggestion.toLowerCase().includes(value.toLowerCase()),
21 );
22 setSuggestions(filteredSuggestions);
23 } else {
24 setSuggestions([]);
25 }
26 };
27
28 const handleSuggestionClick = (value: string) => {
29 setTags((tags: string[]) => {
30 if (tags.some((tag) => tag.toLowerCase() === value.toLowerCase())) {
31 return [...tags];
32 } else {
33 return [...tags, value];
34 }
35 });
36
37 setSuggestions([]);
38 setInputValue('');
39 };
40
41 const onKeyDown = (e: ChangeEvent<HTMLInputElement> & KeyboardEvent) => {
42 handleKeydown(e);
43 if (e.key === 'Enter') {
44 setInputValue('');
45 setSuggestions([]);
46 }
47 };
48
49 return (
50 <>
51 <input
52 type='text'
53 value={inputValue}
54 onChange={handleInputChange}
55 aria-autocomplete='list'
56 aria-controls='autocomplete-list'
57 onKeyDown={onKeyDown}
58 className='text-input'
59 autoFocus
60 />
61 <div className='autocomplete-wrapper'>
62 {suggestions.length > 0 && (
63 <ul id='autocomplete-list' className='suggestions-list' role='listbox'>
64 {suggestions.map((suggestion, index) => (
65 <li key={index} onClick={() => handleSuggestionClick(suggestion)} role='option'>
66 {suggestion}
67 </li>
68 ))}
69 </ul>
70 )}
71 </div>
72 </>
73 );
74 }
75
76 export default Autocomplete;

Key Features:

  1. Autocomplete Suggestions:
  • The component provides a list of suggestions based on the user's input. When the user types into the input field, it filters through possible values and displays matching suggestions.
  1. Duplicate Prevention:
  • The component ensures that duplicate tags are not added.
  1. Keyboard Navigation:
  • The onKeyDown function handles key events, particularly preventing unwanted behavior when the Enter key is pressed.

Breakdown of the Code:

  1. State Management:
  • inputValue: Keeps track of what the user is typing in the input field.
  • suggestions: An array of filtered possible values that match the user's input.
ts
1 const [inputValue, setInputValue] = useState('');
2 const [suggestions, setSuggestions] = useState<string[]>([]);
  1. Handling Input Changes:
  • handleInputChange: Updates inputValue based on user input and generates suggestions by filtering possibleValues.
  • possibleValues is an array of strings passed as props. The filter checks if the input matches any of the possibleValues (case-insensitive).
ts
1 const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
2 const value = event.target.value;
3 setInputValue(value);
4
5 if (value.length > 0) {
6 const filteredSuggestions = possibleValues.filter((suggestion) =>
7 suggestion.toLowerCase().includes(value.toLowerCase()),
8 );
9 setSuggestions(filteredSuggestions);
10 } else {
11 setSuggestions([]);
12 }
13 };
  1. Handling Suggestion Clicks:
  • When a user clicks on a suggestion, the handleSuggestionClick function is triggered.
  • This function adds the selected suggestion to the tags (managed by the parent component via setTags) if it's not already present.
  • After adding the tag, it clears the suggestions and resets inputValue.
ts
1 const handleSuggestionClick = (value: string) => {
2 setTags((tags: string[]) => {
3 if (tags.some((tag) => tag.toLowerCase() === value.toLowerCase())) {
4 return [...tags]; // No duplicate tags
5 } else {
6 return [...tags, value]; // Add the new tag
7 }
8 });
9
10 setSuggestions([]);
11 setInputValue('');
12 };
  1. Handling Keyboard Input:
  • onKeyDown: Handles keyboard events, especially when the Enter key is pressed.
  • It clears both the input and suggestions when Enter is pressed.
  • It also invokes handleKeydown passed from the parent component for further customization or handling.
ts
1 const onKeyDown = (e: ChangeEvent<HTMLInputElement> & KeyboardEvent) => {
2 handleKeydown(e);
3 if (e.key === 'Enter') {
4 setInputValue('');
5 setSuggestions([]);
6 }
7 };
  1. Rendering:
  • The component consists of an input field and a suggestions list.
  • The input field updates inputValue and triggers suggestions filtering, while the suggestions list shows filtered options.
  • When there are matching suggestions, the component renders a list of options. Clicking an option adds it to the tags.
js
1 return (
2 <>
3 <input
4 type='text'
5 value={inputValue}
6 onChange={handleInputChange}
7 aria-autocomplete='list'
8 aria-controls='autocomplete-list'
9 onKeyDown={onKeyDown}
10 className='text-input'
11 autoFocus
12 />
13 <div className='autocomplete-wrapper'>
14 {suggestions.length > 0 && (
15 <ul id='autocomplete-list' className='suggestions-list' role='listbox'>
16 {suggestions.map((suggestion, index) => (
17 <li key={index} onClick={() => handleSuggestionClick(suggestion)} role='option'>
18 {suggestion}
19 </li>
20 ))}
21 </ul>
22 )}
23 </div>
24 </>
25 );

Props:

  • possibleValues: Array of strings that act as potential autocomplete suggestions.
  • handleKeydown: A function passed from the parent component to handle keyboard events.
  • setTags: A function that updates the list of tags when a suggestion is selected.

We just need to replace the input with our custom AutoComplete component and pass all the necessary props to it.

js
1 <Autocomplete
2 possibleValues={['css', 'html', 'react']}
3 handleKeydown={handleKeydown}
4 setTags={setTags}
5 />

Add some styles to index.css

css
1 .autocomplete-wrapper {
2 width: 100%;
3 }
4
5 .suggestions-list {
6 top: 100%;
7 border: 1px solid #ccc;
8 background: white;
9 list-style: none;
10 padding: 0;
11 margin: 0;
12 border-radius: 3px;
13 }
14
15 .suggestions-list li {
16 padding: 8px;
17
18 cursor: pointer;
19 }
20
21 .suggestions-list li:hover {
22 background-color: #e9e9e9;
23 }

Now, let's test it:

npm run dev

There’s only one issue left: we lose focus after adding a tag through a suggestion. To fix this, we need to pass a ref to the input and manually set the focus.

Referencing Values with Refs

Define a ref at the top of AutoComplete complement:

js
1 const inputRef = useRef(null);

then provide it to the input tag

js
1 ref = { inputRef };

Now, at the very end of the handleSuggestionClick() and onKeyDown() functions (after all the code has been executed), add the line:

js
1 inputRef.current.focus();

Conclusion:

This component allows users to type and select from filtered suggestions. It prevents duplicate entries, handles keyboard events, and manages the internal state of user input and suggestions efficiently.

You can also view it on GitHub Gist: AutoComplete.tsx | TextInput.tsx

JavaScript Development Space

© 2024 JavaScript Development Space - Master JS and NodeJS. All rights reserved.