How to show suggestions on typing using Javascript?
Tags: javascript
Published: 2019-08-08 04:43:31

Hello, This is a short tutorial on how to show suggestions when the user types specific keyword in a textbox.

Showing suggestions on typing is something most app does. Here, I'm not talking about search text boxes, which always show you suggestions. (Google search box)

Let's assume that the user is typing in a `<textarea>`, and we need to show some suggestions when they are typing a username starting from @ (`@Supun`)

This requires some work to be done. Here's a good approach that I found effective.

* We have a `<textarea>` element which takes user input.
* We listen to the `keyup` event to check if user is typing what we need.

This article is written in Vanilla JS. You can use a library to manipulate DOM. But, I don't know any popular JS library that provides functions to get and set cursors. So, using Vanilla JS for that would be fine. Let's start!

Before we start, we need to helper functions to deal with **cursor positions**. (To get and set)

```js
function getCursorPosition(el) {
    var start = 0, end = 0, normalizedValue, range, textInputRange, len, endRange;
    if (typeof el.selectionStart == "number" && typeof el.selectionEnd == "number") {
        start = el.selectionStart;
        end = el.selectionEnd;
    } else {
        range = document.selection.createRange();

        if (range && range.parentElement() == el) {
            len = el.value.length;
            normalizedValue = el.value.replace(/\r\n/g, "\n");

            // Create a working TextRange that lives only in the input
            textInputRange = el.createTextRange();
            textInputRange.moveToBookmark(range.getBookmark());

            // Check if the start and end of the selection are at the very end
            // of the input, since moveStart/moveEnd doesn't return what we want
            // in those cases
            endRange = el.createTextRange();
            endRange.collapse(false);

            if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
                start = end = len;
            } else {
                start = -textInputRange.moveStart("character", -len);
                start += normalizedValue.slice(0, start).split("\n").length - 1;

                if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) {
                    end = len;
                } else {
                    end = -textInputRange.moveEnd("character", -len);
                    end += normalizedValue.slice(0, end).split("\n").length - 1;
                }
            }
        }
    }
    return {
        start: start,
        end: end
    };
}

function setCursorPosition(input, start, end) {
    if (arguments.length < 3) end = start;
    if ("selectionStart" in input) {
        setTimeout(function() {
            input.selectionStart = start;
            input.selectionEnd = end;
        }, 1);
    }
    else if (input.createTextRange) {
        var rng = input.createTextRange();
        rng.moveStart("character", start);
        rng.collapse();
        rng.moveEnd("character", end - start);
        rng.select();
    }
}
```

Then, the textarea element which we are going to **listen to**. There's no problem if it is a `input` element. And, the suggestion view where we are going to show the suggestions in.

```html
<textarea id="txt"></textarea>
<div id="suggestions"></div>
```

Now, some useful variables.

```js
var txt = document.getElementById("txt"),
    suggestions = document.getElementById("suggestions"),
    regex = /@([a-zA-Z0-9]*)$/; // the regex we are going to match

// some fake data
var userData = [
  {
  	name: "Supun Kavinda",
    username: "SupunKavinda"
  },
  {
  	name: "John Doe",
    username: "JohnDoe"
  },
  {
  	name: "Anonymous",
    username: "Anonymous"
  }
];
```

Add the keyup event listener to the textarea.
```js
// listen for @...
txt.addEventListener('keyup', handleKeyUp);
```

Then, our handler. Here we will match the string before cursor with `/@[a-zA-Z0-9]*$/`. If matched, we can suggestions of the users taken from a database. (Here I will use some fake data for users)

```js
function handleKeyUp() {
   closeSuggestions();
   var cursor = getCursorPosition(txt),
      val = txt.value,
      strLeft = val.substring(0, cursor.start);
      
  var match = val.match(regex);
      
  if (match) {
     // fetch suggestions
     var username = match[1];
     findSuggestions(username);     
  }
}
```

Finding, showing and closing suggestions...

```js
function findSuggestions(username) {
	
  var matched = [];
  
  userData.forEach(function(data) {
  	var dataUsername = data.username,
    	pos = dataUsername.indexOf(username);
      
    if (pos !== -1) {
    	matched.push(data);
    }
  });
  
  // you can also sort the matches from the index (Best Match)
  
  if (matched.length > 0) {
  	showSuggestions(matched);
  }

}

function showSuggestions(matched) {
  // DOM creation is not that hard if you use a library ;
  suggestions.style.display = "block";
  suggestions.innerHTML = "";
  
  matched.forEach(function(data) {
  	var wrap = document.createElement("div");
    suggestions.appendChild(wrap);
    
    var nameView = document.createElement("span");
    nameView.innerHTML = data.name;
    nameView.className = "name-view";
    
    var usernameView = document.createElement("span");
    usernameView.innerHTML = "@" + data.username;
    usernameView.className = "username-view";
    
    wrap.appendChild(nameView);
    wrap.appendChild(usernameView);
    
    // add the suggested username to the textarea
    wrap.onclick = function() {
    	addToTextarea("@" + data.username + " ");
    }
  });
}

function closeSuggestions() {
	suggestions.style.display = "none";
}
```

The function to add the username to the textarea.

```js
function addToTextarea(valueToAdd) {

   var cursor = getCursorPosition(txt),
      val = txt.value,
      strLeft = val.substring(0, cursor.start),
      strRight = val.substring(cursor.start);
     
    // remove the matched part
    strLeft = strLeft.replace(regex, "");
     
    txt.value = strLeft + valueToAdd + strRight;
    
    // (textarea, positionToAdd)
    setCursorPosition(txt, strLeft.length + valueToAdd.length);
    
    txt.focus();
    
    closeSuggestions();

}
```

Finally, let's add some CSS.

```css
#txt {
  resize:none;
  width:450px;
  height:100px;
  border-radius:5px;
  border:1px solid #eee;
}
#suggestions {
  /* It's better to contain textarea
  and suggestions in a div and make this suggestions view
  absolute positioned */
  box-shadow:0 0 25px rgba(0,0,0,0.05);
  width:450px;
}
#suggestions > div {
  padding:10px;
}
.name-view {
  margin-right:5px;
}
.username-view {
  color: #aaa;
}
```

You are done!

## Possible Improvements

* You can add keyboard navigation to select the suggestions.

Here's the [JSFiddle Demo](https://jsfiddle.net/4w2n1f6z/1/). And, I'm gonna publish this post as the first post of [Javascriptians group](https://groups.hyvor.com/JS) on my website.

Hope it helped.