Skip to main content

How to create custom polymorphic field using LWC ?

How to create custom polymorphic field using LWC ?


In this blog we will see how we can make polymorphic lookup field using LWC.

What is Polymorphic field?
A polymorphic field is one where related object might be one of the several different types of objects.

There can be a use case where a customer wants to connect one object with multiple objects- i.e, relationships between objects or relate a child object to multiple parents's ojects.

For example, in task Object we have three such polymorphic fields.
  1. The WhoId(Name) relationship of Task can be with Contact or a Lead.
  2. Assigned To field can be a User or a Queue.
  3. Similarly, a WhatId(Related To) relationship field of Task can be with many other objects like Accounts, Opportunities etc.
In Salesforce, currently we do not have any OOTB option or may be we can say we do not have a datatype for polymorphic field which we can create but if required we can create a custom component to facilitate the same functionality.

So let's get started and see step by step how we can create custom Polymorphic Lookup field in Salesforce.
Step by Step approach to build Custom Polymorphic field component

Step 1: Create a new Custom Metadata
  • PolymorphicLookupObject__mdt
We are creating custom metadata to store the Objects and its details which will help us to make the functionality dynamic.
When we say dynamic it means we can add or remove any object as needed. The objects that are displayed as dropdown will come from this defined custom Metadata records in PolymorphicLookupObject__mdt custom metadata.

If you have noticed we also have a 'New Record' option for the user. this New Record feature can also be controlled according to the requirement. If we DO NOT want to show this New Record button to the end user we can simply switch off this by making the IsCreateNewRecordAllowed__c field in custom metadata as FALSE and the New Record button will be hidden .

Custom Metadata Fields and Description
Step 2: Create Apex Classes
  • PolymorphicLookupObjects - This class is used to retrieve the Custom Metadata records created in Step 1.
  • searchLooupResult - This Class is used to fetch the records of the selected object from the drop down.
Code snippet:- PolymorphiLookupObjects.cls
PolymorphicLookupObjects.cls -
/****
    @description       :
    @author            : Amit Agarwal
    @group             :
    @last modified on  :
    @last modified by  : Amit Agarwal
****/
public with sharing class PolymorphicLookupObjects {
    public PolymorphicLookupObjects() {

    }
    @AuraEnabled(cacheable=true)
    public static List<sObject> getObjectLookupList(){
        String Query = '';
        Query  = 'SELECT Id, IsCreateNewRecordAllowed__c,sObjectAPIName__c, sObjectLabel__c, sObjectPluralName__c, sObjectRecords_Search_by_field__c, sObjectLookup_subtitle_field__c, sObject_Display_IconName__c FROM PolymorphicLookupObject__mdt ';
        List<SObject> sObjectList = Database.query(Query);
        return sObjectList;
    }
}

Code snippet:- genericLookupCompWrapper.cls 
genericLookupCompWrapper.cls -
/****
    @description       :
    @author            : Amit Agarwal
    @group             :
    @last modified on  :
    @last modified by  : Amit Agarwal
****/
public with sharing class genericLookupCompWrapper {
   
    private Id id;
    private String sObjectType;
    private String icon;
    private String title;
    private String subtitle;

    public genericLookupCompWrapper(Id id, String sObjectName, String icon, String title, String subtitle) {
        this.Id = id;
        this.sObjectType = sObjectName;
        this.icon = icon;
        this.title = title;
        this.subtitle = subtitle;
    }

    @AuraEnabled
    public Id getId(){
        return id;
    }

    @AuraEnabled
    public String getsObjectType(){
        return sObjectType;
    }
    @AuraEnabled
    public String getIcon(){
        return icon;
    }

    @AuraEnabled
    public String getTitle(){
        return title;
    }

    @AuraEnabled
    public String getSubtitle(){
        return subtitle;
    }
}

Code :- searchLookupResult.cls
searchLooupResult.cls-
/****
    @description       :
    @author            : Amit Agarwal
    @group             :
    @last modified on  :
    @last modified by  : Amit Agarwal
****/

public class searchLooupResult {

    private final static Integer MAX_RESULTS = 5000;


    @AuraEnabled(cacheable =true)
    public static List<genericLookupCompWrapper> searchRecords(String searchTerm, List<String> selectedIds, String sObjectName, String[] filterCriteria, String[] titleFields, String[] subtitleFields){
       
       
        System.debug(' searchTerm => ' + searchTerm);
        System.debug('selectedIds => ' + selectedIds);
        System.debug('sObjectName => ' + sObjectName);
        System.debug('filterCriteria => ' + filterCriteria);
        System.debug(' titleFields => ' + titleFields );
        System.debug(' subTitleFieldse => ' + subtitleFields);

        List<sObject> queryRecords = new List<sObject>();

        String strQuery = '';
        String whereClause = '';
        String whereClauseTemp = '';

        String fieldAPI = 'id,' + string.join(titleFields,',') + ',' + string.join(subTitleFields,',') ;
        System.debug(' fieldAPI => ' + fieldAPI);
        if(selectedIds != null && !selectedIds.isEmpty()){
            whereClause = whereClause + 'id NOT IN :selectedIds';
        }
        System.debug(' whereClause => ' + whereClause);

        if(String.isNotBlank(searchTerm)){
           
         

            if(titleFields != null && titleFields.size() > 0){
                for(String titlefield : titleFields){
                    whereClauseTemp += titlefield + ' like \'%' + searchTerm + '%\'' + ' OR ' ;
                }
            }

            if(subTitleFields != null && subTitleFields.size() > 0){
                for(String subtitlefield : subTitleFields){
                    whereClauseTemp += subtitlefield + ' like \'%' + searchTerm + '%\'' + ' OR ' ;
                }
            }
            System.debug(' fieldAPI => ' + fieldAPI);
            whereClauseTemp = (String.isNotBlank(whereClauseTemp) ?  (whereClauseTemp.removeEnd(' OR ')) : ' ');
            if(String.isNotBlank(whereClause)){
                whereClause += ' AND ' +  + ' ('+ whereClauseTemp + ')' ;
            }else{
                whereClause += ' ('+ whereClauseTemp + ')' ;
            }
        }

        if(filterCriteria != null && filterCriteria.size() > 0 ){
            if(String.isNotBlank(whereClause)){
                whereClause = whereClause + ' AND ';
            }
            whereClause += String.join(filterCriteria, ' AND ');
        }
        System.debug(' whereClause => ' + whereClause);
        System.debug(' whereClauseTemp => ' + whereClauseTemp);
        fieldAPI = fieldAPI.removeEnd(',');
        System.debug(' fieldAPI => ' + fieldAPI);
        System.debug(' Final whereClause => ' + whereClause);
        strQuery = 'SELECT ' + fieldAPI + ' FROM ' + sObjectName +(String.isNotBlank(whereClause) ? ' WHERE ' + whereClause : ' ') + ' WITH SECURITY_ENFORCED'  + ' LIMIT ' + MAX_RESULTS;
        System.debug(' Final Query => ' + strQuery);
        queryRecords = Database.query(strQuery);


        // Now make it a Wrapper Class to send teh result in specific format
        List<genericLookupCompWrapper> searchResult = new List<genericLookupCompWrapper>();

        for(sObject record : queryRecords ){
            String title = '';
            String subtitle = '';

            if(titleFields != null && titleFields.size() > 0){
                for(String titlefield : titleFields){
                    String titleVal = (String)record.get(titlefield) ;
                    if(titleVal != null){
                        title = title + titleVal + ',' ;
                    }
                }
            }
            title = title.removeEnd(',');

            if(subTitleFields != null && subTitleFields.size() > 0){
                for(String subtitlefield : subTitleFields){
                    String subtitleVal =  (String)record.get(subtitlefield) ;
                    if(subtitleVal != null){
                        subtitle = subtitle + subtitleVal + ',' ;
                    }
                }
            }
            subtitle = subtitle.removeEnd(',') ;
            searchResult.add(new genericLookupCompWrapper(
                (String)record.get('id'),
                sObjectName,
                getIcon(sObjectName),
                title,
                subtitle
            ));
        }
        return searchResult;
    }

    private static String getIcon(String sObjectName){
        switch on sObjectName{
            when 'Contact' {
                return 'standard:contact';
            }
            when 'User'{
                return 'action:user';
            }
            when null{
                return 'custom:custom101';
            }
            when else {
                return 'custom:custom101';
            }
        }
    }
}


Step 3: Create Lightning Web components

  • polymorphiSelectObject.cmp - This LWC is the child component where we will create a dropdown options for the sObject Selection. We will retrieve the metadata records and show the Sobjects that are configured in the custom metadata object in Step 1.
  • polymorphicSearchRecords.cmp - This LWC component is also a child component . In this we will implement the Lookup search box to show the records for the selected object. In this we will also implement the New Record button .
  • polymorphicLookup.cmp - This LWC is the parent component which will hold the polymorphicSearchRecords.cmp and polymorphicSearchRecords.cmp components. This will help the child components to pass event and values needed.
Code snippet:- polymorphiSelectObject.cmp
polymorphicSelectObject.html -
 <template>
    <div class="slds-combobox_object-switcher slds-combobox-addon_start">
        <div class="slds-form-element">
            <label class="slds-form-element__label slds-assistive-text" for="combobox-id-2"
                    id="combobox-label-id-34">Filter Search by:</label>
            <div class="slds-form-element__control">
                <div class="slds-combobox_container">                                          
                    <div class={getDropDownClass} aria-controls="primary-combobox-id-1">
                        <!--Dropdown Button to show Object List -->
                            <div style="border-color: 2px solid rgb(201, 201, 201);border-radius: 0.25em;"
                                class="slds-combobox__form-element slds-input-has-icon slds-input-has-icon_right"
                                role="none"  onclick={handleObjListToSelect}>
                                <button type="button"
                                        class="slds-button slds-button_icon slds-button_icon-container-more"
                                        aria-expanded="false" title="More Options"
                                        aria-autocomplete="list"
                                        aria-labelledby="combobox-label-id-34 combobox-id-2-selected-value"
                                        id="combobox-id-2-selected-value"
                                        aria-controls="objectswitcher-listbox-id-01"
                                        aria-haspopup="listbox"
                                        onblur={handleInputRecordBlur} >
                                    <span class="slds-truncate">
                                        <lightning-icon icon-name={selectedObject.iconName}
                                            alternative-text={selectedObject.iconName} size='medium'
                                            title={selectedObject.iconName}>
                                        </lightning-icon>
                                    </span>
                                    <span>
                                        <lightning-icon icon-name='utility:down'
                                            class="slds-p-left_x-small" alternative-text='down'
                                            size='x-small' title='down'>
                                        </lightning-icon>
                                    </span>
                                </button>
                            </div>
                        <!--Dropdown Button to show Object List-->

                        <!-- Object List to be selected-->
                            <div id="objectswitcher-listbox-id-01"
                                 class="slds-dropdown slds-dropdown_length-5 slds-dropdown_x-small slds-dropdown_left"
                                 role="listbox">
                                <ul class="slds-listbox slds-listbox_vertical" role="group"
                                    aria-label="Suggested for you">
                                    <!--Iterate over the Object List from custom metadata -->
                                    <template for:each={objectList} for:item="object">
                                        <li role="presentation" key={object.APIName}
                                            data-objapiname={object.APIName}
                                            onclick={handleObjectSelection}
                                            class="slds-listbox__item">
                                            <div class="slds-media slds-listbox__option slds-listbox__option_plain slds-media_small"
                                                 role="option">
                                                <span
                                                    class="slds-media__figure slds-listbox__option-icon">
                                                    <lightning-icon icon-name={object.iconName}
                                                                    alternative-text={object.iconName}
                                                                    size='small' title={object.iconName}>
                                                    </lightning-icon>
                                                </span>
                                                <span class="slds-media__body">
                                                    <span class="slds-truncate"
                                                          title={object.LabelName}>{object.LabelName}</span>
                                                </span>
                                            </div>
                                        </li>
                                    </template>
                                    <!--Iterate over the Object List from custom metadata -->
                                </ul>
                            </div>
                        <!-- Object List to be selected-->
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

polymorphicSelectObject.js
import { LightningElement,api,wire } from 'lwc';
import getObjectLookupList from '@salesforce/apex/PolymorphicLookupObjects.getObjectLookupList';
const DELAY = 100;
export default class PolymorphicSelectObject extends LightningElement {

    openObjectList= false;
    hasInputfocus = false;
    blurTimeout;
    @api selectedObject={};
    @api objectList=[];

   // This is a wire method to fetch the List of Objects to be diaplyed on UI as options to select
    @wire(getObjectLookupList)
    wiredRecords({data,error}){
            if(data) {
                console.log("data wiredRecords => " + JSON.stringify(data));
                let items = [];
               // loop over each record and make a wrapper class
                data.forEach(ele => {
                    let item = {};
                    item.APIName = ele.sObjectAPIName__c;
                    item.LabelName = ele.sObjectLabel__c;
                    item.searchTitle = ele.sObjectRecords_Search_by_field__c;
                    item.subtitleFields = ele.sObjectLookup_subtitle_field__c;
                    item.iconName = ele.sObject_Display_IconName__c;
                    item.createRecord = ele.IsCreateNewRecordAllowed__c;
                  items.push(item);
                });

                // store the items in objectList variable
                this.objectList = items;

                // store the default selected Object as the first object from the list
                this.selectedObject = this.objectList[0];

                console.log("selectedObject wiredRecords => " + JSON.stringify(this.selectedObject));

                //fire an event on object selection
                // to pass the selected object to parent component
                this.fireselectedObjectEvent();
                this.errors = undefined;
            }
            if(error) {
                console.log("error wiredRecords => " +  JSON.stringify(error));
                this.objectList = undefined;
                this.errors = error;
            };
    }

    // toggle the dropdown list onclick of dropdown icon
    handleObjListToSelect() {
        this.openObjectList = !this.openObjectList;
    }

    // remove/hide the dropdown options on blur
    handleInputRecordBlur() {
        this.blurTimeout = window.setTimeout(() => {
            this.hasInputfocus = false;
            this.openObjectList = false;
            this.blurTimeout = null;
        },
            DELAY
        );
    }

    handleObjectSelection(event) {        
        let apiname = event.currentTarget.dataset.objapiname;
        this.openObjectList = false;
        let objectFound = this.objectList.find(function (ele, index) {
            if (ele.APIName === apiname)
                return true;
        });
        this.selectedObject = objectFound;

        //fire an event on object selection
        this.fireselectedObjectEvent();        
    }

    //fire an event on object selection
    // to pass the selected object to parent component
    fireselectedObjectEvent(){
        const selectedObjectEvent = new CustomEvent('selectionchange',{
            detail:{
                selectedObject: this.selectedObject
            }
        });
        this.dispatchEvent(selectedObjectEvent);
    }

    // remove/hide the dropdown options on blur
    get getDropDownClass() {
        let css = 'slds-combobox slds-dropdown-trigger slds-dropdown-trigger_click ';
        return this.openObjectList ? css + 'slds-is-open' : css;
    }
}

polymorphicSelectObject.js-meta.xml 
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>55.0</apiVersion>
    <isExposed>false</isExposed>
</LightningComponentBundle>

Code snippet:- polymorphiSearchRecords.cmp
polymorphicSearchRecords.html -
<template>
    <div class="slds-combobox_container slds-combobox-addon_end">
        <div class={getInputRecordsDropdownclass} id="primary-combobox-id-1">

            <!-- This block shows the lookup searh box => start -->
                <div class={getComboboxClass} role="none">
                    <!-- Toggle input lookup search box -->
                    <!-- This block will display if no record is selected -->
                    <template if:false={isRecordSelected}>
                        <input type="text" class={getInputClass} id="combobox-id-1" aria-autocomplete="list"
                            aria-controls="listbox-id-1" aria-expanded="true" aria-haspopup="listbox" autocomplete="off"
                            role="combobox" onfocus={handleInputRecordFocus} onblur={handleInputRecordBlur}
                            value={getInputValue} placeholder={searchObjectPlaceholder} oninput={updateSearchTerm} />
                        <lightning-icon icon-name="utility:search" size="x-small" alternative-text="search icon"
                            class={getSearchIconClass}>
                        </lightning-icon>
                    </template>

                    <!-- This block will display when a record is selected -->
                    <!-- Toggle input lookup search box -->
                    <template if:true={isRecordSelected}>
                        <div class="slds-p-top_xx-small">
                            <lightning-pill label={recSelected.title} onremove={removeSelectedRecord}>
                                <lightning-icon icon-name={recSelected.icon} alternative-text={recSelected.title}>
                                </lightning-icon>
                            </lightning-pill>
                        </div>
                    </template>
                </div>
            <!-- This block shows the lookup searh box => End-->

            <!-- This block shows the dropdown records and the New Record button on searhing => start -->
                <div id="listbox-id-1" class="slds-dropdown slds-dropdown_length-with-icon-7 slds-dropdown_fluid"
                    role="listbox">
                    <ul class="slds-listbox slds-listbox_vertical" role="presentation">
                        <!--  User New Record accessibility dynamically => start-->
                            <template if:true={hasCreateAllowed}>
                                <li role="presentation" class="slds-listbox__item" onclick={handleNewRecordClick}
                                    data-objselectedapi={selectedObject.APIName}>
                                    <div aria-selected="true" id="option0"
                                        class="slds-media slds-listbox__option slds-listbox__option_entity slds-listbox__option_term slds-has-focus"
                                        role="option">
                                        <lightning-icon icon-name='utility:add' alternative-text='Create New Record'
                                            size='xx-small' title='Create New Record'></lightning-icon>
                                        <span class="slds-media__body">
                                            <span class="slds-listbox__option-text slds-listbox__option-text_entity slds-p-left_x-small"
                                                title="Create New Record">New Record</span>
                                        </span>
                                    </div>
                                </li>
                            </template>
                        <!--  User New Record accessibility dynamically => end-->

                        <!--  Iterate and display the Records of the selected obejct => start-->
                            <template for:each={searchRecords} for:item="rec">
                                <li role="presentation" title={rec.title} key={rec.id} data-recordid={rec.id} class="slds-listbox__item" onclick={handleResultClick}>
                                    <div 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" data-recordid={rec.id}>
                                            <lightning-icon icon-name={rec.icon} alternative-text={rec.title} size='medium'></lightning-icon>
                                        </span>
                                        <span class="slds-media__body">
                                            <span class="slds-listbox__option-text slds-listbox__option-text_entity">{rec.title}</span>
                                            <span class="slds-listbox__option-meta slds-listbox__option-meta_entity">{rec.subtitle}</span>
                                        </span>
                                    </div>
                                </li>
                            </template>
                        <!--  Iterate and display the Records of the selected obejct => end-->
                    </ul>
                </div>
            <!-- This block shows the dropdown records and the New Record button on searhing => end -->
        </div>
    </div>
</template>

polymorphicSearchRecords.js -
import { LightningElement,api,track,wire } from 'lwc';
import fetchLookupRecords from '@salesforce/apex/searchLooupResult.searchRecords';
const DELAY = 100;
const MINIMAL_SEARCH_TERM_LENGTH = -1;
export default class PolymorphicSearchRecords extends LightningElement {


    @api selectedObject;
    @api searchRecords;
    @api searchPlaceholder;
    @track searchTerm = '';
    selectedItem = [];
    recSelected;
    isRecordSelected = false;
    hasInputfocus = false;
    blurTimeout;    
    cleanSearchTerm;
    searchThrottingTimeout;

    // On load of the component this will fecth the records of the default selected Object
    connectedCallback() {
        // console.log('selectedObject in searchRecords '+JSON.stringify(this.selectedObject));
        if (this.selectedObject.APIName != null) {
            this.searchPlaceholder = 'Search ' + this.selectedObject.LabelName + '....';
            this.fetchsObjectRecords({
                searchTerm: '',
                selectedIds: null,
                sObjectName: this.selectedObject.APIName,
                filterCriteria: [],
                titleFields: this.selectedObject.searchTitle,
                subtitleFields: this.selectedObject.subtitleFields
            });
        }
    }

    // this will toggle and show if  'New Record' button should be visible or not
    get hasCreateAllowed(){
        if(this.selectedObject.createRecord === true)
            return true;
        else
            return false;  
    }


    // if search box has is focused
    handleInputRecordFocus() {
        this.hasInputfocus = true;
    }

    // if a record is selected or not from the search lookup dropdown box
    hasSelection() {
        return this.selectedItem.length > 0;
    }

    // if a record is already selected or selection is allowed from the search lookup dropdown box
    isSelectionAllowed() {
        return !this.hasSelection();
    }


    // remove/hide the dropdown options on blur
    handleInputRecordBlur() {
        this.blurTimeout = window.setTimeout(() => {
            this.hasInputfocus = false;
            this.blurTimeout = null;
        },
            DELAY
        );
    }

    //search and fetch the records of the selected object on focus or click of the search box
    updateSearchTerm(event) {
        //check if selection is allowed or not
        if (!this.isSelectionAllowed()) {
            return;
        }      
        //stores the current input values on search box
        this.searchTerm = event.target.value;
        const newCleansearchterm = event.target.value.trim().replace(/\*/g, '').toLowerCase();
        if (this.cleanSearchTerm === newCleansearchterm) {
            return;
        }
        this.cleanSearchTerm = newCleansearchterm;

        if (newCleansearchterm.length < MINIMAL_SEARCH_TERM_LENGTH) {
            this.searchRecords = [];
            return;
        }

        if (this.searchThrottingTimeout) {
            clearTimeout(this.searchThrottingTimeout);
        }


        // check if minimum character is entered and delay teh search a few miliseconds
        this.searchThrottingTimeout = setTimeout(() => {
            if (this.cleanSearchTerm.length > MINIMAL_SEARCH_TERM_LENGTH) {
                console.log("this.searchTerm Updated to  => " + this.searchTerm);
                this.fetchsObjectRecords({
                    searchTerm: this.searchTerm,
                    selectedIds: null,
                    sObjectName: this.selectedObject.APIName,
                    filterCriteria: [],
                    titleFields: this.selectedObject.searchTitle,
                    subtitleFields: this.selectedObject.subtitleFields
                });
            }
            this.searchThrottingTimeout = null;
        },
            DELAY
        );
    }

    //to fetch the records from apex class
    fetchsObjectRecords(detail) {
        //console.log('detail fetchsObjectRecords => ' + JSON.stringify(detail));
        fetchLookupRecords(detail)
            .then(results => {
            //    console.log("results => " + results);
                let items = [];
                results.forEach(ele => {
                    let item={};
                    item.id = ele.id;
                    item.icon = this.selectedObject.iconName;
                    item.title = ele.title;
                    item.subtitle = ele.subtitle;
                    items.push(item);
                });
                this.searchRecords = items;
            })
            .catch(error => {
                this.searchRecords = [];
                console.log('error => ' + (error));
                console.log('error => ' + JSON.stringify(error));
            });
    }
   
    // to remove the current selected record
    handleRemoveSelection(){
        this.selectedItem = [];
    }

    // on click of the record from teh search box dropdown
    handleResultClick(event){
        const recordId = event.currentTarget.dataset.recordid;
        this.selectedItem = this.searchRecords.filter(result => result.id === recordId);
        console.log('selectedItem  => ' + JSON.stringify(this.selectedItem) );
        if(this.selectedItem.length === 0){
            return;
        }
        if(this.selectedItem.length > 0){
            this.isRecordSelected=true;
            this.recSelected = this.selectedItem[0];
        }
        console.log(' this.isRecordSelected   => ' +  this.isRecordSelected );
        console.log(' this.recSelected   => ' +  JSON.stringify(this.recSelected) );
        this.searchTerm = '';
        this.dispatchEvent(new CustomEvent('selectionchange'));
    }

    // fire an event to parent component if a New Record is to be created
    handleNewRecordClick() {
        this.dispatchEvent(new CustomEvent('createnewrecord'));
    }


    // removing the current selected item/record
    removeSelectedRecord(){
        this.isRecordSelected=false;
        this.recSelected = '';
        this.selectedItem =[];
    }

    get getSearchIconClass(){
        let css = 'slds-input__icon slds-input__icon_right ' +  (!this.hasSelection() ? '' : 'slds-hide');
        return css;
   }

    get getClearSelectionButtonClass() {
        return 'slds-button slds-button_icon slds-input__icon slds-input__icon_right ' + (this.hasSelection() ? '' : 'slds-hide');
    }

    get getInputValue() {
        return this.searchTerm;
    }

    get searchObjectPlaceholder() {
        return this.searchPlaceholder;
    }

    get getInputRecordsDropdownclass() {
        let css = 'slds-combobox slds-dropdown-trigger slds-dropdown-trigger_click ';
        return this.hasInputfocus ? css + 'slds-is-open' : css;
    }

    get getComboboxClass(){
        let css ='slds-combobox__form-element slds-input-has-icon ';
            css += (this.hasSelection()? 'slds-input-has-icon_left-right' : 'slds-input-has-icon_right');
        return css;
    }

    get getInputClass() {
        let css = 'slds-input slds-combobox__input';
        css += 'slds-has-focus';
        return this.hasInputfocus ? css + 'slds-has-focus' : css;
    }
}

polymorphicSearchRecords.js-meta.xml -
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>55.0</apiVersion>
    <isExposed>false</isExposed>
</LightningComponentBundle>

Code snippet:- polymorphiLookUp.cmp
polymorphicLookUp.html -
<template>
    <lightning-card title="Custom Polymorphic LookUp field  using LWC" icon-name="standard:bundle_config">
        <lightning-layout multiple-rows="true">
            <lightning-layout-item padding="around-small" size="12">
                <div class="slds-form-element">
                    <label class="slds-form-element__label" for="combobox-id-1"><b>Related To</b></label>
                    <div class="slds-form-element__control">
                        <div class="slds-combobox-group">
                            <!-- Select the Object -->
                                <c-polymorphic-select-object onselectionchange={handleObjectSelection}>
                                </c-polymorphic-select-object>
                            <!-- Select the Object -->

                            <!-- Look Up records for selected Object-->
                                <template if:true={selectedObject}>
                                    <c-polymorphic-search-records search-placeholder={searchObjectPlaceholder}
                                        search-records={sObjectRecords} selected-object={selectedObject}
                                        oncreatenewrecord={handleNewRecordCreation}>
                                    </c-polymorphic-search-records>
                                </template>
                            <!-- Look Up records for selected Object-->

                            <!--Start Modal Pop up to select the recordtype  -->
                                <template if:true={selectRecordType}>
                                    <div>
                                        <section role="dialog" tabindex="-1" aria-labelledby="modal-heading-01"
                                            aria-modal="true" aria-describedby="modal-content-id-1"
                                            class="slds-modal slds-fade-in-open" style="z-index:9999;">
                                            <div class="slds-modal__container">

                                                <!--header of the select Record type Modal pop-->
                                                <header class="slds-modal__header">
                                                    <button
                                                        class="slds-button slds-button_icon slds-modal__close slds-button_icon-inverse"
                                                        title="Close">
                                                        <lightning-button-icon icon-name="utility:close"
                                                            variant="bare-inverse" size="large" onclick={closeModal}>
                                                        </lightning-button-icon>
                                                        <span class="slds-assistive-text">Close</span>
                                                    </button>
                                                    <h2 id="modal-heading-01"
                                                        class="slds-text-heading_medium slds-hyphenate">New
                                                        {selectedObject.LabelName}
                                                    </h2>
                                                </header>

                                                <!--content of the select Record type Modal pop-->
                                                <div class="slds-modal__content slds-p-around_medium"
                                                    id="modal-content-id-1">
                                                    <template if:true={errorStatus}>
                                                        <div class="slds-box slds-box_small slds-theme_error">
                                                            <p>Review the errors on this page.</p>
                                                        </div>
                                                        <div class="slds-m-left_large">
                                                            <p style="color: red;">
                                                                Select at least one recordtype to proceed.
                                                            </p>
                                                        </div>
                                                    </template>
                                                    <div class="slds-m-left_xx-large slds-m-top_medium">
                                                        <lightning-radio-group name="recordType"
                                                            label="Select a record type" options={recordTypeOptions}
                                                            value={recordTypeId} type="radio"
                                                            onchange={handleRecordTypeChange}>
                                                        </lightning-radio-group>
                                                    </div>
                                                </div>

                                                <!--footer of the select Record type Modal pop-->
                                                <footer class="slds-modal__footer">
                                                    <lightning-button label="Cancel" class="slds-p-right_small" onclick={closeModal}>
                                                    </lightning-button>
                                                    <lightning-button label="Next" onclick={handleNext}
                                                        variant="brand"></lightning-button>
                                                </footer>
                                            </div>
                                        </section>
                                        <div class="slds-backdrop slds-backdrop_open"></div>
                                    </div>
                                </template>
                            <!--End Pop up to select the recordtype  -->

                            <!--Start Modal pop up to Create New Record -->
                                <template if:true={openCreateRecord}>
                                    <div>
                                        <section role="dialog" tabindex="-1"
                                            class="slds-modal slds-fade-in-open slds-modal_medium"
                                            aria-labelledby="modal-heading-02" aria-modal="true"
                                            aria-describedby="modal-content-id-2">
                                            <div class="slds-modal__container">

                                                <!--header if the New Record Modal pop-->
                                                <header class="slds-modal__header">
                                                    <button
                                                        class="slds-button slds-button_icon slds-modal__close slds-button_icon-inverse"
                                                        title="Close">
                                                        <lightning-button-icon icon-name="utility:close"
                                                            variant="bare-inverse" size="large" onclick={closeModal}>
                                                        </lightning-button-icon>
                                                        <span class="slds-assistive-text">Close</span>
                                                    </button>
                                                    <h2 id="modal-heading-02"
                                                        class="slds-text-heading_medium slds-hyphenate">New {selectedObject.LabelName}
                                                    </h2>
                                                </header>

                                                <!--content of the New Record Modal pop-->
                                                <div class="slds-modal__content slds-p-around_medium" id="modal-content-id-2">
                                                    <div class="slds-m-left_xx-large slds-m-top_medium">
                                                    <lightning-record-form class="recordForm" object-api-name={selectedObject.APIName}
                                                        record-type-id={recordTypeId} layout-type="Compact" columns="2"
                                                        onload={handleLoad} density="comfy" onerror={handleError}
                                                        onsuccess={handleSuccess}>
                                                    </lightning-record-form>
                                                    </div>
                                                </div>

                                                <!--footer of the New Record Modal pop-->
                                                <footer class="slds-modal__footer">
                                                    <lightning-button label="Save" class="slds-p-right_small" variant="brand" onclick={handleSubmit}>
                                                    </lightning-button>
                                                    <lightning-button label="Cancel" onclick={closeModal}></lightning-button>
                                                </footer>
                                            </div>
                                        </section>
                                        <div class="slds-backdrop slds-backdrop_open"></div>
                                    </div>
                                </template>
                            <!--End Modal pop up to Create New Record -->
                        </div>
                    </div>
                </div>
            </lightning-layout-item>
        </lightning-layout>
    </lightning-card>
</template>

polymorphicLookUp.js -
import { LightningElement, track, api, wire } from 'lwc';
import { getObjectInfo } from 'lightning/uiObjectInfoApi';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import fetchLookupRecords from '@salesforce/apex/searchLooupResult.searchRecords';
export default class PolymorphicLookUp extends LightningElement {

    @track searchTerm = '';
    sObjectRecords = [];
    searchPlaceholder;
    selectedObject = {};
    isError=false;
    record;
    mainRecord;
    openCreateRecord;
    selectRecordType;
    recordTypeOptions;
    recordTypeId;
    error;

    //this holds the selected object
    handleObjectSelection(event) {
        this.selectedObject = event.detail.selectedObject;
        this.searchPlaceholder = 'Search ' + this.selectedObject.LabelName + '....';
        this.fetchsObjectRecords({
            searchTerm: '',
            selectedIds: null,
            sObjectName: this.selectedObject.APIName,
            filterCriteria: [],
            titleFields: this.selectedObject.searchTitle,
            subtitleFields: this.selectedObject.subtitleFields
        });
    }

    //this is used to fetch the lokup records of the selected object
    fetchsObjectRecords(detail) {
        console.log('detail this.selectedObject => ' + JSON.stringify(this.selectedObject));
        console.log('detail fetchsObjectRecords => ' + JSON.stringify(detail));
        fetchLookupRecords(detail)
            .then(results => {
                console.log("results => " + results);
                let items = [];
                results.forEach(ele => {
                    let item = {};
                    item.id = ele.id;
                    item.icon = this.selectedObject.iconName;
                    item.title = ele.title;
                    item.subtitle = ele.subtitle;
                    items.push(item);
                });
                this.sObjectRecords = items;
            })
            .catch(error => {
                this.sObjectRecords = [];
                console.log('error => ' + error);
                console.log('error => ' + JSON.stringify(error));
            });
    }

    // placeholder of search box
    get searchObjectPlaceholder() {
        return this.searchPlaceholder;
    }

    //to show error
    get errorStatus() {
        return this.isError;
    }


    //Used to get the object information of the selected object to create New record
    @wire(getObjectInfo, { objectApiName: '$selectedObject.APIName' })
    wiredObjectInfo({ error, data }) {
        if (data) {
            this.record = data;
            console.log("getObjectInfo this.record ", this.record);
            this.error = undefined;
            this.getRecordTypeOptions();
        } else if (error) {
            this.error = error;
            this.record = undefined;
            console.log("this.error", this.error);
        }
    }

    //Get the all record Types for the selected Object
    getRecordTypeOptions() {
        let recordTypeInfos = Object.entries(this.record.recordTypeInfos);
        console.log("recordTypeInfos length", recordTypeInfos.length);
        if (recordTypeInfos.length > 1) {
            let temp = [];
            recordTypeInfos.forEach(([key, value]) => {
                console.log(key);
                if (value.available === true && value.master !== true) {
                    temp.push({ "label": value.name, "value": value.recordTypeId });
                }
            });
            this.recordTypeOptions = temp;
            console.log("recordTypeOptions", this.recordTypeOptions);
        } else {
            this.recordTypeId = this.record.defaultRecordTypeId;
        }
        console.log("recordTypeOptions", this.recordTypeOptions);
    }

    // handles the chnage of the recordtype selection
    handleRecordTypeChange(event) {
        console.log("In handleRecTypeChange", event.target.value);
        this.recordTypeId = event.target.value;
    }

    // open modal pop up to show recordtype optons to select for the selected object
    handleNewRecordCreation() {
        console.log(" this.recordTypeId ", this.recordTypeId);
        if (this.recordTypeOptions !== undefined && this.recordTypeOptions.length > 0) {
            this.recordTypeId = '';
            this.selectRecordType = true;
            this.openCreateRecord = false;
        } else {
            this.handleNext();
        }
    }

    // open modal pop up to create new records
    handleNext() {
        console.log(" this.recordTypeId ", this.recordTypeId);
        if (this.recordTypeId !== '') {
            this.selectRecordType = false;
            this.openCreateRecord = true;
        } else
            this.isError = true;
    }

    handleSubmit() {
        this.template.querySelector('lightning-record-form').submit();
        this.isError = false;
    }

    handleSuccess(event) {
        this.selectRecordType = false;
        this.openCreateRecord = false;
        this.isError = false;
        this.dispatchEvent(
            new ShowToastEvent({
                title: 'Success',
                message: `Record saved successfully with id: ${event.detail.id}`,
                variant: 'success',
            }),
        )
    }

    handleError() {
        this.dispatchEvent(
            new ShowToastEvent({
                title: 'Error',
                message: 'Error saving the record',
                variant: 'error',
            }),
        )
    }

    closeModal() {
        this.isError = false;
        this.recordTypeOptions = '';
        this.recordTypeId = '';
        this.selectRecordType = false;
        this.openCreateRecord = false;
    }
}

polymorphicLookUp.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>
    <masterLabel>Custom Polymorphic LookUp Field</masterLabel>
    <targets>
        <target>lightning__AppPage</target>
        <target>lightning__HomePage</target>
        <target>lightning__RecordPage</target>
        <target>lightning__Tab</target>
    </targets>
</LightningComponentBundle>

So this is how we can create a custom polymorphic field in Salesforce using LWC .

Components Used:
1. Lightning Web Components
  • polymorphicLookup.cmp
  • polymorphiSelectObject.cmp
  • polymorphicSearchRecords.cmp
2. Apex Classes
  • polymorphicLookupObjects.cls
  • searchLookupResults.cls
  • genericLookupCompWrapper.cls
3. Custom Metadata
  • PolymorphicLookupObject__mdt

Component Structure:

Lightning Component skeleton and structure

Component View:
Object Dropdown List

In the above screen you can see we have four objects as an example to select. Account , Contact and Case are the standard objects whereas Student is a custom object.

We can select any object from the drop down and then from the selected object we can lookup the records from the lookup box to the right. 

We also have a 'New Record' feature whose visibility can be handled or controlled from custom metadata record itself.
Records dropdown to select
In the above screen we can see the lookup records of Student__c Object to select on focusing on the lookup search box.

Steps to be followed to use the custom polymorphic field functionality

Step 1: Create custom metadata records as follows
go to Setup => Type Custom Metadata => Click PolymorphicLookupObject => Click Manage PolymorphicLookup Objects=> Click New and fill data as below

Custom Metadata Record creation

Similarly for other objects as required

Step 2: Go to App Launcher and search for Polymorphic Lookup Tab

*I have made a tab for this demo. We can use it where we we want by calling the parent component and creating the CMDT records.

Step 3: Select the Object from dropdown
Select object from dropdown list

If we select Account we can see the records but we cannot see New Record button . this is because we created the CMDT record for Account with IsCreateNewRecordAllowed__c as False
Example :- Account Object
Select record from dropdown list

If we select any other object we can see the records as well as New Record button . This is because we created the CMDT records for all other objects with IsCreateNewRecordAllowed__c as True.
Example :- Student Object
Select record from dropdown list

Step 4: Select the Record from dropdown and we are done.


Source code :- Git Hub Link

Demo :-

Resources:


Comments

Popular posts from this blog

How to update Field Level Security in bulk ?

Assign Field Level Security(FLS) in Salesforce Demo:- In this blog we will see the easy way to update Field Level Security(FLS) for Permission sets/Profiles in bulk. I have created a tool to make the Admins job a little easier. Before jumping to tool let us see the different standard approach we follow to update FLS in Salesforce.                     As an admin we usually update FLS from salesforce setup/UI. If we are working on a new Application with many Permission sets and Objects with 100+ fields it can be very time consuming task. Using this tool we can assign FLS in just few mins. Let us take an example. Suppose we have a new application and we have created 20 Permission sets and 8 Profiles . Now there are 10 Objects in all and in each object we have 15 fields for which we need to update FLS for above Permission sets and Profiles. Let us first see the different approach to assign FLS in Salesforce. We have two ways in which we can update FLS for a particular field.  1 .To upd

How to use Hyperlink in Custom Toast Notification message using LWC

  Use Hyperlink in Custom Toast Notification message using LWC   We all know that a component can send a toast notification that pops up to alert users of a success, error, or warning. A toast can also simply provide information to the user. But what if we need a hyperlink on the message to navigate to the records on the message body. Yes, we can put Links as well on the message body as we can see in Standard Notification Toasts. This is very simple just we need to remember few things. We should know how to use Navigation Services by using NavigationMixin.GenerateUrl method. Let us see the functionality in this blog. To display a toast notification in Lightning Experience or Experience Builder sites we import ShowToastEvent from the lightning/platformShowToastEvent module. We use this ShowToastEvent to provide feedback to a user following an action, such as after a record is created. Now in this blog we will see how to add hyperlink in the message of the Show Toast event and navig