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:Thank you for your support.


0 Comments
Post a Comment