Feature-Rich, Highly Customizable, Reusable Generic Data Table in Lightning Web Component (LWC) Salesforce

Feature-Rich, Highly Customizable, Reusable Generic Data Table in Salesforce Lightning Web Component (LWC)

Introduction:

Are you looking to build a versatile data table in your Salesforce application? Look no further! In this blog, we will explore how to create a powerful, generic data table Lightning Web Component (LWC) in Salesforce. This LWC component will be highly customizable and offer features such as sorting, clickable URLs, and pagination. Let’s dive in!

Component Properties

To build a flexible and reusable data table LWC, we will first define a set of component properties. These properties will allow users to configure the component according to their specific needs. Here’s a list of the properties we will include in our generic data table LWC:

a. Fields Api Names: This property accepts a comma-separated list of field API names to display in the table. Example: “Name,CreatedDate,CustomField__c”

b. Fields Labels: This property accepts a comma-separated list of field column labels to display in the table. Example: “Name,Created Date,Custom Field”

c. sObjectName: This property allows users to specify the API name of the sObject. The name is used to fetch the data. Example: “Account”

d. Url Field: This property accepts a field API name. It will be displayed as a value in the table column. It will redirect to the sObject record when clicked. Example: “Name”

e. Allow Sorting: This property is a boolean value that determines whether sorting should be enabled for the data table.

f. Sorting Columns: This property accepts a comma-separated list of field API names that should be sortable in the table. Example: “Name,CreatedDate,CustomField__c”

g. Page Size Options: This property accepts a comma-separated list of string values for page size options. Example: “5,10,25”

h. Page Size Default Value: This property sets the default page size for the table. Example: “10”

Implementing Features

Now that we have defined our component properties, let’s explore the features we will implement in our generic data table LWC:

  • Clickable URL to redirect to a record: This feature allows users to click on a specified field. They will be redirected to the sObject record. This provides quick and easy access to the record details.
  • Customizable sorting columns: When the Allow sorting on table columns property is checked, users can choose specific sortable columns. This feature gives them more control over their data analysis.
  • Custom page size options: This feature allows users to select the number of rows displayed per page. It provides a more streamlined view of the data.
  • Default page size selection: Users can set a default page size for the table. This ensures a consistent user experience across different tables.
  • Pagination with dynamic page numbers: This feature provides pagination controls that adapt based on the number of pages. If there are less than 10 pages, the component will display all the page numbers. If there are more than 10 pages, it will show ellipses between page numbers. This format indicates additional pages.
Building the Component

In this section, we will discuss how to build our generic data table LWC component. We will cover its HTML structure. We will also look at its JavaScript implementation and CSS styling.

a. HTML Structure

To create the HTML structure for our data table component, we will use the lightning-datatable base component provided by Salesforce. This component is designed specifically for displaying tabular data and comes with built-in support for sorting and pagination.

Here’s our component’s HTML file:

genericDataTable.html

<template>
<!– Rendering a Lightning card –>
<lightning-card title="">
<!– Displaying a Lightning spinner –>
<template if:true={showSpinner}>
<div id="spinnerDiv" style="height: 10rem">
<div class="slds-spinner_container">
<div role="status" class="slds-spinner slds-spinner_medium">
<span class="slds-assistive-text">Loading</span>
<div class="slds-spinner__dot-a"></div>
<div class="slds-spinner__dot-b"></div>
</div>
</div>
</div>
</template>
<template if:false={showSpinner}>
<!–Section for Page Size starts–>
<div class="slds-m-around_medium slds-align_absolute-center">
<div class="slds-col slds-size_1-of-11 slds-var-p-right_small">
<P
class="slds-var-m-vertical_medium content"
style="padding-left: 10px"
>Show</P
>
</div>
<div class="slds-col slds-size_1-of-11">
<lightning-combobox
name="Show"
value={selectedPageSize}
options={pageSizeOptions}
label=""
onchange={handleRecordsPerPage}
variant="label-hidden"
dropdown-alignment="auto"
>
</lightning-combobox>
</div>
<div class="slds-col slds-size_1-of-11 slds-var-p-left_small">
<P class="slds-var-m-vertical_medium content">Records Per Page</P>
</div>
&nbsp;&nbsp;&nbsp;|
<div class="slds-col slds-size_1-of-11 slds-var-p-left_small">
<p class="slds-var-m-vertical_medium content">
Showing Rows {startingRecord} – {endingRecord} of {records.length}
</p>
</div>
</div>
<!–Section for Page Size ends–>
<!– Pagination buttons section starts –>
<div class="slds-m-around_medium slds-align_absolute-center">
<div>
<!– Button for the first page –>
<lightning-button
label="First"
onclick={handleFirstPage}
disabled={isFirstPage}
>
</lightning-button
>&nbsp;
<!– Button for the previous page –>
<lightning-button
label="Previous"
onclick={handlePreviousPage}
disabled={isFirstPage}
>
</lightning-button
>&nbsp;
<!– Iterating over paginationButtons and rendering numbered page buttons –>
<template for:each={paginationButtons} for:item="button">
<lightning-button
key={button.key}
label={button.value}
value={button.value}
variant={button.variant}
onclick={handlePageButtonClick}
class="slds-m-horizontal_xx-small"
disabled={button.disabled}
>
</lightning-button> </template
>&nbsp;
<!– Button for the next page –>
<lightning-button
label="Next"
onclick={handleNextPage}
disabled={isLastPage}
>
</lightning-button
>&nbsp;
<!– Button for the last page –>
<lightning-button
label="Last"
onclick={handleLastPage}
disabled={isLastPage}
>
</lightning-button>
</div>
</div>
<!– Pagination buttons section ends –>
<!– Displaying a Lightning datatable –>
<div class="myTable">
<lightning-datatable
class="noRowHover"
key-field="Id"
data={displayedRecords}
columns={columns}
hide-checkbox-column
onsort={handleSort}
sorted-by={sortedBy}
sorted-direction={sortedDirection}
>
</lightning-datatable>
</div>
<!–Section for Page Size starts–>
<div class="slds-m-around_medium slds-align_absolute-center">
<div class="slds-col slds-size_1-of-11 slds-var-p-right_small">
<P
class="slds-var-m-vertical_medium content"
style="padding-left: 10px"
>Show</P
>
</div>
<div class="slds-col slds-size_1-of-11">
<lightning-combobox
name="Show"
value={selectedPageSize}
options={pageSizeOptions}
label=""
onchange={handleRecordsPerPage}
variant="label-hidden"
dropdown-alignment="auto"
>
</lightning-combobox>
</div>
<div class="slds-col slds-size_1-of-11 slds-var-p-left_small">
<P class="slds-var-m-vertical_medium content">Records Per Page</P>
</div>
&nbsp;&nbsp;&nbsp;|
<div class="slds-col slds-size_1-of-11 slds-var-p-left_small">
<p class="slds-var-m-vertical_medium content">
Showing Rows {startingRecord} – {endingRecord} of {records.length}
</p>
</div>
</div>
<!–Section for Page Size ends–>
<!– Pagination buttons section starts –>
<div class="slds-m-around_medium slds-align_absolute-center">
<div>
<!– Button for the first page –>
<lightning-button
label="First"
onclick={handleFirstPage}
disabled={isFirstPage}
>
</lightning-button
>&nbsp;
<!– Button for the previous page –>
<lightning-button
label="Previous"
onclick={handlePreviousPage}
disabled={isFirstPage}
>
</lightning-button
>&nbsp;
<!– Iterating over paginationButtons and rendering numbered page buttons –>
<template for:each={paginationButtons} for:item="button">
<lightning-button
key={button.key}
label={button.value}
value={button.value}
variant={button.variant}
onclick={handlePageButtonClick}
class="slds-m-horizontal_xx-small"
disabled={button.disabled}
>
</lightning-button> </template
>&nbsp;
<!– Button for the next page –>
<lightning-button
label="Next"
onclick={handleNextPage}
disabled={isLastPage}
>
</lightning-button
>&nbsp;
<!– Button for the last page –>
<lightning-button
label="Last"
onclick={handleLastPage}
disabled={isLastPage}
>
</lightning-button>
</div>
</div>
<!– Pagination buttons section ends –>
</template>
</lightning-card>
</template>
b. JavaScript Implementation

In the JavaScript file, we will define the component’s properties, fetch the data from the specified sObject, handle sorting, and create the columns based on the provided fields and labels.

Here’s our component’s JavaScript file:

genericDataTable.js

/* eslint-disable no-else-return */
// Import the required modules
import { LightningElement, api } from "lwc";
import getFieldTypes from "@salesforce/apex/GenericRecordController.getFieldTypes";
import getRecords from "@salesforce/apex/GenericRecordController.getRecords";
import { NavigationMixin } from "lightning/navigation";
import { ShowToastEvent } from "lightning/platformShowToastEvent";
import { loadStyle } from "lightning/platformResourceLoader";
import customStyle from "@salesforce/resourceUrl/datatableStyle";
/**
* GenericDataTable represents a reusable data table component in a Salesforce LWC application.
* @class
* @extends LightningElement
*/
export default class GenericDataTable extends NavigationMixin(
LightningElement
) {
@api fields;
@api fieldsLabels;
@api sObjectName;
@api urlField;
@api allowSorting;
@api sortingColumns;
// The default value for the number of records to display per page
@api pageSizeDefaultSelectedValue;
@api pageSizeOptionsString;
// Flag to show/hide spinner
showSpinner = true;
//= 'Loading. Please Wait…';
// The index of the first record on the current page
startingRecord = 1;
// The index of the last record on the current page
endingRecord = 0;
pageSizeOptions;
selectedPageSize;
// The number of records to display per page
pageSize;
// The list of records to display in the data table
records;
// The current page number
currentPage = 1;
// The field by which the records are currently sorted, default "Name"
sortedBy = "Name";
// The direction in which the records are currently sorted ('asc' or 'desc'), default "asc"
sortedDirection = "asc";
// field api name, which shoule be used as a url to redirect to record page
fieldNameURL;
rendercalled = false;
/****************************************************************
* @Description : renderedCallback method is called to load styles
* **************************************************************/
renderedCallback() {
if (this.rendercalled) {
return;
}
this.rendercalled = true;
Promise.all([loadStyle(this, customStyle)])
.then(() => {
this.pageSizeOptions = this.pageSizeOptionsString
.split(",")
.map((pageSize) => ({
label: pageSize,
value: parseInt(pageSize, 10)
}));
this.selectedPageSize = parseInt(this.pageSizeDefaultSelectedValue, 10);
this.pageSize = this.selectedPageSize;
// Split the fieldsNames and fieldsLabels strings into arrays
let fieldAPINames = this.fields.split(",");
let fieldLabels = this.fieldsLabels.split(",");
// Get the field types from the server and do data table column assignment.
this.getFieldTypesFromServer(fieldAPINames, fieldLabels);
})
.catch((error) => {
console.log({ message: "Error onloading", error });
});
}
/**
* @Description : getFieldTypesFromServer method is used to get
* field type from apex method getFieldTypes.
* @param – fieldAPINames – string of sobjects field api names.
* @param – fieldLabels – string of field labels.
* @return – N.A.
*/
getFieldTypesFromServer(fieldAPINames, fieldLabels) {
// Call Apex method getFieldTypes to get field types of the given SObject
getFieldTypes({ sObjectName: this.sObjectName, fieldNames: fieldAPINames })
.then((fieldTypes) => {
// Create an array of column definitions based on the API names, labels, and types
this.columns = fieldAPINames.map((fieldApiName, index) => {
// Initialize the column with label, fieldName, type, cellAttributes, sortable, and hideDefaultActions
let column = {
label: fieldLabels[index],
fieldName: fieldApiName,
type: fieldTypes[fieldApiName],
cellAttributes: {
class: { fieldName: "colColor" }
},
sortable: true,
hideDefaultActions: true
};
// Check if the current field is the urlField and make it clickable
if (
this.urlField !== undefined &&
this.urlField !== null &&
fieldApiName === this.urlField
) {
// Convert urlField to a clickable field with label and target attributes
let convertedFieldName = this.getConvertedFiledName(this.urlField)
? this.getConvertedFiledName(this.urlField)
: null;
column.fieldName = convertedFieldName;
column.type = "url";
column.typeAttributes = {
label: { fieldName: this.urlField },
target: "_blank"
};
column.cellAttributes = {
class: { fieldName: "colColor" }
};
}
// Apply specific cell attributes to the 'Rating' fields
if (fieldApiName === "Rating") {
column.cellAttributes = {
class: { fieldName: "cssClasses" },
iconName: { fieldName: "iconName" },
iconPosition: "right"
};
}
// Format the date field
if (column.type === "date") {
column.typeAttributes = {
day: "numeric",
month: "numeric",
year: "numeric"
};
column.fieldName = fieldApiName;
column.type = "date";
column.cellAttributes = {
class: { fieldName: "colColor" }
};
}
// Format the datetime field
if (column.type === "datetime") {
column.typeAttributes = {
day: "numeric",
month: "numeric",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: true
};
column.type = "date";
column.fieldName = fieldApiName;
column.cellAttributes = {
class: { fieldName: "colColor" }
};
}
// Return the constructed column
return column;
});
// Load records after constructing the columns
this.loadRecords(this.sObjectName);
})
.catch((error) => {
console.error(error);
this.showSpinner = false;
});
}
/*************************************************
* @Description : method to set pagination page size
* ***********************************************/
handleRecordsPerPage(event) {
this.showSpinner = true;
this.selectedPageSize = parseInt(event.target.value, 10);
this.pageSize = this.selectedPageSize;
this.currentPage = 1;
// eslint-disable-next-line @lwc/lwc/no-async-operation
setTimeout(() => {
//this.showSpinner = false;
this.updateDisplayedRecords();
}, 200);
}
/**
* @Description : loadRecords method is used to fetch records for the given sObject name.
* Fetches records from sObject.
* Updates records property.
* @param {string} sObjectName – The API name of the sObject to fetch records from.
* @return – N.A.
*/
loadRecords(sObjectName) {
let propName = null;
if (this.urlField !== undefined) {
propName = this.getConvertedFiledName(this.urlField)
? this.getConvertedFiledName(this.urlField)
: null;
}
getRecords({ sObjectName: sObjectName, fieldNames: this.fields })
.then((result) => {
this.records = result;
this.records = […this.records].map((item) => {
let colColor = "datatable-style";
let rating = null;
let iconName = null;
let url = null;
// showing example of how to apply inline css, with multiple classes
// and icons on a field in datatable.
if (item.Rating != null) {
rating =
item.Rating !== "Hot"
? "slds-text-color_error"
: "slds-text-color_success";
iconName = item.Rating !== "Hot" ? "utility:down" : "utility:up";
}
if (
this.urlField !== undefined &&
this.urlField !== null &&
item[this.urlField] != null
) {
url = "/lightning/r/" + item.Id + "/view";
}
return {
…item,
//example of dynamic css on a datatable column
colColor: colColor,
//example of dynamic property on a datatable column
[propName]: url,
//example of dynamic css with multiple classes on a datatable column
cssClasses: rating + " datatable-styleOther",
//example of appying icon on a datatable column
iconName: iconName
};
});
this.updateDisplayedRecords();
})
.catch((error) => {
this.showSpinner = false;
console.error("Error loading records:", error);
});
}
/**
* @Description : updateDisplayedRecords method is used to update the displayed records.
* Updates records based on page.
* Slices records array.
* @param – N.A.
* @return – N.A.
*/
updateDisplayedRecords() {
const startIndex = (this.currentPage – 1) * this.pageSize;
const endIndex = startIndex + this.pageSize;
this.displayedRecords = this.records.slice(startIndex, endIndex);
// Calculate the starting record based on the current page and number of records to display per page
this.startingRecord = startIndex;
// Calculate the ending record based on the current page and number of records to display per page
this.endingRecord = this.pageSize * this.currentPage;
// If the ending record exceeds the total number of records, set it to the total record count
this.endingRecord =
this.endingRecord > this.records.length
? this.records.length
: this.endingRecord;
// Increment the starting record by 1 to account for 0-based indexing
this.startingRecord = this.startingRecord + 1;
this.showSpinner = false;
}
/**
* @Description : totalPages getter calculates the total number of pages for the pagination component.
* Calculates total pages.
* Uses records and pageSize.
* @param – N.A.
* @return {number|null} – The total number of pages or null if records are not defined.
*/
get totalPages() {
if (this.records !== undefined) {
return Math.ceil(this.records.length / this.pageSize);
}
return null;
}
/**
* @Description : isFirstPage getter is used for checking if the current page is the first page.
* Compares currentPage with 1.
* @param – N.A.
* @return {boolean} – True if currentPage is 1, else false
*/
get isFirstPage() {
return this.currentPage === 1;
}
/**
* @Description : isLastPage getter is used for checking if the current page is the last page.
* Compares currentPage with totalPages.
* @param – N.A.
* @return {boolean} – True if currentPage equals totalPages, else false
*/
get isLastPage() {
return this.currentPage === this.totalPages;
}
/**
* @Description : paginationButtons getter is used for generating pagination buttons.
* Creates an array of button objects for pagination depending on the totalPages.
* Handles totalPages <= 10 and totalPages > 10 cases.
* @param – N.A.
* @return {Array} uniqueButtons – Array of unique button objects for pagination
*/
get paginationButtons() {
const buttons = [];
if (this.totalPages <= 10) {
for (let i = 1; i <= this.totalPages; i++) {
buttons.push({
value: i,
variant: i === this.currentPage ? "brand" : "neutral",
key: i
});
}
} else {
const startIndex = Math.max(3, this.currentPage – 2);
const endIndex = Math.min(startIndex + 4, this.totalPages – 2);
// First pages
for (let i = 1; i <= 2; i++) {
buttons.push({
value: i,
variant: i === this.currentPage ? "brand" : "neutral",
key: i
});
}
// Previous pages
if (startIndex > 3) {
buttons.push({
value: "…",
variant: "neutral",
key: "prevEllipsis",
disabled: true
});
}
// Middle pages
for (let i = startIndex; i <= endIndex; i++) {
buttons.push({
value: i,
variant: i === this.currentPage ? "brand" : "neutral",
key: i
});
}
// Next pages
if (endIndex < this.totalPages – 2) {
buttons.push({
value: "…",
variant: "neutral",
key: "nextEllipsis",
disabled: true
});
}
// Last pages
for (let i = this.totalPages – 1; i <= this.totalPages; i++) {
buttons.push({
value: i,
variant: i === this.currentPage ? "brand" : "neutral",
key: i
});
}
}
// Remove duplicate buttons
const uniqueButtons = buttons.filter(
(button, index, self) =>
index === self.findIndex((t) => t.key === button.key)
);
return uniqueButtons;
}
/**
* @Description : handleFirstPage method is the event handler
* for clicking the "First Page" button.
* Sets current page to 1.
* Updates displayed records.
* @param – N.A.
* @return – N.A.
*/
handleFirstPage() {
// Show the spinner
this.showSpinner = true;
this.currentPage = 1;
this.updateDisplayedRecords();
}
/**
* @Description : handlePreviousPage method is the event handler
* for clicking the "Previous Page" button.
* Decrements current page.
* Updates displayed records.
* @param – N.A.
* @return – N.A.
*/
handlePreviousPage() {
if (!this.isFirstPage) {
// Show the spinner
this.showSpinner = true;
this.currentPage–;
this.updateDisplayedRecords();
}
}
/**
* @Description : handleNextPage method is the event handler
* for clicking the "Next Page" button.
* Increments current page.
* Updates displayed records.
* @param – N.A.
* @return – N.A.
*/
handleNextPage() {
if (!this.isLastPage) {
// Show the spinner
this.showSpinner = true;
this.currentPage++;
this.updateDisplayedRecords();
}
}
/**
* @Description : handleLastPage method is the event handler
* for clicking the "Last Page" button.
* Sets current page to last.
* Updates displayed records.
* @param – N.A.
* @return – N.A.
*/
handleLastPage() {
// Show the spinner
this.showSpinner = true;
this.currentPage = this.totalPages;
this.updateDisplayedRecords();
}
/**
* @Description : handlePageButtonClick method is the event handler
* for clicking a page number button.
* Sets current page.
* Updates displayed records.
* @param {Event} event – The button click event.
* @return – N.A.
*/
handlePageButtonClick(event) {
const targetPage = parseInt(event.target.value, 10);
if (!isNaN(targetPage)) {
// Show the spinner
this.showSpinner = true;
this.currentPage = targetPage;
this.updateDisplayedRecords();
}
}
/**
* @Description : handleSort method is the event handler
* for sorting a column in the data table.
* Sets sortedBy and sortedDirection.
* Calls sortRecords method.
* @param {Event} event – The column sort event.
* @return – N.A.
*/
handleSort(event) {
this.showSpinner = true;
let newSortedBy = event.detail.fieldName;
if (newSortedBy.includes("url")) {
newSortedBy = this.urlField.replace(/(-)([a-z])/g, (letter) =>
letter.toUpperCase()
);
}
const canSort =
this.allowSorting && this.sortingColumns.includes(newSortedBy);
if (canSort) {
if (this.sortedBy === newSortedBy) {
// Toggle sort direction if the same column is clicked again
this.sortedDirection = this.sortedDirection === "asc" ? "desc" : "asc";
} else {
this.sortedBy = newSortedBy;
this.sortedDirection = event.detail.sortDirection;
}
this.sortRecords(this.records, this.sortedBy, this.sortedDirection);
let convertedFieldName = null;
if (this.urlField !== undefined) {
convertedFieldName = this.getConvertedFiledName(this.urlField)
? this.getConvertedFiledName(this.urlField)
: null;
}
const sortFieldName =
this.sortedBy === this.urlField ? convertedFieldName : this.sortedBy;
const columns = […this.columns];
const sortedColumn = columns.find(
(col) => col.fieldName === sortFieldName
);
sortedColumn.sortedDirection = this.sortedDirection;
this.columns = columns;
} else {
this.showSpinner = false;
this.dispatchEvent(
new ShowToastEvent({
title: "Warning",
message: "Sorting not allowed on " + newSortedBy,
variant: "warning"
})
);
}
}
/**
* @Description : compareStrings method compares two strings (a, b)
by breaking them into parts of either all digits or all non-digits.
* Compares parts of the strings in a natural order (i.e., correctly handling numeric parts).
* @param a – The first string to be compared.
* @param b – The second string to be compared.
* @return – A negative, zero, or positive integer depending on whether 'a' comes before,
is equal to, or comes after 'b', respectively.
*/
compareStrings(a, b) {
// Match and break the input strings into parts of either all digits or all non-digits
const partsA = a.match(/\D+|\d+/g);
const partsB = b.match(/\D+|\d+/g);
let i = 0;
// Iterate through parts of both strings while there are still parts to compare
while (i < partsA.length && i < partsB.length) {
// Check if the current parts of both strings are numeric
if (/^\d+$/.test(partsA[i]) && /^\d+$/.test(partsB[i])) {
// Parse the numeric parts and compare them
const num1 = parseInt(partsA[i], 10);
const num2 = parseInt(partsB[i], 10);
// If the numeric parts are different, return the difference
if (num1 !== num2) {
return num1 – num2;
}
} else {
// Compare non-numeric parts using localeCompare
const cmp = partsA[i].localeCompare(partsB[i]);
// If the non-numeric parts are different, return the comparison result
if (cmp !== 0) {
return cmp;
}
}
// Increment the index to compare the next parts of the strings
i++;
}
// If one string has more parts than the other, return the difference in the number of parts
return partsA.length – partsB.length;
}
/**
* @Description : sortRecords method sorts the records based
* on the currently sorted column and direction.
* Sorts records in ascending or descending order.
* Updates sorted records array.
* Resets the current page.
* Updates displayed records.
* @param – N.A.
* @return – N.A.
*/
sortRecords(records, sortedBy, sortedDirection) {
try {
/* Make a copy of the original records array and sort
it based on the provided sort parameters*/
this.records = […records].sort((a, b) => {
// Get the values of the properties being sorted, or null if they don't exist
const valueA = a[sortedBy] === undefined ? null : a[sortedBy];
const valueB = b[sortedBy] === undefined ? null : b[sortedBy];
/* If one of the values is null, move it to the top or bottom of the
sorted list depending on the sort direction*/
if (valueA === null) {
return sortedDirection === "asc" ? 1 : -1;
}
if (valueB === null) {
return sortedDirection === "asc" ? -1 : 1;
}
// If both values are strings, use a custom string comparison function to sort them
if (typeof valueA === "string" && typeof valueB === "string") {
const cmp = this.compareStrings(valueA, valueB);
return sortedDirection === "asc" ? cmp : -cmp;
}
/* If neither value is null and at least one value is not a string,
convert them to lowercase strings for case -insensitive sorting*/
else {
const aValue =
typeof valueA === "string" ? valueA.toLowerCase() : valueA;
const bValue =
typeof valueB === "string" ? valueB.toLowerCase() : valueB;
// Compare the lowercase string values to sort them
if (aValue < bValue) {
return sortedDirection === "asc" ? -1 : 1;
} else if (aValue > bValue) {
return sortedDirection === "asc" ? 1 : -1;
} else {
// If the values are equal, leave them in their original order
return 0;
}
}
});
// Reset the current page to the first page
this.currentPage = 1;
// Update displayed records based on the new sorted records
this.updateDisplayedRecords();
} catch (error) {
console.error("Error in sortRecords:", error);
}
}
/**
* @Description : getConvertedFiledName method convert the url field to
* desired format for other logics in the componnet.
* @param – fieldName , which is the urlField
* @return – convertedFieldName in the format of urlField+'-url'
*/
getConvertedFiledName(fieldName) {
if (fieldName) {
let convertedFieldName = fieldName;
// Replace any field name ending with __c with just the field name
if (convertedFieldName.match(/^[a-z]+__c$/i)) {
convertedFieldName = convertedFieldName.slice(0, -3);
}
// Convert the field name to title case
convertedFieldName = convertedFieldName.replace(
/[ _]+([a-z])/gi,
function (letter) {
return letter.toUpperCase();
}
);
// Convert the field name to kebab-case
convertedFieldName = convertedFieldName.replace(
/[A-Z]/g,
function (letter, index) {
return index === 0 ? letter.toLowerCase() : "-" + letter.toLowerCase();
}
);
// Append the "-url" suffix to the resulting string
convertedFieldName = convertedFieldName + "-url";
return convertedFieldName;
}
else return '';
}
}
c. Metadata File

genericDataTable.js-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata" fqn="myLWCComponent">
	<apiVersion>62.0</apiVersion>
	<isExposed>true</isExposed>
	<masterLabel>Generic Data Table</masterLabel>
	<description>A generic data table LWC component.</description>
	<targets>
		<target>lightning__AppPage</target>
		<target>lightning__RecordPage</target>
		<target>lightning__HomePage</target>
		<target>lightningCommunity__Page</target>
		<target>lightningCommunity__Default</target>
		<target>lightning__Tab</target>
	</targets>
	<targetConfigs>
		<targetConfig targets="lightning__AppPage,lightning__RecordPage,lightning__HomePage">
			<property name="fields" label="Fields Api Names" type="String"
				default="Name,Phone,CreatedDate"
				description="Enter a comma-separated list of field api names to display in the table, 
				like Name,CreatedDate,CustomField__c." />
			<property name="fieldsLabels" label="Fields Labels" type="String"
				default="Name,Phone,Created Date"
				description="Enter a comma-separated list of field column lables to display in the table, 
				like Name,Created Date,Custom Field." />
			<property name="sObjectName" label="Object Name" type="String" default="Account"
				description="Enter the api name of sobject from which you want to show data." />
			<property name="urlField" label="Url Field" type="String" default=""
				description="Enter the field api name, which you want to show as value  in table column, 
				but want it to redirect to sobject record as a link." />
			<property name="allowSorting" label="Allow Sorting" type="Boolean" default="false"
				description="Do you need sorting on fields in data table." />
			<property name="sortingColumns" label="Column Name to allow sorting" type="String"
				default="Name,CreatedDate"
				description="Enter a comma-separated list of string of field api name, which should 
				be allowed for sorting in the table like Name,CreatedDate,CustomField__c." />
			<property name="pageSizeOptionsString" label="Page Size Options" type="String"
				default="5,10,25"
				description="Enter a comma-separated list of string for page size options like 5,10,25." />
			<property name="pageSizeDefaultSelectedValue" label="Page Size Default Value"
				type="String" default="10"
				description="Enter the default page size for table in integer value like 10." />
		</targetConfig>
	</targetConfigs>
</LightningComponentBundle>
d. Styling with CSS

We will use Salesforce Lightning Design System (SLDS) classes to style our component. In this example, we’ve added the below CSS to static resources.

Static Resource – datatableStyle.css

/* Tabler Header Column Color and css*/
.myTable table > thead .slds-th__action {
  background-color: rgba(1, 118, 211, 1) !important;
  color: white !important;
  font-size: 10px;
  font-weight: 700;
  width: 75%;
}
/* Tabler Header Column hover color and css*/
.myTable table > thead .slds-th__action:hover {
  background-color: rgba(35, 118, 204, 1) !important;
  font-size: 10px;
  font-weight: 700;
  width: 75%;
}
/* overriding data table color and background color*/
.datatable-style {
  color: black;
  background-color: white !important;
}
/* overriding data table hovor color and background color*/
.datatable-style:hover {
  color: black;
  background-color: white !important;
}
/* overriding data table special column color and background color*/
.datatable-styleOther {
  background-color: white !important;
}
/* overriding data table special column hover color and background color*/
.datatable-styleOther:hover {
  background-color: white !important;
}
.slds-is-sortable__icon {
  fill: white !important;
}
.dt-outer-container {
  padding-left: 10px;
  padding-right: 10px;
}
.slds-resizable__divider {
  background-color: rgba(35, 118, 204, 1) !important;
}
.noRowHover .slds-truncate {
  overflow-wrap: break-word !important;
  white-space: pre-line !important;
  width: 75%;
}

Apex Class – GenericRecordController

public with sharing class GenericRecordController {
    /**
    * Retrieves a list of records for a specified sObject
      and selected fields.
    * @param sObjectName Name of the sObject to query.
    * @param fieldNames Comma-separated list of fields to retrieve.
    * @return List of sObject records or null if an exception occurs.
    */
    @AuraEnabled(cacheable = true)
    public static List < sObject > getRecords(String sObjectName, 
                                              String fieldNames) 
    {
      try {
        // Split the field names by comma and store them in a list
        List <String> fields = new List <String> (fieldNames.split(','));
   
        // Get the schema description of the sObject
        Schema.DescribeSObjectResult sObjectDescribe =
          Schema.getGlobalDescribe().get(sObjectName).getDescribe();
   
        // Check if the user has access to the sObject
        if (!sObjectDescribe.isAccessible()) {
          throw new AuraHandledException(
            'You do not have access to the ' + sObjectName + ' object.'
          );
        }
   
        // Retrieve the map of fields for the sObject
        Map <String, Schema.SObjectField> fieldMap =
          sObjectDescribe.fields.getMap();
   
        // Validate each field name for accessibility and existence
        for (String fieldName: fields) {
          if (!fieldMap.containsKey(fieldName.trim()) 
				&& !fieldName.contains('.')) {
            throw new AuraHandledException(
              'Invalid field name: ' + fieldName.trim()
            );
          }
   
          // Check if the user has access to the field (non-relationship fields)
          if (!fieldName.contains('.')) {
            if (!fieldMap.get(fieldName.trim()).getDescribe().isAccessible()) {
              throw new AuraHandledException(
                'You do not have access to the ' +
                fieldName.trim() +
                ' field on the ' + sObjectName +
                ' object.'
              );
            }
          }
        }
   
        // Escape single quotes to prevent SOQL injection
        String sObjectNameEscaped = String.escapeSingleQuotes(sObjectName);
        String fieldNamesEscaped = String.escapeSingleQuotes(fieldNames);
   
        // Construct the SOQL query to retrieve the records
        String query =
          'SELECT ID, ' + fieldNamesEscaped + ' FROM ' +
          sObjectNameEscaped + ' ORDER BY Name LIMIT 2000';
   
        // Execute the query and return the result
        return Database.query(query);
      } catch (Exception ex) {
        // Log the exception for debugging purposes
        System.debug(
          'exception+++' + ex.getLineNumber() + ' ' + ex.getMessage()
        );
        return null;
      }
    }
   
    /**
    * Retrieves the data types for specified fields of an sObject.
    * @param sObjectName Name of the sObject to query.
    * @param fieldNames List of field names to retrieve data types for.
    * @return A map of field names to their data types.
    */
    @AuraEnabled(cacheable = true)
    public static Map <String, String> getFieldTypes(String sObjectName, 
                                                     List <String> fieldNames) 
    {
      // Retrieve the map of fields for the specified sObject
      Map <String, Schema.SObjectField> fieldMap =
        Schema.getGlobalDescribe().get(sObjectName).getDescribe().fields.getMap();
   
      // Map to store field names and their data types
      Map <String, String> fieldTypes = new Map <String, String> ();
   
      // Iterate through each field name
      for (String fieldName: fieldNames) {
        // Check if the field exists in the sObject
        if (fieldMap.containsKey(fieldName)) {
          // Get the field description
          Schema.DescribeFieldResult fieldDescribe =
            fieldMap.get(fieldName).getDescribe();
   
          // Store the field name and its type in lowercase in the map
          fieldTypes.put(
            fieldName,
            fieldDescribe.getType().name().toLowerCase()
          );
        }
      }
   
      // Return the map of field names and their types
      return fieldTypes;
    }
}
Usage and Integration

We have built our generic data table LWC component. Now, let’s discuss how to use it and integrate it into a Lightning page.

a. Adding the component to a Lightning page

To add our generic data table LWC to a Lightning app or home page, you need to drag the component. Find it in the Custom Components section. Then drop it into the Lightning App Builder. Once placed, you can configure the component properties as needed.

b. Configuring component properties

After adding the component to your Lightning page, you can configure its properties in the Property Editor. Specify the fields, labels, sObjectName, and other properties according to your requirements. Here’s a quick overview of the properties you can configure:

  • Fields: Enter a comma-separated list of field API names to display in the table.
  • Fields Labels: Enter a comma-separated list of field column labels to display in the table.
  • sObjectName: Enter the API name of the sObject from which you want to fetch data.
  • Url Field: Enter the field API name that will be displayed as a value in the table column. It will redirect to the sObject record when clicked.
  • Allow Sorting: Toggle this option to enable or disable sorting on the data table.
  • Sorting Columns: Enter a comma-separated list of field API names that should be sortable in the table.
  • Page Size Options: Enter a comma-separated list of string values for page size options.
  • Page Size Default Value: Enter the default page size for the table.

First, configure the component properties. Then, save your changes. Afterward, preview the Lightning page to see your generic data table LWC in action.

Conclusion

In this blog, we’ve explored how to create a feature-rich, generic data table Lightning Web Component in Salesforce. We implemented features such as sorting, clickable URLs, and pagination. As a result, we’ve built a highly customizable and reusable component. It can be easily integrated into any Salesforce application. This LWC component possesses versatile properties. It offers user-friendly configuration options. Therefore, it empowers users to display and analyze their data with ease.

We encourage you to further customize and extend this component to better suit your specific use cases and requirements. Build on the foundation provided in this blog. You can create even more powerful and dynamic data table components. These will enhance your Salesforce applications.

Happy coding!

For more helpful articles please visit – https://thesalesforcedev.wordpress.com/

Leave a comment