Lightning Datatable with Pagination and Search on the Client-Side

Introduction

Lightning Datatable is a table that displays columns of data, formatted according to type. This component has many options to customize. Unfortunately, It doesn’t have built-in pagination. When we are retrieving data from the database using the SOQL query, we can create pagination using the OFFSET clause. But the problem is when we are getting records directly from the external system.

Assumptions

In the example, I’m getting data from https://jsonplaceholder.typicode.com/photos website. I have an ImageResponse wrapper with fields: albumId, id, title, url and thumbnailUrl. List of ImageResponse wrapper records is passed to the Lightning Component, displayed in the Lightning Datatable with Pagination and Search logic. I want to search Images by Image Title.

JSON Response

Solution

Here is a ready-to-use Apex Controller and Lightning Component with Lightning Datatable, Pagination and Search logic on the client-side. Let’s take a look at the solution:

Lightning Datatable

ImagesPagePagination.cmp:

<aura:component implements="force:appHostable" controller="ImagesPageController">
    <aura:attribute name="allData" type="List" />
    <aura:attribute name="filteredData" type="List" />
    <aura:attribute name="tableData" type="List" />
    <aura:attribute name="columns" type="Object[]" />
    <aura:attribute name="pageSize" type="Integer" default="15" />
    <aura:attribute name="pageSizeOptions" type="Integer[]" default="10,15,20,25,50,100" />
    <aura:attribute name="currentPageNumber" type="Integer" default="1" />
    <aura:attribute name="totalPages" type="Integer" default="1" />
    <aura:attribute name="searchPhrase" type="String" />
    <aura:attribute name="isLoading" type="Boolean" default="false" />

    <aura:handler name="init" value="{! this }" action="{! c.doInit }" />

    <aura:if isTrue="{! v.isLoading }">
        <lightning:spinner alternativeText="Loading" />
    </aura:if>

    <lightning:card>
        <div class="slds-p-around_small slds-grid slds-grid_align-spread slds-grid_vertical-align-start">
            <div>
                <lightning:select
                    label="Number of records on page:"
                    value="{! v.pageSize }"
                    onchange="{! c.onPageSizeChange }">
                    <aura:iteration items="{! v.pageSizeOptions }" var="opt">
                        <option text="{! opt }"></option>
                    </aura:iteration>
                </lightning:select>
            </div>
            <div>
                <lightning:button 
                    label="First" 
                    iconName="utility:left" 
                    iconPosition="left"
                    onclick="{! c.onFirst }" 
                    disabled="{! v.currentPageNumber == 1 }" />
                <lightning:button 
                    label="Previous"
                    iconName="utility:chevronleft" 
                    iconPosition="left"
                    onclick="{! c.onPrev }" 
                    disabled="{! v.currentPageNumber == 1 }" />
                <span class="slds-var-p-horizontal_x-small">
                    Page {! (v.currentPageNumber) } of {! (v.totalPages) }
                </span>
                <span class="slds-var-p-horizontal_x-small">
                    Number of records: {! (v.filteredData.length) }
                </span>
                <lightning:button 
                    label="Next"
                    iconName="utility:chevronright" 
                    iconPosition="right" 
                    onclick="{! c.onNext }"
                    disabled="{! v.currentPageNumber == v.totalPages }" />
                <lightning:button 
                    label="Last"
                    iconName="utility:right" 
                    iconPosition="right" 
                    onclick="{! c.onLast }"         
                    disabled="{! v.currentPageNumber == v.totalPages }" />
            </div>
            <div class="inline-container">
                <span class="padding-right">
                    <lightning:input 
                        variant="label-hidden" 
                        placeholder="Search Phrase" 
                        type="search" 
                        value="{! v.searchPhrase }" 
                        onchange="{! c.onChangeSearchPhrase }" />
                </span>
                <span>
                    <lightning:button 
                        label="Search" 
                        variant="neutral" 
                        onclick="{! c.handleSearch }" />
                </span>
            </div>
        </div>
        <lightning:datatable
            aura:id="table"
            columns="{! v.columns }"
            data="{! v.tableData }"
            hideCheckboxColumn="true"
            keyField="Id" />
    </lightning:card>
</aura:component>

ImagesPagePagination.css:

.THIS .inline-container {
    display: inline-flex;
}

.THIS .padding-right {
    padding-right: 20px;
}

ImagesPagePaginationController.js

({
    doInit: function (component, event, helper) {
        helper.setupDataTable(component);
        helper.getData(component);
    },

    onNext: function(component, event, helper) {        
        let pageNumber = component.get("v.currentPageNumber");
        component.set("v.currentPageNumber", pageNumber + 1);
        helper.setPageDataAsPerPagination(component);
    },
    
    onPrev: function(component, event, helper) {        
        let pageNumber = component.get("v.currentPageNumber");
        component.set("v.currentPageNumber", pageNumber - 1);
        helper.setPageDataAsPerPagination(component);
    },
    
    onFirst: function(component, event, helper) {        
        component.set("v.currentPageNumber", 1);
        helper.setPageDataAsPerPagination(component);
    },
    
    onLast: function(component, event, helper) {        
        component.set("v.currentPageNumber", component.get("v.totalPages"));
        helper.setPageDataAsPerPagination(component);
    },

    onPageSizeChange: function(component, event, helper) {        
        helper.preparePagination(component, component.get('v.filteredData'));
    },

    onChangeSearchPhrase : function (component, event, helper) {
        if ($A.util.isEmpty(component.get("v.searchPhrase"))) {
            let allData = component.get("v.allData");
            component.set("v.filteredData", allData);
            helper.preparePagination(component, allData);
        }
    },

    handleSearch : function (component, event, helper) {
        helper.searchRecordsBySearchPhrase(component);
    },
})

ImagesPagePaginationHelper.js

({
    setupDataTable: function (component) {
        component.set('v.columns', [
            {label: 'Album Id', fieldName: 'albumId', type: 'text'},
            {label: 'Id', fieldName: 'id', type: 'text'},
            {label: 'Title', fieldName: 'title',  type: 'text', wrapText: true},
            {label: 'URL', fieldName: 'url',  type: 'url'},
            {label: 'Thumbnail URL', fieldName: 'thumbnailUrl',  type: 'url'}
        ]);
    },

    getData: function (component) {
        return this.callAction(component)
            .then(
                $A.getCallback(imageRecords => {
                    component.set('v.allData', imageRecords);
                    component.set('v.filteredData', imageRecords);
                    this.preparePagination(component, imageRecords);
                })
            )
            .catch(
                $A.getCallback(errors => {
                    if (errors && errors.length > 0) {
                        $A.get("e.force:showToast")
                            .setParams({
                                message: errors[0].message != null ? errors[0].message : errors[0],
                                type: "error"
                            })
                            .fire();
                    }
                })
            );
    },

    callAction: function (component) {
        component.set("v.isLoading", true);
        return new Promise(
            $A.getCallback((resolve, reject) => {
                const action = component.get("c.getImageRecords");
                action.setCallback(this, response => {
                    component.set("v.isLoading", false);
                    const state = response.getState();
                    if (state === "SUCCESS") {
                        return resolve(response.getReturnValue());
                    } else if (state === "ERROR") {
                        return reject(response.getError());
                    }
                    return null;
                });
                $A.enqueueAction(action);
            })
        );
    },

    preparePagination: function (component, imagesRecords) {
        let countTotalPage = Math.ceil(imagesRecords.length/component.get("v.pageSize"));
        let totalPage = countTotalPage > 0 ? countTotalPage : 1;
        component.set("v.totalPages", totalPage);
        component.set("v.currentPageNumber", 1);
        this.setPageDataAsPerPagination(component);
    },

    setPageDataAsPerPagination: function(component) {
        let data = [];
        let pageNumber = component.get("v.currentPageNumber");
        let pageSize = component.get("v.pageSize");
        let filteredData = component.get('v.filteredData');
        let x = (pageNumber - 1) * pageSize;
        for (; x < (pageNumber) * pageSize; x++){
            if (filteredData[x]) {
                data.push(filteredData[x]);
            }
        }
        component.set("v.tableData", data);
    },

    searchRecordsBySearchPhrase : function (component) {
        let searchPhrase = component.get("v.searchPhrase");
        if (!$A.util.isEmpty(searchPhrase)) {
            let allData = component.get("v.allData");
            let filteredData = allData.filter(record => record.title.includes(searchPhrase));
            component.set("v.filteredData", filteredData);
            this.preparePagination(component, filteredData);
        }
    },
})

ImagesPageController.cls

public with sharing class ImagesPageController {
    
    @AuraEnabled
    public static List<ImageResponse> getImageRecords() {
        try {
            HttpRequest httpRequest = new HttpRequest();
            httpRequest.setMethod('GET');
            httpRequest.setEndpoint('callout:ImagesEndpoint');
            Http http = new Http();
            HttpResponse httpResponse = http.send(httpRequest);
            return (List<ImageResponse>) JSON.deserialize(httpResponse.getBody(), List<ImageResponse>.class);
        } catch (Exception ex) {
            throw new AuraHandledException(ex.getMessage());
        }
    }

    public class ImageResponse {
        @AuraEnabled
        public Integer albumId { get; set; }
        @AuraEnabled
        public Integer id { get; set; }
        @AuraEnabled
        public String title { get; set; }
        @AuraEnabled
        public String url { get; set; }
        @AuraEnabled
        public String thumbnailUrl { get; set; }
    }
}

ImagesEndpoint named credential:

ImagesEndpointNC

Was it helpful? Check out our other great posts here.

Resources

3.7 3 votes
Article Rating
Subscribe
Notify of
guest
3 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments
srikanth
srikanth
11 months ago

Can you please update the apex class controller code as well

trackback

[…] Let’s say that we have situation when we just finished quick action from opportunity record page. This action uses LWC component and runs apex method which depends on user input, updates data of this particular opportunity. It would be great if user can see updated record view just after action, but data on record page isn’t refreshed by default. Fortunately, there is a way how we can refresh record view in LWC component with really simple solution. Such solution you can combine for instance with lightning data table, which example you can find here. […]

Close Menu
3
0
Would love your thoughts, please comment.x
()
x