Tag Archives: DataTable

Salesforce LWC Custom Datatable Pagination

Apex Class:

public class AccountController {
    
    @AuraEnabled//Get Account Records
    public static String getAccountList(Integer pageSize, Integer pageNumber){
        String jsonDT = '';
        
        //Offset for SOQL
        Integer offset = (pageNumber - 1) * pageSize;
        
        //Total Records
        Integer totalRecords = [SELECT COUNT() FROM Account];
        Integer recordEnd = pageSize * pageNumber;
        
        AccountDTWrapper objDT =  new AccountDTWrapper();  
        objDT.pageSize = pageSize;
        objDT.pageNumber = pageNumber;
        objDT.recordStart = offset + 1;
        objDT.recordEnd = totalRecords >= recordEnd ? recordEnd : totalRecords;
        objDT.totalRecords = totalRecords;
        objDT.accounts = [SELECT Id, Name, AccountNumber, Industry, Phone FROM Account LIMIT :pageSize OFFSET :offset];
        jsonDT = JSON.serialize(objDT);
        return jsonDT;
    }
    
    public class AccountDTWrapper {
        public Integer pageSize {get;set;}
        public Integer pageNumber {get;set;}
        public Integer totalRecords {get;set;}
        public Integer recordStart {get;set;}
        public Integer recordEnd {get;set;}
        public List<Account> accounts {get;set;}
    }
}

accountList.html

<template>
    <template if:true={loader}>
        <lightning-spinner alternative-text="Loading..." size="small"></lightning-spinner>
    </template>

    <div class="slds-box slds-theme_default">
        <lightning-card  title="Accounts">
            <table class="slds-table slds-table_cell-buffer slds-table_bordered">
                <thead>
                    <tr class="slds-line-height_reset slds-text-title_caps">
                        <th  class="slds-is-resizable" scope="col">
                            <div class="slds-truncate" title="Name">
                                Name
                            </div>
                        </th>
                        <th  class="slds-is-resizable" scope="col">
                            <div class="slds-truncate" title="Account Number">
                                Account Number
                            </div>
                        </th>
                        <th  class="slds-is-resizable" scope="col">
                            <div class="slds-truncate" title="Industry">
                                Industry
                            </div>
                        </th>
                        <th class="slds-is-resizable" scope="col">
                            <div class="slds-truncate" title="Phone">
                                Phone
                            </div>
                        </th>
                    </tr>
                </thead>
                <tbody>
                    <template if:true={accounts}>
                        <template for:each={accounts} for:item="acc">
                            <tr key={acc.Id}>
                                <th scope="row" data-label="Name">
                                    <div class="slds-truncate" title={acc.Name}>{acc.Name}</div>
                                </th>
                                <th scope="row" data-label="Account Number">
                                    <div class="slds-truncate" title={acc.AccountNumber}>{acc.AccountNumber}</div>
                                </th>
                                <th scope="row" data-label="Industry">
                                    <div class="slds-truncate" title={acc.Industry}>{acc.Industry}</div>
                                </th>
                                <th scope="row" data-label="Phone">
                                    <template if:true={acc.Phone}>
                                    <div class="slds-truncate" title={acc.Phone}>{acc.Phone}</div>
                                </template>
                                </th>
                            </tr>
                        </template>
                    </template>
                </tbody>
            </table>
            <template if:true={isDisplayNoRecords}>
                <div class="slds-align_absolute-center">
                    <br/>
                    No records found
                </div>
            </template>
            <br/>
            <div class="slds-align_absolute-center"> 
                <div class="slds-p-right_xx-small">
                        
                    <lightning-button label="Prev"
                    disabled={isPrev} onclick={handlePrev}
                                        variant="brand"
                                        icon-name="utility:back"
                                        name="prev"></lightning-button>  
                </div>
                <span class="slds-badge slds-badge_lightest">
                    {recordStart}-{recordEnd} of {totalRecords} | Page {pageNumber} of {totalPages}
                </span>
                <div class="slds-p-left_xx-small">
                    <lightning-button label="Next"
                    disabled={isNext} onclick={handleNext}
                                        variant="brand"
                                        icon-name="utility:forward"
                                        icon-position="right"
                                        name="next"></lightning-button>
                </div>
            </div>  
        </lightning-card>
    </div>
</template>

accountList.js

import { LightningElement, wire, api, track  } from 'lwc';
import getAccountList from '@salesforce/apex/AccountController.getAccountList';

export default class AccountList extends LightningElement {
    @track loader = false;
    @track error = null;
    @track pageSize = 10;
    @track pageNumber = 1;
    @track totalRecords = 0;
    @track totalPages = 0;
    @track recordEnd = 0;
    @track recordStart = 0;
    @track isPrev = true;
    @track isNext = true;
    @track accounts = [];

    //On load
    connectedCallback() {
        this.getAccounts();
    }

    //handle next
    handleNext(){
        this.pageNumber = this.pageNumber+1;
        this.getAccounts();
    }

    //handle prev
    handlePrev(){
        this.pageNumber = this.pageNumber-1;
        this.getAccounts();
    }

    //get accounts
    getAccounts(){
        this.loader = true;
        getAccountList({pageSize: this.pageSize, pageNumber : this.pageNumber})
        .then(result => {
            this.loader = false;
            if(result){
                var resultData = JSON.parse(result);
                this.accounts = resultData.accounts;
                this.pageNumber = resultData.pageNumber;
                this.totalRecords = resultData.totalRecords;
                this.recordStart = resultData.recordStart;
                this.recordEnd = resultData.recordEnd;
                this.totalPages = Math.ceil(resultData.totalRecords / this.pageSize);
                this.isNext = (this.pageNumber == this.totalPages || this.totalPages == 0);
                this.isPrev = (this.pageNumber == 1 || this.totalRecords < this.pageSize);
            }
        })
        .catch(error => {
            this.loader = false;
            this.error = error;
        });
    }

    //display no records
    get isDisplayNoRecords() {
        var isDisplay = true;
        if(this.accounts){
            if(this.accounts.length == 0){
                isDisplay = true;
            }else{
                isDisplay = false;
            }
        }
        return isDisplay;
    }
}

accountList.js-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
   <apiVersion>50.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__Tab</target>
    </targets>
</LightningComponentBundle>

Salesforce Lightning Custom Datatable Pagination & Sorting

Apex Class:

public class AccountController {
    
    @AuraEnabled//Get Account Records
    public static String getAccountList(Integer pageSize, Integer pageNumber, String sortingField, Boolean isSortAsc){
        
        String jsonDT = '';
        //Offset for SOQL
        Integer offset = (pageNumber - 1) * pageSize;
        
        //Total Records
        Integer totalRecords = [SELECT COUNT() FROM Account];
        Integer recordEnd = pageSize * pageNumber;
        
        String sortBy = isSortAsc ? 'ASC' : 'DESC';
        
        String query = 'SELECT Id, Name, AccountNumber, Industry, Phone FROM Account ORDER BY ';
        query += sortingField + ' ' + sortBy;
        query += ' LIMIT :pageSize OFFSET :offset';
        
        AccountDTWrapper objDT =  new AccountDTWrapper();  
        objDT.pageSize = pageSize;
        objDT.pageNumber = pageNumber;
        objDT.recordStart = offset + 1;
        objDT.recordEnd = totalRecords >= recordEnd ? recordEnd : totalRecords;
        objDT.totalRecords = totalRecords;
        objDT.accounts = Database.query(query);
        jsonDT = JSON.serialize(objDT);
        return jsonDT;
    }

    public class AccountDTWrapper {
        public Integer pageSize {get;set;}
        public Integer pageNumber {get;set;}
        public Integer totalRecords {get;set;}
        public Integer recordStart {get;set;}
        public Integer recordEnd {get;set;}
        public List<Account> accounts {get;set;}
    }
}

Lightning Component:

<aura:component implements="force:appHostable,flexipage:availableForAllPageTypes" access="global" controller="AccountController">
    
    <!--Declare Attributes-->
    <aura:attribute name="accounts" type="List"/>
    <aura:attribute name="pageNumber" type="integer" default="1"/>
    <aura:attribute name="pageSize" type="integer" default="10"/>
    <aura:attribute name="totalPages" type="integer" default="0"/>
    <aura:attribute name="totalRecords" type="integer" default="0"/>
    <aura:attribute name="recordStart" type="integer" default="0"/>
    <aura:attribute name="recordEnd" type="integer" default="0"/>
    
    <aura:attribute name="isSortByName" type="Boolean" default="false"/>
    <aura:attribute name="isSortByAccNo" type="Boolean" default="false"/>
    <aura:attribute name="isSortByIndustry" type="Boolean" default="false"/>
    <aura:attribute name="isSortByPhone" type="Boolean" default="false"/>
    <aura:attribute name="isSortAsc" type="Boolean" default="true"/>
    <aura:attribute name="selectedSortingField" type="String" default="Name"/>
    
    <!--Declare Handlers-->
    <aura:handler name="init" action="{!c.doInit}" value="{!this}"/>
    
    <!--Component Start-->
    <div class="slds-m-around_xx-large">
        <lightning:card>
            <aura:set attribute="title">
                Accounts
            </aura:set>
            <aura:set attribute="footer">
                <div class="slds-align_absolute-center"> 
                    <div class="slds-p-right_xx-small">
                        
                        <lightning:button label="Prev"
                                          onclick="{!c.handlePrev}"
                                          disabled="{! v.pageNumber == 1}"
                                          variant="brand"
                                          iconName="utility:back"
                                          name="prev"/>
                    </div>
                    <span class="slds-badge slds-badge_lightest">
                        {!v.recordStart}-{!v.recordEnd} of {!v.totalRecords} | Page {!v.pageNumber} of {!v.totalPages}
                    </span>
                    <div class="slds-p-left_xx-small">
                        <lightning:button label="Next"
                                          disabled="{!v.pageNumber == v.totalPages}"
                                          onclick="{!c.handleNext}"
                                          variant="brand"
                                          iconName="utility:forward"
                                          iconPosition="right"
                                          name="next"/>
                    </div>
                </div>  
                
            </aura:set>
            <table class="slds-table slds-table_cell-buffer slds-table_bordered">
                <thead>
                    <tr class="slds-line-height_reset slds-text-title_caps">
                        <th style="width:30%" data-record="Name" class="slds-is-resizable" scope="col" onclick="{!c.handleSorting}">
                            <a href="javascript:void(0);" class="slds-th__action slds-text-link--reset">
                                <span class="slds-assistive-text">Sort</span>
                                <span class="slds-truncate" title="Name">Name</span>
                                <aura:if isTrue="{!v.isSortByName}">
                                    <aura:if isTrue="{!v.isSortAsc}">
                                        &#9650;
                                        <aura:set attribute="else">
                                            &#9660;
                                        </aura:set>
                                    </aura:if> 
                                </aura:if>
                            </a>
                        </th>
                        <th style="width:20%"  data-record="AccountNumber" class="slds-is-resizable" scope="col" onclick="{!c.handleSorting}">
                            <a href="javascript:void(0);" class="slds-th__action slds-text-link--reset">
                                <span class="slds-assistive-text">Sort</span>
                                <span class="slds-truncate" title="Account Number">Account Number</span>
                                <aura:if isTrue="{!v.isSortByAccNo}">
                                    <aura:if isTrue="{!v.isSortAsc}">
                                        &#9650;
                                        <aura:set attribute="else">
                                            &#9660;
                                        </aura:set>
                                    </aura:if> 
                                </aura:if>
                            </a>
                        </th>
                        <th style="width:20%"  data-record="Industry" class="slds-is-resizable" scope="col" onclick="{!c.handleSorting}">
                            <a href="javascript:void(0);" class="slds-th__action slds-text-link--reset">
                                <span class="slds-assistive-text">Sort</span>
                                <span class="slds-truncate" title="Industry">Industry</span>
                                <aura:if isTrue="{!v.isSortByIndustry}">
                                    <aura:if isTrue="{!v.isSortAsc}">
                                        &#9650;
                                        <aura:set attribute="else">
                                            &#9660;
                                        </aura:set>
                                    </aura:if> 
                                </aura:if>
                            </a>
                        </th>
                        <th style="width:20%"  data-record="Phone" class="slds-is-resizable" scope="col" onclick="{!c.handleSorting}">
                            <a href="javascript:void(0);" class="slds-th__action slds-text-link--reset">
                                <span class="slds-assistive-text">Sort</span>
                                <span class="slds-truncate" title="Phone">Phone</span>
                                <aura:if isTrue="{!v.isSortByPhone}">
                                    <aura:if isTrue="{!v.isSortAsc}">
                                        &#9650;
                                        <aura:set attribute="else">
                                            &#9660;
                                        </aura:set>
                                    </aura:if> 
                                </aura:if>
                            </a>
                        </th>
                        <th scope="col" style="width:10%">
                            
                        </th>
                    </tr>
                </thead>
                <aura:if isTrue="{!not(empty(v.accounts))}">
                    <tbody>
                        <aura:iteration items="{!v.accounts}" var="acc">
                            <tr class="slds-hint-parent">
                                
                                <th data-label="Name" scope="row">
                                    <div class="slds-truncate" title="{!acc.Name}">
                                        {!acc.Name}
                                    </div>
                                </th>
                                <td data-label="Account Number">
                                    <div class="slds-truncate" title="{!acc.AccountNumber}">{!acc.AccountNumber}</div>
                                </td>
                                <td data-label="Industry">
                                    <div class="slds-truncate" title="{!acc.Industry}">{!acc.Industry}</div>
                                </td>
                                <td data-label="Phone">
                                    <div class="slds-truncate" title="{!acc.Phone}">{!acc.Phone}</div>
                                </td>
                                <td>
                                    <div class="slds-align_absolute-center">
                                        <lightning:buttonMenu alternativeText="Show menu" menuAlignment="auto" onselect="{!c.handleRowAction}" value="{!acc.Id}">
                                            <lightning:menuItem value="edit" label="Edit" iconName="utility:edit" title="Edit" />
                                            <lightning:menuItem value="view" label="View" iconName="utility:description" title="View" />
                                        </lightning:buttonMenu>
                                    </div>
                                </td>
                            </tr>
                        </aura:iteration>
                    </tbody>
                </aura:if>
            </table>
            <aura:if isTrue="{!empty(v.accounts)}">
                <div class="slds-align_absolute-center">
                    No records found
                </div>
            </aura:if>
        </lightning:card>
    </div>
    <!--Component End-->
    
</aura:component>

Lightning JS Controller:

({
    doInit : function(component, event, helper) {        
        helper.getAccounts(component, helper);
    },
    
    handleNext : function(component, event, helper) { 
        var pageNumber = component.get("v.pageNumber");
        component.set("v.pageNumber", pageNumber+1);
        helper.getAccounts(component, helper);
    },
    
    handlePrev : function(component, event, helper) {        
        var pageNumber = component.get("v.pageNumber");
        component.set("v.pageNumber", pageNumber-1);
        helper.getAccounts(component, helper);
    },

    handleRowAction: function (component, event, helper) {
        var selectedAction = event.detail.menuItem.get("v.value");
        var selectedAccountId = event.getSource().get("v.value");
        switch (selectedAction) {
            case 'edit':
                helper.editRecord(component, event, selectedAccountId);
                break;
            case 'view':
                helper.viewRecord(component, event, selectedAccountId);
                break;
        }
    },
    
    handleSorting: function (component, event, helper) {
        var selectedItem = event.currentTarget;
        var selectedField = selectedItem.dataset.record;
        
        component.set("v.isSortByName", false);
        component.set("v.isSortByAccNo", false);
        component.set("v.isSortByIndustry", false);
        component.set("v.isSortByPhone", false);
        component.set("v.selectedSortingField", selectedField);
        
        if(selectedField == 'Name'){
            component.set("v.isSortByName", true);
        }else if(selectedField == 'AccountNumber'){
            component.set("v.isSortByAccNo", true);
        }else if(selectedField == 'Industry'){
            component.set("v.isSortByIndustry", true);
        }else if(selectedField == 'Phone'){
            component.set("v.isSortByPhone", true);
        }
        helper.sortColumnData(component, event);
    },
})

Lightning JS Helper:

({
    getAccounts: function(component, event) {
        var action = component.get("c.getAccountList");
        action.setParams({
            "pageSize": component.get("v.pageSize"),
            "pageNumber": component.get("v.pageNumber"),
            "sortingField": component.get("v.selectedSortingField"),
            "isSortAsc": component.get("v.isSortAsc")
        });
        action.setCallback(this, function(response) {
            var state = response.getState();
            if (component.isValid() && state === "SUCCESS"){
                var result = response.getReturnValue();
                if(result){
                    var resultData = JSON.parse(result);
                    var pageSize = component.get("v.pageSize");
                    component.set("v.accounts", resultData.accounts);
                    component.set("v.pageNumber", resultData.pageNumber);
                    component.set("v.totalRecords", resultData.totalRecords);
                    component.set("v.recordStart", resultData.recordStart);
                    component.set("v.recordEnd", resultData.recordEnd);
                    component.set("v.totalPages", Math.ceil(resultData.totalRecords / pageSize));
                }
            }
        });
        $A.enqueueAction(action);
    },
    
    viewRecord : function(component, event, selectedAccountId) {
        var navEvt = $A.get("e.force:navigateToSObject");
        navEvt.setParams({
            "recordId": selectedAccountId,
            "slideDevName": "detail"
        });
        navEvt.fire();
    },
    
    editRecord : function(component, event, selectedAccountId) {
        var editRecordEvent = $A.get("e.force:editRecord");
        editRecordEvent.setParams({
            "recordId": selectedAccountId
        });
        editRecordEvent.fire();
    },
    
    sortColumnData: function(component, event) {
        var accountList = component.get("v.accounts");
        var isSortAsc = component.get("v.isSortAsc");
        var sortingField = component.get("v.selectedSortingField");
        accountList.sort(function(a, b){
            var s1 = a[sortingField] == b[sortingField];
            var s2 = (!a[sortingField] && b[sortingField]) || (a[sortingField] < b[sortingField]);
            return s1? 0: (isSortAsc?-1:1)*(s2?1:-1);
        });
        component.set("v.accounts", accountList);
        component.set("v.isSortAsc", !isSortAsc);
    },
})

Lightning Custom Datatable With Pagination

Apex Class:

public class AccountController {
    
    @AuraEnabled//Get Account Records
    public static List<Account> getAccountList(Integer pageSize, Integer pageNumber){
        Integer offset = (pageNumber - 1) * pageSize;
        List<Account> accList = new List<Account>();
        accList = [SELECT Id, Name, AccountNumber, Industry, Phone FROM Account LIMIT :pageSize OFFSET :offset];
        return accList;
    }
}

Lightning Component:

<aura:component implements="flexipage:availableForAllPageTypes,force:appHostable" controller="AccountController">
    
    <!--Declare Attributes-->
    <aura:attribute name="accList" type="Account[]"/>
    <aura:attribute name="pageNumber" type="Integer" default="1"/>
    <aura:attribute name="pageSize" type="Integer" default="10"/>
    <aura:attribute name="isLastPage" type="Boolean" default="false"/>
    <aura:attribute name="dataSize" type="Integer" default="0"/>
    <!--Declare Handler-->
    <aura:handler name="init" action="{!c.doInit}" value="{!this}"/>
    
    <!--Component Start-->
    <div class="slds-m-around--xxx-large">               
        
        <lightning:card>
            <aura:set attribute="title">
                Account
            </aura:set>
            <aura:set attribute="footer">
                <div class="slds-align_absolute-center"> 
                    <div class="slds-p-right_xx-small">
                        <lightning:button label="Prev"
                                          onclick="{!c.handlePrev}"
                                          disabled="{! v.pageNumber == 1}"
                                          variant="brand"
                                          iconName="utility:back"
                                          name="prev"/>
                    </div>
                    <span class="slds-badge slds-badge_lightest">
                        Page {!v.pageNumber} | Showing records from {! ((v.pageNumber-1)*v.pageSize)+' to '+((v.pageNumber-1)*v.pageSize+v.dataSize)}
                    </span>
                    <div class="slds-p-left_xx-small">
                        <lightning:button label="Next"
                                          disabled="{!v.isLastPage}"
                                          onclick="{!c.handleNext}" 
                                          variant="brand"
                                          iconName="utility:forward"
                                          iconPosition="right"
                                          name="next"/>
                    </div>
                </div>  
                
            </aura:set>
            <table class="slds-table slds-table_cell-buffer slds-table_bordered">
                <thead>
                    <tr class="slds-line-height_reset slds-text-title_caps">
                        <th  class="slds-is-resizable" scope="col">
                            <div class="slds-truncate" title="Name">
                                Name
                            </div>
                        </th>
                        <th  class="slds-is-resizable" scope="col">
                            <div class="slds-truncate" title="Account Number">
                                Account Number
                            </div>
                        </th>
                        <th class="slds-is-resizable" scope="col">
                            <div class="slds-truncate" title="Industry">
                                Industry
                            </div>
                        </th>
                        <th class="slds-is-resizable" scope="col">
                            <div class="slds-truncate" title="Phone">
                                Phone
                            </div>
                        </th>
                        <th scope="col">
                            <div class="slds-truncate" title="Actions">Actions</div>
                        </th>
                    </tr>
                </thead>
                <aura:if isTrue="{!v.accList.length > 0}">
                    <tbody>
                        <aura:iteration items="{!v.accList}" var="acc">
                            <tr class="slds-hint-parent">
                                
                                <th data-label="Name" scope="row">
                                    <div class="slds-truncate" title="{!acc.Name}">
                                        {!acc.Name}
                                    </div>
                                </th>
                                <td data-label="Account Number">
                                    <div class="slds-truncate" title="{!acc.AccountNumber}">{!acc.AccountNumber}</div>
                                </td>
                                <td data-label="Industry">
                                    <div class="slds-truncate" title="{!acc.Industry}">{!acc.Industry}</div>
                                </td>
                                <td data-label="Phone">
                                    <div class="slds-truncate" title="{!acc.Phone}">{!acc.Phone}</div>
                                </td>
                                <td data-label="Action">
                                    <lightning:buttonIcon name="{!acc.Id}" iconName="utility:delete" title="Delete" variant="bare" onclick="{!c.handleDeleteAccount}" alternativeText="Delete" />                                                                            
                                </td>
                                
                            </tr>
                        </aura:iteration>
                    </tbody>
                </aura:if>
            </table>
            <aura:if isTrue="{!empty(v.accList)}">
                <div class="slds-align_absolute-center">
                    No records found
                </div>
            </aura:if>
        </lightning:card>
    </div>
    <!--Component End-->
</aura:component>

Lightning JS Controller :

({
    doInit : function(component, event, helper) {        
        helper.getAccounts(component, event);
    },
    
    handleNext : function(component, event, helper) { 
        var pageNumber = component.get("v.pageNumber");
        component.set("v.pageNumber", pageNumber+1);
        helper.getAccounts(component, helper);
    },
    
    handlePrev : function(component, event, helper) {        
        var pageNumber = component.get("v.pageNumber");
        component.set("v.pageNumber", pageNumber-1);
        helper.getAccounts(component, helper);
    },
    
    handleDeleteAccount: function (component, event, helper) {
        alert('Selected Account to delete - ' + event.getSource().get("v.name"));
    },
})

Lightning JS Helper:

({
    getAccounts : function(component, event) {
        var action = component.get("c.getAccountList");
        action.setParams({
            'pageSize' : component.get("v.pageSize"),
            'pageNumber' : component.get("v.pageNumber")
        });
        action.setCallback(this,function(response) {
            var state = response.getState();
            if (state === "SUCCESS") {
                var result = response.getReturnValue();
                if(result.length < component.get("v.pageSize") || result.length == 0){
                    component.set("v.isLastPage", true);
                } else{
                    component.set("v.isLastPage", false);
                }
                component.set("v.dataSize", result.length);
                component.set("v.accList", result);
            }
        });
        $A.enqueueAction(action);
    },
})

Salesforce Lightning Datatable With Pagination

Apex Class:

public class SampleController {
    
    //Get Account Records
    @AuraEnabled
    public static  List<Account> getAccounts(Integer pageSize, Integer pageNumber){
        Integer offset = (pageNumber - 1) * pageSize;
        List<Account> accList = new List<Account>();
        accList = [SELECT Id, Name, AccountNumber, Industry, Phone FROM Account LIMIT :pageSize OFFSET :offset];
        return accList;
    }
    
    //Delete Account
    @AuraEnabled
    public static void deleteAccount(Account acc){
        Delete acc;
    } 
}

Lightning Component:

<aura:component implements="force:appHostable,flexipage:availableForAllPageTypes" access="global" controller="SampleController">
    
    <aura:attribute name="data" type="Object"/>
    <aura:attribute name="columns" type="List"/>
    <aura:attribute name="pageNumber" type="Integer" default="1"/>
    <aura:attribute name="pageSize" type="Integer" default="10"/>
    <aura:attribute name="isLastPage" type="Boolean" default="false"/>
    <aura:attribute name="dataSize" type="Integer" default="0"/> 
    <aura:handler name="init" action="{!c.doInit}" value="{!this}"/>
    
    <div class="slds-m-around_xx-large">
        <div class="slds-clearfix">
            <div class="slds-page-header" role="banner">
                <p class="slds-page-header__title">Accounts</p>
            </div>
        </div>
        
        <lightning:datatable aura:id = "accDT"
                             columns = "{!v.columns}"
                             maxRowSelection="{!v.maxRowSelection}"
                             data = "{!v.data}"
                             keyField = "Id"
                             selectedRows = "{!v.selectedRowList}"
                             onrowaction="{!c.handleRowAction}"/>
        
        <div class="slds-clearfix">
            <div class="slds-page-header" role="banner">
                <div class="slds-float_right">            
                    <lightning:button label="Prev" iconName="utility:chevronleft" iconPosition="left"
                                      onclick="{!c.handlePrev}" disabled="{! v.pageNumber == 1}"/>
                    <lightning:button label="Next" iconName="utility:chevronright" iconPosition="right" 
                                      disabled="{! v.isLastPage}" onclick="{!c.handleNext}"/>
                </div>
                <p class="slds-page-header__title">Page {!v.pageNumber} | Showing records from {! ((v.pageNumber-1)*v.pageSize)+' to '+((v.pageNumber-1)*v.pageSize+v.dataSize)}</p>
            </div>
        </div>
    </div>
</aura:component>

Lightning JS Controller:

({
    doInit : function(component, event, helper) {        
        helper.getColumnAndAction(component);
        helper.getAccounts(component, helper);
    },
    
    handleNext : function(component, event, helper) { 
        var pageNumber = component.get("v.pageNumber");
        component.set("v.pageNumber", pageNumber+1);
        helper.getAccounts(component, helper);
    },
    
    handlePrev : function(component, event, helper) {        
        var pageNumber = component.get("v.pageNumber");
        component.set("v.pageNumber", pageNumber-1);
        helper.getAccounts(component, helper);
    },

    handleRowAction: function (component, event, helper) {
        var action = event.getParam('action');
        switch (action.name) {
            case 'edit':
                helper.editRecord(component, event);
                break;
            case 'delete':
                helper.deleteRecord(component, event);
                break;
            case 'view':
                helper.viewRecord(component, event);
                break;
        }
    },
})

Lightning JS Helper:

({
    getColumnAndAction : function(component) {
        var actions = [
            {label: 'Edit', name: 'edit'},
            {label: 'Delete', name: 'delete'},
            {label: 'View', name: 'view'}
        ];
        component.set('v.columns', [
            {label: 'Name', fieldName: 'Name', type: 'text'},
            {label: 'AccountNumber', fieldName: 'AccountNumber', type: 'text'},
            {label: 'Industry', fieldName: 'Industry', type: 'text'},
            {label: 'Phone', fieldName: 'Phone', type: 'phone'},
            {type: 'action', typeAttributes: { rowActions: actions } } 
        ]);
    },
    
    getAccounts : function(component, helper) {
        var action = component.get("c.getAccounts");
        var pageSize = component.get("v.pageSize").toString();
        var pageNumber = component.get("v.pageNumber").toString();
        
        action.setParams({
            'pageSize' : pageSize,
            'pageNumber' : pageNumber
        });
        action.setCallback(this,function(response) {
            var state = response.getState();
            if (state === "SUCCESS") {
                var resultData = response.getReturnValue();
                if(resultData.length < component.get("v.pageSize")){
                    component.set("v.isLastPage", true);
                } else{
                    component.set("v.isLastPage", false);
                }
                component.set("v.dataSize", resultData.length);
                component.set("v.data", resultData);
            }
        });
        $A.enqueueAction(action);
    },
    
    viewRecord : function(component, event) {
        var row = event.getParam('row');
        var recordId = row.Id;
        var navEvt = $A.get("event.force:navigateToSObject");
        navEvt.setParams({
            "recordId": recordId,
            "slideDevName": "detail"
        });
        navEvt.fire();
    },
    
    deleteRecord : function(component, event) {
        var action = event.getParam('action');
        var row = event.getParam('row');
        
        var action = component.get("c.deleteAccount");
        action.setParams({
            "acc": row
        });
        action.setCallback(this, function(response) {
            var state = response.getState();
            if (state === "SUCCESS" ) {
                var rows = component.get('v.data');
                var rowIndex = rows.indexOf(row);
                rows.splice(rowIndex, 1);
                component.set('v.data', rows);
                var toastEvent = $A.get("e.force:showToast");
                toastEvent.setParams({
                    "title": "Success!",
                    "message": "The record has been delete successfully."
                });
                toastEvent.fire();
            }
        });
        $A.enqueueAction(action);
    },
    
    editRecord : function(component, event) {
        var row = event.getParam('row');
        var recordId = row.Id;
        var editRecordEvent = $A.get("e.force:editRecord");
        editRecordEvent.setParams({
            "recordId": recordId
        });
        editRecordEvent.fire();
    },    
})

Output:

Salesforce Lightning Datatable Infinite Loading

Apex Controller:

public class SampleAuraController {
    
    //Get Contact List
    @AuraEnabled
    public static List<Contact> getContactList(Integer recordLimit, Integer recordOffset){
        Integer intLimit = Integer.valueof(recordLimit);
        Integer intOffset = Integer.valueof(recordOffset);
        List<Contact> conList = new List<Contact>();
        conList = [SELECT Id, Name, Email, Phone FROM Contact LIMIT :intLimit Offset :intOffset];
        return conList;
    }
    
    //Get Total Number of Contacts
    @AuraEnabled
    public static Integer getTotalContacts(){
        AggregateResult results = [SELECT Count(Id) TotalContacts  From Contact];
        Integer totalContacts = (Integer)results.get('TotalContacts') ; 
        return totalContacts;
    } 
    
    //Delete Contact
    @AuraEnabled
    public static void deleteContact(Contact con){
        Delete con;
    } 
}

Lightning Component:

<!--Sample.cmp--> 
<aura:component controller="SampleAuraController" implements="flexipage:availableForAllPageTypes,force:appHostable">
    
    <!--Declare Attributes-->
    <aura:attribute name="data" type="Object"/>
    <aura:attribute name="columns" type="List"/>
    <aura:attribute name="selectedRowsCount" type="Integer" default="0"/>
    <aura:attribute name="selectedRowsDetails" type="Object" />
    <aura:attribute name="selectedRowsList" type="List" />
    <aura:attribute name="maxRowSelection" type="Integer" default="10"/>
    <aura:attribute name="selectedRows" type="List" />
    <aura:attribute name="enableInfiniteLoading" type="Boolean" default="true"/>
    <aura:attribute name="initialRows" type="Integer" default="10"/>
    <aura:attribute name="rowsToLoad" type="Integer" default="10"/>
    <aura:attribute name="totalNumberOfRows" type="Integer" default="10"/>
    <aura:attribute name="loadMoreStatus" type="String" default="Please scroll down to load more data"/>
    <aura:attribute name="showRowNumberColumn" type="Boolean" default="false"/>
    <aura:attribute name="rowNumberOffset" type="Integer" default="0"/>
    <aura:attribute name="rowsToAdd" type="Integer" default="10"/>
    <aura:attribute name="currentCount" type="Integer" default="10"/>
    <aura:attribute name="sortedBy" type="String"/>
    <aura:attribute name="sortedDirection" type="String"/>
    <aura:attribute name="defaultSortDirection" type="String"/>
    
    <!--Declare Handlers-->
    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
    
    <!--Component Start-->
    <div class="slds-m-around_xx-large" style="height: 300px">
        <lightning:datatable columns="{!v.columns}"
                             data="{!v.data}"
                             keyField="Id"
                             showRowNumberColumn="true"
                             rowNumberOffset="0"
                             onrowaction="{!c.handleRowAction}"
                             selectedRows="{!v.selectedRows}"
                             maxRowSelection="{!v.maxRowSelection}"
                             onrowselection="{!c.handleSelectedRow}"
                             enableInfiniteLoading="true"
                             loadMoreOffset="{!v.loadMoreOffset}"
                             sortedBy="{!v.sortedBy}"
                             sortedDirection="{!v.sortedDirection}"
                             defaultSortDirection="{!v.defaultSortDirection }"
                             onsort="{!c.handleColumnSorting}"
                             onloadmore="{!c.handleLoadMoreContacts}"/>
        <br/>
        <div class="slds-float_left">
            <strong>Total Rows : {!v.totalNumberOfRows}</strong>
            &nbsp;&nbsp;
            <strong>Selected Rows: {!v.selectedRowsCount }</strong>
        </div>
        <div class="slds-float_right">
            <strong>{!v.loadMoreStatus}</strong>
        </div>
        <br/>
        <br/>
        <div class="slds-float_left">
            <lightning:button label="Get Selected Contacts"  variant="brand" onclick="{!c.handleSelectedRows}"/>    
        </div>
    </div>
    <!--Component End-->
</aura:component>

Lightning Component JS Controller:

({
    doInit : function(component, event, helper) {
        helper.getTotalNumberOfContacts(component);
        helper.getColumnAndAction(component);
        helper.getContacts(component);
    },
    
    handleLoadMoreContacts: function (component, event, helper) {
        event.getSource().set("v.isLoading", true);
        component.set('v.loadMoreStatus', 'Loading....');
        helper.getMoreContacts(component, component.get('v.rowsToLoad')).then($A.getCallback(function (data) {
            if (component.get('v.data').length == component.get('v.totalNumberOfRows')) {
                component.set('v.enableInfiniteLoading', false);
                component.set('v.loadMoreStatus', 'No more data to load');
            } else {
                var currentData = component.get('v.data');
                var newData = currentData.concat(data);
                component.set('v.data', newData);
                component.set('v.loadMoreStatus', 'Please scroll down to load more data');
            }
            event.getSource().set("v.isLoading", false);
        }));
    },
    
    handleSelectedRows: function (component, event, helper) {
        var data = component.get('v.data');
        var selectedRowList =  component.get("v.selectedRowsList");
        console.log('selectedRowList-' + selectedRowList);
    },
    
    handleSelectedRow: function(component, event, helper){
        var selectedRows = event.getParam('selectedRows');
        component.set("v.selectedRowsCount", selectedRows.length);
        let obj =[] ; 
        for (var i = 0; i < selectedRows.length; i++){
            obj.push({Name:selectedRows[i].Name});
        }
        component.set("v.selectedRowsDetails", JSON.stringify(obj) );
        component.set("v.selectedRowsList", event.getParam('selectedRows'));
    },
    
    handleRowAction: function (component, event, helper) {
        var action = event.getParam('action');
        switch (action.name) {
            case 'new':
                helper.createContactRecord(component, event);
                break;
            case 'edit':
                helper.editContactRecord(component, event);
                break;
            case 'delete':
                helper.deleteContactRecord(component, event);
                break;
            case 'view':
                helper.viewContactRecord(component, event);
                break;
        }
    },
    
    handleColumnSorting: function (component, event, helper) {
        var fieldName = event.getParam('fieldName');
        var sortDirection = event.getParam('sortDirection');
        component.set("v.sortedBy", fieldName);
        component.set("v.sortedDirection", sortDirection);
        helper.sortData(component, fieldName, sortDirection);
    },
})

Lightning Component JS Helper:

({
    getContacts : function(component) {
        var action = component.get("c.getContactList");
        action.setParams({
            "recordLimit": component.get("v.initialRows"),
            "recordOffset": component.get("v.rowNumberOffset")
        });
        action.setCallback(this, function(response) {
            var state = response.getState();
            if (state === "SUCCESS" ) {
                var resultData = response.getReturnValue();
                component.set("v.data", resultData);
                component.set("v.currentCount", component.get("v.initialRows"));
            }
        });
        $A.enqueueAction(action);
    },
    
    getTotalNumberOfContacts : function(component) {
        var action = component.get("c.getTotalContacts");
        action.setCallback(this, function(response) {
            var state = response.getState();
            if (state === "SUCCESS" ) {
                var resultData = response.getReturnValue();
                component.set("v.totalNumberOfRows", resultData);
            }
        });
        $A.enqueueAction(action);
    },
    
    getColumnAndAction : function(component) {
        var actions = [
            {label: 'New', name: 'new'},
            {label: 'Edit', name: 'edit'},
            {label: 'Delete', name: 'delete'},
            {label: 'View', name: 'view'}
        ];
        component.set('v.columns', [
            {label: 'Name', fieldName: 'Name', type: 'text', sortable:true},
            {label: 'Email', fieldName: 'Email', type: 'email', sortable:true},
            {label: 'Phone', fieldName: 'Phone', type: 'phone', sortable:true},
            {type: 'action', typeAttributes: { rowActions: actions } } 
        ]);
    },
    
    getMoreContacts: function(component , rows){
        return new Promise($A.getCallback(function(resolve, reject) {
            var action = component.get('c.getContactList');
            var recordOffset = component.get("v.currentCount");
            var recordLimit = component.get("v.initialRows");
            action.setParams({
                "recordLimit": recordLimit,
                "recordOffset": recordOffset 
            });
            action.setCallback(this, function(response) {
                var state = response.getState();
                if(state === "SUCCESS"){
                    var resultData = response.getReturnValue();
                    resolve(resultData);
                    recordOffset = recordOffset+recordLimit;
                    component.set("v.currentCount", recordOffset);   
                }                
            });
            $A.enqueueAction(action);
        }));
    },
    
    sortData: function (component, fieldName, sortDirection) {
        var data = component.get("v.data");
        var reverse = sortDirection !== 'asc';
        data.sort(this.sortBy(fieldName, reverse))
        component.set("v.data", data);
    },
    
    sortBy: function (field, reverse, primer) {
        var key = primer ?
            function(x) {return primer(x[field])} :
        function(x) {return x[field]};
        reverse = !reverse ? 1 : -1;
        return function (a, b) {
            return a = key(a), b = key(b), reverse * ((a > b) - (b > a));
        }
    },
    
    viewContactRecord : function(component, event) {
        var row = event.getParam('row');
        var recordId = row.Id;
        var navEvt = $A.get("event.force:navigateToSObject");
        navEvt.setParams({
            "recordId": recordId,
            "slideDevName": "detail"
        });
        navEvt.fire();
    },
    
    deleteContactRecord : function(component, event) {
        var action = event.getParam('action');
        var row = event.getParam('row');
        
        var action = component.get("c.deleteContact");
        action.setParams({
            "con": row
        });
        action.setCallback(this, function(response) {
            var state = response.getState();
            if (state === "SUCCESS" ) {
                var rows = component.get('v.data');
                var rowIndex = rows.indexOf(row);
                rows.splice(rowIndex, 1);
                component.set('v.data', rows);
                
                var toastEvent = $A.get("e.force:showToast");
                toastEvent.setParams({
                    "title": "Success!",
                    "message": "The record has been delete successfully."
                });
                toastEvent.fire();
            }
        });
        $A.enqueueAction(action);
    },
    
    editContactRecord : function(component, event) {
        var row = event.getParam('row');
        var recordId = row.Id;
        var editRecordEvent = $A.get("e.force:editRecord");
        editRecordEvent.setParams({
            "recordId": recordId
        });
        editRecordEvent.fire();
    },
    
    createContactRecord : function (component, event) {
        var createRecordEvent = $A.get("e.force:createRecord");
        createRecordEvent.setParams({
            "entityApiName": "Contact"
        });
        createRecordEvent.fire();
    }
})

Output: