LWC Pagination - Simple and Reusable Paginator in Salesforce Lightning

Datatables play a major role in accessing/viewing the multi records data at single place. When we have large number of records to display in a datatable, displaying all of them at a time is not a good idea and may face page load issues. To overcome these kind of scenarios, adding pagination to the datatable is a good idea/solution. Here is a nice and easy to use pagination implemented in lightning web components framework.


This is a simple reusable paginator component, we can plug it in for any lightning datatable. Let's get started with the components and the code part.

paginator.html

<template>
    <div class="slds-grid slds-grid_vertical-align-center slds-grid_align-spread">
        <div class="slds-col"><!--RECORDS PER PAGE-->
            <div style={controlPagination} class="slds-list_inline slds-p-bottom_xx-small customSelect">
                <label class="slds-text-color_weak slds-p-horizontal_x-small" for="recordsPerPage">Records per page:</label> 
                <div class="slds-select_container">
                    <select class="slds-select" id="recordsPerPage" onchange={handleRecordsPerPage}>
                        <template for:each={pageSizeOptions} for:item="option">
                            <option key={option} value={option}>{option}</option>
                        </template>
                    </select>
                </div>
            </div>
        </div>
        <div class="slds-col"><!--SEARCH BOX-->
            <div if:true={showSearchBox}>
                <div class="slds-p-horizontal_x-small slds-p-bottom_xx-small">
                    <lightning-input label="" type="search" placeholder="Search by any Col Value" variant="label-hidden" onchange={handleKeyChange}></lightning-input>
                </div>
            </div>
        </div>
        <div class="slds-col"><!--PAGE NAVIGATION-->
            <div style={controlPagination}>
                <div class="slds-col slds-p-bottom_xx-small">
                    <span style={controlPrevious}>
                        <lightning-button-icon icon-name="utility:left" variant="bare" size="medium" alternative-text="Previous Page" onclick={previousPage}></lightning-button-icon>
                    </span>
                    <label class="slds-text-color_weak slds-p-horizontal_x-small" for="pageNum">Page:</label> 
                    <input type="number" id="pageNum" value={pageNumber} maxlength="4" onkeypress={handlePageNumberChange} class="customInput" title="Go to a Page"></input>
                    <span>&nbsp;of&nbsp;<b id="totalPages">{totalPages}</b></span>
                    <span style={controlNext}>
                        <lightning-button-icon icon-name="utility:right" variant="bare" size="medium" alternative-text="Next Page" onclick={nextPage} class="slds-p-horizontal_x-small"></lightning-button-icon>
                    </span>
                </div>
            </div>
        </div>
    </div>
</template>

paginator.js

import { LightningElement, api, track} from 'lwc';

const DELAY = 300;
const recordsPerPage = [5,10,25,50,100];
const pageNumber = 1;
const showIt = 'visibility:visible';
const hideIt = 'visibility:hidden'; //visibility keeps the component space, but display:none doesn't
export default class Paginator extends LightningElement {
    @api showSearchBox = false; //Show/hide search box; valid values are true/false
    @api showPagination; //Show/hide pagination; valid values are true/false
    @api pageSizeOptions = recordsPerPage; //Page size options; valid values are array of integers
    @api totalRecords; //Total no.of records; valid type is Integer
    @api records; //All records available in the data table; valid type is Array 
    @track pageSize; //No.of records to be displayed per page
    @track totalPages; //Total no.of pages
    @track pageNumber = pageNumber; //Page number
    @track searchKey; //Search Input
    @track controlPagination = showIt;
    @track controlPrevious = hideIt; //Controls the visibility of Previous page button
    @track controlNext = showIt; //Controls the visibility of Next page button
    recordsToDisplay = []; //Records to be displayed on the page

    //Called after the component finishes inserting to DOM
    connectedCallback() {
        if(this.pageSizeOptions && this.pageSizeOptions.length > 0) 
            this.pageSize = this.pageSizeOptions[0];
        else{
            this.pageSize = this.totalRecords;
            this.showPagination = false;
        }
        this.controlPagination = this.showPagination === false ? hideIt : showIt;
        this.setRecordsToDisplay();
    }

    handleRecordsPerPage(event){
        this.pageSize = event.target.value;
        this.setRecordsToDisplay();
    }
    handlePageNumberChange(event){
        if(event.keyCode === 13){
            this.pageNumber = event.target.value;
            this.setRecordsToDisplay();
        }
    }
    previousPage(){
        this.pageNumber = this.pageNumber-1;
        this.setRecordsToDisplay();
    }
    nextPage(){
        this.pageNumber = this.pageNumber+1;
        this.setRecordsToDisplay();
    }
    setRecordsToDisplay(){
        this.recordsToDisplay = [];
        if(!this.pageSize)
            this.pageSize = this.totalRecords;

        this.totalPages = Math.ceil(this.totalRecords/this.pageSize);

        this.setPaginationControls();

        for(let i=(this.pageNumber-1)*this.pageSize; i < this.pageNumber*this.pageSize; i++){
            if(i === this.totalRecords) break;
            this.recordsToDisplay.push(this.records[i]);
        }
        this.dispatchEvent(new CustomEvent('paginatorchange', {detail: this.recordsToDisplay})); //Send records to display on table to the parent component
    }
    setPaginationControls(){
        //Control Pre/Next buttons visibility by Total pages
        if(this.totalPages === 1){
            this.controlPrevious = hideIt;
            this.controlNext = hideIt;
        }else if(this.totalPages > 1){
           this.controlPrevious = showIt;
           this.controlNext = showIt;
        }
        //Control Pre/Next buttons visibility by Page number
        if(this.pageNumber <= 1){
            this.pageNumber = 1;
            this.controlPrevious = hideIt;
        }else if(this.pageNumber >= this.totalPages){
            this.pageNumber = this.totalPages;
            this.controlNext = hideIt;
        }
        //Control Pre/Next buttons visibility by Pagination visibility
        if(this.controlPagination === hideIt){
            this.controlPrevious = hideIt;
            this.controlNext = hideIt;
        }
    }
    handleKeyChange(event) {
        window.clearTimeout(this.delayTimeout);
        const searchKey = event.target.value;
        if(searchKey){
            this.delayTimeout = setTimeout(() => {
                this.controlPagination = hideIt;
                this.setPaginationControls();

                this.searchKey = searchKey;
                //Use other field name here in place of 'Name' field if you want to search by other field
                //this.recordsToDisplay = this.records.filter(rec => rec.includes(searchKey));
                //Search with any column value (Updated as per the feedback)
                this.recordsToDisplay = this.records.filter(rec => JSON.stringify(rec).includes(searchKey));
                if(Array.isArray(this.recordsToDisplay) && this.recordsToDisplay.length > 0)
                    this.dispatchEvent(new CustomEvent('paginatorchange', {detail: this.recordsToDisplay})); //Send records to display on table to the parent component
            }, DELAY);
        }else{
            this.controlPagination = showIt;
            this.setRecordsToDisplay();
        }        
    }
}

paginator.css

.customSelect select {
    padding-right: 1.25rem;
    min-height: inherit;
    line-height: normal;
    height: 1.4rem;    
}
.customSelect label {
    margin-top: .1rem;
}
.customSelect .slds-select_container::before {
    border-bottom: 0;
}
.customInput {
    width: 3rem;
    height: 1.4rem;
    text-align: center;
    border: 1px solid #dddbda;
    border-radius: 3px;
    background-color:#fff;
}

oppTable.html

<template>
    <lightning-card title="Data Table with Pagination">
        <template if:true={showTable}>
            <c-paginator records={opps} 
                        total-records={opps.length} 
                        show-search-box="true" 
                        onpaginatorchange={handlePaginatorChange}>
            </c-paginator>
            <lightning-datatable key-field="Id" 
                                data={recordsToDisplay} 
                                columns={columns}
                                hide-checkbox-column
                                show-row-number-column
                                row-number-offset={rowNumberOffset}>
            </lightning-datatable>
        </template>
    </lightning-card>
</template>

oppTable.js

import { LightningElement, track, wire } from 'lwc';
import getOpps from '@salesforce/apex/OppTableContoller.getOpportunities';

const columns = [
    { label:'Opportunity Name', fieldName: 'oppLink', type: 'url', typeAttributes: {label: {fieldName: 'Name'}, tooltip:'Go to detail page', target: '_blank'}},
    { label: 'Type', fieldName: 'Type', type: 'text' },
    { label: 'Stage', fieldName: 'StageName', type: 'text', },
    { label: 'Amount', fieldName: 'Amount', type: 'currency', cellAttributes: { alignment: 'left' } },
    { label: 'Close Date', fieldName: 'CloseDate', type: 'date', typeAttributes:{timeZone:'UTC', year:'numeric', month:'numeric', day:'numeric'}},
];
export default class OppTable extends LightningElement {
    @track error;
    @track columns = columns;
    @track opps; //All opportunities available for data table    
    @track showTable = false; //Used to render table after we get the data from apex controller    
    @track recordsToDisplay = []; //Records to be displayed on the page
    @track rowNumberOffset; //Row number

    @wire(getOpps)
    wopps({error,data}){
        if(data){
            let recs = [];
            for(let i=0; i<data.length; i++){
                let opp = {};
                opp.rowNumber = ''+(i+1);
                opp.oppLink = '/'+data[i].Id;
                opp = Object.assign(opp, data[i]);
                recs.push(opp);
            }
            this.opps = recs;
            this.showTable = true;
        }else{
            this.error = error;
        }       
    }
    //Capture the event fired from the paginator component
    handlePaginatorChange(event){
        this.recordsToDisplay = event.detail;
        this.rowNumberOffset = this.recordsToDisplay[0].rowNumber-1;
    }
}

OppTableContoller.apxc

public with sharing class OppTableContoller {    
    @AuraEnabled(cacheable=true)
    public static List getOpportunities() {
        return [SELECT Id, Name, Type, StageName, Amount, CloseDate, OwnerId, Owner.Name FROM Opportunity];
    }
}

PS: Thanks for checking this, hope this helps! Please share your feedback/suggestions.

60 Comments

  1. Great Post. Could be very helpful in some projects.
    After brief code inspection I have noticed that you support search only by "name" field. Some data structures just do not have such a field and your component will be erroring out. What I would suggest is to set a field you want to be able to search in somewhere in component settings (possibly in @api attribute), so developer will be able to pass in field he/she want to be able to search in. Also, it would be good to have a feature to search in all fields, so user will not have to specify them.

    ReplyDelete
    Replies
    1. Thanks for your feedback. I have added that functionality. Now, you can search with any column value. Please check the updated code.

      Delete
    2. I have copied the same code but is not searching by any other field except name.

      Delete
    3. @Ashima, the above code for search is case sensitive. Try matching the case of the string or you can do a case insensitive search by adding below line.
      this.recordsToDisplay = this.records.filter(rec => JSON.stringify(rec).toLowerCase().includes(searchKey.toLowerCase()));

      Delete
  2. Thanks for the post. we need more post like this

    ReplyDelete
  3. Looks like something I could use at my job. Thanks for sharing!

    ReplyDelete
  4. @VK - thanks for such a piece of art, So far the best solution I have come across for datatable implementations. I have one request : Could you please add the checkboxes in datatable to select record and make them persistent so while navigating from one page to another they remain in that state(checked/unchecked).

    ReplyDelete
    Replies
    1. Thanks for checking this. The checkbox functionality with out of the box datatable is little more difficult to make it persistent through pagination or search. Soon, I will post one with such full functionality. Keep checking this blog.

      Delete
  5. Hi VK,

    Can you please include check box functionality along with search feature. I am able to add checkbox functionality but not able to add search feature so that the selection of records will be persistent.
    Thank You

    ReplyDelete
    Replies
    1. Thanks for checking this. The checkbox functionality with out of the box datatable is little more difficult to make it persistent through pagination or search. Soon, I will post one with such full functionality. Keep checking this blog.

      Delete
  6. Why is your search function not working and paginator.html is showing error?

    ReplyDelete
  7. This is best thanks for the post VK.

    ReplyDelete
  8. I am new to LWC, wanted to try as is. could not compile Apex.
    public with sharing class OppTableContoller {
    @AuraEnabled(cacheable=true)
    public static List getOpportunities() {
    return [SELECT Id, Name, Type, StageName, Amount, CloseDate, OwnerId, Owner.Name FROM Opportunity];
    }
    }


    Changed : public static List getOpportunities() {

    Also, getting errors In paginator.html , whereever controlPagination, controlNext is used.


    ReplyDelete
    Replies
    1. I am not sure what errors you are getting, it will definitely work. As you are new to LWC, please just copy paste (like in we do trailhead 😉) the above code and see whether it works or not.

      Delete
    2. This comment has been removed by the author.

      Delete
    3. This comment has been removed by the author.

      Delete
  9. Undoubtedly the best datatable implementation!
    Do you think you would be able to add sorting functionality across pages?

    ReplyDelete
    Replies
    1. Thank you! Please keep an eye on my blog, I am soon going to post a complete reusable data table in lwc with all required functionalities.

      Delete
    2. Good component helped me a lot. Waiting for sort as well. I tried it but not much successful. Below I posted my sort functionality.

      Delete
  10. This is great ! Thanks so much

    ReplyDelete
  11. I have been trying to set the default page size as 50 and the number of records shown are 50, but the select list value displayed defaults to 5. Any suggestions/workarounds for that?

    ReplyDelete
    Replies
    1. So I came up with a workaround for this myself, in case anyone else needs it. In the connectedCallback, instead of setting

      this.pageSize = this.pageSizeOptions[0];

      I set it as :

      this.pageSize = this.pageSizeOptions[3];

      Since the third option is 50 in the constants variable. And then added a renderedCallback method as below:

      renderedCallback() {
      if (this.template.querySelector(".slds-select") !== null) {
      this.template.querySelector("option[value='" + this.pageSize + "']").setAttribute("selected", "");
      }
      }

      I am not exactly sure if this is the best way to do this, but this worked for me.

      Delete
  12. How to make this work with sorting, so that if you sort by date on page 1, then go to page 2, etc., the sorting still applies?

    ReplyDelete
    Replies
    1. It is all depends on how you play with records and recordsToDisplay.

      Delete
  13. Getting the below error, on copy pasting the provided code,

    Error during LWC component connect phase: [Cannot read property '0' of undefined]

    ReplyDelete
  14. This comment has been removed by the author.

    ReplyDelete
  15. setting the template if:true = {opps} fixed the issue

    ReplyDelete
  16. In pagimator.html for every style={controlPagination}, i am getting an error '} expected css(css-rcurlyexpected)'

    Please help!

    ReplyDelete
    Replies
    1. That's not an error, but warning given by eslint. Please ignore them.

      Delete
  17. Hey Great implementation. Thank you for this.
    Can you please suggest how to control pagination with search functionality. Right now it is giving all the searched records in a table but at times when this search results are more then table can be out of page layout. Please suggest.

    ReplyDelete
  18. Hi Venky,
    In LWC lightning datatable, pagination(10 rows in a page) is there. When I selected any row of the first page, automatically the same row is selected on other page. Could you please help me on this?
    I have used max-row-selection as well
    Still it got selected in other page

    ReplyDelete
  19. Hey dude.

    Really nice code. I took it and adapted it a bit.

    I would suggest you to use "slds-hidden" instead of modifying the style :

    Example:

    get computedNext(){
    return classSet()
    .add({
    'slds-hidden':this.pageNumber >= this.totalPages || !this.showPagination
    })
    .toString();
    }

    get computedPrevious(){
    return classSet()
    .add({
    'slds-hidden':this.pageNumber <= 1 || !this.showPagination
    })
    .toString();
    }

    get computedPagination(){
    return classSet()
    .add({
    'slds-hidden':!this.showPagination
    })
    .toString();
    }

    ReplyDelete
    Replies
    1. Thank you and that's a nice suggestion. I may not update this now, but will remember it in my future implementations.

      Delete
  20. Forgot to add one thing:

    In the "handleKeyChange" function, you need to add : event.stopPropagation(); to avoid issue

    ReplyDelete
    Replies
    1. Hi Unknown,

      thanks for your help. Can you please help me in the below code?

      previousHandler() {
      if (this.page > 1) {
      this.page = this.page - 1; //decrease page by 1
      this.displayRecordPerPage(this.page);
      }
      }

      //clicking on next button this method will be called
      nextHandler() {
      if ((this.page < this.totalPage) && this.page !== this.totalPage) {
      this.page = this.page + 1; //increase page by 1
      this.displayRecordPerPage(this.page);
      }
      }

      //this method displays records page by page
      displayRecordPerPage(page) {
      this.startingRecord = ((page - 1) * this.pageSize);
      this.endingRecord = (this.pageSize * page);

      this.endingRecord = (this.endingRecord > this.totalRecountCount) ? this.totalRecountCount : this.endingRecord;

      this.items = this.customerOrg.slice(this.startingRecord, this.endingRecord);

      this.startingRecord = this.startingRecord + 1;
      }

      -
      Thanks,
      Zakeer

      Delete
  21. JS side:
    @track sortedBy;
    @track sortedDirection = 'asc';
    handleSortdata(event){
    this.sortedBy = event.detail.fieldName;
    this.sortedDirection = event.detail.sortDirection;
    this.sortData(this.sortedBy,this.sortedDirection);
    }
    sortData(fieldName, sortDirection){
    let data = JSON.parse(JSON.stringify(this.cons));
    let key =(a) => a[fieldName];
    let reverse = sortDirection === 'asc' ? 1: -1;
    data.sort((a,b) => {
    let valueA = key(a) ? key(a).toLowerCase() : '';
    let valueB = key(b) ? key(b).toLowerCase() : '';
    return reverse * ((valueA > valueB) - (valueB > valueA));
    });
    this.recordsToDisplay= data;
    }
    Template Side:


    I tried soring but it sorting only first page records, i.e., In page 1 if I have Name: A,b,c,d,e record and page 2 if I have Name: F,g,h,i,j. If i clcik on name sort only page 1 A,b,c,d,e is coming as e,d,c,b,A. Can anyone help how to sort for all the records with pagination.

    ReplyDelete
    Replies
    1. I know, implementing checkbox and sorting functionality is a bit difficult. I will post a full functional datatable soon.

      Delete
  22. Getting Following Error - Cannot read property 'success' of undefined

    ReplyDelete
  23. Search is case Sensitive...How to remove that?

    ReplyDelete
    Replies
    1. Please try this.
      this.recordsToDisplay = this.records.filter(rec => JSON.stringify(rec).toLowerCase().includes(searchKey.toLowerCase()));

      Delete
  24. Hey, if I am having an hyperlink, say a link to case record, in search result it is considering the record's Id as well. Can we exclude it someway?

    ReplyDelete
    Replies
    1. May you should modify the below filter functionality according to your needs. Explore javascript filter or try another way of filtering out the records from an object.

      this.recordsToDisplay = this.records.filter(rec => JSON.stringify(rec).toLowerCase().includes(searchKey.toLowerCase()));

      Delete
  25. hi Venky, any suggestion how to refresh paginator when source data is changed in the parent component? I am trying to implement filtering in the parent component and even when "opps" changed, paginator seems to not respond, when I try to navigate to a different page - it catches up with the changes



    ReplyDelete
    Replies
    1. Hi SM, are you using @track decorator for your variables? Please add it if you are not using it in front of object or array variables already.

      Delete
  26. That's too good . But can you please add Inline Edit and Delete Functionality for the same code? Would be useful if you did so! I tried but had some refreshing issues. Please update the code for Edit and Delete Functionality

    By:
    A Fan Of Yours

    ReplyDelete
    Replies
    1. Thanks Ajaay! Please check my latest post. It will cover all your needs. Here is the link to it,

      https://vkambham.blogspot.com/2021/01/lwc-datatable.html

      Delete
  27. Yes it works as described. Thanks you for sharing.

    ReplyDelete
  28. For some reason it isn't populating Page 1...I can click the 'Next' button and then the 'Previous' button and then the first page will be populated but it isn't populated upon load. Anyone else having this problem?

    ReplyDelete
    Replies
    1. Im facing same issue

      Delete
    2. Did you try the same code or made any changes? Do you see any errors in the console?

      Delete
  29. Could you please add code for First Previous Next And LAst buttons Code for Above code

    ReplyDelete
    Replies
    1. First and Last page navigation is quite simple. You can add it yourself. On first button click, set the page number to 1 and On last button click, set the page number to totalPages.

      Delete
  30. Pagination in LWC datatable in salesforce.

    https://www.youtube.com/watch?v=y6xuWLofP5o&t=115s

    ReplyDelete
    Replies
    1. Hey Nahar, not sure, what you were trying to post. But the video you have posted is not working.

      Delete
  31. Did you try to use the slice function?

    var offset = (this.pageNumber-1)*this.pageSize;
    this.recordsToDisplay = this.records.slice(offset).slice(0,this.pageSize);

    // it is taking more time to process the records for ex when you have more than 500 records
    for(let i=(this.pageNumber-1)*this.pageSize; i < this.pageNumber*this.pageSize; i++){
    if(i === this.totalRecords) break;
    this.recordsToDisplay.push(this.records[i]);
    }

    ReplyDelete
    Replies
    1. Hey Praveen, I did not try that. But if it is working and effective process, I will update it. Thanks for your inputs.

      Delete
Post a Comment
Previous Post Next Post