Implement inline input suggestions

phuocng

Phuoc Nguyen

Posted on December 22, 2023

Implement inline input suggestions

In our last post, we learned how to give users a sneak peek of suggested text 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.

<div class="container" id="container">
    <input id="input" class="container__input" type="text" />
</div>
Enter fullscreen mode Exit fullscreen mode

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:

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

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.

let matches = [];
let currentMatchIndex = 0;
Enter fullscreen mode Exit fullscreen mode

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:

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

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:

postCursorEle.textContent = matches[currentMatchIndex];
Enter fullscreen mode Exit fullscreen mode

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.

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

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.

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

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.

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

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

case 'ArrowUp':
    if (matches.length > 0 && currentMatchIndex >= 1) {
        e.preventDefault();
        currentMatchIndex--;
        previewSuggestion();
    }
Enter fullscreen mode Exit fullscreen mode

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.

const suggestions = [
    'Alabama',
    'Alaska',
    'Arizona',
    'Arkansas',
    'California',
    'Colorado',
    'Connecticut',
    ...
];
Enter fullscreen mode Exit fullscreen mode

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, simply press the Tab key to insert the current suggestion into the input field.


It's highly recommended that you visit the original post to play with the interactive demos.

If you found this series helpful, please consider giving the repository 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:

šŸ’– šŸ’Ŗ šŸ™… šŸš©
phuocng
Phuoc Nguyen

Posted on December 22, 2023

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

Sign up to receive the latest update from our blog.

Related