Implement inline input suggestions
Tags: tutorial, webdev, javascript
Published: 2023-12-22 07:39:42

In our last post, we learned how to give users a [sneak peek of suggested text](https://phuoc.ng/collection/mirror-a-text-area/provide-a-preview-of-the-suggestion-as-users-type-in-a-text-area/) as they type in a text area. Users can then add the suggestion by hitting Enter.

Inline suggestions are versatile and can be used in many ways. Let's consider a real-life example of inline suggestions in action. Picture yourself buying concert tickets online. As you type in the name of the artist, inline suggestions appear beneath the text input field, offering possible matches for what you're typing. This feature saves time and prevents errors by ensuring that the artist name is spelled correctly and avoiding any confusion with similar-sounding names. With just one click or tap, you can select the correct suggestion and move on to the next step.

In this post, we'll show you how to add similar functionality to a text input. If there are multiple suggestions, users can preview them by pressing the arrow up or arrow down keys.

## Previewing the suggestion

Let's assume that the layout is organized as follows: The main input field is placed inside a container.

```html
<div class="container" id="container">
    <input id="input" class="container__input" type="text" />
</div>
```

Let's take a moment to revisit our previous post. In order to ensure the cursor is placed at the end of the input, we handle the `input` event by comparing the current cursor position with the length of the input's value. If the cursor is at the end, we extract the current word and search for suggestions that match.

Here's a sample code to refresh your memory on what we've accomplished so far:

```js
inputEle.addEventListener('input', () => {
    const currentValue = inputEle.value;
    const cursorPos = inputEle.selectionStart;
    if (cursorPos !== currentValue.length) {
        hideSuggestion();
        return;
    }

    const startIndex = findIndexOfCurrentWord();

    // Extract just the current word
    const currentWord = currentValue.substring(startIndex + 1, cursorPos);
    if (currentWord === '') {
        hideSuggestion();
        return;
    }

    matches = suggestions.filter((suggestion) => suggestion.toLowerCase().indexOf(currentWord.toLowerCase()) > -1);
    currentMatchIndex = 0;
    previewSuggestion();
});
```

In this example, we store the list of suggestions that match the current word in the `matches` variable. We also use the `currentMatchIndex` variable to track the index of the current match.

```js
let matches = [];
let currentMatchIndex = 0;
```

The `previewSuggestion()` function previews the current word. In our previous post, we explained how we created a mirrored element of the input using three elements: two text nodes for the text before and after the cursor, and an empty `span` for the current caret. But since we can't customize the appearance of a text node, we replaced them with `span` elements.

The text before and after the cursor are now `span` elements. We updated the post-cursor element to display the first matching suggestion instead of the entire text before the cursor. This allows users to see the suggestion that's coming up next.

Here's an example of what the `previewSuggestion()` function could look like:

```js
const previewSuggestion = () => {
    if (matches.length === 0) {
        hideSuggestion();
        return;
    }

    const currentValue = inputEle.value;
    const cursorPos = inputEle.selectionStart;
    const textBeforeCursor = currentValue.substring(0, cursorPos);

    const preCursorEle = document.createElement('span');
    preCursorEle.textContent = textBeforeCursor;
    preCursorEle.classList.add('container__pre-cursor');

    const postCursorEle = document.createElement('span');
    postCursorEle.classList.add('container__post-cursor');
    postCursorEle.textContent = matches[currentMatchIndex];

    const caretEle = document.createElement('span');
    caretEle.innerHTML = '&nbsp;';

    mirroredEle.innerHTML = '';
    mirroredEle.append(preCursorEle, caretEle, postCursorEle);
};
```

In this example, we first check if there are any matches by comparing the length of matches with zero. If there are no matches, we hide the suggestions. However, if there are matches, we construct the mirror element by using three elements, as previously described.

The key line in the `previewSuggestion()` function is this one:

```js
postCursorEle.textContent = matches[currentMatchIndex];
```

In this code snippet, we're setting the content of the preview element by using the matches and the current match index.

## Switching between suggestions with ease

In the previous post, we always chose the first suggestion that matched the word being typed. However, sometimes that suggestion wasn't the best one. What if there was a way for users to preview and choose between multiple matches?

Well, good news! We can implement a mechanism that allows users to navigate between matches using the arrow keys.

All we need to do is handle the `keydown` event and check if the user pressed either the arrow up or down key. If so, we update the index of the current match and preview it.

```js
inputEle.addEventListener('keydown', (e) => {
    switch (e.key) {
        case 'ArrowDown':
            // Preview the next suggestion ...
            break;
        case 'ArrowUp':
            // Preview the previous suggestion ...
            break;
        default:
            break;
    }
});
```

In this example, we're checking if the user presses either the arrow up or arrow down key. If they do, we check if there are any matches.

When the user presses the arrow down key, we move to the next matching suggestion and show a preview. We only update the current match index if there are more matches to display. If there are no more matches, we keep displaying the last matching suggestion.

```js
case 'ArrowDown':
    if (matches.length > 0 && currentMatchIndex < matches.length - 1) {
        e.preventDefault();
        currentMatchIndex++;
        previewSuggestion();
    }
    break;
```

We use the `currentMatchIndex` variable to keep track of the suggestion that should be displayed next. We retrieve the suggestion at this index from our list of matching suggestions and display it.

```js
const previewSuggestion = () => {
    // ...
    const postCursorEle = document.createElement("span");
    postCursorEle.classList.add("container__post-cursor");
    postCursorEle.textContent = matches[currentMatchIndex];
};
```

Similarly, we can check if the user presses the up arrow key and show the previous suggestion accordingly.

```js
case 'ArrowUp':
    if (matches.length > 0 && currentMatchIndex >= 1) {
        e.preventDefault();
        currentMatchIndex--;
        previewSuggestion();
    }
```

Users can effortlessly cycle through all available suggestions by pressing the arrow down or up keys until they come across the desired option.

## Demo

Check out the final demo below where we use the states of the United States as a list of suggestions.

```js
const suggestions = [
    'Alabama',
    'Alaska',
    'Arizona',
    'Arkansas',
    'California',
    'Colorado',
    'Connecticut',
    ...
];
```

Give it a try! Just type a few characters and use the arrow keys to see how it suggests different states that match your keyword. And as mentioned in the [previous post](https://phuoc.ng/collection/mirror-a-text-area/provide-a-preview-of-the-suggestion-as-users-type-in-a-text-area/), simply press the Tab key to insert the current suggestion into the input field.

{% codepen https://codepen.io/phuocng/pen/vYPYxVb %}

---

It's highly recommended that you visit the [original post](https://phuoc.ng/collection/mirror-a-text-area/implement-inline-input-suggestions/) to play with the interactive demos. 

If you found this series helpful, please consider giving the [repository](https://github.com/phuocng/mirror-a-text-area) a star on GitHub or sharing the post on your favorite social networks 😍. Your support would mean a lot to me!

If you want more helpful content like this, feel free to follow me:

-   [Twitter](https://twitter.com/_phuocng)
-   [GitHub](https://github.com/phuocng)