How to Build a Publisher Salesforce Lightning Web Component

Through our work for our clients, my team and I have built an entire library of Salesforce Lightning Web Components. Today I’m going to show you how to make a publisher lightning web component. Think of any input where you @mention a person or resource and have that resource linked to and notified of your post.

Today we’ll just concentrate on the mechanics of how to make dynamic input tool that can show a dropdown that corresponds with an @mention entry.

This is much harder to do that you might think. There are several libraries out there that can add this functionality. Unfortunately, you then have third part dependency and in most cases, a pretty large download to support this.

Our Custom Publisher Lightning Web Component

The version that I’m going to demonstrate handles nearly all inputs to a good standard. It is possible to sometimes have the dropdown appear in not quite the right place (normally associated with text wrapping), but I feel that these few instances are not worth adding a library.

Also, I’m not going to be providing all the code you need. This is because I’m not keen to have this valuable source scraped for the next large language model. Please contact me at charmer@soliantconsulting.com for the full source (which you will be restricted from sharing onwards).

If you don’t want to do this, below I’ll provide enough information so that you’ll be able to recreate this yourself with some effort.

How the Component Works

The essence of how this tool works is quite simple:

  1. The user enters text.
  2. The input watches for an @ symbol.
  3. When one is received, waits until at least 3 characters have been entered.
  4. Following this, it performs a search (you can configure the search any way you like of course).
  5. It simultaneously determines x,y co-ords for the location of the dropdown to show results. (This is the difficult bit, because the selection api is not very well developed in browsers).
  6. When results are returned, it displays a dropdown in the correct location which allows the user to make a selection.
  7. When a selection is made, an anchor tag is placed where the cursor is and the search term is removed.

You of course can then perform any other back-end tasks that you feel are appropriate.

Building Your Publisher Lightning Web Component

First the input markup, just a content editable div:

-note `position:relative;` to allow the child dropdown to be placed anywhere within its boundaries:

    <div lwc:if={isIdle} 
         class="slds-text-color_weak slds-publisher__input slds-textarea slds-text-longform " 
         contenteditable="true" 
         role="combobox" 
         style="position: relative;" 
         onfocus={handleOnFocus} >
         Share your thoughts...
    </div>

Next, detection of the @mention symbol:

    //contact charmer@soliantconsulting.com for this
    let {text,rect,abort} = this.getSelectedLine();
    //extract the last @ symbol in the input text
    let {nosearch,symbol,symbolIndex} = this.getSearchSymbol(text);
    this.searchType = symbol === '@' ? 'user':'object';

The function referenced about `getSelectedLine` works (roughly) by:

  • Getting the current selection from the window
  • Getting the bounding rectangle
  • Extending the selection to the lineboundary
  • Extracting the text of the selection
  • Returning the rectangle (with x,y), and the text

Running a Query on the Backend

Once the text has been extracted and we have a location to put our dropdown, we can run a query on the backend. This query can be anything you want, but in our case, we return a custom Apex object `AutoCompleteItem`.

The object has 5 members: type,name,id,icon,extra.

Don’t forget to mark your method as `@AuraEnabled(cacheable=true)`

Our method is designed to return up to 10 items. Because we are querying for more than one object type, we merge the results using a pro-rata technique.

The key part is this loop:

      Integer recordCount = 0;
      Integer loopCount = 0;
      while ( loopCount < 10 && recordCount < 10) {
        if (accounts.size() > loopCount) {
          Account acct = accounts[loopCount];
          matches.add(new AutoCompleteItem('Account',acct.Name, acct.Id));
          recordCount++;
        }
        if (leads.size() > loopCount) {
          Lead ld = leads[loopCount];
          matches.add(new AutoCompleteItem('Lead',ld.Name, ld.Id));
          recordCount++;
        }
        loopCount++;
      }

For example if there are 20 Accounts and 20 Leads that match the search, then there will be 5 of each in the results.

If there are 3 Accounts and 20 Leads, then the result will contain 3 Accounts and 7 Leads.

Rendering Your Results

The results then get rendered using this template:

    <template for:each={matches} for:item="match">
    <li class="slds-listbox__item" onclick={handleOptionSelect} role="presentation" 
        key={match.id} data-id={match.id} data-name={match.name} data-type={match.type} >
      <div class="slds-media slds-listbox__option slds-listbox__option_has-meta" role="option">
        <span class="slds-media__figure slds-listbox__option-icon">
          <lightning-icon size="small" icon-name={match.icon} style=" margin-left: 0.5rem;"></lightning-icon>
        </span>
        <span class="slds-media__body">
          <span class="slds-listbox__option-text slds-listbox__option-text_entity">{match.name}</span>
          <span class="slds-listbox__option-meta slds-listbox__option-meta_entity">{match.type} • {match.extra}</span>
        </span>
      </div>
    </li>
    </template>

When a user clicks a match, then `handleOptionSelect` is called. This method modifies the text entered to this:

    const replacement = `<span data-id="${id}" class="at-mention" contenteditable="false" data-name="${name}" data-type="${type}">@[${name}]</span> `; 

This is done so that on submit, this span can be removed and replaced with a name – and then the type and id can be used to perform the @mention funtionality.

Submit Button 

Here’s the method that handles the submit button click. Note the regex depends on the `data-id` attribute being present:

    handleSubmitClick(){
        const mentions = this.querySelectorAll(".at-mention");
        const editor = this.querySelector('.ql-editor');
        let postText = editor.innerHTML;
        let recordIds = [];
        //loop and extract data from the @mentions
        mentions.forEach(mention => {
          const name = mention.dataset.name;
          const id = mention.dataset.id;
          const search = `<span data-id=\\\"${id}\\\" (.*?)<\\/span>`;
          const reg = new RegExp(search,'igm');
          //const replacement = `<a href="https://yoursite.sandbox.my.site.com/sitename/s/detail/${id}">@${name}</a>`;
          const replacement = `{${id}}`;
          recordIds.push(id);
          postText = postText.replace(reg,replacement);
        });

In this case, since this will be submitted to the chatter API, the anchor tag and be directly posted and rendered. This is running in a community which can’t be used to @mention records; hence the replacement is just the original record id (user id in this case).

Sharing Other Records

If you do want to share OTHER records (any custom or standard object), Soliant has an implementation of both the publisher and the chatter feed that allows this. The chatter feed component is a sophisticated component that can render most chatter feed elements and renders likes and comments. Contact us if you would like to get access to this codebase.

Launching More Customization in Salesforce

My team and I help organizations of all kinds build custom functionality in Salesforce. If you’re looking to further tailor the platform to meet your business needs, we can serve as a consulting and development partner. Contact our team to set up a call.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top