Showing posts with label Pagination. Show all posts
Showing posts with label Pagination. Show all posts

Wednesday, September 25, 2019

Pagination using Salesforce Lightning Web Components with array slicing

Motivation behind this


Currently I am exploring Lightning Web Components (LWC) and trying to build pagination functionality using LWC. I was searching on google on the same, but didn't find this functionality on LWC, though there are tons of materials available on the same either using Lightning Aura Components or by Visualforce StandardSetController.

I have seen couple of solutions where developers either used offset to fetch page by page data every time directly from controller's SOQL query or taken entire dataset on the client side controller but every time looping through the array in the for loop to get paginated data.

Both of the solutions are not favorable to me, which motivates me to build a proof-of-concept and sharing a knowledge to Salesforce community members.

I have gone through Create and Dispatch Events documentation, there is some light on pagination but that topic is used to understand on how events are generated and dispatched.

Let's get started.

Use Case


Business has a requirement to view the tabular data retrieving from the database in a paginated way. 

Developer wants to build the same using Lightning Web Components.


Expected Behavior




In the above demo, I am trying to show Account data is displaying page by page.

Before going to solution approach it is recommended to read Salesforce Lightning Web Components Cheat Sheet to have an understanding on different type of variables, dispatching events and calling apex classes' method from js controller.



Solution Approach


There are two components have been used.

1. Paginator component - which contains previous and next button as specified in Create and Dispatch Events documentation. For the sake of simplicity, take all the code as specified there.

2. displayPaginatedRecords component - which contains lightning datatable and Paginator component to implement entire functionality.

Let's first discuss about displayPaginatedRecords component.

displayPaginatedRecords.html

This html contains datatable and Paginator component using c-paginator which is listening events using onprevious and onnext event listeners.



<template>
    <lightning-card title="List of Accounts" icon-name="custom:custom9">
        <div class="slds-m-around_medium">
            <p class="slds-p-horizontal_medium">Display records in paginated way </p>
            <br></br>
            <div style="height: 180px;">
                <lightning-datatable 
                    key-field="id"
                    data={data}
                    columns={columns}>
                </lightning-datatable>
            </div>
        </div>
        <div class="slds-m-around_medium">
            <p class="slds-m-vertical_medium content">
                     Displaying {startingRecord} to {endingRecord} of {totalRecountCount} records.
                     Page {page} of {totalPage}. </p>
            <c-paginator onprevious={previousHandler} onnext={nextHandler}></c-paginator>
        </div>
    </lightning-card>
</template>

PaginationController.cls

This class has retrieveAccounts method which retrieves the data from Account object. For simplicity, WHERE clause and filter criteria have not given.


public with sharing class PaginationController {
    @AuraEnabled (cacheable=true)
    public static List<Account> retrieveAccounts(){
        return [SELECT Id, Name, Type, BillingCountry
                FROM Account
                LIMIT 1000];
    }
}

displayPaginatedRecords.js

Let's talk main important points about this js controller.


  • All the respective @track variables have been declared which have been used in the page, like page, startingRecord, endingRecord, pageSize, totalRecountCount, totalPage, data, columns. All are self-explanatory but written their usage as comments for each.
  • default pageSize = 5, so every page will display 5 records.
  • Datatable columns have been defined with label and fieldName attributes.
  • @wire to function i.e retrieveAccounts which performs following:

  1. retrieves the data from Apex controller and assigns all data in items array
  2. calculates totalRecountCount from data.length and calculates totalPage to be displayed.
  3. Now, main part is: use of Array.slice() which returns selected elements from the array

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

The above principle has been applied in displayRecordPerPage method and everything is written as comments for better understanding.

For example, on 2nd page, label will shown as => "Displaying 6 to 10 of 23 records. Page 2 of 5"
        page = 2; pageSize = 5; startingRecord = 5, endingRecord = 10
        so, slice(5,10) will give 5th to 9th records.
        
  • previousHandler and nextHandler methods changes the page and calls displayRecordPerPage method
If we remove the comments from the method bodies then code will be compact. I have mentioned them for easy understanding.



/* eslint-disable no-console */
import { LightningElement, track, wire } from 'lwc';

import retrieveAccounts from '@salesforce/apex/PaginationController.retrieveAccounts';
//define columns of the datatable
const columns = [ { label: 'Id', fieldName: 'Id' }, { label: 'Name', fieldName: 'Name' }, { label: 'Type', fieldName: 'Type' }, { label: 'BillingCountry', fieldName: 'BillingCountry' }, ]; let i=0; export default class DisplayPaginatedRecords extends LightningElement { @track page = 1; //this will initialize 1st page @track items = []; //it contains all the records. @track data = []; //data to be displayed in the table @track columns; //holds column info. @track startingRecord = 1; //start record position per page @track endingRecord = 0; //end record position per page @track pageSize = 5; //default value we are assigning @track totalRecountCount = 0; //total record count received from all retrieved records @track totalPage = 0; //total number of page is needed to display all records @wire(retrieveAccounts) wiredAccounts({ error, data }) { if (data) { //if you want to perform data transformation then following code will be used, //so that individual values to be assigned into each columns /* for(i=0; i<data.length; i++) { this.items = [...this.items, {Id:data[i].Id, Name:data[i].Name, Type:data[i].Type, BillingCountry:data[i].BillingCountry}]; } */ this.items = data; this.totalRecountCount = data.length; //here it is 23 this.totalPage = Math.ceil(this.totalRecountCount / this.pageSize); //here it is 5 //initial data to be displayed -----------> //slice will take 0th element and ends with 5, but it doesn't include 5th element //so 0 to 4th rows will be displayed in the table this.data = this.items.slice(0,this.pageSize); this.endingRecord = this.pageSize; this.columns = columns; this.error = undefined; } else if (error) { this.error = error; this.data = undefined; } } //clicking on previous button this method will be called 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){ /*let's say for 2nd page, it will be => "Displaying 6 to 10 of 23 records. Page 2 of 5" page = 2; pageSize = 5; startingRecord = 5, endingRecord = 10 so, slice(5,10) will give 5th to 9th records. */ this.startingRecord = ((page -1) * this.pageSize) ; this.endingRecord = (this.pageSize * page); this.endingRecord = (this.endingRecord > this.totalRecountCount) ? this.totalRecountCount : this.endingRecord; this.data = this.items.slice(this.startingRecord, this.endingRecord); //increment by 1 to display the startingRecord count, //so for 2nd page, it will show "Displaying 6 to 10 of 23 records. Page 2 of 5" this.startingRecord = this.startingRecord + 1; } }


Let's take paginator component.


paginator.html


This html contains two button Previous and Next for firing events.


<template>
    <lightning-layout>
        <lightning-layout-item>
            <lightning-button label="Previous" icon-name="utility:chevronleft" onclick={previousHandler}></lightning-button>
        </lightning-layout-item>
        <lightning-layout-item flexibility="grow"></lightning-layout-item>
        <lightning-layout-item>
            <lightning-button label="Next" icon-name="utility:chevronright" icon-position="right" onclick={nextHandler}></lightning-button>
        </lightning-layout-item>
    </lightning-layout>
</template>

paginator.js

This js controller dispatches two events from event handlers.


// paginator.js
import { LightningElement } from 'lwc';

export default class Paginator extends LightningElement {
    previousHandler() {
        this.dispatchEvent(new CustomEvent('previous'));
    }

    nextHandler() {
        this.dispatchEvent(new CustomEvent('next'));
    }
}


displayPaginatedRecords.js-meta.xml

This meta xml contains the information as to where this component will be exposed.


<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata" fqn="displayPaginatedRecords">
    <apiVersion>46.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordPage</target>
        <target>lightning__AppPage</target>
        <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>

Now, from Lightning App Builder page, drag and drop the displayPaginatedRecords component and open the page.


Final Outcome



Finally, we are done and thanks for reading.

Further Reading





Wednesday, December 20, 2017

Visualforce with JQuery Datatable for sorting, pagination and handling 10000 records

Use Case


Sometimes we get a requirement to display tabular data in visualforce which will have following features:


  • Data will be displayed in paginated way.
  • Column headers must be sortable
  • Keyword search in the list itself.
  • More over if we want to display 10000 records in the list.

Solution

We can leverage JQuery datatable library to achieve those functionalities.

Controller



 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public with sharing class SortWrapper 
{

    public List<ContactInfoWrapper> ContactsWrapper {get; set;}    
    //added for paginnation
    public List<List<ContactInfoWrapper>> listofContactsWrapper {get; set;}
    
    public SortWrapper()
    {
  ContactsWrapper = new List<ContactInfoWrapper>();
  listofContactsWrapper = new List<List<ContactInfoWrapper>>();
        getContacts();
    } 
     
    public void getContacts()   
    {
        for(Contact s:[SELECT Id, Name, Email, Phone, Account.Name FROM Contact])
        {            
            ContactsWrapper.add(new ContactInfoWrapper(s)); 
            if(ContactsWrapper.size() == 999)
   {
    listofContactsWrapper.add(ContactsWrapper);
    ContactsWrapper= new List<ContactInfoWrapper>();
            }                
        } 
        if(ContactsWrapper.size() != 0){
   listofContactsWrapper.add(ContactsWrapper);
     }   
        
    }
    
 //Wrapper class
    public class ContactInfoWrapper
    {
        public Contact sObj{get;set;}
        public Boolean checked {get;set;}
        public ContactInfoWrapper(Contact con)
        {
            sObj = con;  
            checked=false;  
        } 
    }
}

Visualforce



 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<apex:page id="SortPage" controller="SortWrapper"  showHeader="false">
    <!--added for pagination-->
       <head>
        <apex:includescript value="//code.jquery.com/jquery-1.11.1.min.js" / >
        <apex:includescript value="//cdn.datatables.net/1.10.4/js/jquery.dataTables.min.js" />
        <apex:stylesheet value="//cdn.datatables.net/1.10.4/css/jquery.dataTables.css" />
        <script>
            j$ = jQuery.noConflict();
            j$(document).ready( function () {
                var contactTable = j$('[id$="contacttable"]').DataTable({
                    
                });
            });
        </script>
    </head>
   <!---End of Pagination--->
   
    <apex:form id="myForm">
   
        <apex:sectionHeader title="Contact"/><br/>
        
        <apex:pageBlock>
            
            <apex:pageBlockSection title="Contact Records" collapsible="false" columns="1"/>
            <br></br>
            <body>                
                <table id="contacttable" class="display">       
                    <thead>
                        <tr>
                            <th style="width:10%;">Select</th>
                            <th style="width:25%;">Name</th>
                            <th style="width:30%;">Account Name</th>                                
                        </tr>
                    </thead>
                    <tbody>
                        <apex:repeat value="{!listofContactsWrapper}" var="contactinfowrp" id="tableInd">
                              <apex:repeat value="{!contactinfowrp}" var="con">
                               <tr >
                                  <td><apex:inputCheckbox value="{!con.checked}"/></td>
                                  <td>{!con.sObj.Name}</td>
                                  <td>{!con.sObj.Account.Name}</td>
                              </tr>
                            </apex:repeat>
                        </apex:repeat>
                    </tbody>
                </table>
            </body>
        </apex:pageBlock>  
    </apex:form>
</apex:page>

Outcome


Saturday, April 1, 2017

Keyword Search Series : Custom Controller Pagination ~ easiest way

Motivation

So far we know, pagination can be easily be achieved by StandardSetController class.

I have seen cases where developer has a notion like until and unless we use controller extension and StandardSetController as an argument in Constructor, pagination is difficult to achieve. For this, I have tried this approach in Custom Controller with the search input from Visualforce screen.

Use Case


User will provide the input and clicking on Search button, the results will be displayed in paginated format. User can change the page size and system will fetch the records based on selected page size.

Solution

For the sake of learning, I have created a visualforce page as below.


Visualforce

It will take Opportunity Name as input search criteria and clicking on Search button it will pass to the controller.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<apex:page id="filterPage" controller = "FilterSearch">
 <apex:form style="overflow-y:auto;overflow-x:auto;margin-right: 5px;margin-left: 5px;margin-top: 3px;" id="searchForm">
        <apex:actionFunction name="refreshPageSize" action="{!refreshPageSize}" status="fetchStatus" reRender="searchBlock"/>
        
  <apex:outputLabel value="Search By Opportunity Name:"/>          
        <apex:inputText value="{!searchString}" label="Opportunity Name:"/>            
        <apex:commandButton value="Search " action="{!showAll}"/>
  
        <apex:pageBlock title="Results" rendered="{!showResult}" id="searchBlock"> 
            <apex:pageblockTable value="{!Opps}" var="a" id="tabId">  
                <apex:column>
     <a href="/{!a.id}" target="_blank"> {!a.name}</a>
    </apex:column>
    <apex:column rendered="false" id="id">
     <apex:outputField value="{!a.id}" />
    </apex:column>
    <apex:column headerValue="Account Name">
     <apex:outputField value="{!a.account.name}" />
    </apex:column>

    <apex:column headerValue="Stage">
     <apex:outputField value="{!a.stagename}" />
    </apex:column>

    <apex:column headerValue="Forecast Category" >
     <apex:outputField value="{!a.ForecastCategoryName}"  />
    </apex:column>

            </apex:pageBlockTable>
            <apex:panelGrid columns="8">             
                <apex:selectList value="{!size}" multiselect="false" size="1" onchange="refreshPageSize();">
                    <apex:selectOptions value="{!paginationSizeOptions}"/>
                </apex:selectList>           
                <apex:commandButton status="fetchStatus" reRender="searchBlock" value="<<" action="{!setCon.first}" disabled="{!!setCon.hasPrevious}" title="First Page"/>
                <apex:commandButton status="fetchStatus" reRender="searchBlock" value="<" action="{!setCon.previous}" disabled="{!!setCon.hasPrevious}" title="Previous Page"/>
                <apex:commandButton status="fetchStatus" reRender="searchBlock" value=">" action="{!setCon.next}" disabled="{!!setCon.hasNext}" title="Next Page"/>
                <apex:commandButton status="fetchStatus" reRender="searchBlock" value=">>" action="{!setCon.last}" disabled="{!!setCon.hasNext}" title="Last Page"/>
                <apex:outputText >{!(setCon.pageNumber * size)+1-size}-{!IF((setCon.pageNumber * size)>noOfRecords, noOfRecords,
                     (setCon.pageNumber * size))} of {!noOfRecords}
                </apex:outputText>                 
                <apex:outputPanel >                      
                </apex:outputPanel>
            </apex:panelGrid>                                      
      </apex:pageBlock> 
    </apex:form>    
</apex:page>


Custom Controller

It has following activities:

1. In the Constructor, assigning default page size and preparing the list of page size options.

2. showAll() - This method is getting called with Search button is clicked. In this method, searchString is getting collected and getting passed into dynamic query.

Now, main important part, this line, create QueryLocator object using Database.getQueryLocator(query) and assign that into ApexPages.StandardSetController constructor to create StandardSetController object.

Rest is easy. StandardSetController will take all burden for pagination.


setCon = new ApexPages.StandardSetController(Database.getQueryLocator(query));


3. getOpps() - This method return List of records based on pageSize.

4. refreshPageSize() - This method is getting called when paginated size is changed on screen.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public class FilterSearch  
{
    public String searchString{get;set;}
    public Boolean showResult{get;set;}
    public Integer noOfRecords{get; set;}
    public Integer size{get;set;}
    public ApexPages.StandardSetController setCon{get;set;}    
    public List<SelectOption> paginationSizeOptions{get;set;}
 
    String errorStr = '';    
         
    /**
    * FilterSearch
    * Constructor to initiate the default values at the time of Loading
    */    
    public FilterSearch()
    {
        size=10; //default page size

  //pagination size options
        paginationSizeOptions = new List<SelectOption>();
        paginationSizeOptions.add(new SelectOption('10','10'));
        paginationSizeOptions.add(new SelectOption('20','20'));
        paginationSizeOptions.add(new SelectOption('30','30'));
              
    }
    
    public void showAll()
    {
        try
        {
            if(String.isNotBlank(searchString))
            {  
              //build SOQL query string
             String query='Select Id,Name, account.name, stagename, ForecastCategoryName FROM Opportunity WHERE Name LIKE \'%' + searchString + '%\' order by Name LIMIT 1000';
    
               //return querylocator to an instance of StandardSetController
                setCon = new ApexPages.StandardSetController(Database.getQueryLocator(query));            
                if(setCon.getResultSize() >0)
                {
                    showResult = true;
                }else{
                    showResult = false;
                    ApexPages.addmessage(new ApexPages.message(ApexPages.severity.INFO,'No Records Found.'));
                }        
            }            
        }catch(Exception ex)
        {
            errorStr ='Error Occured while Searching.';
            ApexPages.addmessage(new ApexPages.message(ApexPages.severity.ERROR, 'errorStr:' +ex.getMessage()));
        }   
    }

    public List<Opportunity> getOpps() 
    {
        if(setCon.getResultSize() >0)
        {
          setCon.setPageSize(size);  
          noOfRecords = setCon.getResultSize();
          return (List<Opportunity>)setCon.getRecords();
        }else
        {
          return null;
        }
        return null;
    }
      
    /**
    * refreshPageSize
    * Changes the size of Pagination.
    * @param    
    * @return void
    */
    public void refreshPageSize() 
    {
        setCon.setPageSize(size);         
    }
}

Results



Hope it helps! and if so, then share with your friends.

Link to my answer in stackexchange: My Answer in Stackechange

Further Reading