Lazy loading is an optimization technique to load the content on-demand. Instead of loading the entire data and rendering it to the user in one go as in bulk loading, the concept of lazy loading assists in loading only the required section and delays the remaining, until it is needed by the user.
Managing and visualizing large datasets is a critical requirement in modern applications, especially in Salesforce. Lightning Web Components (LWC empower developers to create dynamic, scalable user interfaces with minimal latency. This article delves into building a robust LWC for dynamic list views and implementing lazy loading for seamless record navigation.
Efficiently managing data in Salesforce is crucial for delivering a seamless user experience, especially when dealing with large datasets. This blog dives into an innovative solution combining SObject selection and lazy loading in Salesforce Lightning Web Components (LWC). The approach empowers users to dynamically choose objects, view relevant records, and optimize page performance—all without compromising on speed or functionality.
What is Lazy Loading in Salesforce?
- Speeds up page load times by fetching only what’s immediately required.
- Reduces system resource usage, ensuring smoother performance.
- Enhances user experience, particularly for users navigating through extensive record lists.
Salesforce users often work with multiple types of objects (SObjects), such as Accounts, Contacts, or custom objects. By integrating SObject selection with lazy loading, you achieve:
- Dynamic Flexibility: Users can switch between objects seamlessly without needing separate components for each object type.
- Improved Performance: Only relevant data is loaded for the selected object, keeping the interface responsive.
- Enhanced Productivity: A unified, efficient interface reduces navigation time and effort for end-users.
Lazy loading in Lightning Web Component
In this article, we’ll explore how to implement lazy loading in Salesforce Lightning Web Components (LWC), specifically using a lightning-datatable with infinite scrolling.
Key Attributes for Lazy Loading in lightning-datatable:
- enable-infinite-loading: Enables partial data loading and triggers more data retrieval when users scroll to the end of the table.
- load-more-offset: Determines when to trigger infinite loading, based on how close the scroll position is to the table’s bottom. The default is 20 pixels.
- onloadmore: The event handler that triggers when more data is required for infinite scrolling.
SObject Selection:
- Purpose: Enables the user to select a Salesforce object, such as Account, Opportunity, or Contact.
- UI Element: A lightning-combobox serves as a dropdown menu for selecting the desired SObject.
- Condition: The dropdown is shown only when an SObject has not been selected (if:false={sobject}).
SObject Selected View:
- After the user selects an SObject, the UI changes dynamically to show additional options and data.
- Back Button: A lightning-button allows users to go back and deselect the SObject, resetting the view.
- Add Column Button: Provides a way to open a modal window where users can choose which fields to display in the datatable.
Column Picker Modal:
- Purpose: Offers a user-friendly interface for selecting fields to display as columns in the datatable.
- Behavior: The modal appears only when the “Add Column” button is clicked (if:true={showColumnPicker}).
- Behavior: The modal appears only when the “Add Column” button is clicked (if:true={showColumnPicker}).
- Header: Displays the title “Select Fields for Columns.”
- Dual Listbox: A lightning-dual-listbox shows available fields on the left and selected fields on the right. Users can move fields between the two lists.
- Footer: Contains “Save” and “Cancel” buttons to confirm or dismiss the selection.
List View Selection:
- Purpose: Allows users to choose a predefined list view for the selected SObject (e.g., “Recently Viewed”).
- UI Element: Another lightning-combobox is used to display list view options.
Datatable for Records:
- Purpose: Displays records for the selected SObject and list view in a tabular format.
- Displays dynamic columns based on the fields selected in the column picker.
- Includes action buttons (like edit, delete, or view) for each record.
- Supports lazy loading, which loads additional records as the user scrolls.
- Condition: The datatable is shown only when records are available (if:true={records}).
html Code:
<template>
<!– SObject Selection –>
<template if:false={sobject}>
<lightning-combobox
class=”sObjctdropdown”
label=”Select SObject”
value={value}
placeholder=”Select an Object”
options={options}
onchange={handleChange}>
</lightning-combobox>
</template>
<!– SObject is selected –>
<template if:true={sobject}>
<!– Back button to deselect SObject –>
<lightning-button
label=”Back”
variant=”neutral”
class=”slds-m-bottom_medium”
title=”Close”
onclick={BackClick}>
</lightning-button>
<!– Add Column Button –>
<div class=”selectButtons”>
<lightning-button
label=”Add Column”
class=”slds-m-top_medium”
onclick={openColumnPicker}>
</lightning-button>
</div>
<!– Column Picker Modal –>
<template if:true={showColumnPicker}>
<section role=”dialog” tabindex=”-1″ class=”slds-modal slds-fade-in-open”>
<div class=”slds-modal__container”>
<header class=”slds-modal__header”>
<button class=”slds-button slds-button_icon slds-modal__close slds-button_icon-inverse” title=”Close” onclick={closeColumnPicker}>
<lightning-icon icon-name=”utility:close” alternative-text=”close” size=”small”></lightning-icon>
<span class=”slds-assistive-text”>Close</span>
</button>
<h2 class=”slds-text-heading_medium”>Select Fields for Columns</h2>
</header>
<div class=”slds-modal__content slds-p-around_medium”>
<lightning-dual-listbox
name=”fieldSelector”
label=”Select Fields”
source-label=”Available Fields”
selected-label=”Selected Fields”
options={fieldOptions}
value={selectedFields}
onchange={handleSelectedFields}>
</lightning-dual-listbox>
</div>
<footer class=”slds-modal__footer”>
<lightning-button variant=”neutral” label=”Cancel” onclick={closeColumnPicker}></lightning-button>
<lightning-button variant=”brand” label=”Save” onclick={saveSelectedFields}></lightning-button>
</footer>
</div>
</section>
<div class=”slds-backdrop slds-backdrop_open”></div>
</template>
<lightning-combobox
class=”slds-combobox-width”
label=”Available List Views”
value={selectedListView}
placeholder=”Select a List View”
options={listViewOptions}
onchange={handleListViewChange}>
</lightning-combobox>
<!– Records Datatable –>
<template if:true={records}>
<lightning-card title=”Records” icon-name=”custom:custom14″>
<lightning-datatable
key-field=”Id”
data={records}
columns={columns}
onrowaction={handleRowAction}
selected-rows={selectedRows}
onloadmore={handleLoadMore}
is-loading={isLoading}>
<template if:true={isLoading}>
<div class=”spinner-container”>
<lightning-spinner alternative-text=”Loading” size=”small”></lightning-spinner>
</div>
</template>
</lightning-datatable>
</lightning-card>
</template>
</template>
</template>
Js code:
import { LightningElement, track, wire } from ‘lwc’;
import getAllSObjects from ‘@salesforce/apex/SObjectDropdownController.getAllSObjects’;
import getListViewRecords from ‘@salesforce/apex/SObjectDropdownController.getTaskListviewRecord’;
import getListViews from ‘@salesforce/apex/SObjectDropdownController.getListViews’;
import deleteRecord from ‘@salesforce/apex/SObjectDropdownController.DeleteRecord’;
import getFields from ‘@salesforce/apex/SObjectDropdownController.getFields’;
import getTotalRecordCount from ‘@salesforce/apex/SObjectDropdownController.getTotalRecordCount’;
import { ShowToastEvent } from ‘lightning/platformShowToastEvent’;
import { getListUi } from ‘lightning/uiListApi’;
import { refreshApex } from ‘@salesforce/apex’;
export default class SObjectTab extends LightningElement {
@track options = [];
@track value = ”;
@track selectedObjectLabel = ”;
@track listViewOptions = [];
@track selectedListView = ”;
@track records = [];
@track columns = [];
@track sobject = false;
@track error;
@track fields = false;
@track fieldOptions = [];
@track selectedFields = [];
@track showColumnPicker = false;
@track storedMultiPicklistValues = [];
// Lazy loading variables
@track isLoading = false;
@track totalRecords = 0;
@track offset = 0;
@track limit = 25;
@track wiredRecordsResult;
connectedCallback() {
this.fetchSObjects();
}
// Fetch all available SObjects from Apex
fetchSObjects() {
getAllSObjects()
.then(data => {
if (data) {
this.options = Object.keys(data).map(key => ({
label: data[key],
value: key
}));
}
})
.catch(error => {
console.error(‘Error fetching objects:’, error);
});
}
resetLazyLoading(){
this.records = [];
this.offset = 0;
this.totalRecords = 0;
this.isLoading = false;
}
// Handle SObject dropdown change
handleChange(event) {
this.value = event.detail.value;
this.selectedObjectLabel = this.options.find(opt => opt.value === this.value).label;
this.fetchListViewRecords(this.value);
this.resetLazyLoading();
this.fetchgetTotalRecordCount();
}
// Fetch list views for the selected SObject
fetchListViewRecords(sObjectName) {
getListViews({ sObjectApiName: sObjectName })
.then(data => {
if (data) {
this.listViewOptions = data.map(view => ({
label: view.Name,
value: view.Id
}));
this.sobject = true;
} else {
this.listViewOptions = [];
}
})
.catch(error => {
console.error(‘Error fetching list views:’, error);
});
}
// Fetch records for the selected list view
fetchRecords() {
this.isLoading = true;
getListViewRecords({
objectName: this.value,
listViewId: this.selectedListView,
limitSize: this.limit,
pageToken: this.offset
})
.then(result => {
this.records = […this.records, …result];
this.generateColumns();
this.isLoading = false;
})
.catch(error => {
console.error(‘Error fetching records:’, error);
this.isLoading = false;
});
}
async handleLoadMore(event) {
if (!this.wiredRecordsResult.data || this.records.length >= this.wiredRecordsResult.data.records.totalSize) {
return; // No more records to load
}
const { target } = event;
target.isLoading = true;
this.offset += this.limit;
this.fetchMoreRecords(this.selectedListView);
target.isLoading = false;
}
fetchMoreRecords(listViewId) {
this.isLoading = true;
// Fetch records via the wire service (using pagination)
this.wiredListView({ pageSize: this.limit, pageNumber: this.offset / this.limit + 1 });
}
fetchgetTotalRecordCount(objectName) {
getTotalRecordCount({ objectName: objectName })
.then(total => {
this.totalRecords = total;
})
.catch(error => {
console.error(‘Error fetching total record count:’, error);
});
}
// Dynamically generate columns based on selected fields and record keys
generateColumns() {
if (this.records.length > 0) {
this.columns = Object.keys(this.records[0]).map(key => {
let type = ‘text’;
if (key === ‘CloseDate’) {
type = ‘date’;
} else if (key === ‘Amount’) {
type = ‘currency’;
}
return {
label: key.charAt(0).toUpperCase() + key.slice(1),
fieldName: key,
type: type
};
});
this.columns.push({
type: ‘action’,
typeAttributes: { rowActions: this.getRowActions() }
});
} else {
this.columns = [];
}
}
// Define row actions (edit, delete, view)
getRowActions() {
return [
{ label: ‘Edit’, name: ‘edit’ },
{ label: ‘Delete’, name: ‘delete’ },
{ label: ‘View’, name: ‘view’ }
];
}
// Handle row action (Edit, Delete, View)
handleRowAction(event) {
const actionName = event.detail.action.name;
const row = event.detail.row;
switch (actionName) {
case ‘edit’:
window.open(`/lightning/r/${this.value}/${row.Id}/edit`, “_blank”);
break;
case ‘delete’:
this.deleteRecords(row.Id);
break;
case ‘view’:
window.open(`/lightning/r/${this.value}/${row.Id}/view`, “_blank”);
break;
default:
}
}
// Delete a record
deleteRecords(recordId) {
const SObjectName = this.value;
deleteRecord({ RecordId: recordId, SobjectApiName: SObjectName })
.then(() => {
this.records = this.records.filter(record => record.Id !== recordId);
this.showNotification(‘Success’, ‘Record deleted successfully’, ‘success’);
})
.catch(error => {
console.error(‘Error deleting record:’, error);
this.error = error;
});
}
// Open the column picker modal
openColumnPicker() {
this.showColumnPicker = true; // Show the modal
this.fetchFields(); // Fetch fields for the selected SObject
}
closeColumnPicker() {
this.showColumnPicker = false;
}
fetchFields() {
getFields({ sObjectApiName: this.value })
.then(data => {
if (data) {
this.fieldOptions = data.map(field => ({
label: field.Label,
value: field.FieldName
}));
}
})
.catch(error => {
console.error(‘Error fetching fields:’, error);
});
}
// Handle the selection of fields for the column picker
handleSelectedFields(event) {
this.selectedFields = event.detail.value; // Store selected fields
}
// Save the selected fields and generate the table columns
saveSelectedFields() {
this.selectedFields = […this.selectedFields];
this.generateColumns();
this.closeColumnPicker();
if(this.records.length >0 && this.selectedFields.length > 0){
this.columns = this.selectedFields.map(field => {
let type= ‘text’;
if (field === ‘CloseDate’) {
type = ‘date’;
} else if (field === ‘Amount’) {
type = ‘currency’;
}
return {
label: field.charAt(0).toUpperCase() + field.slice(1).replace(/([A-Z])/g, ‘ $1’), // Formatting label
fieldName: field,
type: type
};
});
this.columns.push({
type: ‘action’,
typeAttributes: { rowActions: this.getRowActions() }
});
}else{
this.columns = [];
}
}
// Refresh records based on selected list view
handleListViewChange(event) {
this.selectedListView = event.detail.value;
this.resetLazyLoading();
this.fetchRecords();
}
// Show toast notification
showNotification(title, message, variant) {
const evt = new ShowToastEvent({
title: title,
message: message,
variant: variant,
});
this.dispatchEvent(evt);
}
// Handle record selection (if needed for future logic)
handleRowSelection(event) {
this.selectedRows = event.detail.selectedRows;
}
@wire(getListUi, { listViewId: ‘$selectedListView’, objectApiName: ‘$value’})
wiredListView(result) {
this.wiredRecordsResult = result;
const { data, error } = result;
if (data && data.records && data.records.records) {
const records = data.records.records.map(record => {
let recordData = { Id: record.id };
for (const field in record.fields) {
recordData[field] = record.fields[field] ? record.fields[field].value : ”;
}
return recordData;
});
this.records = […this.records, …records,this.isLoading = true];
alert(`this total records is ${this.records.length}`);
this.generateColumns();
this.error = undefined;
} else if (error) {
console.error(‘Error fetching records:’, error);
this.error = error;
this.records = [];
}
}
// Refresh the entire component
BackClick() {
window.location.reload();
this.sobject = false;
}
}
Apex code:
public with sharing class SObjectDropdownController {
@AuraEnabled(cacheable=true)
public static Map<String, String> getAllSObjects() {
Map<String, String> objectMap = new Map<String, String>();
Map<String, Schema.SObjectType> globalDescribe = Schema.getGlobalDescribe();
for (Schema.SObjectType objTyp : globalDescribe.values()) {
Schema.DescribeSObjectResult describeResult = objTyp.getDescribe();
String name = describeResult.getName();
String label = describeResult.getLabel();
if (describeResult.isQueryable() &&
describeResult.isAccessible() &&
!describeResult.isCustomSetting() &&
describeResult.isCreateable() &&
describeResult.isSearchable() &&
describeResult.getRecordTypeInfos().size() > 0)
objectMap.put(name, label);
}
}
return objectMap.isEmpty() ? null : objectMap;
}
@AuraEnabled(cacheable=true)
public static List<Map<String, String>> getListViews(String sObjectApiName) {
List<Map<String, String>> listViewList = new List<Map<String, String>>();
try {
List<ListView> listViews = [SELECT Id, Name FROM ListView WHERE SObjectType = :sObjectApiName];
for (ListView lv : listViews) {
Map<String, String> listViewInfo = new Map<String, String>();
listViewInfo.put(‘Id’, lv.Id);
listViewInfo.put(‘Name’, lv.Name);
listViewList.add(listViewInfo);
}
} catch (Exception e) {
System.debug(‘Error fetching list views: ‘ + e.getMessage());
}
return listViewList;
}
@AuraEnabled(cacheable=true)
public static List<SObject> getRecordsByObjectName(String objectName, List<String> fields, Integer limitSize, Integer offsetValue) {
String fieldList = String.join(fields, ‘,’);
String query = ‘SELECT ‘ + fieldList + ‘ FROM ‘ + objectName + ‘ LIMIT ‘+ limitSize +’ OFFSET ‘ + offsetValue;
return Database.query(query);
}
@AuraEnabled(cacheable=true)
public static List<sObject> getTaskListviewRecord(String objectName, String listViewId, Integer limitSize, String pageToken) {
List<sObject> result = new List<sObject>();
try {
// Construct endpoint URL
String baseUrl = URL.getOrgDomainUrl().toExternalForm();
String endPointURL = baseUrl + ‘/services/data/v60.0/sobjects/’ + objectName + ‘/listviews/’ + listViewId + ‘/results’;
// Create HTTP request
HttpRequest req = new HttpRequest();
req.setEndpoint(endPointURL);
req.setMethod(‘GET’);
// Add session ID to the request header for authentication
String sessionId = UserInfo.getSessionId();
if (String.isEmpty(sessionId)) {
System.debug(‘Invalid session ID’);
return result;
}
req.setHeader(‘Authorization’, ‘Bearer ‘ + sessionId);
req.setHeader(‘Content-Type’, ‘application/json’);
// Set query parameters for lazy loading (limit and offset)
if (limitSize != null) {
endPointURL += ‘?pageSize=’ + limitSize;
if (pageToken != null) {
endPointURL += ‘&pageToken=’ + pageToken;
}
}
req.setEndpoint(endPointURL);
// Send the HTTP request
Http http = new Http();
HttpResponse res = http.send(req);
// Parse the response and return the records
if (res.getStatusCode() == 200) {
Map<String, Object> responseBody = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
if (responseBody.containsKey(‘records’)) {
List<Object> records = (List<Object>) responseBody.get(‘records’);
Schema.SObjectType sObjectType = Schema.getGlobalDescribe().get(objectName);
for (Object record : records) {
Map<String, Object> recordMap = (Map<String, Object>) record;
sObject sObj = sObjectType.newSObject(); // Create a new instance of the sObject type
for (String fieldName : recordMap.keySet()) {
if (sObjectType.getDescribe().fields.getMap().containsKey(fieldName)) {
sObj.put(fieldName, recordMap.get(fieldName)); // Populate the fields
}
}
result.add(sObj);
}
}
} else {
System.debug(‘Failed to fetch records. Status: ‘ + res.getStatusCode());
System.debug(‘Response Body: ‘ + res.getBody());
}
} catch (Exception ex) {
System.debug(‘Error fetching list view records: ‘ + ex.getMessage());
}
return result;
}
@AuraEnabled(cacheable=true)
public static Integer getTotalRecordCount(String objectName) {
String query = ‘SELECT COUNT(Id) TotalRecords FROM ‘ + objectName;
AggregateResult result = Database.query(query);
Integer totalRecords = (Integer) result.get(‘TotalRecords’);
return totalRecords;
}
@AuraEnabled
public static String DeleteRecord(Id RecordId, String SobjectApiName) {
try {
// Query the record to delete
sObject recordToDelete = Database.query(‘SELECT Id FROM ‘ + SobjectApiName + ‘ WHERE Id = :RecordId LIMIT 1’);
// Delete the record
delete recordToDelete;
// Return success message
return ‘Record deleted successfully’;
} catch (QueryException qe) {
// Handle case where the query fails (e.g., record not found)
return ‘Error: Record not found for deletion’;
} catch (DmlException dmle) {
// Handle any DML exceptions during the delete process
return ‘Error: Unable to delete the record. ‘ + dmle.getMessage();
} catch (Exception e) {
// Handle any other general exceptions
return ‘Error: ‘ + e.getMessage();
}
}
@AuraEnabled(cacheable=true)
public static List<Map<String, String>> getFields(String sObjectApiName) {
List<Map<String, String>> fieldList = new List<Map<String, String>>();
SObjectType sObjectType = Schema.getGlobalDescribe().get(sObjectApiName);
if (sObjectType != null) {
Map<String, Schema.SObjectField> fieldsMap = sObjectType.getDescribe().fields.getMap();
for (Schema.SObjectField field : fieldsMap.values()) {
Schema.DescribeFieldResult fieldDescribe = field.getDescribe();
Map<String, String> fieldInfo = new Map<String, String>();
fieldInfo.put(‘FieldName’, fieldDescribe.getName());
fieldInfo.put(‘Label’, fieldDescribe.getLabel());
fieldList.add(fieldInfo);
}
}
return fieldList; // Return field names and labels
}
}
See how FieldAx can transform your Field Operations.
Try it today! Book Demo
You are one click away from your customized FieldAx Demo!
Conclusion:
The component efficiently handles dynamic SObject selection, lazy loading for performance, and customizable columns for a better user experience. Its modular structure ensures easy maintenance, scalability, and smooth handling of large datasets in Salesforce.