# Combined Mentions with ActionText (Part 1)

*Published on October 11, 2022*

This past weekend on a side project (RelationKit) I implemented a combined mentions feature to let my users do multiple quick actions while writing text within Trix (ActionText) and the Twitter world thought it&#39;d be a great idea to write a blog post... so here I am 😄.

# Setting the scene / What are we building?

We&#39;ll build roughly the following:  

![](https://afomera.dev/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBKdz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--37280586a2a8fb4b4a345ec4168dd821ddb0736e/CleanShot-202022-10-11-20at-2007.25.13.gif)

# Getting started

So let&#39;s get started with a brand new rails application, and spin up a new app using Esbuild. It may be possible to do this with import maps, but those aren&#39;t my cup of tea so I skip straight to ESBuild.

```
bin/rails new combined-mentions -j esbuild
```

Then let&#39;s install our dependencies we need

```
cd combined-mentions
bin/rails action_text:install
yarn add tributejs
```

Now let&#39;s scaffold out three models we&#39;ll use throughout this post, an Article model, a Post model and lastly a SavedReply model.

```
bin/rails g scaffold Post title body:rich_text
bin/rails g scaffold Article title body:rich_text
bin/rails g scaffold SavedReply title body:rich_text
```

Then we need to run migrations

```
bin/rails db:migrate
```

  
There&#39;s one last dependency we&#39;ll want to install if we want Emoji&#39;s in our application (and like, who doesn&#39;t? they&#39;re fun!). We&#39;ll be using a forked version of GitHub&#39;s gemoji project.   
  
Why a forked version? I&#39;m glad you asked, because I like the latest emojis and the gem hasn&#39;t been updated in two years. Feel free to use the original if you&#39;d like, the code we use will work either way.  
  
Original repo: [https://github.com/github/gemoji](https://github.com/github/gemoji)  
  
In your Gemfile:

```
gem &quot;gemoji&quot;, github: &quot;afomera/gemoji&quot;, branch: &quot;emoji-15&quot;
```

Then bundle your app to get the new gem

```
bundle install
```

The last two things I&#39;d like us to do is add some links to the application.html.erb layout so we can navigate between our three models.  
Add the following wherever you&#39;d like

```
&lt;%= link_to &quot;Articles&quot;, articles_path %&gt; | &lt;%= link_to &quot;Posts&quot;, posts_path %&gt; | &lt;%= link_to &quot;Saved Replies&quot;, saved_replies_path %&gt;
```

  
Then we want to update our routes.rb file to add a root route to posts#index.&amp;nbsp; Your routes should look similar to this when you&#39;re ready for the next step.

```
Rails.application.routes.draw do
  root to: &quot;posts#index&quot;

  resources :articles
  resources :posts
  resources :saved_replies
end
```

Now we can start our Rails server and get to the fun part of the app.

```
bin/dev
```

  

# The Stimulus Controller

Let&#39;s generate a Stimulus controller called... combined-mentions. We&#39;ll apply one target value to it called &quot;field&quot;.

```
bin/rails g stimulus combined-mentions
```

Now let&#39;s update our stimulus controller on connect to initialize a new Tribute instance and on disconnect, we&#39;ll detach and cleanup after ourselves.

```
import { Controller } from &quot;@hotwired/stimulus&quot;
import Tribute from &quot;tributejs&quot;
import Trix from &quot;trix&quot;

// Connects to data-controller=&quot;combined-mentions&quot;
export default class extends Controller {
  static targets = [&quot;field&quot;]

  connect() {
    this.editor = this.fieldTarget.editor
    this.initializeTribute()
  }

  initializeTribute() {
    this.tribute = new Tribute({
      // More code to come
    });

    // More code to come
  }

  disconnect() {
    this.tribute.detach(this.fieldTarget)
  }
}
```

Then let&#39;s go to the posts/\_form.html.erb partial and tell our app to use the new Stimulus controller

```
&lt;%= form.label :body, style: &quot;display: block&quot; %&gt;
&lt;%= form.rich_text_area :body, data: { controller: &quot;combined-mentions&quot;, combined_mentions_target: &quot;field&quot; } %&gt;
```

Wherever we&#39;d like to use the combined mentions we just need to add the data-controller and data-combined-mentions-target attributes and Stimulus will take care of the rest for us.  
Let&#39;s start off by hardcoding some static values and getting Tribute rendering for us on the form.  
Inside initializeTribute let&#39;s update the code inside the following hardcoded values

```
this.tribute = new Tribute({
      collection: [
         // Article mentions, but perhaps this is a user mention for your app.
         {
           trigger: &quot;@&quot;,
           allowSpaces: true,
           lookup: &quot;key&quot;,
           menuShowMinLength: 1,
           menuItemLength: 10,
           values: [
             { key: &quot;John Doe&quot;, value: &quot;johndoe&quot; },
             { key: &quot;Jane Doe&quot;, value: &quot;janedoe&quot; }
           ]
         },
         // Saved replies
         {
           trigger: &quot;!&quot;,
           allowSpaces: true,
           lookup: &quot;key&quot;,
           menuShowMinLength: 1,
           menuItemLength: 10,
           values: [
             { key: &quot;Alex Smith&quot;, value: &quot;alexsmith&quot; },
             { key: &quot;John Smith&quot;, value: &quot;johnsmit&quot; }
           ]
         }
       ]
    });
    
    this.tribute.attach(this.fieldTarget)
    // more code to come below this
```

  
This sets up two different &#39;collections&#39; in tribute, one we&#39;ll use for Articles (@) and one we&#39;ll use for saved replies (!). Right now they look like they have pretty similar data, but we&#39;ll change that later.  
  
Go ahead and try it out! In your Posts editor try trying @John and you&#39;ll see it... works kind of, but it doesn&#39;t look great. We&#39;re missing the css for Tribute, we should go ahead and add the following to application.css now.

```
/* Tribute styles */
 .tribute-container {
   border-radius: 4px;
   z-index: 100;
   background-color: #FFFFFF;
   border: 1px solid rgba(0, 0, 0, 0.1);
   box-shadow: 0 0 4px rgba(0, 0, 0, 0.1), 0 5px 20px rgba(0, 0, 0, 0.05);
 }

 .tribute-container ul {
   list-style: none;
   margin: 0;
   padding: 0;
 }

 .tribute-container li {
   background: #fff;
   padding: 0.2em 1em;
   min-width: 15em;
   max-width: 100%;
 }

 .tribute-container .highlight {
   background-color: #0ea5e9;
   color: #fff;
 }

 .tribute-container .highlight span {
   font-weight: bold;
 }
```

Now trying the app again we should see better styling  

![](https://afomera.dev/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBLQT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--3b2fa7c92f0a4d96ce4a4bc5595b72a56ef8fc01/CleanShot%202022-10-11%20at%2021.01.08@2x.png)
  
Now selecting one by pressing enter won&#39;t do anything but paste in the value, which isn&#39;t quite what we want.  
  
Let&#39;s quickly update the code to finish off the basic functionality. Under this.tribute.attach add the following code replacing the more to come comment.

```
this.fieldTarget.addEventListener(&quot;tribute-replaced&quot;, this.replaced)
    this.tribute.range.pasteHtml = this._pasteHtml.bind(this)
```

One thing to note is that TributeJS emits a custom tribute-replaced event which we&#39;re making use of to call our replaced event.   
  
Next we need to go define the two functions we&#39;re calling _replaced_ and **\_pasteHtml**

```
replaced(event) {
     let mention = event.detail.item.original

     this.editor.insertHTML(`[${mention.key}](/users/%24%7Bmention.value%7D)`)
   }

   _pasteHtml(html, startPos, endPos) {
     let range = this.editor.getSelectedRange()
     let position = range[0]
     let length = endPos - startPos

     this.editor.setSelectedRange([position - length, position])
     this.editor.deleteInDirection(&quot;backward&quot;)
   }
```

The replaced function tells TributeJS how to use the selected mention element and what to do with it (in this case, telling Trix to insert the HTML a tag). The \_pasteHtml function replaces what was used to trigger tribute with the selected value (ie you typing @john gets removed and replaced with the replaced function&#39;s return value).  
  
  

# Recap

So far in this part, we&#39;ve setup our basic application and models, installed all our dependencies and have a basic demonstration of how TributeJS works. In the next part we&#39;ll implement saved replies and emojis.  
  
[**Part 2 can be found here**](https://afomera.dev/posts/2022-10-12-combined-mentions-part-two) **.**  
  
Thanks for reading this far!

---

By [Andrea Fomera](https://afomera.dev) | [View original post](https://afomera.dev/posts/2022-10-11-combined-mentions-part-one)
