Opening statement:
Greetings to all! Building on our earlier blog post about implementing pagination in HTML tables within Salesforce Lightning Web Components (LWC) using JavaScript, this article will delve into the addition of column filtering and sorting in the same context. The presented component facilitates the presentation of paginated data, along with the ability to apply filters and sort columns. We’ll thoroughly explore the features and code of this LWC component to comprehend its functionality.
Situation:
Imagine, we have a requirement to show account data in a table with pagination in order to show multiple account records, and we also need to filter account data using column filters. We will create the LWC component for the same and will use HTML table and JavaScript to show data on UI.
Structure of HTML:
Let’s break down the component code section by section.
- Loading Spinner: The initial part of the template introduces a loading spinner, visible or hidden based on the boolean property showSpinner. When showSpinner is true, the spinner, styled with Salesforce Lightning Design System (SLDS) classes, is presented. This practice contributes to an enhanced user experience during data loading or processing.
- Lightning Card: The core content is encapsulated within a lightning-card component, featuring a title and containing an HTML table styled with SLDS classes.
- Table Headers: The table headers showcase the column names (Name, Phone, Industry, Created Date, and Last Modified Date), each equipped with sorting and filtering functionalities. Sorting is managed by the handleSort method, activated upon the user clicking on the column name. Each header cell incorporates two actions for filtering: Filter and Clear Filter, both controlled by the handleHeaderAction method. The current filter applied to each column is also visibly displayed in the header cell.
- Table Body: The table body involves a loop iterating through the displayAccounts array, presenting details for each account in a row. Lightning-formatted-* components are employed to format Phone, CreatedDate, and LastModifiedDate fields. In cases where there is no data (when accounts.length is false), a message “No data to display” is presented.
- Pagination: Pagination controls are generated using lightning-button elements, facilitating navigation to the First, Previous, Next, and Last pages, along with numbered page navigation buttons. Button states are dynamically adjusted (disabled or enabled) based on the context, such as disabling the ‘Previous’ button on the first page.
- Filter Dialog: A modal dialog is employed for the filtering feature. This modal exhibits a list of options or an input field (date or text) contingent on the data type in the selected column. The handleFilterChange method captures the user’s filtering choices, and applyFilter is responsible for implementing the filter on the table data.
!-- 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 title="Pagination Example with html table and column having sorting and filtering."> <!-- 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 class="slds-text-title_caps" scope="col"> <span title="Click on header name to sort" data-field-name="Name" onclick={handleSort}>Name</span> <template if:true={filters.Name}> <span style="text-align: left; padding: 8px; width: 100px; word-wrap: break-word; overflow-wrap: break-word; white-space: pre-line;font-weight: 600; font-size: 10px; ">: {filters.Name}</span> </template> <!-- Add for Name --> <lightning-button-menu alternative-text="actions" onselect={handleHeaderAction} data-column="Name" icon-size="small" tooltip="Click to filter records"> <lightning-menu-item value="filter" label="Filter" icon-name="utility:filter" icon-size="small">></lightning-menu-item> <lightning-menu-item value="clearFilter" label="Clear Filter" icon-name="utility:clear" icon-size="small"></lightning-menu-item> </lightning-button-menu> <span data-field-id="Name" lwc:dom="manual"></span> </th> <!-- A header cell for the 'Phone' column --> <th class="slds-text-title_caps" scope="col"> <span data-field-name="Phone" onclick={handleSort}>Phone</span> <template if:true={filters.Phone}> <span style="text-align: left; padding: 8px; width: 100px; word-wrap: break-word; overflow-wrap: break-word; white-space: pre-line;font-weight: 600; font-size: 10px; ">: {filters.Phone}</span> </template> <!-- Add for Phone --> <lightning-button-menu alternative-text="actions" onselect={handleHeaderAction} data-column="Phone" icon-size="small" tooltip="Click to filter records"> <lightning-menu-item value="filter" label="Filter" icon-name="utility:filter" icon-size="small"></lightning-menu-item> <lightning-menu-item value="clearFilter" label="Clear Filter" icon-name="utility:clear" icon-size="small"></lightning-menu-item> </lightning-button-menu> <span data-field-id="Phone" lwc:dom="manual"></span> </th> <!-- A header cell for the 'Industry' column --> <th class="slds-text-title_caps" scope="col"> <span data-field-name="Industry" onclick={handleSort}>Industry</span> <template if:true={filters.Industry}> <span style="text-align: left; padding: 8px; width: 100px; word-wrap: break-word; overflow-wrap: break-word; white-space: pre-line;font-weight: 600; font-size: 10px; ">: {filters.Industry}</span> </template> <!-- Add for Industry --> <lightning-button-menu alternative-text="actions" onselect={handleHeaderAction} data-column="Industry" icon-size="small" tooltip="Click to filter records"> <lightning-menu-item value="filter" label="Filter" icon-name="utility:filter" icon-size="small"></lightning-menu-item> <lightning-menu-item value="clearFilter" label="Clear Filter" icon-name="utility:clear" icon-size="small"></lightning-menu-item> </lightning-button-menu> <span data-field-id="Industry" lwc:dom="manual"></span> </th> <!-- <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 class="slds-text-title_caps" scope="col"> <span data-field-name="CreatedDate" onclick={handleSort}>Created Date</span> <template if:true={filters.CreatedDate}> <span style="text-align: left; padding: 8px; width: 100px; word-wrap: break-word; overflow-wrap: break-word; white-space: pre-line;font-weight: 600; font-size: 10px; ">: {filters.CreatedDate}</span> </template> <!-- Add for CreatedDate --> <lightning-button-menu alternative-text="actions" onselect={handleHeaderAction} data-column="CreatedDate" icon-size="small" tooltip="Click to filter records"> <lightning-menu-item value="filter" label="Filter" icon-name="utility:filter" icon-size="small"></lightning-menu-item> <lightning-menu-item value="clearFilter" label="Clear Filter" icon-name="utility:clear" icon-size="small"></lightning-menu-item> </lightning-button-menu> <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}> LastModified Date <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> <br/> <!-- 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> <!-- Popup for filter entry --> <template if:true={showModal}> <section role="dialog" tabindex="-1" aria-labelledby="modal-heading-01" aria-modal="true" aria-describedby="modal-content-id-1" class="slds-modal slds-fade-in-open"> <div class="slds-modal__container"> <!-- Modal/Popup Box LWC body here --> <div class="slds-modal__content slds-p-around_medium" id="modal-content-id-1"> <p style="font-weight: 700;float: left;"> {columnFilterName} </p><br /> <template if:true={isPicklist}> <lightning-dual-listbox name="picklistValues" variant="label-hidden" source-label="Available" selected-label="Selected" options={picklistOptions} onchange={handleFilterChange}> </lightning-dual-listbox> </template> <template if:false={isPicklist}> <template if:true={isDate}> <lightning-input type="date" variant="label-hidden" onchange={handleFilterChange} date-style="short"></lightning-input> </template> <template if:false={isDate}> <lightning-input variant="label-hidden" type="text" label={columnFilterName} onchange={handleFilterChange}></lightning-input> </template> </template> </div> <!-- Modal/Popup Box LWC footer here --> <footer class="slds-modal__footer"> <lightning-button variant="brand" label="Apply Filter" title="Apply Filter" onclick={applyFilter} class="slds-m-left_x-small"></lightning-button> <lightning-button variant="brand" class="slds-m-left_x-small" label="Close" title="Close" onclick={closeModal}></lightning-button> </footer> </div> </section> <div class="slds-backdrop slds-backdrop_open"></div> </template> </template>!-- 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 title="Pagination Example with html table and column having sorting and filtering."> <!-- 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 class="slds-text-title_caps" scope="col"> <span title="Click on header name to sort" data-field-name="Name" onclick={handleSort}>Name</span> <template if:true={filters.Name}> <span style="text-align: left; padding: 8px; width: 100px; word-wrap: break-word; overflow-wrap: break-word; white-space: pre-line;font-weight: 600; font-size: 10px; ">: {filters.Name}</span> </template> <!-- Add for Name --> <lightning-button-menu alternative-text="actions" onselect={handleHeaderAction} data-column="Name" icon-size="small" tooltip="Click to filter records"> <lightning-menu-item value="filter" label="Filter" icon-name="utility:filter" icon-size="small">></lightning-menu-item> <lightning-menu-item value="clearFilter" label="Clear Filter" icon-name="utility:clear" icon-size="small"></lightning-menu-item> </lightning-button-menu> <span data-field-id="Name" lwc:dom="manual"></span> </th> <!-- A header cell for the 'Phone' column --> <th class="slds-text-title_caps" scope="col"> <span data-field-name="Phone" onclick={handleSort}>Phone</span> <template if:true={filters.Phone}> <span style="text-align: left; padding: 8px; width: 100px; word-wrap: break-word; overflow-wrap: break-word; white-space: pre-line;font-weight: 600; font-size: 10px; ">: {filters.Phone}</span> </template> <!-- Add for Phone --> <lightning-button-menu alternative-text="actions" onselect={handleHeaderAction} data-column="Phone" icon-size="small" tooltip="Click to filter records"> <lightning-menu-item value="filter" label="Filter" icon-name="utility:filter" icon-size="small"></lightning-menu-item> <lightning-menu-item value="clearFilter" label="Clear Filter" icon-name="utility:clear" icon-size="small"></lightning-menu-item> </lightning-button-menu> <span data-field-id="Phone" lwc:dom="manual"></span> </th> <!-- A header cell for the 'Industry' column --> <th class="slds-text-title_caps" scope="col"> <span data-field-name="Industry" onclick={handleSort}>Industry</span> <template if:true={filters.Industry}> <span style="text-align: left; padding: 8px; width: 100px; word-wrap: break-word; overflow-wrap: break-word; white-space: pre-line;font-weight: 600; font-size: 10px; ">: {filters.Industry}</span> </template> <!-- Add for Industry --> <lightning-button-menu alternative-text="actions" onselect={handleHeaderAction} data-column="Industry" icon-size="small" tooltip="Click to filter records"> <lightning-menu-item value="filter" label="Filter" icon-name="utility:filter" icon-size="small"></lightning-menu-item> <lightning-menu-item value="clearFilter" label="Clear Filter" icon-name="utility:clear" icon-size="small"></lightning-menu-item> </lightning-button-menu> <span data-field-id="Industry" lwc:dom="manual"></span> </th> <!-- <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 class="slds-text-title_caps" scope="col"> <span data-field-name="CreatedDate" onclick={handleSort}>Created Date</span> <template if:true={filters.CreatedDate}> <span style="text-align: left; padding: 8px; width: 100px; word-wrap: break-word; overflow-wrap: break-word; white-space: pre-line;font-weight: 600; font-size: 10px; ">: {filters.CreatedDate}</span> </template> <!-- Add for CreatedDate --> <lightning-button-menu alternative-text="actions" onselect={handleHeaderAction} data-column="CreatedDate" icon-size="small" tooltip="Click to filter records"> <lightning-menu-item value="filter" label="Filter" icon-name="utility:filter" icon-size="small"></lightning-menu-item> <lightning-menu-item value="clearFilter" label="Clear Filter" icon-name="utility:clear" icon-size="small"></lightning-menu-item> </lightning-button-menu> <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}> LastModified Date <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> <br/> <!-- 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> <!-- Popup for filter entry --> <template if:true={showModal}> <section role="dialog" tabindex="-1" aria-labelledby="modal-heading-01" aria-modal="true" aria-describedby="modal-content-id-1" class="slds-modal slds-fade-in-open"> <div class="slds-modal__container"> <!-- Modal/Popup Box LWC body here --> <div class="slds-modal__content slds-p-around_medium" id="modal-content-id-1"> <p style="font-weight: 700;float: left;"> {columnFilterName} </p><br /> <template if:true={isPicklist}> <lightning-dual-listbox name="picklistValues" variant="label-hidden" source-label="Available" selected-label="Selected" options={picklistOptions} onchange={handleFilterChange}> </lightning-dual-listbox> </template> <template if:false={isPicklist}> <template if:true={isDate}> <lightning-input type="date" variant="label-hidden" onchange={handleFilterChange} date-style="short"></lightning-input> </template> <template if:false={isDate}> <lightning-input variant="label-hidden" type="text" label={columnFilterName} onchange={handleFilterChange}></lightning-input> </template> </template> </div> <!-- Modal/Popup Box LWC footer here --> <footer class="slds-modal__footer"> <lightning-button variant="brand" label="Apply Filter" title="Apply Filter" onclick={applyFilter} class="slds-m-left_x-small"></lightning-button> <lightning-button variant="brand" class="slds-m-left_x-small" label="Close" title="Close" onclick={closeModal}></lightning-button> </footer> </div> </section> <div class="slds-backdrop slds-backdrop_open"></div> </template> </template>
JavaScript Functionality:
The JavaScript code employs various Lightning Web Components (LWC) features to deliver extensive functionality to the component. Let’s delve into some key aspects:
- Imports: At the code’s outset, multiple imports are observed, including:
LightningElement
from ‘lwc’: The foundational class for Lightning Web Components; all LWCs extend this base class.getAccounts
,fetchPicklistValues
,getFieldType
from ‘@salesforce/apex/AccountController’: These are Apex methods imported from the AccountController Apex class.- Images from “@salesforce/resourceUrl/Images”: A static resource import facilitating access to image files stored in Salesforce.
- ‘./htmlTablePagination.css’: Import of a CSS file specific to the HtmlTablePagination component.
- Data Properties: Following the class export, various variables essential throughout the class are declared. These include the
accounts
array,displayAccounts
array, and others for pagination, sorting, and filtering data. - Wire Adapter: The
@wire
decorator is utilized to invoke an Apex method, fetching a list of accounts from the Salesforce database. - Functions and Methods: There exist numerous methods in this component, such as:
handleFilterChange()
: Manages the event when a filter value is altered.handleHeaderAction()
: Determines the action to be executed when the column header is clicked.applyFilter()
: Applies filters to the data.filterData()
: Filters the data based on the selected filters.removeSortIcon()
: Removes the sort icon from the UI.clearFilter()
: Removes a specific filter.applyRemainingFilters()
: Applies all remaining filters after a filter has been removed.openModal()
,closeModal()
: Handle the opening and closing of a modal used for filter selection.- Navigation methods (
navigateToFirstPage()
,navigateToLastPage()
,navigateToPage()
,navigateToPreviousPage()
,navigateToNextPage()
): Manage pagination functionality. handleSort()
: Deals with the event when the user wishes to sort the data.sortData()
: Sorts the data based on the selected sort field and order.compareValues()
: Compares values during sorting.handleSortIcon()
: Manages the UI for the sorting icons.
// Import the LightningElement class and the track decorator from the lwc module import { LightningElement, wire } from "lwc"; // Import the getAccounts method from the AccountController Apex class import getAccounts from "@salesforce/apex/AccountController.getAccounts"; import fetchPicklistValues from '@salesforce/apex/AccountController.fetchPicklistValues'; import getFieldType from '@salesforce/apex/AccountController.getFieldType'; // Import the Images resource for sort up and down icon from static resources import Images from "@salesforce/resourceUrl/Images"; import './htmlTablePagination.css'; // Declare the Pagenationex class as the default export export default class HtmlTablePagination extends LightningElement { // Declare variables for data binding // Accounts data array to store account records returned from apex controller accounts = []; // Accounts data array to be displayed on current page displayAccounts = []; // Array of page numbers pageNumbers = []; // Current page number currentPage = 1; // Total number of pages totalPages = 0; // Number of records per page pageSize = 5; // Total number of records totalRecords; // Flag to indicate if current page is first page isFirstPage = true; // Flag to indicate if current page is last page isLastPage = false; // Flag to disable previous button isPreviousDisabled = true; // Flag to disable next button isNextDisabled = false; // Field on which data is to be sorted sortField; // Flag to indicate if data is to be sorted in ascending order sortAscending = true; // Flag to show/hide spinner showSpinner = true; // Flag to show/hide paginationbuttons showPageButtons = false; filteredData = []; showModal = false; filterValue = ''; // stores current filter values filters = {}; columnFilterName; selectedColumnName; isPicklist = false; isDate = false; picklistOptions = []; columnDataType; metadata = []; selectedNameValue = ''; selectedPhoneValue = ''; selectedIndustryValue = ''; //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; this.filteredData = [...this.accounts]; // 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; // Assign the flag for previous button to be disabled this.isPreviousDisabled = true; if (this.filteredData.length <= this.pageSize) { // Assign the flag for next button to be enabled / disabled this.isNextDisabled = true; // Assign the flag for last button to be enabled / disabled this.isLastPage = true; } else { // Assign the flag for next button to be enabled / disabled this.isNextDisabled = false; // Assign the flag for last button to be enabled / disabled this.isLastPage = false; } //this.isNextDisabled = false; // Assign the accounts to be displayed on the first page this.displayAccounts = this.filteredData.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.filteredData.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.filteredData.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.filteredData.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.filteredData.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.filteredData = [...this.filteredData].sort((a, b) => { let valueA = a[sortField] === undefined ? null : a[sortField]; let valueB = b[sortField] === undefined ? null : b[sortField]; if (valueA === null) { return sortAscending ? 1 : -1; } if (valueB === null) { return sortAscending ? -1 : 1; } return this.compareValues(valueA, valueB, sortAscending); }); this.displayAccounts = this.filteredData.slice(0, this.pageSize); this.handleSortIcon(sortAscending, sortField); this.navigateToFirstPage(); } /** * compareValues method is used to compare two values in a natural order. * @param {String} valueA - the first value to be compared. * @param {String} valueB - the second value to be compared. * @param {Boolean} sortAscending - the sort order, true for ascending and false for descending */ compareValues(valueA, valueB, sortAscending) { if (typeof valueA === "string" && typeof valueB === "string") { const partsA = valueA.match(/\D+|\d+/g); const partsB = valueB.match(/\D+|\d+/g); let i = 0; while (i < partsA.length && i < partsB.length) { if (/^\d+$/.test(partsA[i]) && /^\d+$/.test(partsB[i])) { const num1 = parseInt(partsA[i], 10); const num2 = parseInt(partsB[i], 10); if (num1 !== num2) { return sortAscending ? num1 - num2 : num2 - num1; } } else { const cmp = partsA[i].localeCompare(partsB[i]); if (cmp !== 0) { return sortAscending ? cmp : -cmp; } } i++; } return partsA.length - partsB.length; } else { const aValue = typeof valueA === "string" ? valueA.toLowerCase() : valueA; const bValue = typeof valueB === "string" ? valueB.toLowerCase() : valueB; if (aValue < bValue) { return sortAscending ? -1 : 1; } else if (aValue > bValue) { return sortAscending ? 1 : -1; } else { return 0; } } } /** * handleSortIcon method is used to create, remove and set the sort icon * @param {Boolean} sortAscending - the sort order, true for ascending and false for descending * @param {String} sortField - the field name by which to sort the data. */ handleSortIcon(sortAscending, sortField) { let existingIcon = this.template.querySelectorAll('img[id="sorticon"]'); if (existingIcon[0]) { existingIcon[0].parentNode.removeChild(existingIcon[0]); } let icon = document.createElement("img"); if (sortAscending) { icon.setAttribute("src", Images + "/Images/arrowup.png"); } if (!sortAscending) { icon.setAttribute("src", Images + "/Images/arrowdown.png"); } icon.setAttribute("id", "sorticon"); icon.style.height = "15px"; icon.style.width = "15px"; icon.style.paddingBottom = "2px"; let nodes = this.template.querySelectorAll( 'span[data-field-id="' + sortField + '"]' ); nodes.forEach((input) => { input.appendChild(icon); }); } /** * handleFilterChange method is used to handle the entered filter value */ handleFilterChange(event) { this.filterValue = event.detail.value; } /** * handleHeaderAction method is used to handler the apply filter button * @param {Object} event - name of column, where filter is selected. */ handleHeaderAction(e) { const action = e.detail.value; const column = e.target.dataset.column; this.selectedColumnName = column; this.columnFilterName = 'Filter Column : ' + this.selectedColumnName.toUpperCase(); if (action === 'filter') { getFieldType({ objectName: 'Account', fieldName: this.selectedColumnName }) .then(result => { this.columnDataType = result; let fieldMetadata = this.metadata.find(metadata => metadata.fieldType === this.columnDataType); if (fieldMetadata) { // if metadata exists, update the fieldType fieldMetadata.fieldType = result; } else { // if metadata does not exist, add it to the metadata array this.metadata.push({ fieldName: this.selectedColumnName, fieldType: result }); } let fieldMetadata2 = this.metadata.find(metadata => metadata.fieldName === this.selectedColumnName); let fieldType = fieldMetadata2 ? fieldMetadata2.fieldType : 'text'; // default to 'text' if fieldType is not found in metadata if (fieldType.toLowerCase() === 'picklist') { this.isPicklist = true; fetchPicklistValues({ objectName: 'Account', fieldName: this.selectedColumnName }) .then(result2 => { this.picklistOptions = result2.map(value => ({ label: value, value: value })); this.openModal(); }) .catch(error => { console.error('Error getting picklist values', error); }); } else if (fieldType.toLowerCase() === 'datetime') { this.isDate = true; this.openModal(); } else { this.isPicklist = false; this.isDate = false; this.openModal(); } }) .catch(error => { console.error('Error determining if field is a picklist', error); }); } else if (action === 'clearFilter') { this.clearFilter(column); } } /** * filterData method is used to apply the filter to data */ applyFilter() { this.filters[this.selectedColumnName] = this.filterValue; this.filterData(); this.sortField = null; this.sortAscending = true; this.removeSortIcon(); this.closeModal(); } /** * filterData method is used to filter the account list based on enetered filter value */ filterData() { for (let key in this.filters) { if (this.hasProperty(this.filters, key)) { // get the fieldType of the column from metadata let fieldMetadata = this.metadata.find(metadata => metadata.fieldName === key); let fieldType = fieldMetadata ? fieldMetadata.fieldType : 'string'; // default to 'string' if fieldType is not found in metadata this.filteredData = this.filteredData.filter(row => { if (row[key]) { // check if the column is a picklist based on its metadata if (fieldType.toLowerCase() === 'picklist') { let rowValue = row[key].toLowerCase(); // check if the row's value is included in any of the selected options return this.filters[key].some(filterVal => filterVal.toLowerCase().includes(rowValue)); } else if (fieldType.toLowerCase() === 'date') { // convert both filter date and row date to the same format (yyyy-mm-dd) for comparison let rowDate = new Date(row[key]).toISOString().split('T')[0]; // converts row date to 'yyyy-mm-dd' format let filterDate = this.filters[key].split('-').reverse().join('-'); // converts 'dd-mm-yyyy' to 'yyyy-mm-dd' return rowDate === filterDate; } else if (fieldType.toLowerCase() === 'datetime') { // transform the row's value to the date format 'yyyy-mm-dd' let rowValue = new Date(row[key]).toISOString().split('T')[0]; return rowValue === this.filters[key]; } else { let rowValue = row[key].toString().toLowerCase(); return rowValue.includes(this.filters[key].toLowerCase()); } } return false; }); } } this.totalPages = Math.ceil(this.filteredData.length / this.pageSize); //.setPages(this.filteredData); this.navigateToFirstPage(); } /** * removeSortIcon method is used to remove the sort icon from column */ removeSortIcon() { // 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]); } } /** * clearFilter method is used to clear the selected column filters * @param {Object} event - name of column, where filter is selected. */ clearFilter(column) { if (this.hasProperty(this.filters, column)) { delete this.filters[column]; delete this.metadata[column]; this.filterValue = ''; this.applyRemainingFilters(); } } /** * applyRemainingFilters method is used to apply the filters, in case multiple filters are there and any one of them is removed. */ applyRemainingFilters() { // if no more filters, reset to original data if (!this.hasProperties(this.filters)) { this.filteredData = [...this.accounts]; this.metadata = []; this.isPicklist = false; this.isDate = false; } else { // reset before applying remaining filters this.filteredData = [...this.accounts]; this.filterData(); } this.totalPages = Math.ceil(this.filteredData.length / this.pageSize); this.setPages(this.filteredData); this.navigateToFirstPage(); // reset sorting this.sortField = null; this.sortAscending = true; // remove sort icon this.removeSortIcon(); } /** * hasProperties method is used to check the properties in a array. */ hasProperties(obj) { return Object.keys(obj).length > 0; } /** * hasProperties method is used to check the property in a array. */ hasProperty(obj, property) { return Object.prototype.hasOwnProperty.call(obj, property); } /** * Open Modal method is used to open the modal popup. */ openModal() { this.showModal = true; } /** * closeModal method is used to close the modal popup. */ closeModal() { this.showModal = false; } }
<?xml version="1.0" encoding="UTF-8"?> <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata"> <apiVersion>57.0</apiVersion> <isExposed>true</isExposed> <masterLabel>Pagination Example With HTML Table</masterLabel> <description>Component that shows an example of doing pagination with html table.</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> </LightningComponentBundle>
Styles in CSS:
The CSS code provides styling to enhance the appearance and functionality of the component. Key styles include:
- The table element is styled with a fixed layout and collapsed borders.The th and td elements have left-aligned text and a padding of 8 pixels.The width of both th and td elements is set to 100 pixels.The word-wrap property breaks long words, and the overflow-wrap property allows breaking words at any character in case of overflow.The white-space property is set to pre-line, preserving line breaks and collapsing consecutive spaces.
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; white-space: pre-line; }
Apex Class:
public with sharing class AccountController { @AuraEnabled(cacheable=true) public static List<String> fetchPicklistValues(String objectName, String fieldName) { List<String> picklistValues = new List<String>(); Schema.SObjectType sObjectType = Schema.getGlobalDescribe().get(objectName); Schema.DescribeSObjectResult describeSObjectResult = sObjectType.getDescribe(); Schema.DescribeFieldResult describeFieldResult = describeSObjectResult.fields.getMap().get(fieldName).getDescribe(); for (Schema.PicklistEntry picklistEntry : describeFieldResult.getPicklistValues()) { picklistValues.add(picklistEntry.getValue()); } return picklistValues; } @AuraEnabled(cacheable=true) public static String getFieldType(String objectName, String fieldName) { Map<String, Schema.SObjectType> schemaMap = Schema.getGlobalDescribe(); Schema.SObjectType objectSchema = schemaMap.get(objectName); Schema.DescribeSObjectResult describe = objectSchema.getDescribe(); Map<String, Schema.SObjectField> fieldMap = describe.fields.getMap(); Schema.DescribeFieldResult fieldDescribe = fieldMap.get(fieldName).getDescribe(); return fieldDescribe.getType().name(); } @AuraEnabled(cacheable=true) public static List<Account> getAccounts() { return [ SELECT Id, Name, Phone, Industry, CreatedDate, LastModifiedDate,Rating,Test__c,Type FROM Account WITH SECURITY_ENFORCED ORDER BY Name ASC,LastModifiedDate DESC ]; } }