Custom Reusable Lookup in LWC

Salesforce

Hi Developer, As we all know that LWC is a complete new way to develop the Lightning component and there are very limited no of resources to learn LWC.

So, here is this tutorial we are going to learn how we can create a Custom Reusable Lookup based on user input.

Setp1:- Create searchComponent

This component is the one, who enables the search functionality. Use the below code for HTML markup and JavaScript.

<template>
    <div class="slds-grid slds-wrap">
        <div class="slds-col slds-size_4-of-4">
            <div>
                <!-- Create an Input Field where user can enter text to find the
                Records-->
                <lightning-input variant="label-hidden" 
                    label="Search Record" value={searchKey} type="search"
                    onchange={handleChange} placeholder="type text here">
                </lightning-input>
            </div>
        </div>
    </div>
</template>

searchComponent.js

import { LightningElement, track } from 'lwc';

export default class SearchComponent extends LightningElement {
    
    @track searchKey;
    handleChange(event){
        /* eslint-disable no-console */
        //console.log('Search Event Started ');
        const searchKey = event.target.value;
        /* eslint-disable no-console */
        event.preventDefault();
        const searchEvent = new CustomEvent(
            'change', 
            { 
                detail : searchKey
            }
        );
        this.dispatchEvent(searchEvent);
    }
}

Step2: – Create recordList component

In this component, we will display the list of all the records which will be returned matching with the entered text. Here is the code for HTML markup & JavaScript Class.

<template>
    <!--<a href="JavaScript:Void(0)" style="text-decoration:none;" onclick={handleSelect}>
        <lightning-layout>
            <lightning-layout-item>
                <lightning-icon icon-name={iconname} size="small"></lightning-icon>
                Β Β 
            </lightning-layout-item>
            <lightning-layout-item>
                {record.Name}
            </lightning-layout-item>
        </lightning-layout>
    </a> -->
    <div >
        <div class="slds-grid slds-wrap 
                        slds-dropdown_length-with-icon-7 
                        slds-dropdown_fluid
                        slds-p-left_small"
                 >
                <div class="slds-col slds-size_4-of-4 ">
                    <ul class="slds-listbox slds-listbox_vertical" role="presentation">
                        <li role="presentation" class="slds-listbox__item">
                            <div class="slds-media slds-listbox__option 
                                                        slds-listbox__option_entity 
                                                        slds-listbox__option_has-meta" 
                                                        role="option"
                                onclick={handleSelect}>
                                <span class="slds-media__figure slds-listbox__option-icon">
                                    <lightning-icon icon-name={iconname} size="small"></lightning-icon>
                                </span>
                                <span class="slds-media__body" 
                                    style="padding-top: 9px;font-weight: 600;">
                                    <span class="slds-listbox__option-text 
                                                                 slds-listbox__option-text_entity">
                                        {record.Name}
                                    </span>
                                </span>
                            </div>
                        </li>
                    </ul>
                </div>
        </div>
    </div>
</template>

recordList.js

import { LightningElement, api } from 'lwc';

export default class RecordList extends LightningElement {
    @api record;
    @api fieldname;
    @api iconname;

    handleSelect(event){
        event.preventDefault();
        const selectedRecord = new CustomEvent(
            "select",
            {
                detail : this.record.Id
            }
        );
        /* eslint-disable no-console */
        //console.log( this.record.Id);
        /* fire the event to be handled on the Parent Component */
        this.dispatchEvent(selectedRecord);
    }
}

Step3: – Create customLookup Component

Main component which will contain both the component searchComponent and recordList component and also, responsible for calling the apex method.

<template>
    <template if:false={selectedRecord}>
        <div class="slds-p-around_x-small">
            <c-search-component 
                onchange={handleOnchange}>
            </c-search-component>
        </div>
    </template>
    <div >
        <template if:true={error}>
            <template if:true={error.details}>
                <template if:true={error.details.body}>
                    {error.details.body.message}
                </template>
            </template>
        </template>
    </div>
    <div>
        <template if:false={selectedRecord}>
            <template if:true={records}>
                <template for:each={records} for:item="record">
                    <c-record-list key={record.Id} record={record} 
                        onselect={handleSelect} iconname={iconname}
                        fieldname={searchfield}>
                    </c-record-list>
                </template>
            </template>
        </template>
        <template if:false={selectedRecord}>
        </template>
    </div>
    <div class="slds-p-around_x-small">
        <template if:true={selectedRecord}>
            <div class="slds-combobox__form-element slds-input-has-icon 
                            slds-input-has-icon_left-right" role="none">
                    <span class="slds-icon_container
                                slds-icon-standard-account 
                                slds-combobox__input-entity-icon" title="Account">
                        <lightning-icon icon-name={iconname} ></lightning-icon>
                    </span>
                    <input class="slds-input slds-combobox__input
                           slds-combobox__input-value" 
                           id="combobox-id-5" aria-controls="listbox-id-5" 
                           autocomplete="off" role="textbox" type="text" 
                           placeholder="Select an Option" readonly=""
                           value={selectedRecord.Name}
                           disabled
                           />
                    <button class="sicon_container slds-button slds-button_icon 
                                   slds-input__icon slds-input__icon_right" 
                            title="Remove selected option"
                            onclick={handleRemove}>
                        <lightning-icon icon-name="utility:close" size="small">

                        </lightning-icon>
                        <span class="slds-assistive-text">Remove selected option</span>
                    </button>
                </div>
            <!--<lightning-layout>
                <lightning-layout-item>
                    <lightning-pill label={selectedRecord.Name} onremove={handleRemove}>
                        <lightning-icon icon-name={iconname}></lightning-icon>
                    </lightning-pill>
                </lightning-layout-item>
            </lightning-layout> -->
        </template>
    </div>
</template>

customLookup.js

import { LightningElement, track, api } from 'lwc';
import findRecords from '@salesforce/apex/CustomLookupController.findRecords';
export default class CustomLookup extends LightningElement {
    @track records;
    @track error;
    @track selectedRecord;
    @api index;
    @api relationshipfield;
    @api iconname = "standard:account";
    @api objectName = 'Account';
    @api searchfield = 'Name';

    /*constructor(){
        super();
        this.iconname = "standard:account";
        this.objectName = 'Account';
        this.searchField = 'Name';
    }*/

    handleOnchange(event){
        //event.preventDefault();
        const searchKey = event.detail.value;
        //this.records = null;
        /* eslint-disable no-console */
        //console.log(searchKey);

        /* Call the Salesforce Apex class method to find the Records */
        findRecords({
            searchKey : searchKey, 
            objectName : this.objectName, 
            searchField : this.searchfield
        })
        .then(result => {
            this.records = result;
            for(let i=0; i < this.records.length; i++){
                const rec = this.records[i];
                this.records[i].Name = rec[this.searchfield];
            }
            this.error = undefined;
            //console.log(' records ', this.records);
        })
        .catch(error => {
            this.error = error;
            this.records = undefined;
        });
    }
    handleSelect(event){
        const selectedRecordId = event.detail;
        /* eslint-disable no-console*/
        this.selectedRecord = this.records.find( record => record.Id === selectedRecordId);
        /* fire the event with the value of RecordId for the Selected RecordId */
        const selectedRecordEvent = new CustomEvent(
            "selectedrec",
            {
                //detail : selectedRecordId
                detail : { recordId : selectedRecordId, index : this.index, relationshipfield : this.relationshipfield}
            }
        );
        this.dispatchEvent(selectedRecordEvent);
    }

    handleRemove(event){
        event.preventDefault();
        this.selectedRecord = undefined;
        this.records = undefined;
        this.error = undefined;
        /* fire the event with the value of undefined for the Selected RecordId */
        const selectedRecordEvent = new CustomEvent(
            "selectedrec",
            {
                detail : { recordId : undefined, index : this.index, relationshipfield : this.relationshipfield}
            }
        );
        this.dispatchEvent(selectedRecordEvent);
    }


}

Step4:- Create CustomLookupController Apex class

public with sharing class CustomLookupController {
    public CustomLookupController() {

    }
    @AuraEnabled(cacheable = true)
    public static List<SObject> findRecords(String searchKey, String objectName, String searchField){
        String key = '%' + searchKey + '%';
        String QUERY = 'Select Id, '+searchField+' From '+objectName +' Where '+searchField +' LIKE :key';
        System.debug(System.LoggingLevel.DEBUG, QUERY);
        List<SObject> sObjectList = Database.query(QUERY);
        return sObjectList;
    }
}

Step5 :- Create Demo App and test lookup

<aura:application extends="force:slds">
    <c:customLookup iconname="standard:case" 
                    objectName="Case"
                    searchfield="CaseNumber"/>
</aura:application>	

If you have any Queries or Suggestions DM me @cloudyamit or email me @ [email protected]

Happy Learning  πŸ™‚ πŸ˜‰

#SFDCPanther #Trailblazer

22 comments

    • For this you need to add one more action to the List say it New Record and once use clicks on that open a pop for creating a new record and after saving the record get the Record Id, dispatch an event and then handle in the parent component. Seems complex yes it is but this is the approach you can follow.

  • Hi Amit,
    How do I use this component inside a LWC.

    I called customLookup and passed three values which you are passing in app, but it is not appearing in my app page.

    Pls help.

  • Amit,

    Great work on this. One small suggestion for your Demo App is to show how to retrieve the data:

    DemoApp:

    DemoAppController.js
    ({
    handleSelectedRec : function(component, event, helper)
    {
    console.log(“Selected Record Id: ” + event.getParam(“recordId”));
    }
    })

  • Hello SFDC Panther,

    First of all, thanks for a good post to do this workaround for lookup fields.

    I can see that you tried to create an Aura Application in the last step, in order to showcase the custom lookup component to drive the functionality.

    Instead of that, i created a LWC itself having the following code:

    Then i displayed the LWC on the Lightning App Page (One Region)

    However, i noticed, that when i am typing some keyword, it does not do a search, of records, but whenever i press a backspace, i would get the list of records.

    For Example:
    If i open the page and type keyword “Uni” to search for list of records, i wont get any search results, but whenever i press the backspace on the keyboard resulting in searching for “Un” then the list of records appear.

    I noticed that there is some issue here:
    .then(result => {
    this.records = result;
    for(let i=0; i < this.records.length; i++) {
    this.records[i].Name = this.records[i][this.searchfield];
    }

    In the custom lookup js class.

    I wanted to know if you came across this?

    Thanks mate!

    • Hey Rajat,

      I used application to show the output as we can not create the application in LWC and you used the component in Lightning Page in both ways the goal is being achieved πŸ™‚

      Thanks,
      SFDCPanther

  • Further to my comment before, i noticed, that if you remove the (cacheable=true) in the Apex class, the code starts working, without giving the backspace in order to create results (you may want to check this, by just using LWC instead of an aura application right at the end.)

    The other question i still have is, that whenever events are dispatched from the search component captured by the custom component, it appears that the event is being captured twice and not once.

    Let me know if there is any way to share screen shots of the console log of the screen

    • Yeah, I removed cacheable and it starts working thanks for the catch. For the event thing, it’s very strange but it might be as the event is being dispatched on the change of the input so every time you type an input it will dispatch the event. Let me know if this is the case.

      Thanks,
      SFDCPanther

  • Hi,
    I’m new with event handling.
    I know when we dispatch event from a child component, parent component get that event.
    But here in the Custom Lookup component, we are dispatching a custom event: this.dispatchEvent(selectedRecordEvent);
    What is this for?

    Thanks in Advance

    • Hey Jayant,

      Once we will be using this component inside any component in real-time then we need to get the selected record Id. This event will be handled by the parent component of Custom Lookup Component.

      Hope this clears your doubt.

      Thanks & Regards,
      SFDCPanther

    • Hi Anuj,

      You can definitely do that but for that, you have to make the changes in the List Component and in the Parent Component. In the child, component sends the Index no while using a component inside the parent component and while firing the event on select send the index no as well.