Opening statement:
Are you seeking to construct a versatile data table for your Salesforce application? Your search ends here! This blog will guide you through the process of building a robust, generic data table Lightning Web Component (LWC) in Salesforce. This LWC component is designed to be highly customizable, featuring capabilities like sorting, clickable URLs, and pagination. Let’s delve into the details!
Properties of the Component:
To construct a versatile and reusable Lightning Web Component (LWC) data table, we will initially define a set of component properties. These properties enable users to configure the component based on their specific requirements. Here’s a list of the properties we will incorporate into our generic data table LWC:
a. Fields Api Names: This property accepts a comma-separated list of field API names to be displayed in the table. Example: “Name, CreatedDate, CustomField__c”
b. Fields Labels: This property accepts a comma-separated list of field column labels to be displayed in the table. Example: “Name, Created Date, Custom Field”
c. sObjectName: This property allows users to specify the API name of the sObject from which the data will be fetched. Example: “Account”
d. Url Field: This property accepts a field API name that will be displayed as a value in the table column but 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”
Enabling 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 and 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 specify which columns they want to be sortable, giving them more control over their data analysis.
- Custom page size options: This feature allows users to select the number of rows displayed per page, providing a more streamlined view of the data.
- Default page size selection: Users can set a default page size for the table, ensuring 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) in between the page numbers to indicate additional pages.
Constructing the Component
Constructing the Component In this segment, we will explore the process of building our generic data table Lightning Web Component (LWC), encompassing its HTML framework, JavaScript implementation, and CSS styling.
a. HTML Structure To establish the HTML structure for our data table component, we will leverage the lightning-datatable base component supplied by Salesforce. Tailored for presenting tabular data, this component inherently supports sorting and pagination.
Below is the HTML file for our component:
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> | <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 > <!-- Button for the previous page --> <lightning-button label="Previous" onclick={handlePreviousPage} disabled={isFirstPage} > </lightning-button > <!-- 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 > <!-- Button for the next page --> <lightning-button label="Next" onclick={handleNextPage} disabled={isLastPage} > </lightning-button > <!-- 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> | <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 > <!-- Button for the previous page --> <lightning-button label="Previous" onclick={handlePreviousPage} disabled={isFirstPage} > </lightning-button > <!-- 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 > <!-- Button for the next page --> <lightning-button label="Next" onclick={handleNextPage} disabled={isLastPage} > </lightning-button > <!-- 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 Within the JavaScript file, we will delineate the component’s properties, retrieve data from the designated sObject, manage sorting, and formulate columns based on the supplied fields and labels.
The JavaScript file for our component is outlined below:
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 ''; } }
metadata file for genericDataTable.js
<?xml version="1.0" encoding="UTF-8"?> <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata" fqn="myLWCComponent"> <apiVersion>56.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__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>
c. Styling via 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 { @AuraEnabled(cacheable=true) public static List<sObject> getRecords(String sObjectName, String fieldNames) { try{ List<String> fields = new List<String>(fieldNames.split(',')); Schema.DescribeSObjectResult sObjectDescribe = Schema.getGlobalDescribe().get(sObjectName).getDescribe(); if (!sObjectDescribe.isAccessible()) { throw new AuraHandledException('You do not have access to the ' + sObjectName + ' object.'); } Map<String, Schema.SObjectField> fieldMap = sObjectDescribe.fields.getMap(); for (String fieldName : fields) { if (!fieldMap.containsKey(fieldName.trim()) && !fieldName.contains('.')) { throw new AuraHandledException('Invalid field name: ' + fieldName.trim()); } 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 in the sObjectName and fieldNames variables String sObjectNameEscaped = String.escapeSingleQuotes(sObjectName); String fieldNamesEscaped = String.escapeSingleQuotes(fieldNames); String query = 'SELECT ID, ' + fieldNamesEscaped + ' FROM ' + sObjectNameEscaped + ' ORDER BY Name LIMIT 2000'; return Database.query(query); }catch(Exception ex){ System.debug('exception+++'+ex.getLineNumber()+' '+ex.getMessage()); return null; } } @AuraEnabled(cacheable=true) public static Map<String, String> getFieldTypes(String sObjectName, List<String> fieldNames) { Map<String, Schema.SObjectField> fieldMap = Schema.getGlobalDescribe().get(sObjectName).getDescribe().fields.getMap(); Map<String, String> fieldTypes = new Map<String, String>(); for (String fieldName : fieldNames) { if (fieldMap.containsKey(fieldName)) { Schema.DescribeFieldResult fieldDescribe = fieldMap.get(fieldName).getDescribe(); fieldTypes.put(fieldName, fieldDescribe.getType().name().toLowerCase()); } } return fieldTypes; } }
Application and Integration
Now that we have completed the construction of our generic data table Lightning Web Component (LWC), let’s explore how to utilize and integrate it into a Lightning page.
a. Adding the Component to a Lightning PageTo incorporate our generic data table LWC into a Lightning app or home page, simply drag and drop the component from the Custom Components section of the Lightning App Builder. Once positioned, you can adjust the component properties as necessary.b. Configuring Component PropertiesAfter incorporating the component into your Lightning page, configure its properties in the Property Editor. Specify the fields, labels, sObjectName, and other properties based on your requirements. Here’s a concise overview of the properties available for configuration:
- 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 but 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.
Once you’ve configured the component properties, save your changes and preview the Lightning page to observe your generic data table LWC in action.
Closing Thoughts
In this article, we have delved into the creation of a versatile and feature-rich generic data table Lightning Web Component in Salesforce. By incorporating functionalities like sorting, clickable URLs, and pagination, we’ve developed a highly adaptable and reusable component seamlessly integrable into any Salesforce application. With its flexible properties and user-friendly configuration options, this Lightning Web Component empowers users to effortlessly showcase and analyze their data.We encourage you to further tailor and expand upon this component to align it with your specific use cases and requirements. By leveraging the groundwork provided in this article, you can craft even more robust and dynamic data table components to elevate the capabilities of your Salesforce applications.Happy coding!