Tuesday, April 9, 2019

Salesforce : Lightning Web Components : Pagination with Search and List View - Step by Step Implementation


Hello Guys,
As we have seen Salesforce recently published a very very important development feature/platform called 'Lightning Web Components'. If you are new to these, I have a sample Pagination implementation example which will try to cover some of the use cases in Lightning web components.



Let's begin with our journey to Pagination. :)

  • Create a component called paginatorBottom. This component will have the buttons for the pagination such as PreviousNextFirstLast. This component will fire events on the button actions which will be caught by the parent components.




paginatorBottom.html

 <template>  
   <lightning-layout>  
     <lightning-layout-item>  
       <lightning-button label="First" icon-name="utility:chevronleft" onclick={handleFirst}  
         disabled={showFirstButton}></lightning-button>  
     </lightning-layout-item>  
     <lightning-layout-item>  
       <lightning-button label="Previous" icon-name="utility:chevronleft" onclick={handlePrevious}  
         disabled={showFirstButton}></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={handleNext}  
         disabled={showLastButton}></lightning-button>  
     </lightning-layout-item>  
     <lightning-layout-item>  
       <lightning-button label="Last" icon-name="utility:chevronright" icon-position="right" onclick={handleLast}  
         disabled={showLastButton}></lightning-button>  
     </lightning-layout-item>  
   </lightning-layout>  
 </template>  

paginatorBottom.js

 import { LightningElement, api } from 'lwc';  
 export default class PaginatorBottom extends LightningElement {  
   // Api considered as a reactive public property.  
   @api totalrecords;  
   @api currentpage;  
   @api pagesize;  
   // Following are the private properties to a class.  
   lastpage = false;  
   firstpage = false;  
   // getter  
   get showFirstButton() {  
     if (this.currentpage === 1) {  
       return true;  
     }  
     return false;  
   }  
   // getter  
   get showLastButton() {  
     if (Math.ceil(this.totalrecords / this.pagesize) === this.currentpage) {  
       return true;  
     }  
     return false;  
   }  
   //Fire events based on the button actions  
   handlePrevious() {  
     this.dispatchEvent(new CustomEvent('previous'));  
   }  
   handleNext() {  
     this.dispatchEvent(new CustomEvent('next'));  
   }  
   handleFirst() {  
     this.dispatchEvent(new CustomEvent('first'));  
   }  
   handleLast() {  
     this.dispatchEvent(new CustomEvent('last'));  
   }  
 }  

  • Create a Lightning Web component called as a recordList. This component will fetch the records from the Apex class by calling the method. This component will give us information about the Total number of records, Total pages, Accounts data.
  • We can see that the recordList.js will have the chaining of the apex method calls(First record count method call then retrieve actual data method call). I intentionally added chaining to showcase how it works.
  • This component also has the Search functionality. It showcases the usage of standard renderedCallback function and how to avoid the recursive of infinite calls. Ex. isSearchChangeExecuted variable.


recordList.html

 <template>  
   <lightning-card title="Accounts List" icon-name="custom:custom63">  
     <div class="slds-m-around_medium">  
       <lightning-input type="search" onchange={handleKeyChange} class="slds-m-bottom_small" label="Search"  
         value={searchKey}></lightning-input>  
       <template if:true={accounts}>  
         <table  
           class="slds-table slds-table_bordered slds-table_striped slds-table_cell-buffer slds-table_fixed-layout">  
           <thead>  
             <tr class="slds-text-heading_label">  
               <th scope="col">  
                 <div class="slds-truncate" title="ID">ID</div>  
               </th>  
               <th scope="col">  
                 <div class="slds-truncate" title="Name">Name</div>  
               </th>  
             </tr>  
           </thead>  
           <tbody>  
             <!-- Use the Apex model and controller to fetch server side data -->  
             <template for:each={accounts} for:item="account">  
               <tr key={account.Id}>  
                 <th scope="row">  
                   <div class="slds-truncate" title={account.Id}>{account.Id}</div>  
                 </th>  
                 <td>  
                   <div class="slds-truncate" title={account.Name}>{account.Name}</div>  
                 </td>  
               </tr>  
             </template>  
           </tbody>  
         </table>  
       </template>  
     </div>  
     <p class="slds-m-vertical_medium content">Total records: <b>{totalrecords} </b> Page <b>{currentpage}</b> of  
       <b> {totalpages}</b></p>  
   </lightning-card>  
 </template>  

recordList.js

 import { LightningElement, track, api } from 'lwc';  
 import getAccountsList from '@salesforce/apex/ManageRecordsController.getAccountsList';  
 import getAccountsCount from '@salesforce/apex/ManageRecordsController.getAccountsCount';  
 export default class RecordList extends LightningElement {  
   @track accounts;  
   @track error;  
   @api currentpage;  
   @api pagesize;  
   @track searchKey;  
   totalpages;  
   localCurrentPage = null;  
   isSearchChangeExecuted = false;  
   // not yet implemented  
   pageSizeOptions =  
     [  
       { label: '5', value: 5 },  
       { label: '10', value: 10 },  
       { label: '25', value: 25 },  
       { label: '50', value: 50 },  
       { label: 'All', value: '' },  
     ];  
   handleKeyChange(event) {  
     if (this.searchKey !== event.target.value) {  
       this.isSearchChangeExecuted = false;  
       this.searchKey = event.target.value;  
       this.currentpage = 1;  
     }  
   }  
   renderedCallback() {  
     // This line added to avoid duplicate/multiple executions of this code.  
     if (this.isSearchChangeExecuted && (this.localCurrentPage === this.currentpage)) {  
       return;  
     }  
     this.isSearchChangeExecuted = true;  
     this.localCurrentPage = this.currentpage;  
     getAccountsCount({ searchString: this.searchKey })  
       .then(recordsCount => {  
         this.totalrecords = recordsCount;  
         if (recordsCount !== 0 && !isNaN(recordsCount)) {  
           this.totalpages = Math.ceil(recordsCount / this.pagesize);  
           getAccountsList({ pagenumber: this.currentpage, numberOfRecords: recordsCount, pageSize: this.pagesize, searchString: this.searchKey })  
             .then(accountList => {  
               this.accounts = accountList;  
               this.error = undefined;  
             })  
             .catch(error => {  
               this.error = error;  
               this.accounts = undefined;  
             });  
         } else {  
           this.accounts = [];  
           this.totalpages = 1;  
           this.totalrecords = 0;  
         }  
         const event = new CustomEvent('recordsload', {  
           detail: recordsCount  
         });  
         this.dispatchEvent(event);  
       })  
       .catch(error => {  
         this.error = error;  
         this.totalrecords = undefined;  
       });  
   }  
 }  
  • Now we will create a parent component which will include both these components.
  • This component will handle the events fired by buttons.

paginationParent.html

 <template>  
   <lightning-card>  
     <c-record-list currentpage={page} onrecordsload={handleRecordsLoad} pagesize={pagesize}></c-record-list>  
     <div class="slds-m-around_medium">  
       <c-paginator-bottom onprevious={handlePrevious} onnext={handleNext} onfirst={handleFirst}  
         onlast={handleLast} currentpage={page} totalrecords={totalrecords} pagesize={pagesize}>  
       </c-paginator-bottom>  
     </div>  
   </lightning-card>  
 </template>  

paginationParent.js


 import { LightningElement, track, api } from 'lwc';  
 const PAGE_SIZE = 5;  
 export default class PaginationParent extends LightningElement {  
   @api page = 1;  
   @api totalrecords;  
   @api _pagesize = PAGE_SIZE;  
   get pagesize() {  
     return this._pagesize;  
   }  
   set pagesize(value) {  
     this._pagesize = value;  
   }  
   handlePrevious() {  
     if (this.page > 1) {  
       this.page = this.page - 1;  
     }  
   }  
   handleNext() {  
     if (this.page < this.totalPages)  
       this.page = this.page + 1;  
   }  
   handleFirst() {  
     this.page = 1;  
   }  
   handleLast() {  
     this.page = this.totalPages;  
   }  
   handleRecordsLoad(event) {  
     this.totalrecords = event.detail;  
     this.totalPages = Math.ceil(this.totalrecords / this.pagesize);  
   }  
   handlePageChange(event) {  
     this.page = event.detail;  
   }  
 }  
Now finally create Apex class ManageRecordsController, which will give us the required information such as record count, account list.

Note: We have intentionally created separate methods for record count and account list, to showcase the chaining of the Apex method calls.


ManageRecordsController.cls

 public with sharing class ManageRecordsController {  
   @AuraEnabled(cacheable = true)  
   public static List<Account> getAccountsList(Integer pagenumber, Integer numberOfRecords, Integer pageSize, String searchString) {  
     String searchKey = '%' + searchString + '%';  
     String query = 'select id, Name from Account ';  
     if (searchString != null && searchString != '') {  
       query += ' where name like \'%' + searchString + '%\' ';  
     }  
     query += ' limit ' + pageSize + ' offset ' + (pageSize * (pagenumber - 1));  
     return Database.query(query);  
   }  
   @AuraEnabled(cacheable = true)  
   public static Integer getAccountsCount(String searchString) {  
     String query = 'select count() from Account ';  
     if (searchString != null && searchString != '') {  
       query += ' where name like \'%' + searchString + '%\' ';  
     }  
     return Database.countQuery(query);  
   }  
 }  
We are ready with our pagination Lightning Web Component implementation. You can include this component anywhere. 

Example

<template>
<c-pagination-bar></c-pagination-bar>
</template>


And don't forget to include following lines in the meta.xml file, in case you wanna expose this pagination on App/Record/Hope page.

 <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">  
 <apiVersion>45.0</apiVersion>  
 <isExposed>true</isExposed>  
 <targets>  
 <target>lightning__AppPage</target>  
 <target>lightning__RecordPage</target>  
 <target>lightning__HomePage</target>  
 </targets>  
 </LightningComponentBundle>  

I hope you enjoyed the learning, please write me back the suggestions, comments or any issues. Let's meet in our next blog with more learnings and fun. :)


THANK YOU.

23 comments:

  1. I am getting error while deploying ManageRecordsController.
    #Avoid long parameter listsapex pmd

    ReplyDelete
    Replies
    1. Thank you for your comment.

      Are you trying to run any additional tools on top of APEX, like pmd?

      Delete
    2. This page has an error. Please help me on this

      You might just need to refresh it. render threw an error in 'c:dspPaginationParent' [Maximum call stack size exceeded] Failing descriptor: {markup://c:dspPaginationParent}


      Delete
  2. Hi, Thanks for your code it help me lot. i am using lightning combobox and depending on combobox value datatable records vary and records divide by page...but i am facing one problem for combobox valu1 if are recodes 40 i divided it as 20 per page.when going from page 1 to 2 and click combobox for value2 from page 2 its shows me like total records 6
    page 2 of 1. For value2 records are 6 only. All Buttons also not get disabled.

    ReplyDelete
    Replies
    1. Thank you Satish for you comment. Can you please share the specific code you are working on, may be I can help you in it?

      Also I am planning to include/publish the Page Size feature in this Pagination blog.

      Delete
  3. Hi Amol,
    Thanks for your valuable response.I sort out my issue by refreshing pagination on selecting combobox value.Your code helps me lot.Also how i can make pagesize dynamic.

    ReplyDelete
  4. I am getting this error No MODULE named markup://c:paginatorBottom found : [markup://c:paginationParent]

    ReplyDelete
    Replies
    1. Can you please try creating a paginatorBottom component first and try saving it again? Thank you.

      Delete
  5. Thanks for the code. Very nice explanation. If I want to add the functionality of selection of rows to this pagination,how can we do that? It would be very useful.

    ReplyDelete
  6. Hello,
    Thanks a lot for your code sample I learned a lot.
    Still I don't understand why on paginatorBottom.js line 11 and 18 It's required to use the keyword get.
    Some help will be useful on this.
    Jonathan

    ReplyDelete
    Replies
    1. https://developer.salesforce.com/docs/component-library/documentation/en/48.0/lwc/lwc.js_props_getter

      Delete
  7. Im getting below error ,
    Please suggest some solution.
    No MODULE named markup://c:paginatorBottom found : [markup://c:record-List]

    ReplyDelete
    Replies
    1. Can you please try creating a paginatorBottom component first and try saving it again? Thank you.

      Delete
  8. Hi Anmol,
    Nice post. Helped me alot.
    Can we handle search at js side?

    ReplyDelete
  9. Is there a reason why you are creating a custom table instead of using Lightning-datatable component?

    ReplyDelete
    Replies
    1. Hey No reason as such, we could definitely leverage the Datatable here. Thank you for the observation tho.

      Delete
    2. does anything changes on the recordlist.js and ManageRecordsController.cls files, if we go lightning-datatable route in the mark up?

      Delete
    3. I think you will just need to pass columns(JSON) to datatable, so either you can create a wrapper class in ManageRecordsController and return it or use the existing SObject list to map it to datatable.

      Delete
    4. Is wrapper class necessary, can't I use the way you have implemented?

      Delete
  10. you'r amazing , it helps me alot
    but can you make us other tuto and show us how can we make the same datatable repsonsive on mobile and editable.
    thanks you ..

    ReplyDelete
  11. Offset is useful if less than 50000 records but if more than that it will fall to many soql queries. Do you have another idea?

    ReplyDelete
  12. Hi , In the place of search bar,how to have a customsearchlookup bar and get the results based on lookup selection in datatable?

    ReplyDelete

Salesforce: Export to Excel with Lightning Web Component

Hello Guys, I hope you are doing well. In this post, we are going to see an implementation of " Export to Excel" in lightn...