Implement Pagination In HTML Table within Salesforce Lightning Web Component (LWC) using JavaScript


In this article, we will explore the implementation of pagination in an HTML Table within a Salesforce Lightning Web Component (LWC) using JavaScript. Pagination is a commonly employed feature in web development, allowing users to efficiently navigate through substantial datasets by breaking them into more manageable segments.

Scenario: Consider a scenario where we need to display account data in a table with pagination to showcase multiple account records. We will create a Lightning Web Component for this purpose, utilizing HTML table and JavaScript to present the data on the user interface.

Code Explanation: To introduce pagination in an HTML Table, we began by importing essential classes and methods from the LWC module. This includes the LightningElement class, the track decorator for data binding, and the wire decorator to establish a connection with an Apex method responsible for retrieving the data intended for pagination. Additionally, we imported the getAccounts method from the AccountController Apex class and the Images resource.

Subsequently, the Pagenationex class was declared as the default export, and variables for data binding were defined. These variables encompass the accounts data array, storing records obtained from the Apex controller, the displayAccounts array holding records to be displayed on the current page, and the pageNumbers array containing page numbers for the pagination component.

Variables for the current page number, total number of pages, number of records per page, and total number of records were declared. Flags indicating whether the current page was the first or last page, and for disabling the previous and next buttons were also included. Furthermore, a variable specifying the field based on which data is to be sorted, along with a flag indicating whether the data is to be sorted in ascending order or not, were part of the declarations.

Using the wire decorator, a connection to the getAccounts Apex method was established, retrieving the data to be paginated. In the callback function, a check was made for returned data, and if present, it was assigned to the accounts property. The total number of pages was calculated, and the setPages method was called, passing in the data. Additionally, the navigateToFirstPage method was invoked to navigate to the first page.

The setPages method generated an array of page numbers based on the data length and the page size, assigning it to the pageNumbers property. The getPagesList method calculated the middle of the page size and returned a slice of page numbers from the current page – middle to the current page + middle – 1.

The navigateToFirstPage method assigned the current page to the first page, set the flag for the first page to true, the flag for the last page to false, disabled the previous button, and enabled the next button. It also assigned the accounts to be displayed on the first page.

Other methods in the component included navigateToLastPage, navigateToPreviousPage, and navigateToNextPage, handling navigation to the last page, previous page, and next page respectively. The component also featured a sortData method for sorting data based on a specified field and the handlePageClick method for navigating to a specific page.

Sample Code :

HTML

<!-- sldsValidatorIgnore -->
<template>
  <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>
  <!-- A lightning-card component to display the content -->
  <lightning-card>
    <!-- A table to display the accounts data -->
    <table class="slds-table slds-table_bordered slds-table_cell-buffer">
      <!-- The header of the table -->
      <thead>
        <!-- A row to contain the header cells -->
        <tr class="slds-line-height_reset">
          <!-- A header cell for the 'Name' column -->
          <th
            data-field-name="Name"
            class="slds-text-title_caps"
            scope="col"
            onclick={handleSort}
          >
            Name
            <span data-field-id="Name" lwc:dom="manual"></span>
          </th>
          <!-- A header cell for the 'Phone' column -->
          <th
            data-field-name="Phone"
            class="slds-text-title_caps"
            scope="col"
            onclick={handleSort}
          >
            Phone
            <span data-field-id="Phone" lwc:dom="manual"></span>
          </th>
          <!-- A header cell for the 'Industry' column -->
          <th
            data-field-name="Industry"
            class="slds-text-title_caps"
            scope="col"
            onclick={handleSort}
          >
            Industry
            <span data-field-id="Industry" lwc:dom="manual"></span>
          </th>
          <!-- A header cell for the 'CreatedDate' column -->
          <th
            data-field-name="CreatedDate"
            class="slds-text-title_caps"
            scope="col"
            onclick={handleSort}
          >
            CreatedDate
            <span data-field-id="CreatedDate" lwc:dom="manual"></span>
          </th>
          <!-- A header cell for the 'LastModifiedDate' column -->
          <th
            data-field-name="LastModifiedDate"
            class="slds-text-title_caps"
            scope="col"
            onclick={handleSort}
          >
            LastModifiedDate
            <span data-field-id="LastModifiedDate" lwc:dom="manual"></span>
          </th>
        </tr>
      </thead>
      <!-- The body of the table -->
      <tbody>
        <!-- A conditional template to display the accounts data if there are any -->
        <template if:true={accounts.length}>
          <!-- A for:each template to iterate through the displayAccounts array -->
          <template for:each={displayAccounts} for:item="account">
            <!-- A row to display a single account data -->
            <tr key={account.Id}>
              <!-- A cell for the 'Name' column -->
              <td>{account.Name}</td>
              <!-- A cell for the 'Phone' column -->
              <td>
                <!-- A lightning-formatted-phone component to format the phone number -->
                <lightning-formatted-phone
                  value={account.Phone}
                ></lightning-formatted-phone>
              </td>
              <!-- A cell for the 'Industry' column -->
              <td>{account.Industry}</td>
              <!-- A cell for the 'CreatedDate' column -->
              <td>
                <!-- A lightning-formatted-date-time component to format the date and time -->
                <lightning-formatted-date-time
                  value={account.CreatedDate}
                  year="numeric"
                  month="numeric"
                  day="numeric"
                  hour="2-digit"
                  minute="2-digit"
                >
                </lightning-formatted-date-time>
              </td>
              <!-- A cell for the 'LastModifiedDate' column -->
              <td>
                <!-- A lightning-formatted-date-time component to format the date and time -->
                <lightning-formatted-date-time
                  value={account.LastModifiedDate}
                  year="numeric"
                  month="numeric"
                  day="numeric"
                  hour="2-digit"
                  minute="2-digit"
                >
                </lightning-formatted-date-time>
              </td>
            </tr>
          </template>
        </template>
        <!-- A conditional template to display a message if there are no accounts data -->
        <template if:false={accounts.length}>
          <tr>
            <td colspan="5">No data to display</td>
          </tr>
        </template>
      </tbody>
    </table>
    <!-- A lightning-layout component to organize the content into multiple rows -->
    <lightning-layout multiple-rows="true">
      <!-- A lightning-layout-item component to specify the size of the content -->
      <lightning-layout-item size="12">
        <!-- A div with slds-align_absolute-center class to align the content to the center -->
        <div class="slds-align_absolute-center">
          <!-- A ul with slds-button-group-row class to display the buttons in a row -->
          <ul class="slds-button-group-row">
            <!-- A li with slds-button-group-item class to display the first button in the group -->
            <li class="slds-button-group-item">
              <!-- A lightning-button component to represent the first page button -->
              <lightning-button
                label="First"
                onclick={navigateToFirstPage}
                disabled={isFirstPage}
              >
              </lightning-button>
            </li>
            <!-- A li with slds-button-group-item class to display the previous button in the group -->
            <li class="slds-button-group-item">
              <!-- A lightning-button component to represent the previous page button -->
              <lightning-button
                label="Previous"
                onclick={navigateToPreviousPage}
                disabled={isPreviousDisabled}
              >
              </lightning-button>
            </li>
            <template lwc:if={showPageButtons}>
              <!-- A template with if:true condition to check if the pageNumbers array has length -->
              <template if:true={pageNumbers.length}>
                <!-- A template with for:each loop to iterate through the pageNumbers array -->
                <template for:each={pageNumbers} for:item="pageNumber">
                  <!-- A li with slds-button-group-item class to display each page number button in the group -->
                  <li class="slds-button-group-item" key={pageNumber}>
                    <!-- A button with data-id attribute and slds-button_neutral class to represent each page number -->
                    <button
                      data-id={pageNumber}
                      class="slds-button slds-button_neutral"
                      onclick={navigateToPage}
                    >
                      {pageNumber}
                    </button>
                  </li>
                </template>
              </template>
            </template>
            <template lwc:else>
              <li class="slds-button-group-item" style="padding: 5px">
                Showing Page {currentPage} of {totalPages}
              </li>
            </template>
            <!-- A li with slds-button-group-item class to display the next button in the group -->
            <li class="slds-button-group-item">
              <!-- A lightning-button component to represent the next page button -->
              <lightning-button
                label="Next"
                onclick={navigateToNextPage}
                disabled={isNextDisabled}
              >
              </lightning-button>
            </li>
            <!-- A li with slds-button-group-item class to display the last button in the group -->
            <li class="slds-button-group-item">
              <!-- A lightning-button component to represent the last page button -->
              <lightning-button
                label="Last"
                onclick={navigateToLastPage}
                disabled={isLastPage}
              >
              </lightning-button>
            </li>
          </ul>
        </div>
      </lightning-layout-item>
    </lightning-layout>
  </lightning-card>
</template>

CSS

table {
    table-layout: fixed;
    border-collapse: collapse;
    width: 100%;
}  
th,
td {
    text-align: left;
    padding: 8px;
    width: 100px;
    word-wrap: break-word;
    overflow-wrap: break-word;
}

Javascript controller

/* eslint-disable no-else-return */
/* eslint-disable default-case */
// Import the LightningElement class and the track decorator from the lwc module
import { LightningElement, track, wire } from "lwc";
// Import the getAccounts method from the AccountController Apex class
import getAccounts from "@salesforce/apex/AccountController.getAccounts";
// Import the Images resource for sort up and down icon from static resources
import Images from "@salesforce/resourceUrl/Images";
import './pagenationex.css';
 
// Declare the Pagenationex class as the default export
export default class Pagenationex extends LightningElement {
  // Declare variables for data binding
  // Accounts data array to store account records returned from apex controller
  @track accounts = [];
  // Accounts data array to be displayed on current page
  @track displayAccounts = [];
  // Array of page numbers
  @track pageNumbers = [];
  // Current page number
  @track currentPage = 1;
  // Total number of pages
  @track totalPages = 0;
  // Number of records per page
  @track pageSize = 10;
  // Total number of records
  @track totalRecords;
  // Flag to indicate if current page is first page
  @track isFirstPage = true;
  // Flag to indicate if current page is last page
  @track isLastPage = false;
  // Flag to disable previous button
  @track isPreviousDisabled = true;
  // Flag to disable next button
  @track isNextDisabled = false;
  // Field on which data is to be sorted
  @track sortField;
  // Flag to indicate if data is to be sorted in ascending order
  @track sortAscending = true;
  // Flag to show/hide spinner
  @track showSpinner = true;
  // Flag to show/hide paginationbuttons
  @track showPageButtons = true;
 
  //using the @wire decorator to connect to the getAccounts Apex method
  @wire(getAccounts)
  /**
   * wiredAccounts method is used to handle the data returned from the Apex method.
   * It assigns the data to the accounts property and sets the total number of records and total number of pages.
   * It also calls the setPages and navigateToFirstPage methods to set the pagination and navigate to the first page.
   * @param {Object} data - data returned from the Apex method
   * @param {Object} error - error returned from the Apex method
   */
  wiredAccounts({ data, error }) {
    // If data is returned from the Apex method
    if (data) {
      // Assign the data to the accounts property
      this.accounts = data;
      // Assign the total number of records to the totalRecords property
      this.totalRecords = data.length;
      // Calculate the total number of pages based on the page size and the total number of records
      this.totalPages = Math.ceil(this.accounts.length / this.pageSize);
      // Call the setPages method, passing in the data
      this.setPages(data);
      // Call the navigateToFirstPage method to navigate to the first page
      this.navigateToFirstPage();
      this.showSpinner = false;
    } else if (error) {
      // If an error is returned, handle it
      this.showSpinner = false;
    }
  }
  /**
   * setPages method is used to set the page numbers for the pagination component.
   * It creates an array of page numbers based on the length of the data and the page size.
   * The created array is assigned to this.pageNumbers so that it can be used in the pagination component.
   * @param {Object} data - data used to calculate the number of pages
   */
  setPages(data) {
    // Create an array of page numbers based on the length of the data and the page size.
    // this.pageNumbers is assigned the array so that it can be used in the pagination component.
    this.pageNumbers = Array.from(
      // Using the Array.from method with the length of Math.ceil(data.length / this.pageSize)
      { length: Math.ceil(data.length / this.pageSize) },
      // _ is a placeholder for the value of the array and i is the index of the array, and it starts with 1.
      (_, i) => i + 1
    );
  }
  /**
     * getPagesList method is used to return the list of page numbers to be displayed in the pagination component.
     * It calculates the middle of the page size and checks if the total number of pages is greater than the middle of the page size.
     * If so, it returns a slice of page numbers from the current page - middle to the current page + middle - 1.
     * If the total number of pages is less than or equal to the middle of the page size, 
     it returns a slice of page numbers from the start to the page size
    */
  getPagesList() {
    //Calculates the middle of the page size
    let mid = Math.floor(this.pageSize / 2) + 1;
    //Checks if the total number of pages is greater than the middle of the page size
    if (this.pageNumbers > mid) {
      //Returns a slice of page numbers from the current page - middle to the current page + middle - 1
      return this.pageNumbers.slice(
        this.currentPage - mid,
        this.currentPage + mid - 1
      );
    }
    //If the total number of pages is less than or equal to the middle of the page size,
    //returns a slice of page numbers from the start to the page size
    return this.pageNumbers.slice(0, this.pageSize);
  }
  /**
     * navigateToFirstPage method is used to navigate to the first page of the pagination component.
     * It assigns the current page to the first page, sets the flags for the first page, last page, 
     previous button, and next button, and assigns the accounts to be displayed on the first page.
    */
  navigateToFirstPage() {
    // Assign the current page to the first page
    this.currentPage = 1;
    // Assign the flag for first page to true
    this.isFirstPage = true;
    // Setting the flag for last page to false
    this.isLastPage = false;
    // Assign the flag for previous button to be disabled
    this.isPreviousDisabled = true;
    // Assign the flag for next button to be enabled
    this.isNextDisabled = false;
    // Assign the accounts to be displayed on the first page
    this.displayAccounts = this.accounts.slice(0, this.pageSize);
  }
  /**
     * navigateToLastPage method is used to navigate to the last page of the pagination.
     * It assigns the current page variable to the total number of pages, updates the isFirstPage and isLastPage variables, 
     and sets the isPreviousDisabled and isNextDisabled variables.
    * It also assigns the displayAccounts variable to the slice of the accounts array that corresponds to the current page
    */
  navigateToLastPage() {
    // Assign the current page variable to the total number of pages
    this.currentPage = this.totalPages;
    // Assign the isFirstPage variable to false, indicating that the current page is not the first page
    this.isFirstPage = false;
    // Assign the isLastPage variable to true, indicating that the current page is the last page
    this.isLastPage = true;
    // This line sets the isPreviousDisabled variable to false, indicating that the "previous" button should be enabled
    this.isPreviousDisabled = false;
    // Assign the isNextDisabled variable to true, indicating that the "next" button should be disabled
    this.isNextDisabled = true;
    // Assign the displayAccounts variable to the slice of the accounts array that corresponds to the current page, using the currentPage, pageSize and the accounts array
    this.displayAccounts = this.accounts.slice(
      (this.currentPage - 1) * this.pageSize,
      this.currentPage * this.pageSize
    );
  }
  /**
     * navigateToPage method is used to navigate to a specific page.   
     * It sets the currentPage variable to the page number that was clicked and checks if the current page 
     is the first page, last page, and if the previous and next buttons should be disabled.
    * It also calls the displayAccounts property and slice the accounts array to display the accounts for the current page.
    * @param {Object} event - event object passed when a page number is clicked.
    */
  navigateToPage(event) {
    //Sets the currentPage variable to the page number that was clicked
    this.currentPage = parseInt(event.target.textContent, 10);
    //Checks if the current page is the first page
    this.isFirstPage = this.currentPage === 1;
    //Checks if the current page is the last page
    this.isLastPage = this.currentPage === this.totalPages;
    //Checks if the previous button should be disabled
    this.isPreviousDisabled = this.currentPage === 1;
    //Checks if the next button should be disabled
    this.isNextDisabled = this.currentPage === this.totalPages;
    //Displays the accounts for the current page
    this.displayAccounts = this.accounts.slice(
      //Calculates the start index for the current page
      (this.currentPage - 1) * this.pageSize,
      //Calculates the end index for the current page
      this.currentPage * this.pageSize
    );
  }
  /**
     * navigateToPreviousPage method is used to navigate to the previous page in the pagination.
     * It updates the currentPage, isFirstPage, isLastPage, isPreviousDisabled and isNextDisabled properties
     and also updates the accounts to be displayed on the current page
    */
  navigateToPreviousPage() {
    // Assign the current page variable to the current page minus 1
    this.currentPage = this.currentPage - 1;
    // Assign the isLastPage variable to false, indicating that the current page is not the last page
    this.isLastPage = false;
    // If the current page is equal to 1
    if (this.currentPage === 1) {
      // Assign the isFirstPage variable to true, indicating that the current page is the first page
      this.isFirstPage = true;
      // Assign the isPreviousDisabled variable to true, indicating that the "previous" button should be disabled
      this.isPreviousDisabled = true;
    }
    // Assign the isNextDisabled variable to false, indicating that the "next" button should be enabled
    this.isNextDisabled = false;
    // Assign the accounts to be displayed on the previous page
    this.displayAccounts = this.accounts.slice(
      (this.currentPage - 1) * this.pageSize,
      this.currentPage * this.pageSize
    );
  }
  /**
   * navigateToNextPage method is used to navigate to the next page of accounts.
   * It checks if the current page is less than the total number of pages.
   * If true, it increments the current page by 1, updates the isFirstPage, isLastPage, isPreviousDisabled and isNextDisabled properties.
   * It also updates the displayAccounts array to show the accounts for the current page
   */
  navigateToNextPage() {
    //Checks if the current page is less than the total number of pages
    if (this.currentPage < this.totalPages) {
      //Increments the current page by 1
      this.currentPage++;
      //Checks if the current page is equal to the total number of pages
      if (this.currentPage === this.totalPages) {
        //Sets isFirstPage and isPreviousDisabled to false, isLastPage and isNextDisabled to true
        this.isFirstPage = false;
        this.isLastPage = true;
        this.isPreviousDisabled = false;
        this.isNextDisabled = true;
      } else {
        //Sets isFirstPage, isLastPage, isPreviousDisabled, and isNextDisabled to false
        this.isFirstPage = false;
        this.isLastPage = false;
        this.isPreviousDisabled = false;
        this.isNextDisabled = false;
      }
      //Updates the displayAccounts array to show the accounts for the current page
      this.displayAccounts = this.accounts.slice(
        (this.currentPage - 1) * this.pageSize,
        this.currentPage * this.pageSize
      );
    }
  }
  /**
   * handleSort method is used to handle the sorting of data when user clicks on the table header.
   * It gets the field name of the clicked table header and checks if the current sort field is the same as the field name of the clicked table header.
   * If the same, it toggles the sort order. If not, it updates the sort field and sets the sort order as ascending.
   * It also calls the sortData method to sort the data.
   * @param {Event} event - event object of the table header click
   */
  handleSort(event) {
    // get the field name of the clicked table header
    let fieldName = event.target.dataset.fieldName;
    // check if the current sort field is the same as the field name of the clicked table header
    if (this.sortField === fieldName) {
      // if the same, toggle the sort order
      this.sortAscending = !this.sortAscending;
    } else {
      // if not the same, update the sort field and set sort order as ascending
      this.sortField = fieldName;
      this.sortAscending = true;
    }
    // call the sortData method to sort the data
    this.sortData(this.sortField, this.sortAscending);
  }
  /**
     * sortData method is used to sort the data based on the given field name and sort order.
     * It creates a copy of the accounts array, sorts it using the provided field name and sort order, and assigns the sorted data back to the accounts array.
     * It also assigns a slice of the sorted data to the displayAccounts array to only show a certain number of records per page,
     and creates and adds a sort icon to indicate the current sort order.
    * @param {String} sortField - the field name by which to sort the data.
    * @param {Boolean} sortAscending - the sort order, true for ascending and false for descending
    */
  sortData(sortField, sortAscending) {
    this.accounts = [...this.accounts].sort((a, b) => {
      let valueA;
      let valueB;
      // Check if the field name exists in the object, if not set value to null
      if (a[sortField] === undefined) {
        valueA = null;
      } else {
        valueA = a[sortField];
      }
      if (b[sortField] === undefined) {
        valueB = null;
      } else {
        valueB = b[sortField];
      }
      if (valueA === null) {
        // return 1 for ascending and -1 for descending if valueA is null
        return sortAscending ? 1 : -1;
      }
      if (valueB === null) {
        // return -1 for ascending and 1 for descending if valueB is null
        return sortAscending ? -1 : 1;
      }
      if (typeof valueA === "string") {
        // convert valueA to lowercase if it is a string
        valueA = valueA.toLowerCase();
      }
      if (typeof valueB === "string") {
        // convert valueB to lowercase if it is a string
        valueB = valueB.toLowerCase();
        valueB = valueB.toLowerCase();
      }
      if (sortAscending) {
        return valueA > valueB ? 1 : -1;
      } else {
        return valueA < valueB ? 1 : -1;
      }
    });
    /* Assign a slice of the sorted data to the displayAccounts
     *  array to only show a certain number of records per page
     */
    this.displayAccounts = this.accounts.slice(0, this.pageSize);
    // Select any existing sort icon on the page
    let existingIcon = this.template.querySelectorAll('img[id="sorticon"]');
    // If an existing sort icon is found, remove it
    if (existingIcon[0]) {
      existingIcon[0].parentNode.removeChild(existingIcon[0]);
    }
    // Create a new sort icon element
    let icon = document.createElement("img");
    /* If sortAscending is true, set the sort icon's
     *source to the ascending arrow image
     */
    if (sortAscending) {
      icon.setAttribute("src", Images + "/Images/arrowup.png");
    }
    /* If sortAscending is false, set the sort icon's
     *  source to the descending arrow image
     */
    if (!sortAscending) {
      icon.setAttribute("src", Images + "/Images/arrowdown.png");
    }
    // Set the sort icon's id attribute to "sorticon"
    icon.setAttribute("id", "sorticon");
    // Set the sort icon's height and width
    icon.style.height = "15px";
    icon.style.width = "15px";
    icon.style.paddingBottom = "2px";
    // Select the table header for the sortField passed in
    let nodes = this.template.querySelectorAll(
      'span[data-field-id="' + sortField + '"]'
    );
    // Append the sort icon to the selected table header
    nodes.forEach((input) => {
      input.appendChild(icon);
    });
    this.navigateToFirstPage();
  }
}

Apex Class

public with sharing class AccountController {
    @AuraEnabled(cacheable=true)
    public static List<Account> getAccounts() {
        return [SELECT Id, Name,Phone, Industry,CreatedDate,
                LastModifiedDate FROM Account 
                WIth SECURITY_ENFORCED ORDER BY NAME];
    }
}