Hey CF Users,
Multi Select lookup lightning component is easy to create and is re-usable.
Lightning comes with many pre-built components that are easy to use but still many components are missing by date. One of them is the multi-select lookup component. We will create this within a single component without using any child component or event. You can find the complete source code in this post.
Multi Select Lookup component Features:
- Single component
- Will work on any object
- Spinner to show processing
- Ability to set sObject Icon
- Label & placeholder functionality
- Ability to show error in the dropdown if any occurred.
- Pills of selected records will be shown
- You can pre-populate selected records t passing their Ids
- Search functionality to filter results
- You can set a number of records to show at once. Default is 5
Spinner
A Spinner will be shown when you will enter a keyword in the search box until the search results are fetched and shown.
Demo GIF:
Step-1: Create an Apex controller
Our apex controller will fetch records on passing object and field name to it. Here we are using a wrapper class which will return records in the form of value and label.
Create MultiSelectLookupController.cls Apex class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
public with sharing class MultiSelectLookupController { @AuraEnabled public static List<RecordsData> fetchRecords(String objectName, String filterField, String searchString, String values) { try { List<RecordsData> recordsDataList = new List<RecordsData>(); List<String> selectedvalues = (List<String>) JSON.deserialize(values, List<String>.class); String query = 'SELECT Id, ' + filterField + ' FROM '+objectName; if(selectedvalues != null && selectedvalues.size() > 0) { query += ' WHERE Id IN: selectedvalues LIMIT 49999'; } else { query += ' WHERE '+filterField+ ' LIKE ' + '\'' + String.escapeSingleQuotes(searchString.trim()) + '%\' LIMIT 49999'; } for(SObject s : Database.query(query)) { recordsDataList.add( new RecordsData((String)s.get(filterField), (String)s.get('id')) ); } return recordsDataList; } catch (Exception err) { if ( String.isNotBlank( err.getMessage() ) && err.getMessage().contains( 'error:' ) ) { throw new AuraHandledException(err.getMessage().split('error:')[1].split(':')[0] + '.'); } else { throw new AuraHandledException(err.getMessage()); } } } public class RecordsData { @AuraEnabled public String label; @AuraEnabled public String value; public RecordsData(String label, String value) { this.label = label; this.value = value; } } } |
Step-2: Now we will create the Multi Select Lookup lightning component.
For creating a new lightning component navigate to Developer Console > File > New > Lightning Component
Enter Name & Description and click Submit. Now component markup file will open y default. In the right sidebar, click on Controller, Helper, and Style to create those files too.
Now since all the files are created, we will write markup for our component I have used standard (Salesforce Lightning Design System) SLDS here for styling.
Markup for MultiSelectLookup.cmp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
<!-- Code by CafeForce || www.cafeforce.com || support@cafeforce.com || Mandatory Header --> <aura:component controller="MultiSelectLookupController"> <!-- Attributes that can be set while component calling--> <aura:attribute name="objectName" type="string" default="" required="true" /> <aura:attribute name="fieldName" type="string" default="" required="true" /> <aura:attribute name="selectedRecords" type="List" default="[]" description="selected Records Id Array" /> <aura:attribute name="recordCount" type="string" default="5" description="Records visible at once"/> <aura:attribute name="label" type="string" default="" description="Label will be displayed above input Box" /> <aura:attribute name="placeholder" type="string" default="Search..." description="placeholder for input Box" /> <aura:attribute name="disabled" type="Boolean" default="false" description="To disable the combobox"/> <aura:attribute name="iconName" type="string" default="standard:drafts" description="complete icon name eg. standard:account" /> <!-- Internal Use Attributes --> <aura:handler name="init" value="{!this}" action="{!c.doInit}"/> <aura:attribute name="message" type="String" access="private" default="" /> <aura:attribute name="searchString" type="string" access="private" default="" description="String to search"/> <aura:attribute name="recordsList" type="List" access="private" description="List of Records having data" /> <aura:attribute name="selectedDataObj" type="List" access="private" description="Selected Records Object List" /> <!-- Component Markup --> <div> <p>{!if(!empty(v.label), v.label, '')}</p> <div class="slds-combobox_container"> <div class="slds-combobox slds-dropdown-trigger slds-dropdown-trigger_click slds-is-open" aura:id="resultsDiv" aria-expanded="true" aria-haspopup="listbox" role="combobox"> <div class="slds-input-has-icon slds-input-has-icon_right"> <!-- Icon, Search Bar, Search Icon --> <lightning:icon class="slds-combobox__input-entity-icon" iconName="{!v.iconName}" size="large" alternativeText="icon"/> <lightning:input aura:id="inputLookup" class="inputBox" placeholder="{!v.placeholder}" onblur="{!c.blurEvent}" onclick="{!c.showRecords}" disabled="{!v.disabled}" onkeyup="{!c.searchRecords}" value="{!v.searchString}" autoComplete="off" variant="label-hidden" id="combobox-id-1" /> <lightning:icon class="slds-input__icon" iconName="utility:search" size="x-small" alternativeText="search"/> <lightning:spinner class="slds-hide" aura:id="Spinner" alternativeText="Loading" size="small" variant="brand"/> </div> <!-- Dropdown List --> <div id="listbox-id-1" class="slds-dropdown slds-dropdown_length-5 slds-dropdown_fluid" style="{! 'max-height:' + (8 + (v.recordCount * 40)) + 'px' }"> <ul class="slds-listbox slds-listbox_vertical recordListBox"> <aura:if isTrue="{!empty(v.message)}" > <!-- To display Drop down List --> <aura:iteration items="{!v.recordsList}" var="rec" > <li id="{!rec.value}" class="slds-listbox__item eachItem" onmousedown="{!c.selectItem}"> <lightning:icon class="{!if(rec.isSelected,'','slds-hide')}" iconName="utility:check" size="x-small" alternativeText="icon" /> <span class="verticalAlign slds-truncate">{!rec.label}</span> </li> </aura:iteration> <!-- To display Error Message --> <aura:set attribute="else"> <span class="slds-media slds-listbox__option_entity">{!v.message}</span> </aura:set> </aura:if> </ul> </div> </div> </div> <aura:iteration items="{!v.selectedDataObj}" var="data"> <lightning:pill class="slds-m-around_xx-small" name="{!data.value}" label="{!data.label}" onremove="{!c.removePill}"/> </aura:iteration> </div> </aura:component> <!-- Code by CafeForce Website: http://www.cafeforce.com DO NOT REMOVE THIS HEADER/FOOTER FOR FREE CODE USAGE --> |
Now open the controller file MultiSelectLookupController.js and paste the below code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
({ // To prepopulate the seleted value pill if value attribute is filled doInit : function( component, event, helper ) { $A.util.toggleClass(component.find('resultsDiv'),'slds-is-open'); if( !$A.util.isEmpty(component.get('v.selectedRecords')) ) { helper.searchRecordsHelper(component, event, helper, component.get('v.selectedRecords')); } }, // When a keyword is entered in search box searchRecords : function( component, event, helper ) { if( !$A.util.isEmpty(component.get('v.searchString')) ) { helper.searchRecordsHelper(component, event, helper, []); } else { $A.util.removeClass(component.find('resultsDiv'),'slds-is-open'); } }, // When an item is selected selectItem : function( component, event, helper ) { if(!$A.util.isEmpty(event.currentTarget.id)) { var recordsList = component.get('v.recordsList'); var selectedRecords = component.get('v.selectedRecords') || []; var selectedDataObj = component.get('v.selectedDataObj') || []; var index = recordsList.findIndex(x => x.value === event.currentTarget.id) if(index != -1) { recordsList[index].isSelected = recordsList[index].isSelected === true ? false : true; if(selectedRecords.includes(recordsList[index].value)) { selectedRecords.splice(selectedRecords.indexOf(recordsList[index].value), 1); var ind = selectedDataObj.findIndex(x => x.value === event.currentTarget.id) if(ind != -1) {selectedDataObj.splice(ind, 1)} } else { selectedRecords.push(recordsList[index].value); selectedDataObj.push(recordsList[index]); } } component.set('v.recordsList', recordsList); component.set('v.selectedRecords', selectedRecords); component.set('v.selectedDataObj', selectedDataObj); } $A.util.removeClass(component.find('resultsDiv'),'slds-is-open'); }, removePill : function( component, event, helper ){ var recordId = event.getSource().get('v.name'); var recordsList = component.get('v.recordsList'); var selectedRecords = component.get('v.selectedRecords'); var selectedDataObj = component.get('v.selectedDataObj'); selectedRecords.splice(selectedRecords.indexOf(recordId), 1); var index = selectedDataObj.findIndex(x => x.value === recordId) if(index != -1) { selectedDataObj.splice(index, 1) } var ind = recordsList.findIndex(x => x.value === recordId) if(ind != -1) { recordsList[ind].isSelected = false; } component.set('v.recordsList', recordsList); component.set('v.selectedDataObj', selectedDataObj); component.set('v.selectedRecords', selectedRecords); }, showRecords : function( component, event, helper ){ var disabled = component.get('v.disabled'); if(!disabled && !$A.util.isEmpty(component.get('v.recordsList')) && !$A.util.isEmpty(component.get('v.searchString'))) { $A.util.addClass(component.find('resultsDiv'),'slds-is-open'); } }, // To close the dropdown if clicked outside the inputbox. blurEvent : function( component, event, helper ){ $A.util.removeClass(component.find('resultsDiv'),'slds-is-open'); }, }) /* Code by CafeForce Website: http://www.cafeforce.com DO NOT REMOVE THIS HEADER/FOOTER FOR FREE CODE USAGE */ |
Now open the helper file MultiSelectLookupHelper.js and paste the below code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
({ searchRecordsHelper : function(component, event, helper, selectedRecords) { $A.util.removeClass(component.find("Spinner"), "slds-hide"); component.set('v.message', ''); component.set('v.recordsList', []); var searchString = component.get('v.searchString'); var action = component.get('c.fetchRecords'); action.setParams({ 'objectName' : component.get('v.objectName'), 'filterField' : component.get('v.fieldName'), 'searchString' : searchString, 'values' : JSON.stringify(selectedRecords) }); action.setCallback(this,function(response){ var result = response.getReturnValue(); if(response.getState() === 'SUCCESS') { if(result.length > 0) { // To check if value attribute is prepopulated or not if( $A.util.isEmpty(selectedRecords) ) { var selectedRcrds = component.get('v.selectedRecords') || []; for(var i = 0; i < result.length; i++) { if(selectedRcrds.includes(result[i].value)) result[i].isSelected = true; } component.set('v.recordsList', result); } else { component.set('v.selectedDataObj', result); } } else { component.set('v.message', "No Records Found for '" + searchString + '"'); } } else { // If server throws any error var errors = response.getError(); if (errors && errors[0] && errors[0].message) { component.set('v.message', errors[0].message); } } // To open the drop down list of records if( $A.util.isEmpty(selectedRecords) ) $A.util.addClass(component.find('resultsDiv'),'slds-is-open'); $A.util.addClass(component.find("Spinner"), "slds-hide"); }); $A.enqueueAction(action); } }) /* Code by CafeForce Website: http://www.cafeforce.com DO NOT REMOVE THIS HEADER/FOOTER FOR FREE CODE USAGE */ |
Here helper calls the apex class method and fetches record data from there. Now finally paste the CSS in MultiSelectLookup.css file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
.THIS .verticalAlign { cursor: pointer; margin: 6px 0px 0px 6px!important; } .THIS .recordListBox { margin-top:0px !important; overflow-y: scroll; } .THIS .inputBox input { padding-left: 30px; } .THIS .eachItem { padding: 10px; } .THIS .eachItem:hover { background-color: #EEE; cursor: pointer; } /* For Scrolling */ .THIS ::-webkit-scrollbar { width: 7px; height: 7px; } .THIS ::-webkit-scrollbar-track { display: none !important; } .THIS ::-webkit-scrollbar-thumb { border-radius: 10px; background: rgba(0,0,0,0.4); } |
Step-3: Now we will call the component.
1 2 3 4 |
<!-- Code by CafeForce || www.cafeforce.com || support@cafeforce.com --> <c:MultiSelectLookup objectName="Account" fieldName="Name" label="Account Name" iconName="standard:account" placeholder="Enter Value"/> |
Finally, our component is ready to use. Now while calling this component, you need to pass the objectName and fieldName to this custom component. You can also set additional attributes.
Lightning Icon
You can set any standard icon that lightning provides. To see the complete list of lightning icons Click Here.
Here you go, the easy and optimized multi-select lookup component is ready to use.
Also Check:
For any queries or suggestions regarding this post, comment below:
Cheers … 🙂
Hi there, I’m a Lightning Component newbie and am not sure where in salesforce I should be putting the code to call the component. I’ve inputted all of the other sections but am unsure of where I’d input this code. Thank you for your helpful guide!
Hi Kate,
You need to put this code on the screen where you want your lookup to appear.
-> If you want to put this lookup in another component, just put this line in your parent component.
-> If you want this to open in a lightning Application, put this line in your App.
-> If you want this to open on your record detail, then put implements=”flexipage:availableForRecordHome” in the first line of your cmp file. Then go to the record’s lightning page and drop your component from the left pane.
I’m a business user admin and would love to be able to use something like this for as simple of an application as selecting multiple users to send an email to using an email action in a flow. I’ve poked around to try to add this component to be available within a flow, but it does not seem to be working, and as a non-developer without the bandwidth to focus on it, I’m not sure what next steps are. Have you considered tweaking this and publishing this out for use in screen flows? That would be a killer use case – why this is not a standard component is beyond me.
Hi David,
You can use this on flow screen as well. Just put this line implements=”lightning:availableForFlowScreens” on top of cmp file like this:
and this component will be visible in screen section of Flow.
Do you have a version of this specific component using Lightning Web Components instead of Aura?
Hi Dylan,
No, not this specific one in LWc. I will soon create and upload one.
How to save the values in the database?
In the object, I created one multi select picklist to store the values, but not able to save. Can you please help.
Hi Chaitanya,
If you want to use this for multi select picklist, please consider using Multi Select Combobox
For database saving, you need to send selected values from component to Apex.
Hi Suyash, thanks for this post it’s really helpful.
I have a question link to Chaitanya’s.
I use your component in another but I want to send selected records to apex class in my parent component but I don’t know how to do it. I can’t add a list attribute and value to your component. Could you help me please ?
Hi Thomas,
The easiest way to do this is you can fire an event from the JS controller after line 40. Fire an event with your selected data and handle that event in your parent component.
Hi Suyash,
thanks for sharing such reusable custom component which can be easily implemented in any org. But I got some minor issues.
1.Unable to scroll through the listed search suggestions, the suggestions disappear when user tries to scroll through the suggestions
2. Unable to clear all pills at a time when user clicks on Clear Fields button. X-icon is remaining.
3. Search suggestions list is getting displayed properly, but unable to make selections using up/down arrow key on keyboard (however selection though mouse cursor is working).
If you could help me to fix the same, that would be really appreciated.
Hi buddy,
Thanks for the appreciation.
Here is the resolution for your comments.
1) That may be because of the mouse event. You can restrict the size of the fetched records or increase the dropdown height by overriding the class.
2) Try to console the list in the removePill method and see if data is getting cleared from the list.
3) Arrow keys are not supported now in the code. You have to write the logic for that.
Hi Suyash, Thanks for sharing the above code.
I am able to display multiple records through this multiselect lookup component. But not able to save the multiple records in another object where I have implemented the component. ( Ex: There are 2 objects Obj A and Obj B, I have displayed records of B(look up component) in A) Please could you share the code to save the multiple records selected from multiselect_lookup when the Obj A is saved.
Hi Prerana,
You can also use selectedRecords attribute to get the selected records Ids. Iterate through Ids and create records.
I have the same question as Kate did a year ago.
<c:MultiSelectLookup objectName=”Account” fieldName=”Name” label=”Account Name” iconName=”standard:account” placeholder=”Enter Value”/>
wherein salesforce should I be putting the code to call the component? I’ve inputted all of the other sections but am unsure of where I’d input this code.
Can you share a screenshot of where it would go?
I have included a screenshot of where I have it placed and the error I’m getting when looking up records
error when searching “Unexpected token: “From”
Hi Richard,
You can put this component wherever you want on the Home page or record page or inside any other component. Regarding the error that you are getting, please add some debug logs in Apex to see what issue is coming.
@Suyash
Thank you for the reply, I was able to get over the error and search for records. However, I do have another set of questions for you, hoping you can help.
Thanks for building re-usable Multi-select lookup functionality. This logic helped implementing similar functionality in one of my project. But I have difficulity with Input box size retain same when screen size is Zoom in or out. Actually I am calling MultiSelectLook component from parnet component. Please suggest how to align MultiSelectLook component input box size should be same as parent component fields
Hi Harikishore,
For lightning, you can override the input CSS. lightning-input creates a standard input tag in DOM. You can override that input CSS and provide different height-width based on your device width inside media queries in the CSS file.
I wish it could also give us an option to create a record which we don’t find by the search. Can you direct me to a way/resource to incorporate this in my solution?
Hi Rishabh
You can add a button outside the component and use Navigation mixin plugin to open a New Record modal.
I wanted to show the selected contacts inside the search bar not in under the lightning:input is that posible if how? Thanks
Hi Ghulam,
You need to modify the code for this. Instead of Input, you need to show a string will all selected values and give it a border so it looks like an input.