Versatile Customizable, And Feature Rich Generic Data Table For Lightning Web Components LWC In Salesforce

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>
        &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 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!