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.

Features available in component

  1. Display the records in a Table or another format.
  2. In-Line Editing for LWC Data Table
  3. Sorting in LWC Data Table
  4. Supports Row Level Action
  5. Displays the details of the page and no of records

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.

1. Some useful variables


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

2. Calculate No of Pages

this.totalPages = Math.ceil(recordList.length / recordPerPage );

3. Create Navigation Buttons and their Methods.

In Pagination, we usually look for 4 main navigation buttons.

  1. First
  2. Previous
  3. Next
  4. Last
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();
    }

4. Prepare the Pagination Records

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.

Create a doPagination component

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>
        &nbsp;
            <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>
            &nbsp;
            <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>&nbsp;{pageNo}/{totalPages}&nbsp;</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;
}

Public Properties in the Component

This component has the following properties which need to be passed from the parent where you wanted to use the data table.

  1. showTable – Accepts true/false. If true, it will display the data table.
  2. records – The Complete list of records
  3. recordsperpage – Accepts no values and defines the no of records that need to be displayed on a single page.
  4. columns – if the value for showTable property is true it requires the valid data column. For more details visit here.

Events in the Component

This component contains

  1. pagination – This event sends the list of all the records which are being displayed on the current page and use if you do not want to use Lightning Data Table to display the records. For Example, You wanted to use Lightning accordion for pagination instead of the data table.
  2. actions – This event sends the data about the current row and the row-level action. So, if you have any row-level actions you can handle this event in your parent component.

Test the pagination component.

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.

Here is the full code for test component

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>

Create SharedJs component

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
};

Output

Pagination in Lightning Web Component

Resources

  1. https://www.thatsoftwaredude.com/content/6125/how-to-paginate-through-a-collection-in-javascript
  2. https://jasonwatmore.com/post/2018/08/07/javascript-pure-pagination-logic-in-vanilla-js-typescript
  3. https://developer.salesforce.com/docs/component-library/bundle/lightning-datatable/example

Thanks for Reading 🙂

If you have any doubt, please feel free to reach out to me.

#Salesforce #DeveloperGeeks #SFDCPanther

Our Score
Click to rate this post!
[Total: 2 Average: 5]

Leave a Reply