Typeahead input implementation

Hello 🙂

This post is about the basic typeahead input implementation using ReactJS that can be customised based on the use case you have.

What is typeahead?

Many of you must have seen/used the view where google provides suggestions to your input by showing them as a greyed out text after your input A.K.A Smart compose.

This solution is now used across different tools provided by Google such Gmail, Gsheet, Gdoc etc.

What is the use case?

The use case is pretty straightforward i.e. when you want to offer a suggestion to user. Now the focus is on the word “a suggestion” and not “suggestions”, since in the traditional way when the system isn’t confident on what is the best acceptable suggestion, a dropdown would be shown with a list of possible suggestions. That works fine in most of the use cases, but a thing to remember is that, it is an extra step for user to click on, plus it adds additional time in reading through all suggestions, hence the use case of showing just one in a neat way which is easy to accept with the use of either Tab keypress or Right arrow keypress and saves time.

Implementation

Deciding the Choice of HTML Elements

Since the view requires to render a suggestion as a greyed out text and user should be able to write in the input field, we’re clear that for rendering the suggestion we would need a span element, which can be styled and suggestion can be rendered in it.

Now, we can go with using the HTML input field for the user to be able to write, but to show suggestion you would need to render the suggestion span with position absolute, right after the cursor ends. Having position absolute and deciding the positioning can be challenging which is why we’re going for a different solution which is to use the content-editable property of the div.

Now having span as a child node of this div is a much simpler way than positioning it inside input element. Let’s write down how it would look like

import React from "react";
import "./styles.css";

export default class App extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
         suggestion: ''
      }
      this.inputRef = React.createRef();
    }

    return (
      <div
        className="chatarea"
        ref={this.inputRef}
        contentEditable="true"
        spellCheck="true"
      >
        {!!this.state.suggestion.length && (
          <span className="suggestion">{this.state.suggestion}</span>
        )}
      </div>
    );
}
// styles.css
.chatarea {
  width: 500px;
  min-height: 50px;
  max-height: 100px;
  height: auto;
  overflow-y: scroll;
  border: 1px solid #ccc;
  border-radius: 3px;
  text-align: left;
  padding: 10px;
}

.chatarea:empty:before {
  content: "Type your message!";
  color: #ccc;
}

.suggestion {
  color: #ccc;
}

JSX Code

You would notice the div that renders the input typed by the user has a state variable to store the suggestion which is rendered conditionally based on whether the suggestion length is greater than 0 or not. To make sure we’re able to retrieve the values for the user typed input a reference has been created and added to div.
Additionally the browser provided spellcheck property has been enabled on the div.

Css code

The styling of the input is pretty standard for the sake of simplicity. To emulate the placeholder behaviour like what you get in an HTML input field, there is a empty:before css selector to render the content of placeholder when input is empty. This is also one of the reason why suggestion span has been rendered conditionally. Other reason being that it makes it easier to clear the input (after submission), as we shouldn’t delete the span node as it’s rendered in virtual DOM by React.

Listening to input

Next step is to listen to user’s input by adding a key down event listener on input div. The events we have to listen to would be
1. Tab press
2. Right arrow press
3. A character that would trigger suggestion fetch (For this post, we’re going to use space as that trigger character, so whenever space key is pressed a suggestion would be pulled and shown to user)

import React from "react";
import "./styles.css";

export default class App extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
         suggestion: ''
      }
      this.inputRef = React.createRef();
    }

    handleKeyDown(e) {
      const suggestion = "How can I help you?"; // hardcoding the suggestion for this post
      if (e.keyCode === 9 || e.keyCode === 39) { // Tab or right arrow press
        e.preventDefault();
        this.setState({
          suggestion: ""
        });
        const sel = window.getSelection();
        if (sel.getRangeAt && sel.rangeCount) {
          const range = sel.getRangeAt(0);
          // if the cursor is at the end of input and there is a suggestion
          if (
            range.startOffset === range.startContainer.length &&
            this.state.suggestion.length > 0
          ) {
            range.insertNode(document.createTextNode(suggestion));
            range.collapse(); // Move the cursor after the inserted text node
          }
        }
      } else if (e.keyCode === 32) { // Space character press
        // fetch suggestion
        const sel = window.getSelection();
        if (sel.getRangeAt && sel.rangeCount) {
          const range = sel.getRangeAt(0);
          // if the cursor is at the end on the input
          if (range.startOffset === range.startContainer.length) {
            this.setState({
              suggestion
            });
          }
        }
      } else {
        // hide suggestion
        this.setState({
          suggestion: ""
        });
      }
    }

    return (
      <div
        className="chatarea"
        ref={this.inputRef}
        contentEditable="true"
        spellCheck="true"
        onKeyDown={this.handleKeyDown}
      >
        {!!this.state.suggestion.length && (
          <span className="suggestion">{this.state.suggestion}</span>
        )}
      </div>
    );
}

JSX code

As you can see there is a method handleKeyDown that listens to keydown events inside the div and the handler has logic to figure out which key was pressed based on the keyCode property of the event.

Key things happening in handleKeyDown are
1. On Tab or Right arrow keypress, a suggestion is accepted by inserting the content of suggestion from state and then setting the suggestion in state to empty.
2. On Space keypress, a suggestion is fetched which has been hardcoded in this implementation and stored in state.
3. If any other key is pressed, the suggestion is cleared out.

To determine the current cursor position we will use the window.getSelection() along with sel.getRangeAt(0). Window’s getSelection method will give the reference to the selected area on the page. Since while typing in user input, the user’s selection would be in the input, window.getSelection should return a reference to that. You can read more about selection property from here.

Selection’s getRange method returns the range of selected area in the DOM. Here we’re trying to retrieve the range existing at index 0. You can read more about Range from here.

Thing to note about the implementation is that the suggestion is only fetched when the cursor is at the end of the already inputted text.
This has been done this way, since there isn’t a prominent use case where user would go to somewhere in middle of already typed text and would want the suggestion to be offered. At that stage they are likely to correct a mistake than to insert something new.

working implementation in codesandbox

Having some troubles in making the code execute with embed from codesandbox into wordpress. Please use https://codesandbox.io/s/hopeful-ritchie-j9zct?file=/src/App.js in the meanwhile

Hope you liked the post. Please do share any feedback you have. 🙂

Author: shellophobia

just passing time here.. and enjoying

Leave a comment