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.
PS: Thanks for checking this, hope this helps! Please share your feedback/suggestions.
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> of <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.
Great Post. Could be very helpful in some projects.
ReplyDeleteAfter 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.
Thanks for your feedback. I have added that functionality. Now, you can search with any column value. Please check the updated code.
DeleteI have copied the same code but is not searching by any other field except name.
Delete@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.
Deletethis.recordsToDisplay = this.records.filter(rec => JSON.stringify(rec).toLowerCase().includes(searchKey.toLowerCase()));
Thanks for the post. we need more post like this
ReplyDeleteDefinitely, thanks!
DeleteLooks like something I could use at my job. Thanks for sharing!
ReplyDelete@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).
ReplyDeleteThanks 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.
DeleteSure , thanks.
DeleteHi VK,
ReplyDeleteCan 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
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.
DeleteWhy is your search function not working and paginator.html is showing error?
ReplyDeleteCan you post the error here.
DeleteThis is best thanks for the post VK.
ReplyDeleteI am new to LWC, wanted to try as is. could not compile Apex.
ReplyDeletepublic 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.
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.
DeleteThis comment has been removed by the author.
DeleteThis comment has been removed by the author.
DeleteUndoubtedly the best datatable implementation!
ReplyDeleteDo you think you would be able to add sorting functionality across pages?
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.
DeleteGood component helped me a lot. Waiting for sort as well. I tried it but not much successful. Below I posted my sort functionality.
DeleteThis is great ! Thanks so much
ReplyDeleteI 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?
ReplyDeleteSo I came up with a workaround for this myself, in case anyone else needs it. In the connectedCallback, instead of setting
Deletethis.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.
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?
ReplyDeleteIt is all depends on how you play with records and recordsToDisplay.
DeleteGetting the below error, on copy pasting the provided code,
ReplyDeleteError during LWC component connect phase: [Cannot read property '0' of undefined]
This comment has been removed by the author.
ReplyDeletesetting the template if:true = {opps} fixed the issue
ReplyDeleteIn pagimator.html for every style={controlPagination}, i am getting an error '} expected css(css-rcurlyexpected)'
ReplyDeletePlease help!
That's not an error, but warning given by eslint. Please ignore them.
DeleteHey Great implementation. Thank you for this.
ReplyDeleteCan 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.
Hi Venky,
ReplyDeleteIn 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
Hey dude.
ReplyDeleteReally 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();
}
Thank you and that's a nice suggestion. I may not update this now, but will remember it in my future implementations.
DeleteForgot to add one thing:
ReplyDeleteIn the "handleKeyChange" function, you need to add : event.stopPropagation(); to avoid issue
Hi Unknown,
Deletethanks 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
JS side:
ReplyDelete@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.
I know, implementing checkbox and sorting functionality is a bit difficult. I will post a full functional datatable soon.
DeleteGetting Following Error - Cannot read property 'success' of undefined
ReplyDeleteSearch is case Sensitive...How to remove that?
ReplyDeletePlease try this.
Deletethis.recordsToDisplay = this.records.filter(rec => JSON.stringify(rec).toLowerCase().includes(searchKey.toLowerCase()));
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?
ReplyDeleteMay 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.
Deletethis.recordsToDisplay = this.records.filter(rec => JSON.stringify(rec).toLowerCase().includes(searchKey.toLowerCase()));
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
ReplyDeleteHi 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.
DeleteThat'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
ReplyDeleteBy:
A Fan Of Yours
Thanks Ajaay! Please check my latest post. It will cover all your needs. Here is the link to it,
Deletehttps://vkambham.blogspot.com/2021/01/lwc-datatable.html
Yes it works as described. Thanks you for sharing.
ReplyDeleteThanks Aradhika!
DeleteFor 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?
ReplyDeleteIm facing same issue
DeleteDid you try the same code or made any changes? Do you see any errors in the console?
DeleteCould you please add code for First Previous Next And LAst buttons Code for Above code
ReplyDeleteFirst 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.
DeletePagination in LWC datatable in salesforce.
ReplyDeletehttps://www.youtube.com/watch?v=y6xuWLofP5o&t=115s
Hey Nahar, not sure, what you were trying to post. But the video you have posted is not working.
DeleteDid you try to use the slice function?
ReplyDeletevar 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]);
}
Hey Praveen, I did not try that. But if it is working and effective process, I will update it. Thanks for your inputs.
Delete