ReactJS: Building Github-style User Mentions

Using react-input-trigger

Abinav Seelan
campvanilla
Published in
9 min readMar 9, 2018

--

Most editors and writing apps, including Medium here, provide some means to reference other users on their platform. A common pattern that a lot of applications use (Github, Slack, Facebook, Twitter, Medium, etc) is suggesting users to the writer whenever the writer starts typing with the at symbol — @.

This article is a fairly detailed guide on building a Github-style user mentions in React, using just a textarea, and react-input-trigger.

Note: If you want to just skip ahead and view the final code, it can be found here.

Setting things up

Let’s quickly bootstrap our application and keep it really simple — A single React component called App with a single textarea.

Since we’re building a Github-style suggestor, we need a way to trigger some functionality whenever we press @ inside our <textarea />. This is where react-input-trigger comes in!

It is an encapsulating component that you can wrap around any input field or textarea, and provide it a trigger character to look for. Whenever the component sees the trigger occur inside the input field or textarea, it fires hooks that you can listen on and build logic around.

So let’s bring this component into our application.

You can install it using npm install react-input-trigger. Once it’s done installing, let’s pull it into our application.

Cool! Running the above should give us a simple textarea. We’re all set to start building our user suggestor!

Adding the trigger

<InputTrigger> takes a prop called trigger where we can define what the component should consider a trigger to start firing its hooks.

The @ key is a two part keyboard character — it has a character code of 50, and it needs the shift key to be pressed at the same time.

Here, we’ve provided <InputTrigger> with an object that tells the component that any keyboard event that has the shift key pressed and has a character code of 50 should be considered as a trigger!

Once we’re done providing <InputTrigger> with the trigger prop, we now have access to a couple of hooks — onStart, onCancel and onType.

The onStart hook

Let’s handle the onStart hook first. This hook fires whenever <InputTrigger> encounters the trigger for the first time. The hook provides us metadata about the cursor position, which we need to know in order to position the suggestor within the textarea.

onStart is a prop that we need to provide to<InputTrigger>, the prop takes a function that it calls whenever the hook fires.

Here, we have specified the onStart prop, and whenever the component encounters the trigger within <textarea /> we log the meta information we receive to the console.

As you can see in the above GIF, the moment we press @, the onStart function is fired and we immediately see the meta information logged to the console! Now we can use this to display our suggestor.

But first … we need to build the suggestor. 😛

This suggestor is going to be an absolutely positioned <div> over the <textarea />. We can use the top and left values in the meta information provided to decide where this suggestor should appear.

Let’s build the markup (and add some styles to it as well, so it looks neat!)

Here, we have the markup for the suggestor (it’s got some inline styles for now, which are just to make it look nice). We’ve also encapsulated our entire application inside a <div> with position: relative so that we can position the suggestor absolutely within it.

The styles that are dynamic and crucial to the suggestor are the display, top and left properties. We need the top and left values to position the suggestor, but we also need to show and hide the suggestor based on whether the user has entered the trigger@ or not. To do this, we’ve set up some state variables to hold the dynamic values — showSuggestor, top and left.

We now need a class method that we can use to update these values when the onStart hook fires. Let’s build that!

We’ve defined a class method called toggleSuggestor that takes the meta information that react-input-trigger gives us via onStart. In this method, we check if metaInformation.hookType is start, and if so, we set the our state variables with the meta information provided.

Let’s see this in action!

It works! 🎉

The onCancel hook

What do we do when we want to remove the suggestor? 🤔

react-input-trigger fires a onCancel hook whenever the user hits backspace and removes the @ that triggered onStart.

onCancel, like onStart, is a prop that you can pass to react-input-trigger. This prop takes a function that is called when the hook is fired. So let’s wire in toggleSuggestor to work with this hook as well!

Here, we call this.showSuggestor whenever onCancel is fired. And inside this.showSuggestor we check if the hookType is cancel. If it is, we just reset our state variables.

Let’s see this in action.

As you can see in ☝️, when we type @ for the first time, showSuggestor gets called and since the hookType is start, it shows the suggestor at that position.

When we start removing text we’ve typed after @, nothing happens. But as soon as the @ is removed, showSuggestor is called again, except this time by onCancel. Since the hookType is cancel, it resets all the state variables and the suggestor goes away!

Adding Users to search through

The main purpose for the suggestor is to suggest users to the writer. As the writer continues typing, we need to filter out suggestions. Let’s add some mock users that we can search and filter through as we type.

We’ve added the users (or rather pokemon 😛) to our state, and we iterate over these users to display them in our suggestor.

The onType Hook

The last hook that react-input-trigger provides us is the onType hook. This hook fires every time something is typed after onStart.

onType is defined the same way as onStart and onCancel — it’s a prop that accepts a function that is called any time the hook is fired. The meta information this time though has information regarding the text typed after the trigger!

Let’s create a class method that will take this text value from the meta information and store it in the state. We can also filter out the users based on this text by using .filter().

Here, we define a class method called handleInput that takes the meta information made available by onType and sets this.state.text with the text value present.

We’ve also made a small modification on line 49. We filter out only those users who have the text typed by the writer present in them.

Let’s see this in action now!

Working to spec! 🤓

Now, the last thing to handle is selecting users from the suggestor.

Selection and Ending a trigger

So here’s the interaction we’ll be building for selection.

  • When the writer presses the down arrow key, the suggestor cycles through the suggestions.
  • When the writer hits enter, the currently selected suggestion in the suggestor is filled in the textarea.

Let’s build the down arrow interaction first.

All it needs is a class method that will hijack the down arrow keypresses. Whenever this key is pressed, let’s just increment a state variable called currentSelection that will go between 0 and this.state.users.length.

Also, let’s style the suggestor so that it looks like we’re selecting users. Let’s just change the style of the user in the suggestor whose index in the array matches the currentSelection value.

Let’s add this class method to the onKeyDown event of the top most <div>.

Here, we’ve defined a class method called handleKeyDown, that checks if the key pressed is the down arrow (character code is 40) and if yes, it increments the this.state.currentSelection value. We also do a % users.length so that this.state.currentSelection can only take values between 0 and total number of users.

And in our .map() we use the index parameter as well and if the user’s index matches this.state.currentSelection we change the background colour of the user so it looks like it’s highlighted.

Now, let’s see how everything looks so far.

Looks good! 🎉

Now let’s handle the selection part. When we hit enter, we need to replace the partially typed out user with the complete username.

We need to update a few things now.

Since we need to replace the textarea’s contents, we need to know from where we need to start the replacement. So we need to make a small modification to onStart to store the text position of the @. This information is available to us in metaInformation.cursor.selectionStart.

We also need to add another check to handleKeyDown to hijack the enter key.

When the enter key is pressed, we need to take the currently selected user and replace the partially typed text with the username. Once we’ve done that, let’s also reset all the state variables and end the trigger by telling react-input-trigger to reset itself.

To end the trigger, react-input-trigger takes a prop called endTrigger. This prop takes a function, except here it provides us with a function that we need to call whenever we want to tell react-input-trigger that we’re done with the current trigger.

Ok. So I know that there’s a lot happening in ☝️so I’ll break it down. 😛

First thing’s first, we need to have access to the text typed in the textarea if we are to modify and replace parts of it with our suggestions, right?

To do this, we have a new class method called handleTextareaInput that we call on onChange of the textarea. We also associate this.state.textareaValue to the textarea via the value property so that when we modify the text via code, it updates inside the textarea as well.

Another update has been made to toggleSuggestor. As mentioned above, we need to know the starting position of the text to be replaced. For that we store the metaInformation.cursor.startSelection value in this.state.startPosition.

The next big update is in handleKeyDown. Here, we check if the key pressed was the enter key (character code 13). If yes, we take the current text inside the textarea, which we have access to in this.state.textareaValue. The currently highlighted user in the suggestor is available to us in this.state.users[this.state.currentSelection].

We generate the new text for the textarea by slicing the start of the current text till the start position of the @, which is available in this.state.startPosition. We then insert our currently selected user into the text and subsequently append the remaining part of the text after it.

Phew! I know it’s a lot to process. Give the code a read, maybe twice, and it’ll all come together. And now on the topic of things coming together, let’s see what we finally have! 🤞

Woot! It’s finally done! 🎉

We’ve built a working prototype of Github’s famed user mentions. If you’ve read this far, kudos to you! 😛 Hope you found this useful and if you want to check out the full source for the above demo, it can be found here.

~Fin~

If you found this helpful, do leave a 👏!

Stuck somewhere, need more help, or just want to say hi? Send me a Direct Question on Hashnode or hit me up on Twitter. You can also find me on Github. 🙃

--

--