Hello everyone! In this post, we’ll explore a code example that helps you to build a component with client-side search and date filter. So, let’s dive right in without any delay! 

Data table with client-side search and date filter in LWC

To implement this example, I have used the Opportunity object. I have shown all related opportunities to a particular account record and then added this component to the account record page to pull the related opportunities record on the UI.

OpportunityController.apxc

 public with sharing class OpportunityController {
    
    @AuraEnabled
    public static OpportunityResponseWrapper fetchAccountOpportunities(Id recordId){
        try{
            List<OpportunityWrapper> allOpportunity=new List<OpportunityWrapper>();
            MAP<String,Object> binds=new MAP<String,Object>{'accountId'=>recordId};
            String sQuery='SELECT Id,Name,AccountId,Account.Name,StageName,CloseDate,Amount,CreatedDate '+
            			  'FROM Opportunity WHERE accountId = :accountId';
            List<Opportunity> opps = Database.queryWithBinds(sQuery,binds,AccessLevel.SYSTEM_MODE);
            for(Opportunity opp:opps){
                allOpportunity.add(bindOppData(opp));
            }
            allOpportunity.sort();
            return new OpportunityResponseWrapper(true,allOpportunity.size(),allOpportunity,null);
        }
        catch(Exception ex){
            return new OpportunityResponseWrapper(
                false,
                0,
                new List<OpportunityWrapper>(),
                ex.getMessage()
            );
        }
    }
    
    public static OpportunityWrapper bindOppData(Opportunity opp){
        OpportunityWrapper obj=new OpportunityWrapper();
        obj.id=opp.Id;
        obj.oppName=opp.Name;
        obj.accountId=opp.AccountId;
        obj.accountName=opp.Account.Name;
        obj.stageName=opp.StageName;
        obj.amount=opp.Amount;
        obj.closedDate=opp.CloseDate;
        obj.createdDate=opp.CreatedDate;
        return obj;
    }
    
    public class OpportunityResponseWrapper{
        @AuraEnabled public boolean success;
        @AuraEnabled public integer totalcount;
        @AuraEnabled public List<OpportunityWrapper> data;
        @AuraEnabled public string errorMessage;
        public OpportunityResponseWrapper(boolean success,integer totalcount,
                                          List<OpportunityWrapper> data,string errorMessage){
                                              this.success=success;
                                              this.totalcount=totalcount;
                                              this.data=data;
                                              this.errorMessage=errorMessage;                                              
                                          }
    }
    
    public class OpportunityWrapper implements comparable{
        @AuraEnabled public string id;
        @AuraEnabled public string oppName;
        @AuraEnabled public string accountId;
        @AuraEnabled public string accountName;
        @AuraEnabled public string stageName;
        @AuraEnabled public decimal amount;
        @AuraEnabled public date closedDate;
        @AuraEnabled public datetime createdDate;
        
        public Integer compareTo(Object compareTo) {
            OpportunityWrapper compareToOppy = (OpportunityWrapper)compareTo;              
            if (this.createdDate == compareToOppy.createdDate) return 0;
            return this.createdDate > compareToOppy.createdDate ? -1 : 1;
        }        
    }
 }

OppListLWC.html

 <template>
    <lightning-card title={cardTitle} icon-name={cardIcon}>
        <template lwc:if={isLoading}>
            <div class="slds-is-relative loading-container">
                <lightning-spinner alternative-text="Loading" size="medium"></lightning-spinner>
            </div>
        </template>

        <template lwc:if={showContent}>
            <div slot="actions">
                <lightning-button-icon icon-name="utility:refresh" alternative-text="Refresh" title="Refresh"
                    onclick={handleRefresh} class="refresh-button">
                </lightning-button-icon>
            </div>
            <!--Filters Section-->
            <div class="slds-m-around_medium filter-section">
                <div class="slds-grid slds-gutters slds-wrap">
                    <div class="slds-col slds-size_1-of-4">
                        <!--Search-->
                        <lightning-input type="search" label="Search" value={searchTerm} onchange={handleSearch}
                            placeholder="Search by opportunity,account and stage..." aria-label="Search..">
                        </lightning-input>
                    </div>
                    <div class="slds-col slds-size_1-of-4">
                        <!--Start Date-->
                        <lightning-input type="date" label="Start Date" value={startDate}
                            onchange={handleStartDateChange} max={endDate} aria-label="Filter by start date">
                        </lightning-input>
                    </div>
                    <div class="slds-col slds-size_1-of-4">
                        <!--End Date-->
                        <lightning-input type="date" label="End Date" value={endDate} onchange={handleEndDateChange}
                            min={startDate} aria-label="Filter by end date">
                        </lightning-input>
                    </div>
                    <div class="slds-col slds-size_1-of-4">
                        <!--Clear Dates Button-->
                        <template lwc:if={hasDateRange}>
                            <div class="slds-m-top_small slds-m-bottom_x-small" style="margin-top: 24px">
                                <lightning-button label="Clear Date Range" onclick={handleClearDateRange}
                                    variant="neutral" icon-name="utility:clear" icon-position="left"
                                    class="clear-date-button">
                                </lightning-button>
                            </div>
                        </template>
                    </div>
                </div>
                <div class="slds-grid slds-gutters slds-wrap">
                    <div class="slds-col slds-size_1-of-4">
                        <!--Close Start Date-->
                        <lightning-input type="date" label="Close Start Date" value={closestartDate}
                            onchange={handleCloseStartDateChange} max={closeendDate}
                            aria-label="Filter by close start date">
                        </lightning-input>
                    </div>
                    <div class="slds-col slds-size_1-of-4">
                        <!--Close End Date-->
                        <lightning-input type="date" label="Close End Date" value={closeendDate}
                            onchange={handleCloseEndDateChange} min={closestartDate}
                            aria-label="Filter by close end date">
                        </lightning-input>
                    </div>
                    <div class="slds-col slds-size_1-of-4">
                        <!--Clear Close Dates Button-->
                        <template lwc:if={hasCloseDateRange}>
                            <div class="slds-m-top_small slds-m-bottom_x-small" style="margin-top: 24px">
                                <lightning-button label="Clear Close Date Range" onclick={handleClearCloseDateRange}
                                    variant="neutral" icon-name="utility:clear" icon-position="left"
                                    class="clear-date-button">
                                </lightning-button>
                            </div>
                        </template>
                    </div>
                    <div class="slds-col slds-size_1-of-4">
                    </div>
                </div>
                <!--Summery Text-->
                <div class="slds-m-top_small">
                    <p class="slds-text-body_small slds-text-color-weak summary-text">{summaryText}</p>
                </div>
            </div>
            <!--Data Table-->
            <template lwc:if={hasData}>
                <div class="datatable-container">
                    <lightning-datatable key-field="id" data={displayData} columns={columns} hide-checkbox-column
                        show-row-number-column onrowaction={handleRowAction} onsort={handleSort} sorted-by={sortBy}
                        sorted-direction={sortedDirection}>
                    </lightning-datatable>
                </div>
                <!--Pagination-->
                <div class="slds-m-around_medium pagination-container">
                    <div class="slds-grid slds-grid_align-spread slds-grid_vertical-align-center">
                        <div class="slds-col">
                            <p class="slds-text-body_small page-info">{pageInfo}</p>
                        </div>
                        <div class="slds-col">
                            <lightning-button-group>
                                <lightning-button label="Previous" icon-name="utility:chevronleft"
                                    onclick={handlePreviousPage} disabled={isFirstPage}
                                    aria-label="Go to previous page">
                                </lightning-button>
                                <lightning-button label="Next" icon-name="utility:chevronright" icon-position="right"
                                    onclick={handleNextPage} disabled={isLastPage} aria-label="Go to next page">
                                </lightning-button>
                            </lightning-button-group>
                        </div>
                    </div>
                </div>
            </template>

            <!--No Data Message-->
            <template lwc:else>
                <div class="slds-m-around_large slds-text-align_center no-data-container">
                    <lightning-icon icon-name="utility:info" size="large" alternative-text="No data"
                        class="no-data-icon">
                    </lightning-icon>
                    <p class="slds-m-top_medium slds-text-heading_small no-data-text">
                        No comments found
                    </p>
                    <p class="slds-m-top_small slds-text-color_weak no-data-subtext">
                        Try adjusting your filters or add some files and comments to this record.
                    </p>
                </div>
            </template>
        </template>
    </lightning-card>
 </template>

OppListLWC.js

 import { LightningElement, api } from 'lwc';
 import { ShowToastEvent } from 'lightning/platformShowToastEvent';
 import { NavigationMixin } from 'lightning/navigation';
 import fetchAccountOpportunities from '@salesforce/apex/OpportunityController.fetchAccountOpportunities';

 export default class OppListlwc extends NavigationMixin(LightningElement) {
    @api recordId;
    @api pageSize = 4;
    cardTitle = 'Opportunity List';
    cardIcon = 'standard:file';
    isLoading = false;
    commentsData = [];
    allData = [];
    displayData = [];

    currentPage = 1;
    totalCount = 0;
    error;

    searchTerm = '';
    startDate = null;
    endDate = null;
    closestartDate = null;
    closeendDate = null;
    sortedBy;
    sortedDirection = 'asc';
    //Memoization cache
    _filteredDataCache = null;
    _cacheKey = '';

    columns = [
        {
            label: 'Opportunity Name', fieldName: 'oppName', type: 'button',
            typeAttributes: {
                label: { fieldName: 'oppName' },
                name: 'view_record',
                variant: 'base'
            }
        },
        {
            label: 'Account Name', fieldName: 'accountName', type: 'button',
            typeAttributes: {
                label: { fieldName: 'accountName' },
                name: 'view_account',
                variant: 'base'
            }
        },
        { label: 'Stage', fieldName: 'stageName', type: 'text', sortable: true },
        {
            label: 'Amount', fieldName: 'amount', type: 'currency',
            typeAttributes: {
                currencyCode: 'USD',
                step: '0.01'
            },
            sortable: true
        },
        {
            label: 'Close Date', fieldName: 'closedDate', type: 'date',
            typeAttributes: {
                year: 'numeric',
                month: 'short',
                day: '2-digit'
            },
            sortable: true
        },
        {
            label: 'Created Date', fieldName: 'createdDate', type: 'date',
            typeAttributes: {
                year: 'numeric',
                month: 'short',
                day: '2-digit',
                hour: '2-digit',
                minute: '2-digit'
            },
            sortable: true
        }
    ];

    handleRowAction(event) {
        const actionName = event.detail.action.name;
        const row = event.detail.row;

        const recordIdToOpen = actionName === 'view_record'
            ? row.id
            : (actionName === 'view_account' ? row.accountId : null);

        if (recordIdToOpen) {
            this[NavigationMixin.Navigate]({
                type: 'standard__recordPage',
                attributes: {
                    recordId: recordIdToOpen,
                    actionName: 'view'
                }
            });
        }
    }

    connectedCallback() {
        this.loadData();
    }

    async loadData() {
        this.isLoading = true;
        this.error = undefined;
        try {
            const response = await fetchAccountOpportunities({
                recordId: this.recordId
            });
            this.handleResponse(response);
        } catch (error) {
            this.error = error.body?.message || error.message;
            this.showToast('Error', this.error, 'error');
            // Reset data on error to prevent stale data
            this.allData = [];
            this.displayData = [];
        } finally {
            this.isLoading = false;
        }
    }

    handleResponse(response) {
        if (response.success) {
            this.totalCount = response.totalCount;
            // Transform data once
            this.allData = response.data;
            this.invalidateCache();
            this.updateDisplayData();
        } else {
            this.error = response.errorMessage || 'An error occurred while loading data';
            this.showToast('Error', this.error, 'error');
            this.allData = [];
            this.displayData = [];
        }
    }

    handleSort(event) {
        const { fieldName, sortDirection } = event.detail;
        this.sortedBy = fieldName;
        this.sortedDirection = sortDirection;
        this.invalidateCache();
        this.updateDisplayData();
    }

    get filteredData() {
        const cacheKey = this.getCacheKey();
        if (this._cacheKey === cacheKey && this._filteredDataCache) {
            return this._filteredDataCache;
        }
        let filtered = [...this.allData];

        // Text search filter        
        if (this.searchTerm) {
            const searchLower = this.searchTerm.toLowerCase();
            filtered = filtered.filter(item => this.matchesSearchTerm(item, searchLower));
        }
        
        // Date range filter
        if (this.startDate || this.endDate) {
            filtered = filtered.filter(item => this.isWithinDateRange(item));            
        }

        // Close Date range filter
        if (this.closestartDate || this.closeendDate) {           
            filtered = filtered.filter(item => this.isCloseDateWithinDateRange(item));
        }

        // Apply sorting
        if (this.sortedBy) {
            filtered = this.sortData(filtered, this.sortedBy, this.sortedDirection);
        }

        // Cache the result
        this._filteredDataCache = filtered;
        //this._cacheKey = cacheKey;

        return filtered;
    }

    getCacheKey() {
        return `${this.searchTerm}|${this.startDate}|${this.endDate}|${this.closestartDate}|${this.closeendDate}|${this.sortedBy}|${this.sortedDirection}|${this.allData.length}`;
    }

    invalidateCache() {
        this._filteredDataCache = null;
        this._cacheKey = '';
    }

    matchesSearchTerm(item, searchLower) {
        return (
            item.oppName?.toLowerCase().includes(searchLower) ||
            item.accountName?.toLowerCase().includes(searchLower) ||
            item.stageName?.toLowerCase().includes(searchLower)          
        );
    }

    isWithinDateRange(item) {
        if (!item.createdDate) return false;
        const itemDate = new Date(item.createdDate);
        const itemDateStr = `${itemDate.getFullYear()}-${String(itemDate.getMonth() + 1)
            .padStart(2, '0')}-${String(itemDate.getDate()).padStart(2, '0')}`;
        return (!this.startDate || itemDateStr >= this.startDate) &&
            (!this.endDate || itemDateStr <= this.endDate);
    }

    isCloseDateWithinDateRange(item) {
        if (!item.closedDate) return false;
        const itemDate = new Date(item.closedDate);
        const itemDateStr = `${itemDate.getFullYear()}-${String(itemDate.getMonth() + 1)
            .padStart(2, '0')}-${String(itemDate.getDate()).padStart(2, '0')}`;
        return (!this.closestartDate || itemDateStr >= this.closestartDate) &&
            (!this.closeendDate || itemDateStr <= this.closeendDate);
    }

    //Consolidated sorting logic
    sortData(data, fieldName, direction) {
        const sortedData = [...data];
        const sortField = fieldName;

        sortedData.sort((a, b) => {
            let valueA = a[sortField] || '';
            let valueB = b[sortField] || '';

            if (sortField === 'createdDate') {
                valueA = new Date(valueA).getTime();
                valueB = new Date(valueB).getTime();
            } else {
                valueA = String(valueA).toLowerCase();
                valueB = String(valueB).toLowerCase();
            }

            if (valueA < valueB) return direction === 'asc' ? -1 : 1;
            if (valueA > valueB) return direction === 'asc' ? 1 : -1;
            return 0;
        });

        return sortedData;
    }

    get hasData() {
        return this.displayData?.length > 0;
    }

    get summaryText() {
        const filtered = this.filteredData;
        let summary = `Showing ${this.displayData.length} of ${filtered.length} items`;

        if (this.startDate || this.endDate) {
            summary += ' | Date Range: ';
            if (this.startDate && this.endDate) {
                summary += `${this.formatDate(this.startDate)} to ${this.formatDate(this.endDate)}`;
            } else if (this.startDate) {
                summary += `From ${this.formatDate(this.startDate)}`;
            } else {
                summary += `Until ${this.formatDate(this.endDate)}`;
            }
        }
        if (this.closestartDate || this.closeendDate) {
            summary += ' | Close Date Range: ';
            if (this.closestartDate && this.closeendDate) {
                summary += `${this.formatDate(this.closestartDate)} to ${this.formatDate(this.closeendDate)}`;
            } else if (this.closestartDate) {
                summary += `From ${this.formatDate(this.closestartDate)}`;
            } else {
                summary += `Until ${this.formatDate(this.closeendDate)}`;
            }
        }
        return summary;
    }

    get totalPages() {
        return Math.ceil(this.filteredData.length / this.pageSize);
    }

    get pageInfo() {
        const filtered = this.filteredData;
        const start = (this.currentPage - 1) * this.pageSize + 1;
        const end = Math.min(this.currentPage * this.pageSize, filtered.length);
        return `${start}-${end} of ${filtered.length}`;
    }

    get isFirstPage() {
        return this.currentPage === 1;
    }

    get isLastPage() {
        return this.currentPage >= this.totalPages;
    }

    handlePreviousPage() {
        if (!this.isFirstPage) {
            this.currentPage--;
            this.updateDisplayData();
        }
    }

    handleNextPage() {
        if (!this.isLastPage) {
            this.currentPage++;
            this.updateDisplayData();
        }
    }

    updateDisplayData() {
        const filtered = this.filteredData;
        const start = (this.currentPage - 1) * this.pageSize;
        const end = start + this.pageSize;
        this.displayData = filtered.slice(start, end);
    }

    get showContent() {
        return !this.isLoading && !this.error;
    }

    get isDateRangeEmpty() {
        return !this.startDate && !this.endDate;
    }

    get isCloseDateRangeEmpty() {
        return !this.closestartDate && !this.closeendDate;
    }

    get hasDateRange() {
        return !this.isDateRangeEmpty;
    }

    get hasCloseDateRange(){
        return !this.isCloseDateRangeEmpty;
    }

    formatDate(dateStr) {
        if (!dateStr) return '';
        return new Date(dateStr).toLocaleDateString('en-US', {
            year: 'numeric',
            month: 'short',
            day: 'numeric'
        });
    }

    handleSearch(event) {
        this.searchTerm = event.target.value;
        this.currentPage = 1;
        this.invalidateCache();
        this.updateDisplayData();
    }

    handleStartDateChange(event) {
        this.startDate = event.target.value;
        this.currentPage = 1;
        this.invalidateCache();
        this.updateDisplayData();
    }

    handleEndDateChange(event) {
        this.endDate = event.target.value;
        this.currentPage = 1;
        this.invalidateCache();
        this.updateDisplayData();
    }

    handleClearDateRange() {
        this.startDate = null;
        this.endDate = null;
        this.currentPage = 1;
        this.invalidateCache();
        this.updateDisplayData();
    }

    handleCloseStartDateChange(event) {
        this.closestartDate = event.target.value;
        this.currentPage = 1;
        this.invalidateCache();
        this.updateDisplayData();
    }

    handleCloseEndDateChange(event) {
        this.closeendDate = event.target.value;
        this.currentPage = 1;
        this.invalidateCache();
        this.updateDisplayData();
    }

    handleClearCloseDateRange() {
        this.closestartDate = null;
        this.closeendDate = null;
        this.currentPage = 1;
        this.invalidateCache();
        this.updateDisplayData();
    }

    handleRefresh() {
        this.isLoading = true;
        this.error = undefined;
        this.invalidateCache();
        this.loadData();
        this.showToast('success', 'Success', 'Data has been refreshed.');
    }

    showToast(strVarient, strTitle, strMag) {
        const evt = new ShowToastEvent({
            title: strTitle,
            message: strMag,
            variant: strVarient,
            mode: 'dismissable'
        });
        this.dispatchEvent(evt);
    }
 }

OppListLWC.js-meta.xml

 <?xml version="1.0" encoding="UTF-8"?>
 <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>66.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>Opportunity List</masterLabel>
    <targets>        
        <target>lightning__RecordPage</target>        
    </targets>
 </LightningComponentBundle>
Output:



Hope you like this post. For any feedback or suggestions, please feel free to comment. I would appreciate your feedback and suggestions.

Thank you for your support.