top of page
  • Writer's pictureD. Dewangan

Global, Local Search and Record Filtering in LWC



Hey Salesforce developers! Ever wondered when to use backend vs. JavaScript for searches? Let's break it down simply. Discover the best methods for handling large and small datasets, ensuring real-time updates, and keeping your data secure.

Learn how to combine both approaches for a lightning-fast user experience. Follow our easy guide to filter account records by industry, type, rating, and ownership with multi-select options. Transform your Salesforce apps with clever search strategies and dynamic filtering. Make your LWC projects more powerful and user-friendly today!

Let's break down the comparison between performing a global search (using a backend query) versus a local search (using JavaScript filtering).


Global Search (Backend) vs Local Search (JavaScript)

Criteria

Global Search (Backend)

Local Search (JavaScript)

Performance

Suitable for large datasets as filtering is done at the database level.

Best for smaller datasets due to client-side processing.

Data Integrity

Ensures consistency as all data is fetched from the server.

Depending on client-side data, might not reflect real-time changes.

Network Traffic

Potentially higher due to data transfer over the network.

Minimal once data is loaded initially.

Complexity

Requires backend query implementation and optimization.

Simple JavaScript array filtering.

Real-time Updates

Fetches current data from the server.

Requires refreshing or re-filtering for real-time updates.

Security

Ensures security rules are applied at the server level.

Data visibility and access control must be handled carefully.

Scalability

Scales are better for large datasets with optimized queries.

Limited by client-side processing capabilities.

Initial Load Time

May have a longer initial load time due to network latency.

Faster initial load since data is already available.

Implementation Effort

Requires backend development effort for query optimization.

Simple JavaScript array methods for filtering.

User Experience

Provides consistent search results but might have a slight delay.

Immediate response with minute network delay after initial load.

Use Cases

Best for scenarios where real-time data accuracy is critical.

Suitable when immediate response and interactivity are prioritized.


Here's a tabular comparison to illustrate when to use each approach:

When to Use Each Approach:

  • Global Search (Backend):

    • Use when dealing with large datasets where performance is critical.

    • When ensuring data consistency real-time updates are necessary.

    • When implementing complex search queries or when security rules need to be strictly enforced.

  • Local Search (JavaScript):

    • Suitable for smaller datasets or when immediate interactivity is needed.

    • When avoiding unnecessary network traffic and reducing server load.

    • When the application design allows for client-side processing without compromising user experience or data integrity.

By assessing these aspects, you can determine the most suitable approach to deliver a responsive and secure search experience tailored to your application's requirements.


Hybrid Approach:


In some cases, a hybrid approach may be suitable. Start with a client-side search for immediate user interaction and then refine results or apply more complex filters with a backend call. It balances responsiveness with efficiency, tailored to meet user expectations and application requirements in LWC development.


Example: In a product inventory app using Lightning Web Components, users search for products. As they type, client-side search quickly shows matching products. For complex queries like filtering by category and price range, a backend call refines results, ensuring accuracy with large datasets. This hybrid approach balances instant user interaction with efficient backend processing for comprehensive search capabilities.


Let's now look at how we can filter records in LWC using multi-select and search filter functionality using local search. We will be developing the code to filter accounts based on Industry, Type, Rating, and Ownership. Here’s how we will be developing the functionality:


  1. Create a class to retrieve picklist values. (PicklistValuesController, PicklistValuesProcessor).

  2. Create a class to fetch Account records(AccountWrapper, AccountProcessor, AccountDomain, AccountController).

  3. Create LWC filter child component(multiSelectPicklist).

  4. Create LWC filter parent component(filterAccounts).


PicklistValuesController.cls

public with sharing class PicklistValuesController {

    /*Purpose/Methods: This Method will return Map of String, Object    
      fieldnameToPicklistValues
      @Param :  String objectApiName, List<String> fieldNames
      @return : Map<String, Object>
    */
    @AuraEnabled
    public static Map<String, Object> getPicklistValues(
            String objectApiName,
            List<String> fieldNames
    ) {
        Map<String, Object> picklistNameToValues = 
           PicklistValuesProcessor.getPicklistValues(objectApiName, fieldNames);
        return picklistNameToValues;
    }
}

PicklistValuesProcessor.cls

public class PicklistValuesProcessor {

    /*Purpose/Methods: This Method will return Map of String, Object 
      fieldnameToPicklistValues
      @Param :  String objectApiName, List<String> fieldNames
      @return : Map<String, Object>
    */
    @AuraEnabled
    public static Map<String, Object> getPicklistValues(
            String objectApiName,
            List<String> fieldNames
    ) {
        Map<String, Object> fieldNameToOptions = new Map<String, Object>();
        Schema.SObjectType objectName = Schema.getGlobalDescribe().get(objectApiName) ;
        Schema.DescribeSObjectResult objectDescription = objectName.getDescribe() ;
        Map<String, Schema.SObjectField> fields = objectDescription.fields.getMap() ;
        for (String fieldName : fieldNames) {
            List<String> picklistValues = new List<String>();
            Schema.DescribeFieldResult fieldResult = fields.get(fieldName).getDescribe();
            List<Schema.PicklistEntry> getPicklistValues = fieldResult.getPicklistValues();
            for (Schema.PicklistEntry pickListVal : getPicklistValues) {
                picklistValues.add(pickListVal.getLabel());
            }
            fieldNameToOptions.put(fieldName, picklistValues);
        }
        return fieldNameToOptions;
    }
}

AccountWrapper.cls

public with sharing class AccountWrapper {
    @AuraEnabled
    public Account account;
    public AccountWrapper(Account account) {
        this.account = account;
    }
}

AccountDomain.cls

public class AccountDomain {

    /**
    * This method aims to get Account record
    * @param NA
    * @return List<Account>
    */
    public static List<Account> getAccountRecords() {
        return [
            SELECT
                Id,
                Name,
                Phone,
                Industry,
                Type,
                Rating,
                Ownership
            FROM
                Account
        ];
    }
}

AccountProcessor.cls

public with sharing class AccountProcessor {

    /* This Method aims to get accounts
      @Param :  N/A
      @return : List<AccountWrapper>
    */
    @AuraEnabled
    public static List<AccountWrapper> getWrappedAccounts() {
        List<AccountWrapper> wrappedAccounts = new List<AccountWrapper>();
		for(Account accountObj : AccountDomain.getAccountRecords()) {
                wrappedAccounts.add(new AccountWrapper(accountObj));
        }
        return wrappedAccounts;
    }
}

AccountController.cls

public with sharing class AccountController {
    @AuraEnabled(cacheable=true)
    public static List<AccountWrapper> getAccounts() {
        return AccountProcessor.getWrappedAccounts();
    }
}

MultiSelectPicklist.html

<template>
    <article class="slds-card" part="card">
        <div class="slds-m-left_large slds-m-right_large" onmouseleave={mousehandler}>

            <!-- Below code is for lightning input search box which will filter picklist result based on inputs given by user -->
            <lightning-input type="search" label={label} onchange={handleSearch} value={searchTerm} onblur={blurhandler} onfocusout={focushandler} onclick={clickhandler} placeholder={itemcounts}>
            </lightning-input>

            <!-- Below code is for Select/Clear All function -->
            <div class="slds-grid slds-wrap">
                <template if:true={showselectall}>
                    <div class="slds-col slds-large-size_10-of-12 slds-medium-size_1-of-12 slds-size_1-of-12">
                        <a href="javascript.void(0)" onclick={selectall}>{labels.SELECT_ALL_MESSAGE}</a>
                    </div>
                    <div class="slds-col slds-large-size_2-of-12 slds-medium-size_1-of-12 slds-size_1-of-12">
                        <div class="slds-float_right">
       
                            <a href="javascript.void(0)" onclick={handleClearall}>{labels.CLEAR_ALL_MESSAGE}</a>
                        </div>
                    </div>
                </template>
                <template if:false={showselectall}>
                    <div class="slds-col slds-large-size_10-of-12 slds-medium-size_1-of-12 slds-size_1-of-12">
                    </div>
                    <div class="slds-col slds-large-size_2-of-12 slds-medium-size_1-of-12 slds-size_1-of-12">
                        <div class="slds-float_right">
                            <a href="javascript.void(0)" onclick={handleClearall}>{labels.CLEAR_ALL_MESSAGE}</a>
                        </div>
                    </div>
                </template>
            </div>

            <!-- Below code will show dropdown picklist -->
            <template if:true={showDropdown}>
                <div class="slds-box_border">
                    <ul class="dropdown-list slds-dropdown_length-7 slds-p-left_medium dropdown-item">
                        <template for:each={filteredResults} for:item="option">
                            <li key={option.Id}>
                                <lightning-input type="checkbox" checked={option.isChecked} label={option.Name} value={option.Id} onchange={handleSelection}>
                                </lightning-input>
                            </li>
                        </template>
                    </ul>
                </div>
            </template>

            <!-- Below code will show selected options from picklist in pills -->
            <div class="selection-summary">
                <div class="slds-p-around_x-small ">
                    <template for:each={selectedItems} for:item="selectedItem">
                        <lightning-pill key={selectedItem.Id} label={selectedItem.Name} name={selectedItem.Id} onremove={handleRemove}>
                        </lightning-pill>
                    </template>
                </div>
            </div>
        </div>
    </article>
</template>


multiSelectPicklist.js

import {LightningElement, track, api} from 'lwc';
import SELECT_ALL_MESSAGE from '@salesforce/label/c.SELECT_ALL_MESSAGE';
import CLEAR_ALL_MESSAGE from '@salesforce/label/c.CLEAR_ALL_MESSAGE';
import SELECTED_MESSAGE from '@salesforce/label/c.SELECTED_MESSAGE';

export default class multiSelectPicklist extends LightningElement {

    @track allValues = []; 
    // this will store end result or selected values from picklist
    selectedObject = false;
    valuesSelected = undefined;    
    showDropdown = false;
    itemcounts = '';
    showselectall = true;
    errors;
    searchKey = '';
    mouse;
    focus;
    blurred;
    labels = {
        SELECT_ALL_MESSAGE,
        CLEAR_ALL_MESSAGE,
        SELECTED_MESSAGE
    };

    @api options;
    @api label;
    @api selectedItems = [];
    @api clearall() {
        this.handleClearall(event);
    }

    //this function is used to filter the dropdown list based on user input
    handleSearch(event) {
        this.searchKey = event.target.value;
        this.showDropdown = true;
        this.mouse = false;
        this.focus = false;
        this.blurred = false;
    }

    //this function is used to show the dropdown list
    get filteredResults() {
        if (this.valuesSelected == undefined) {
            this.valuesSelected = this.options;
            //convert object to array
            Object.keys(this.valuesSelected).map(option => {
                this.allValues.push({ Id: option, Name: this.valuesSelected[option] });
            })
            this.valuesSelected = this.allValues.sort(function (a, b) { return a.Id - b.Id });
            this.allValues = [];
        }
        //if values not selected
        if (this.valuesSelected != null && this.valuesSelected.length != 0) {
            if (this.valuesSelected) {
                const optionNames = this.selectedItems.map(option => option.Name);
                return this.valuesSelected.map(option => {
                    //below logic is used to show check mark (✓) in dropdown checklist
                    const isChecked = optionNames.includes(option.Name);
                    return {
                        ...option,
                        isChecked
                    };
                }).filter(option =>
                    option.Name.toLowerCase().includes(this.searchKey.toLowerCase())
                ).slice(0, 20);
            } else {
                return [];
            }
        }
    }

    //this function is used when user check/uncheck/selects (✓) an item in dropdown picklist
    async handleSelection(event) {
        const optionId = event.target.value;
        const isChecked = event.target.checked;
        //below logic is used to show check mark (✓) in dropdown checklist
        if (isChecked) {
            const selectedOption = this.valuesSelected.find(option => option.Id === optionId);
            if (selectedOption) {
                this.selectedItems = [...this.selectedItems, selectedOption];
                this.allValues.push(optionId);
            }
        } else {
            this.selectedItems = this.selectedItems.filter(option => option.Id !== optionId);
            this.allValues.splice(this.allValues.indexOf(optionId), 1);
        }
        this.itemcounts = this.selectedItems.length > 0 ? this.selectedItems.length + "  " + this.labels.SELECTED_MESSAGE : '';
        if (this.itemcounts == '') {
            this.selectedObject = false;
        } else {
            this.selectedObject = true;
        }
        await this.passSelections();
    }

    //custom function used to close/open dropdown picklist
    clickhandler(event) {
        this.mouse = false;
        this.showDropdown = true;

        this.clickHandle = true;
        this.showselectall = true;
    }

    mousehandler(event) {
        this.mouse = true;
        this.dropdownclose();
    }

    blurhandler(event) {
        this.blurred = true;
        this.dropdownclose();
    }

    focushandler(event) {
        this.focus = true;
    }

    dropdownclose() {
        if (this.mouse == true && this.blurred == true && this.focus == true) {
            this.showDropdown = false;
            this.clickHandle = false;
        }
    }

    //this function is invoked when user deselect/remove (✓) items from dropdown picklist
    async handleRemove(event) {
        const valueRemoved = event.target.name;
        this.selectedItems = this.selectedItems.filter(option => option.Id !== valueRemoved);
        this.allValues.splice(this.allValues.indexOf(valueRemoved), 1);
        this.itemcounts = this.selectedItems.length > 0 ? `${this.selectedItems.length}` + "  " + this.labels.SELECTED_MESSAGE : '';
        if (this.itemcounts == '') {
            this.selectedObject = false;
        } else {
            this.selectedObject = true;
        }
        await this.passSelections();
    }

    //this function is used to deselect/uncheck (✓) all of the items in dropdown picklist
    handleClearall(event) {
        event.preventDefault();
        this.showDropdown = false;
        this.selectedItems = [];
        this.allValues = [];
        this.itemcounts = '';
        this.selectedObject = false;
        this.passSelections();
    }

    //this function is used to select/check (✓) all of the items in dropdown picklist
    selectall(event) {
        event.preventDefault();
        if (this.valuesSelected == undefined) {
            this.valuesSelected = this.picklistinput;
            //convert object to array
            Object.keys(this.valuesSelected).map(option => {
                this.allValues.push({ Id: option, Name: this.valuesSelected[option] });
            })
            this.valuesSelected = this.allValues.sort(function (a, b) { return a.Id - b.Id });
            this.allValues = [];
        }
        this.selectedItems = this.valuesSelected;
        this.itemcounts = this.selectedItems.length +  "  " + this.labels.SELECTED_MESSAGE ;
        this.allValues = [];
        this.valuesSelected.map((value) => {
            for (let property in value) {
                if (property == 'Id') {
                    this.allValues.push(`${value[property]}`);
                }
            }
        });
        this.passSelections();
        this.selectedObject = true;
    }

    //pass the selected items to the parent
    passSelections() {
        let messageEvent = new CustomEvent('selection', {
            detail : {
                data : this.selectedItems,
                label : this.label
            }
        });
        this.dispatchEvent(messageEvent);
    }    
}

multiSelectPicklist.js-meta.xml

<?xml version="1.0"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
	<apiVersion>57.0</apiVersion>
	<isExposed>true</isExposed>
</LightningComponentBundle>

filterAccounts.html

<template>
    <lightning-card title="Account Filter">
         <lightning-layout multiple-rows="true">
            <lightning-layout-item size="12">
                <lightning-layout horizontal-align="spread" class="slds-grid slds-wrap">
                    <lightning-layout-item size="3" small-device-size="3" class="slds-p-around_x-small">
                        <c-multi-select-picklist label="Industry" options={industryOptions} onselection={addSelections}></c-multi-select-picklist>
                    </lightning-layout-item>
                    <lightning-layout-item size="3" small-device-size="3" class="slds-p-around_x-small">
                        <c-multi-select-picklist label="Type" options={typeOptions} onselection={addSelections}></c-multi-select-picklist>
                    </lightning-layout-item>
                    <lightning-layout-item size="3" small-device-size="3" class="slds-p-around_x-small">
                        <c-multi-select-picklist label="Rating" options={ratingOptions} onselection={addSelections}></c-multi-select-picklist>
                    </lightning-layout-item>
                    <lightning-layout-item size="3" small-device-size="3" class="slds-p-around_x-small">
                        <c-multi-select-picklist label="Ownership" options={ownershipOptions}  onselection={addSelections}></c-multi-select-picklist>
                    </lightning-layout-item>
                </lightning-layout>
            </lightning-layout-item>
        </lightning-layout>
        <!-- Show results/ errors-->
        <lightning-card>
            <template if:true={searchResults.length}>
                <lightning-datatable key-field="Id" data={searchResults} columns={columns} hide-checkbox-column></lightning-datatable>
            </template>
            <template if:true={showError}>
                <div class="slds-text-color_error slds-align_absolute-center">{error}</div>
            </template>
        </lightning-card>
    </lightning-card>
</template>

filterAccounts.js

import {LightningElement, track} from 'lwc';
import picklistOptions from '@salesforce/apex/PicklistValuesController.getPicklistValues';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';
import ACCOUNT_FILTER_HEADING from '@salesforce/label/c.ACCOUNT_FILTER_HEADING';
import NO_RESULTS_MESSAGE from '@salesforce/label/c.NO_RESULTS_MESSAGE';

export default class AccountFilter extends LightningElement {
    industryOptions = [];
    typeOptions = [];
    ratingOptions = [];
    ownershipOptions = [];
    selectedIndustries = [];
    selectedTypes = [];
    selectedRatings = [];
    selectedOwnerships = [];
    allAccounts = [];
    filteredAccounts = [];
    error;
    showError = false;
    labels = {
      ACCOUNT_FILTER_HEADING,
      NO_RESULTS_MESSAGE
    }
    columns = [
        { label: 'Account Name', fieldName: 'Name', type: 'text' },
        { label: 'Industry', fieldName: 'Industry', type: 'text' },
        { label: 'Type', fieldName: 'Type', type: 'text' },
        { label: 'Rating', fieldName: 'Rating', type: 'text' },
        { label: 'Ownership', fieldName: 'Ownership', type: 'text' }
    ];
    filteringfields = ['Industry', 'Type', 'Rating', 'Ownership'];
    @track searchResults = []; //store the search result
    @track selectedValuesByLabel = {}; //stores the latest selectedValues json

    connectedCallback() {
        this.getAccountsData();
        this.setPicklistOptions();
    }

    getAccountsData(){
        getAccounts()
        .then( result => {
          this.filteredAccounts = result;
          this.searchResults = this.filteredAccounts.map(wrapper => wrapper.account);
        })
        .catch( error => {})
    }

    setPicklistOptions() {
        picklistOptions({
        objectApiName: 'Account',
        fieldNames: this.filteringfields
        }).then(result => {
        // Iterate over the keys of the object
        let data = result;
        for (const key in result) {
            if (data.hasOwnProperty(key)) {
                // Iterate over the values of each key
                for (const value of data[key]) {
                    if (`${key}` === 'Industry') {
                    this.industryOptions = data[key];
                    } else if (`${key}` === 'Type') {
                    this.typeOptions = data[key];
                    } else if (`${key}` === 'Rating') {
                    this.ratingOptions = data[key];
                    } else if (`${key}` === 'Ownership') {
                    this.ownershipOptions = data[key];
                    }
                }
            }
        }
    }).catch(error => {
      this.error = error;
    })
  }

   addSelections(event) {
    const { label, selectedValues } = event.detail;
    this.selectedValuesByLabel[label] = event.detail.data;
    // make the search set
    this.makeFilterObject();
  }

  //handles the object formation in [key :[searchkey]] and removing of the childvalues of no [searchkey]
  makeFilterObject() {
    let objectFormat = this.selectedValuesByLabel;
    //stores the searchKey to searchArray
    let filterValues = {};
    for (let key in objectFormat) {
      if (objectFormat.hasOwnProperty(key)) {
        filterValues[`${key}`] = [];
        for (let item of objectFormat[key]) {
          filterValues[`${key}`].push(item.Name);
        }
      }
    }
    filterValues = Object.fromEntries(
      Object.entries(filterValues).filter(([key, value]) => value.length > 0)
    );
    this.performFilter(filterValues);
  }

  //handles filtering of records
  performFilter(filterValues) {
    try {
      let dataToFilter = this.filteredAccounts;
      let filteredRecords = dataToFilter.filter(record => {
      // this.searchResults = dataToFilter.filter(record => {
        return this.filteringfields.every(field => {
          // Convert to lowercase for case-insensitive comparison
          const fieldValue = record.account && record.account[field]
            ? record.account[field].toString().toLowerCase()
            : '';
          const filterValue = filterValues[field];
          if (Array.isArray(filterValue)) {
            return filterValue.some(value => fieldValue.includes(value.toString().toLowerCase()));
          } else if (typeof filterValue === 'string') {
            return fieldValue.includes(filterValue.toLowerCase());
          }
          return true;
        });
      });
      this.searchResults = filteredRecords.map(wrapper => wrapper.account);

      if (this.searchResults.length == 0) {
        this.showError = true;
        this.error = this.labels.NO_RESULTS_MESSAGE;
      }
      else {
        this.error = undefined;
      }
    } catch (error) {
      this.showError = true;
      // Handle any potential errors
      this.error = error.message;
      this.searchResults = [];
    }
  }

}

filterAccounts.js-meta.xml

<?xml version="1.0"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>57.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordPage</target>
    </targets>
</LightningComponentBundle>

Output:

  1. Upon no matching results, No Result Found is shown.

  2. Upon a match, the result is shown in Account Datatable format.

  3. We can Select All to select all filter values.

  4. We can clear filter values.


Custom labels used:

Label

Value

ACCOUNT_FILTER_HEADING

Account Filter

CLEAR_ALL_MESSAGE

Clear

NO_RESULTS_MESSAGE

No Result found.

  SELECT_ALL_MESSAGE

Select All

SELECTED

Selected


Conclusion

By mastering the differences between global and local searches, you can optimize your Salesforce applications for performance and user experience. Using our step-by-step guide, you'll be able to implement efficient and dynamic filtering, ensuring your users have the best possible interaction with your apps. Dive in and start enhancing your LWC projects now!


If you'd like to see the code and resources used in this project, you can access the repository on GitHub.To access the AVENOIRBLOGS repository, click here. Feel free to explore the code and use it as a reference for your projects.


Thank You! You can leave a comment to help me understand how the blog helped you. If you need further assistance, please contact us. You can click "Reach Us" on the website and share the issue with me.


Reference 



Blog Credit:

D. Dewangan

   Salesforce Developer

   Avenoir Technologies Pvt. Ltd.

  Reach us: team@avenoir.ai


Comments


bottom of page