Dear #Trailblazers, #Ohana,
In this post, I am going to show you how you can create a re-usable Pagination Lightning web component which you can use for any component to display the data either in table form or any other form that you would like to display.
Before, we start working on implementations let’s talk about the concept of Pagination.
Pagination is one of those annoying features that aren’t fun to implement in any language, but that is pretty much essential for a good UI.
When we talk about pagination, what usually we have is a list of records and then we wanted to display those records on many pages.
So if we say that we are fetching some records from the list ( we can also say slice the records like we slice cake) and display on the page. Below is the step by step implementation of the pagination using JavaScript.
You can find the complete code From Here.
var recordList; // The List of Complete Records
var pageList; // The record List which needs to be displayed in a page
var currentPage = 1;
// by default will always be 1
var recordPerPage = 10;
// The no of records needs to be displayed in a single page
var totalPages = 1; // calculates the total number of pages
this.totalPages = Math.ceil(recordList.length / recordPerPage );
In Pagination, we usually look for 4 main navigation buttons.
handleNext() {
this.pageNo += 1;
this.preparePaginationList();
}
handlePrevious() {
this.pageNo -= 1;
this.preparePaginationList();
}
handleFirst() {
this.pageNo = 1;
this.preparePaginationList();
}
handleLast() {
this.pageNo = this.totalPages;
this.preparePaginationList();
}
let begin = (this.pageNo - 1) * parseInt(this.recordsperpage);
let end = parseInt(begin) + parseInt(this.recordsperpage);
this.recordsToDisplay = this.records.slice(begin, end);
In the above code, we are using the slice method of JavaScript to get the exact record based on Page No so that we can display the correct records on the page.
HTML Code
<template>
<div class="slds-m-aroung_small slds-align_absolute-center">
<lightning-spinner if:true={isLoading} alternative-text="Loading" size="small"></lightning-spinner>
<div slot="actions">
<lightning-button
variant="neutral"
title="first"
label="First"
class="slds-float_left"
icon-name="utility:chevronleft"
icon-position="left"
onclick={handleClick}
></lightning-button>
<lightning-button
variant="neutral"
title="previous"
class="slds-float_left"
label="Previous"
icon-name="utility:chevronleft"
icon-position="left"
onclick={handleClick}
></lightning-button>
<template if:true={pagelinks}>
<lightning-button-group>
<template for:each={pagelinks} for:item="page">
<lightning-button
key={page}
label={page}
onclick={handlePage}
></lightning-button>
</template>
</lightning-button-group>
</template>
<lightning-button
variant="neutral"
title="next"
class="slds-float_right"
label="Next"
icon-name="utility:chevronright"
icon-position="right"
onclick={handleClick}
></lightning-button>
<lightning-button
variant="neutral"
title="last"
class="slds-float_right"
label="Last"
icon-name="utility:chevronright"
icon-position="right"
onclick={handleClick}
></lightning-button>
</div>
</div>
<div class="slds-m-top_small"></div>
<h2
class="slds-m-aroung_small slds-align_absolute-center"
style="color: firebrick;"
>
Displaying Page No:
<strong> {pageNo}/{totalPages} </strong>and displaying records
<template if:true={end}>
from {endRecord}/{totalRecords}
</template>
<template if:false={end}>
from ({startRecord}-{endRecord})/{totalRecords}
</template>
</h2>
<div class="slds-m-top_small"></div>
<div class="slds-m-aroung_small">
<template if:true={showTable}>
<lightning-datatable
key-field="Id"
data={recordsToDisplay}
show-row-number-column="false"
hide-checkbox-column
columns={columns}
onrowaction={handleRowAction}
default-sort-direction={defaultSortDirection}
sorted-direction={sortDirection}
sorted-by={sortedBy}
onsort={onHandleSort}
onsave={handleSave}
draft-values={draftValues}
>
</lightning-datatable>
</template>
</div>
</template>
JsvaScript Code
import { LightningElement, api, track } from "lwc";
import { updateRecord } from 'lightning/uiRecordApi';
import { refreshApex } from '@salesforce/apex';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
const DELAY = 300;
export default class DoPaginaton extends LightningElement {
@api showTable = false;
@api records;
@api recordsperpage;
@api columns;
@track draftValues = [];
@track recordsToDisplay;
totalRecords;
pageNo;
totalPages;
startRecord;
endRecord;
end = false;
pagelinks = [];
isLoading = false;
defaultSortDirection = 'asc';
sortDirection = 'asc';
ortedBy;
connectedCallback() {
this.isLoading = true;
this.setRecordsToDisplay();
}
setRecordsToDisplay() {
this.totalRecords = this.records.length;
this.pageNo = 1;
this.totalPages = Math.ceil(this.totalRecords / this.recordsperpage);
this.preparePaginationList();
for (let i = 1; i <= this.totalPages; i++) {
this.pagelinks.push(i);
}
this.isLoading = false;
}
handleClick(event) {
let label = event.target.label;
if (label === "First") {
this.handleFirst();
} else if (label === "Previous") {
this.handlePrevious();
} else if (label === "Next") {
this.handleNext();
} else if (label === "Last") {
this.handleLast();
}
}
handleNext() {
this.pageNo += 1;
this.preparePaginationList();
}
handlePrevious() {
this.pageNo -= 1;
this.preparePaginationList();
}
handleFirst() {
this.pageNo = 1;
this.preparePaginationList();
}
handleLast() {
this.pageNo = this.totalPages;
this.preparePaginationList();
}
preparePaginationList() {
this.isLoading = true;
let begin = (this.pageNo - 1) * parseInt(this.recordsperpage);
let end = parseInt(begin) + parseInt(this.recordsperpage);
this.recordsToDisplay = this.records.slice(begin, end);
this.startRecord = begin + parseInt(1);
this.endRecord = end > this.totalRecords ? this.totalRecords : end;
this.end = end > this.totalRecords ? true : false;
const event = new CustomEvent('pagination', {
detail: {
records : this.recordsToDisplay
}
});
this.dispatchEvent(event);
window.clearTimeout(this.delayTimeout);
this.delayTimeout = setTimeout(() => {
this.disableEnableActions();
}, DELAY);
this.isLoading = false;
}
disableEnableActions() {
let buttons = this.template.querySelectorAll("lightning-button");
buttons.forEach(bun => {
if (bun.label === this.pageNo) {
bun.disabled = true;
} else {
bun.disabled = false;
}
if (bun.label === "First") {
bun.disabled = this.pageNo === 1 ? true : false;
} else if (bun.label === "Previous") {
bun.disabled = this.pageNo === 1 ? true : false;
} else if (bun.label === "Next") {
bun.disabled = this.pageNo === this.totalPages ? true : false;
} else if (bun.label === "Last") {
bun.disabled = this.pageNo === this.totalPages ? true : false;
}
});
}
handleRowAction(event) {
const actionName = event.detail.action.name;
const row = event.detail.row;
const rowAction = new CustomEvent('actions', {
detail: {
actionName : actionName,
data : row
}
});
this.dispatchEvent(rowAction);
}
handlePage(button) {
this.pageNo = button.target.label;
this.preparePaginationList();
}
onHandleSort(event) {
const { fieldName: sortedBy, sortDirection } = event.detail;
const cloneData = [...this.recordsToDisplay];
cloneData.sort(this.sortBy(sortedBy, sortDirection === 'asc' ? 1 : -1));
this.recordsToDisplay = cloneData;
this.sortDirection = sortDirection;
this.sortedBy = sortedBy;
}
sortBy( field, reverse, primer ) {
const key = primer
? function( x ) {
return primer(x[field]);
}
: function( x ) {
return x[field];
};
return function( a, b ) {
a = key(a);
b = key(b);
return reverse * ( ( a > b ) - ( b > a ) );
};
}
handleSave(event) {
this.isLoading = true;
const recordInputs = event.detail.draftValues.slice().map(draft => {
const fields = Object.assign({}, draft);
return { fields };
});
const promises = recordInputs.map(recordInput => updateRecord(recordInput));
window.console.log(' Updating Records.... ');
Promise.all(promises).then(record => {
this.dispatchEvent(
new ShowToastEvent({
title: 'Success',
message: 'All Records updated',
variant: 'success'
})
);
this.draftValues = [];
eval("$A.get('e.force:refreshView').fire();");
return refreshApex(this.recordsToDisplay);
}).catch(error => {
window.console.error(' error **** \n '+error);
})
.finally(()=>{
this.isLoading = false;
})
}
}
CSS Code
.customSelect select {
padding-right: 1.25rem;
min-height: inherit;
line-height: normal;
height: 1.4rem;
}
.customSelect label {
margin-top: 0.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;
}
This component has the following properties which need to be passed from the parent where you wanted to use the data table.
This component contains
To test this, you need to create an Apex Class which will return a list of records. I have used the below Query.
public with sharing class ContactController {
@AuraEnabled
public static List<Contact> getContacts() {
List<Contact> contactList = [
SELECT
Id,
Name,
AccountId,
Account.Name,
Title,
Phone,
Email,
OwnerId,
Owner.Name,
Owner.Email
FROM CONTACT
WITH SECURITY_ENFORCED
];
return contactList;
}
}
Create the Lightning Component and call the doPagination component inside this component.
<c-do-pagination
records={records}
show-table="true"
columns={columns}
recordsperpage="8"
onactions={handleRowActions}
onpagination={handlePagination}
>
</c-do-pagination>
If you have noticed that, we are passing the value for required property and handling the required events that we discussed above.
HTML
<!--
@File Name : contactDataTable.html
@Description :
@Author : Amit Singh (SFDCPanther)
@Group :
@Last Modified By : Amit Singh
@Last Modified On : 12-06-2020
@Modification Log :
Ver Date Author Modification
1.0 5/28/2020 Amit Singh (SFDCPanther) Initial Version
-->
<template>
<lightning-card
variant="Narrow"
title="Contact Records"
icon-name="standard:contact"
>
<div class="slds-m-around_small">
<template if:true={errors}>
</template>
</div>
<div class="slds-m-around_small">
<template if:false={errors}>
<template if:true={records}>
<c-do-pagination
records={records}
show-table="true"
columns={columns}
recordsperpage="8"
onactions={handleRowActions}
onpagination={handlePagination}
>
</c-do-pagination>
</template>
</template>
</div>
</lightning-card>
</template>
JS code
/**
* @File Name : contactDataTable.js
* @Description :
* @Author : A Singh
* @Group :
* @Last Modified By : Amit Singh
* @Last Modified On : 12-06-2020
* @Modification Log :
* Ver Date Author Modification
* 1.0 6/5/2020 A Singh Initial Version
**/
import { LightningElement, track } from 'lwc';
import getContacts from '@salesforce/apex/ContactController.getContacts';
import sharedjs from 'c/sharedjs';
const columns = [
{ label: 'Name', fieldName: 'Name', wrapText: 'true', sortable: true, editable: true },
{ label: 'Email', fieldName: 'Email', type: 'email', sortable: true, editable: true },
{ label: 'Phone', fieldName: 'Phone', type: 'phone', sortable: true, editable: true },
{ label: 'Title', fieldName: 'Title', sortable: true, editable: true },
{
label: 'Account',
fieldName: 'ACC_NAME',
wrapText: 'true',
cellAttributes: {
iconName: { fieldName: 'accIconName' },
iconPosition: 'left'
},
sortable: true
},
{
label: 'Owner',
fieldName: 'OWNER',
cellAttributes: {
iconName: { fieldName: 'iconName' },
iconPosition: 'left'
},
sortable: true
},
{
label: 'View',
fieldName: 'URL',
type: 'url',
wrapText: 'true',
typeAttributes: {
tooltip: { fieldName: 'Name' },
label: {
fieldName: 'Name'
},
target: '_blank'
}
},
{ label: 'View', type: 'button', typeAttributes: {
label: 'View', name: 'View', variant: 'brand-outline',
iconName: 'utility:preview', iconPosition: 'right'
}
},
];
export default class ContactDataTable extends LightningElement {
@track records;
@track errors;
columns = columns;
connectedCallback() {
this.handleDoInit();
}
handleDoInit() {
sharedjs._servercall(
getContacts,
undefined,
this.handleSuccess.bind(this),
this.handleError.bind(this)
);
}
handleSuccess(result) {
result.forEach(element => {
if (element.OwnerId) {
element.OWNER = element.Owner.Name;
element.iconName = 'standard:user';
}
if (element.AccountId) {
element.ACC_NAME = element.Account.Name;
element.accIconName = 'standard:account';
}
element.URL = 'https://' + window.location.host + '/' + element.Id;
});
this.records = result;
this.errors = undefined;
}
handleError(error) {
this.errors = error;
this.records = undefined;
}
handleRowActions(event){
window.console.log(' Row Level Action Handled ', event.detail.actionName);
window.console.log(' Row Level Action Handled ', JSON.stringify(event.detail.data));
}
handlePagination(event){
//window.console.log('Pagination Action Handled ', JSON.stringify(event.detail.records));
}
}
.XML file
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>50.0</apiVersion>
<isExposed>true</isExposed>
<masterLabel>Contact Data Table</masterLabel>
<targets>
<target>lightning__RecordPage</target>
<target>lightning__AppPage</target>
<target>lightning__HomePage</target>
<target>lightningCommunity__Page</target>
<target>lightningCommunity__Default</target>
</targets>
</LightningComponentBundle>
The above test component have the dependency of a new Lightning Web Component and below is the JS code for the same component.
/* eslint-disable no-else-return */
/**
* @File Name : sharedjs.js
* @Description :
* @Author : [email protected]
* @Group :
* @Last Modified By : Amit Singh
* @Last Modified On : 12-06-2020
* @Modification Log :
* Ver Date Author Modification
* 1.0 5/17/2020 [email protected] Initial Version
**/
import { ShowToastEvent } from "lightning/platformShowToastEvent";
/*
! To store all the JS functions for the various LWC
* This JavaScript file is used to provide many reusability functionality like pubsub
* Reusable Apex Calls to Server, Preparing Dynamic Toasts
Todo : PubSub JS file of LWC & Aura Components
? V2
*/
var callbacks = {};
/**
* Registers a callback for an event
* @param {string} eventName - Name of the event to listen for.
* @param {function} callback - Function to invoke when said event is fired.
*/
const subscribe = (eventName, callback) => {
if (!callbacks[eventName]) {
callbacks[eventName] = new Set();
}
callbacks[eventName].add(callback);
};
/**
* Unregisters a callback for an event
* @param {string} eventName - Name of the event to unregister from.
* @param {function} callback - Function to unregister.
*/
const unregister = (eventName, callback) => {
if (callbacks[eventName]) {
callbacks[eventName].delete(callback);
// ! delete the callback from callbacks variable
}
};
const unregisterAll = () => {
callbacks = {};
};
/**
* Fires an event to listeners.
* @param {string} eventName - Name of the event to fire.
* @param {*} payload - Payload of the event to fire.
*/
const publish = (eventName, payload) => {
if (callbacks[eventName]) {
callbacks[eventName].forEach(callback => {
try {
callback(payload);
} catch (error) {
// fail silently
}
});
}
};
/**
* Todo: Calls an Apex Class method and send the response to call back methods.
* @param {*} _serveraction - Name of the apex class action needs to execute.
* @param {*} _params - the list of parameters in JSON format
* @param {*} _onsuccess - Name of the method which will execute in success response
* @param {*} _onerror - Name of the method which will execute in error response
*/
const _servercall = (_serveraction, _params, _onsuccess, _onerror) => {
if (!_params) {
_params = {};
}
_serveraction(_params)
.then(_result => {
if (_result && _onsuccess) {
_onsuccess(_result);
}
})
.catch(_error => {
if (_error && _onerror) {
_onerror(_error);
}
});
};
/**
* Todo: Prepare the toast object and return back to the calling JS class
* @param {String} _title - title of of the toast message
* @param {String} _message - message to display to the user
* @param {String} _variant - toast type either success/error/warning or info
* @param {String} _mode - defines either toast should auto disappear or it should stick.
*/
const _toastcall = (_title, _message, _variant, _mode) => {
const _showToast = new ShowToastEvent({
title: _title,
message: _message,
mode: _mode,
variant: _variant
});
return _showToast;
};
/**
* Todo: Parse the Error message and returns the parsed response to calling JS method.
* @param {Array} errors - Error Information
*/
const _reduceErrors = errors => {
if (!Array.isArray(errors)) {
errors = [errors];
}
return errors
.filter(error => !!error)
.map(error => {
if (Array.isArray(error.body)) {
return error.body.map(e => e.message);
} else if (error.body && typeof error.body.message === "string") {
return error.body.message;
} else if (typeof error.message === "string") {
return error.message;
}
return error.statusText;
})
.reduce((prev, curr) => prev.concat(curr), [])
.filter(message => !!message);
};
/*
Todo: Export all the functions so that these are accisible from the other JS Classes
*/
export default {
subscribe,
unregister,
publish,
unregisterAll,
_servercall,
_toastcall,
_reduceErrors
};
Thanks for Reading 🙂
If you have any doubt, please feel free to reach out to me.
#Salesforce #DeveloperGeeks #SFDCPanther
Hi! Congrats for this amazing post, and thanks for sharing your knowledge! Could you please help me? I’d like to know what is recommended to persist selected rows when adopting pagination in LWC by using datatable?
It is not suggested to persist the selected records in pagination. However, if you want then you have to use custom logic to store all the selected records inside an array and then use the selected-rows attribute of datatable to keep the records selected.
if you have pages more than 20, it will go out of page styles, you have to add some styles
I appreciate your response and concern. The purpose of the blog is to showcase the ability. Now, it’s the individual responsibility to add the required css