Lightning Pills Input Component

In this post we’ll create a custom Lightning component which breaks down the input and utilizes Salesforce lightning:pill components to display the results. You can see a sample of this component in the image below:

pills_input

The component will also be able to validate the provided input upon entering, and will prepare it for saving to database. So, let’s get to it.

Getting started

NOTE: Updated source code is provided in the following GitHub repo

First of all, the implementation will use some utility code for generating random GUID and validating the input pattern with regular expressions. This code is stored as a static resource and you’ll need to upload it to your org for this implementation to work. You can find the static resource here.

We’ll call this custom component pillsInput, so go ahead create it and set the following markup and code in their respective component files:

pillsInput.cmp

<aura:component >
    <ltng:require scripts="{!join(',', $Resource.LEX + '/ms.js', $Resource.LEX + '/ms-regex.js')}" afterScriptsLoaded="{!c.scriptsLoaded}"/>

    <aura:attribute name="label" type="String" required="true"/>
    <aura:attribute name="placeholderText" type="String" />
    <aura:attribute name="name" type="String" required="true"/>
    <aura:attribute name="value" type="String" required="true"/>
    <aura:attribute name="delimiter" type="String" default=" "
        description="Character used to split the input"/>
    <aura:attribute name="delimiterInDatabase" type="String" default=";"
        description="Character used to split the input for database storage"/>
    <aura:attribute name="validationTypes" type="String" default=""
        description="Possible values (for multiple add them as comma separated): emailAddress, ipAddress, weburl, positiveDecimalNumberWithTwoDecimals, positiveInteger, nonEmptyString"/>

    <aura:attribute name="pills" type="List" default="[]" access="private"/>
    <aura:attribute name="scriptsLoaded" type="Boolean" default="false" access="private"/>
    <aura:attribute name="valueDataLoaded" type="Boolean" default="false" access="private"/>

    <aura:handler name="init" value="{! this }" action="{! c.init }"/>
    <aura:handler name="change" value="{!v.pills}" action="{!c.handlePillsChanged}"/>
    <aura:handler name="change" value="{!v.value}" action="{!c.handleValueChanged}"/>

    <div class="slds-form-element">
        <label class="slds-form-element__label" for="{!v.name}">{!v.label}</label>
        <div class="slds-form-element__control">
            <input aura:id="inputText" type="text" id="{!v.name}" class="slds-input" placeholder="{!v.placeholderText}" value="" onkeyup="{!c.onKeyUpInput}"/>
            <div id="{!'listbox-selections-' + v.name}" role="listbox" aria-orientation="horizontal">
                <ul class="slds-listbox slds-listbox_inline slds-p-top_xxx-small" role="group" aria-label="Inputted Options:">
                    <aura:iteration items="{!v.pills}" var="pill">
                        <li role="presentation" class="slds-listbox__item">
                            <lightning:pill name="{!pill.id}" label="{!pill.label}"
                                hasError="{!!pill.isValid}" onremove="{!c.onRemovePill}"/>
                        </li>
                    </aura:iteration>
                </ul>
            </div>
        </div>
    </div>

</aura:component>

Let’s go over the attributes. The label, placeholderText, name, and value attributes are pretty much standard for any Salesforce Lightning component used for data input. The more interesting attributes are the following bunch:

  • delimiter - this is a character that is going to be used to split the inputted string into pills. This approach, for example, enables the user to copy/paste a long list of IP Addresses separated by space (or by any other specified delimiter value), which would then be separated into individual pills.
  • delimiterInDatabase - this is a character used to split the pills when they are stored in a text field in database. Consider this as a semi-colon separator used to split the multi-picklist values.
  • validationTypes - here we can specify what type of pattern the input needs to be in. If the pattern isn’t matched, the pills are shown in red.

We have a couple of more private attributes in the component but their function is trivial and self explanatory. Let’s move to the controller part.

pillsInputController.js

({
  scriptsLoaded: function(cmp, event, helper) {
    cmp.set('v.scriptsLoaded', true);
    helper.init(cmp, helper);
  },

  init: function(cmp, event, helper) {
    helper.init(cmp, helper);
  },

  handlePillsChanged: function(cmp, event, helper) {
    helper.parsePillsToField(cmp, helper);
  },

  handleValueChanged: function(cmp, event, helper) {
    if (cmp.get('v.valueDataLoaded') === false && cmp.get('v.scriptsLoaded') === true && cmp.get('v.value') !== null) {
      cmp.set('v.valueDataLoaded', true);
      helper.parseFieldToPills(cmp, helper);
    }
  },

  onKeyUpInput: function(cmp, event, helper) {
    var delimiter = cmp.get('v.delimiter');
    var inputText = cmp.find('inputText').getElement();
    var currentInput = inputText.value;

    if (currentInput[currentInput.length - 1] === delimiter || event.keyCode === 13) {
      helper.addNewPills(cmp, helper, currentInput.split(delimiter));
      inputText.value = '';
    }
  },

  onRemovePill: function(cmp, event, helper) {
    var pillId = event.getSource().get('v.name');
    var pills = cmp.get('v.pills');

    for (var i = 0; i < pills.length; i++) {
      if (pillId === pills[i].id) {
        pills.splice(i, 1);
        break;
      }
    }

    cmp.set('v.pills', pills);
  }
})

The controller is handling various events that occur within the component. The most important handler is the onKeyUpInput. This handler will evaluate if the latest key captured by the pillsInput component is a specified delimiter or an enter key. If one of those is true, then the inputted text will be added as a pill, and the input element will be cleared.

handleValueChanged function is there to parse the initially binded value attribute (e.g. - binded with a sObject field) into pills. After that initial action we need to set the valueDataLoaded flag in order to avoid calling this code when we add new pills. Now let’s see what is in the component’s helper.

pillsInputHelper.js

({
  init: function(cmp, helper) {
    if (cmp.get('v.scriptsLoaded') === true) {
      helper.parseFieldToPills(cmp, helper);
    }
  },

  addNewPills: function(cmp, helper, values) {
    var pills = cmp.get('v.pills');

    for (var i = 0; i < values.length; i++) {
      var trimmedVal = values[i].trim();
      if (trimmedVal !== "") {
        pills.push({
          id: lexUtil.guidGenerator(),
          label: trimmedVal,
          isValid: helper.isInputValid(cmp, helper, trimmedVal)
        });
      }
    }

    cmp.set('v.pills', pills);
  },

  isInputValid: function(cmp, helper, value) {
    return regexUtil.validateInput(cmp.get('v.validationTypes'), value);
  },

  parsePillsToField: function(cmp, helper) {
    var pills = cmp.get('v.pills');
    var delimiterInDatabase = cmp.get('v.delimiterInDatabase');
    var fieldStr = '';

    for (var i = 0; i < pills.length; i++) {
      //NOTE: I think that it's smarter to allow saving to database, but it's debatable. It could be abstracted to component's attribute.

      //if(pills[i].isValid)

      fieldStr += pills[i].label + delimiterInDatabase;
    }

    try {
      cmp.set('v.value', fieldStr);
    } catch (e) {
      //ignore issue that occurs when trying to set unbinded value

    }
  },

  parseFieldToPills: function(cmp, helper) {
    var fieldStr = cmp.get('v.value');
    var delimiterInDatabase = cmp.get('v.delimiterInDatabase');
    var pills = [];
    var splitFieldStr = [];
    if (fieldStr != null) {
      splitFieldStr = fieldStr.split(delimiterInDatabase);
    }

    for (var i = 0; i < splitFieldStr.length; i++) {
      if (splitFieldStr[i] != "") {
        pills.push({
          id: lexUtil.guidGenerator(),
          label: splitFieldStr[i],
          isValid: helper.isInputValid(cmp, helper, splitFieldStr[i])
        });
      }
    }

    cmp.set('v.pills', pills);
  }
})

Inside the helper the functions parseFieldToPills and parsePillsToField are parsing the binded field’s inital value into pills, and then parsing those pills into a string that can be safely stored into database.

isInputValid function is using the regexUtil object to validate the input. This object is stored in the static resources that are provided at the beginning of this blog post.

Finally, the addNewPills function is used to convert the list of strings to pills. This function also utilizes an object from the static resources called lexUtil, which has a function to generate a unique GUID. The reason why I haven’t used a label to identify individual pills is because I wanted to keep the option for users to input pills with same values.

How to use it?

Using the component is rather easy - just paste the below code snippet to your Lightning Component, bind the value attribute to your appropriate object (or leave it blank "" for a quick test), and you are ready to go.

<c:PillsInput label="IP Addresses" name="ipAddresses"
    placeholderText="Input IP Addresses separated by space"
    validationTypes="ipAddress" value="{!v.someObject.someField__c}" />

The end result should look something like this:

pills_input

Possible improvements

There are a couple of possible improvements & optimizations that I can think of right now. The one would be to add a class attribute to provide additional styling to the component. The validation logic could also be improved to optionally prevent users from saving the input if there are invalid pills.

I hope that somebody will find this component useful, and, as always, feel free to leave your comments below.

Written on November 26, 2017