Reusable Lookup Field In LWC

Reusable Lookup in LWC are one of the important element in custom development. Generally, Lookup is an autocomplete combobox that will search against a database object. We are going to build a custom dynamic reusable Lookup field in Lightning Web Component. Some of the key features are:-

1. Dynamic Lookup Component shows two fields for each item in search panel
2. You can pre-populate the lookup field by just passing the id of the selected record
3. It shows the Icon of the related SObject and can be set dynamically
4. Custom Lookup also shows you the object label in search panel for each item
5. This Reusable Lookup can be extended with other LWC as it fires a custom event whenever a value is selected so that consumer can handle it
6. It accepts parent record id and parent field api name so that you can build dependent lookups.

Code for Single Select Reusable Lookup

Let’s create a Lightning Web Component in VS Code with name reusableLookup.

reusableLookup.html

The html file of the reusable lookup component has code derived from slds-lookup. It has three main parts.

reusable lookup in lwc, Search Lookup, Lightning Lookup


1. When lookup is in selected state
2. When lookup is not in selected state
3. When Lookup shows result in serach panel

<template>
    <div class="slds-form-element">
        <div class="slds-form-element__control">
            <div class="slds-combobox_container" if:false={isValueSelected}>
                <div class="slds-combobox slds-dropdown-trigger slds-dropdown-trigger_click slds-is-open">
                    <div class="slds-combobox__form-element slds-input-has-icon slds-input-has-icon_right" role="none">
                        <lightning-input onchange={handleChange} type="search" autocomplete="off" label={label}
                            required={required} field-level-help={helpText} placeholder={placeholder}
                            onblur={handleInputBlur}></lightning-input>
                    </div>
                </div>
            </div>
            <template if:true={isValueSelected}>
                <label class="slds-form-element__label" for="combobox-id-5" id="combobox-label-id-35">{label}</label>
                <template if:true={required}>
                    <span style="color:red">*</span>
                </template>
                <div tabindex="0" class="slds-combobox_container slds-has-selection">
                    <div class="slds-combobox slds-dropdown-trigger slds-dropdown-trigger_click">
                        <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={selectedIconName} alternative-text={selectedIconName}
                                    size="x-small"></lightning-icon>
                            </span>
                            <button type="button"
                                class="slds-input_faux slds-combobox__input slds-combobox__input-value"
                                aria-labelledby="combobox-label-id-34 combobox-id-5-selected-value"
                                id="combobox-id-5-selected-value" aria-controls="listbox-id-5" aria-expanded="false"
                                aria-haspopup="listbox">
                                <span class="slds-truncate" id="combobox-value-id-19">{selectedRecordName}</span>
                            </button>
                            <button class="slds-button slds-button_icon slds-input__icon slds-input__icon_right"
                                title="Remove selected option" onclick={handleCommit}>
                                <lightning-icon icon-name="utility:close" alternative-text="Remove selected option"
                                    size="x-small"></lightning-icon>
                            </button>
                        </div>
                    </div>
                </div>
            </template>
            <template if:true={showRecentRecords}>
                <div id="listbox-id-4" tabindex="0" onblur={handleBlur} onmousedown={handleDivClick}
                    class="slds-dropdown slds-dropdown_length-with-icon-7 slds-dropdown_fluid" role="listbox">
                    <ul class="slds-listbox slds-listbox_vertical" role="presentation">
                        <template for:each={recordsList} for:item="rec">
                            <li role="presentation" key={rec.id} class="slds-listbox__item">
                                <div onclick={handleSelect} data-id={rec.id} data-mainfield={rec.mainField}
                                    data-subfield={rec.subField}
                                    class="slds-media slds-listbox__option slds-listbox__option_entity slds-listbox__option_has-meta"
                                    role="option">
                                    <span class="slds-media__figure slds-listbox__option-icon">
                                        <lightning-icon icon-name={selectedIconName} alternative-text={selectedIconName}
                                            size="small"></lightning-icon>
                                    </span>
                                    <span class="slds-media__body">
                                        <span class="slds-listbox__option-text slds-listbox__option-text_entity">
                                            <span>
                                                <mark>{rec.mainField}</mark>
                                            </span>
                                        </span>
                                        <span class="slds-listbox__option-meta slds-listbox__option-meta_entity">
                                            {objectLabel} • {rec.subField}
                                        </span>
                                    </span>
                                </div>
                            </li>
                        </template>
                    </ul>
                </div>
            </template>
        </div>
    </div>
</template>

reusableLookup.js

The js file of the lwc has exposed required parameters as public so that they can be set. It also has several private methods to handle the various events on input element.

import { LightningElement, api } from 'lwc';
import fetchRecords from '@salesforce/apex/ReusableLookupController.fetchRecords';
/** The delay used when debouncing event handlers before invoking Apex. */
const DELAY = 500;

export default class ReusableLookup extends LightningElement {
    @api helpText = "custom search lookup";
    @api label = "Parent Account";
    @api required;
    @api selectedIconName = "standard:account";
    @api objectLabel = "Account";
    recordsList = [];
    selectedRecordName;

    @api objectApiName = "Account";
    @api fieldApiName = "Name";
    @api otherFieldApiName = "Industry";
    @api searchString = "";
    @api selectedRecordId = "";
    @api parentRecordId;
    @api parentFieldApiName;

    preventClosingOfSerachPanel = false;

    get methodInput() {
        return {
            objectApiName: this.objectApiName,
            fieldApiName: this.fieldApiName,
            otherFieldApiName: this.otherFieldApiName,
            searchString: this.searchString,
            selectedRecordId: this.selectedRecordId,
            parentRecordId: this.parentRecordId,
            parentFieldApiName: this.parentFieldApiName
        };
    }

    get showRecentRecords() {
        if (!this.recordsList) {
            return false;
        }
        return this.recordsList.length > 0;
    }

    //getting the default selected record
    connectedCallback() {
        if (this.selectedRecordId) {
            this.fetchSobjectRecords(true);
        }
    }

    //call the apex method
    fetchSobjectRecords(loadEvent) {
        fetchRecords({
            inputWrapper: this.methodInput
        }).then(result => {
            if (loadEvent && result) {
                this.selectedRecordName = result[0].mainField;
            } else if (result) {
                this.recordsList = JSON.parse(JSON.stringify(result));
            } else {
                this.recordsList = [];
            }
        }).catch(error => {
            console.log(error);
        })
    }

    get isValueSelected() {
        return this.selectedRecordId;
    }

    //handler for calling apex when user change the value in lookup
    handleChange(event) {
        this.searchString = event.target.value;
        this.fetchSobjectRecords(false);
    }

    //handler for clicking outside the selection panel
    handleBlur() {
        this.recordsList = [];
        this.preventClosingOfSerachPanel = false;
    }

    //handle the click inside the search panel to prevent it getting closed
    handleDivClick() {
        this.preventClosingOfSerachPanel = true;
    }

    //handler for deselection of the selected item
    handleCommit() {
        this.selectedRecordId = "";
        this.selectedRecordName = "";
    }

    //handler for selection of records from lookup result list
    handleSelect(event) {
        let selectedRecord = {
            mainField: event.currentTarget.dataset.mainfield,
            subField: event.currentTarget.dataset.subfield,
            id: event.currentTarget.dataset.id
        };
        this.selectedRecordId = selectedRecord.id;
        this.selectedRecordName = selectedRecord.mainField;
        this.recordsList = [];
        // Creates the event
        const selectedEvent = new CustomEvent('valueselected', {
            detail: selectedRecord
        });
        //dispatching the custom event
        this.dispatchEvent(selectedEvent);
    }
    
    //to close the search panel when clicked outside of search input
    handleInputBlur(event) {
        // Debouncing this method: Do not actually invoke the Apex call as long as this function is
        // being called within a delay of DELAY. This is to avoid a very large number of Apex method calls.
        window.clearTimeout(this.delayTimeout);
        // eslint-disable-next-line @lwc/lwc/no-async-operation
        this.delayTimeout = setTimeout(() => {
            if (!this.preventClosingOfSerachPanel) {
                this.recordsList = [];
            }
            this.preventClosingOfSerachPanel = false;
        }, DELAY);
    }

}

reusableLookup.js-meta.xml

We have exposed to home, app and record page.

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>55.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__AppPage</target>
        <target>lightning__RecordPage</target>
        <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>

Apex Controller of Reusable Lookup – ReusableLookupController

public with sharing class ReusableLookupController {
    @AuraEnabled
    public static List<ResultWrapper> fetchRecords(SearchWrapper inputWrapper) {
        try {
            if(inputWrapper != null){
                String fieldsToQuery = 'SELECT Id, ';
                if(string.isNotBlank(inputWrapper.fieldApiName)){
                    fieldsToQuery = fieldsToQuery + inputWrapper.fieldApiName;
                }
                if(string.isNotBlank(inputWrapper.otherFieldApiName)){
                    fieldsToQuery = fieldsToQuery + ', ' + inputWrapper.otherFieldApiName;
                }
                String query = fieldsToQuery + ' FROM '+ inputWrapper.objectApiName;
                String filterCriteria = inputWrapper.fieldApiName + ' LIKE ' + '\'' + String.escapeSingleQuotes(inputWrapper.searchString.trim()) + '%\' LIMIT 10';
                if(String.isNotBlank(inputWrapper.selectedRecordId)) {
                    query += ' WHERE Id = \''+ inputWrapper.selectedRecordId + '\'';
                }else if(String.isNotBlank(inputWrapper.parentFieldApiName) && String.isNotBlank(inputWrapper.parentRecordId)){
                    query += ' WHERE '+ inputWrapper.parentFieldApiName+ ' = \''+ inputWrapper.parentRecordId + '\'';
                    query += ' AND ' + filterCriteria;
                } 
                else {
                    query += ' WHERE '+ filterCriteria;
                }
                List<ResultWrapper> returnWrapperList = new List<ResultWrapper>();
                for(SObject s : Database.query(query)) {
                    ResultWrapper wrap = new ResultWrapper();
                    wrap.mainField = (String)s.get(inputWrapper.fieldApiName);
                    wrap.subField = (String)s.get(inputWrapper.otherFieldApiName);
                    wrap.id = (String)s.get('id');
                    returnWrapperList.add(wrap);
                }
                return returnWrapperList;
            }
            return null;
        } catch (Exception err) {
            throw new AuraHandledException(err.getMessage());
        }
    }

    public class ResultWrapper{
        @AuraEnabled public String mainField{get;set;}
        @AuraEnabled public String subField{get;set;}
        @AuraEnabled public String id{get;set;}
    }

    public class SearchWrapper {
        @AuraEnabled public String objectApiName{get;set;}
        @AuraEnabled public String fieldApiName{get;set;}
        @AuraEnabled public String otherFieldApiName{get;set;}
        @AuraEnabled public String searchString{get;set;}
        @AuraEnabled public String selectedRecordId{get;set;}
        @AuraEnabled public String parentRecordId{get;set;}
        @AuraEnabled public String parentFieldApiName{get;set;}
    }
}

Demo – Reusable Lookup In LWC

You can call the reusable lwc lookup in any other component as below. Let’s create a LWC with name demoLookup.

demoLookup.html

<template>
    <c-reusable-lookup label="Parent Account" selected-icon-name="standard:account" object-label="Account"
        object-api-name="Account" field-api-name="Name" other-field-api-name="Industry"
        onvalueselected={handleValueSelectedOnAccount}>
    </c-reusable-lookup>
</template>

demoLookup.js

handleValueSelectedOnAccount method handles the valueselected event fired from reusable search lookup component. parentAccountSelectedRecord holds the selected record information. It will have id of the record, main field value and sub field value.

import { LightningElement } from 'lwc';
export default class DemoLookup extends LightningElement {
    parentAccountSelectedRecord;
    handleValueSelectedOnAccount(event) {
        this.parentAccountSelectedRecord = event.detail;
    }
}

demoLookup.js-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>55.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__AppPage</target>
        <target>lightning__RecordPage</target>
        <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>

We have placed the component to home page under a tab component. You can see the demo below:

Do you need help?

Are you stuck while working on this requirement? Do you want to get review of your solution? Now, you can book dedicated 1:1 with me on Lightning Web Component completely free.

GET IN TOUCH

Schedule a 1:1 Meeting with me

Also check out https://salesforcediaries.com/category/lightning-web-component/ to learn more on LWC.

One comment

Leave a Reply