本來lightning-datatable這種標簽,基本上任何的項目都會用到而且很精通,所以當時感覺沒有太大的單獨一篇寫的必要,在Salesforce LWC學習(三十) lwc superbadge項目實現 中也有使用這個標簽的demo,所以有類似需要的小伙伴參考一下也可以照貓畫虎搞定需求。項目中遇見了兩個datatable的問題,解決以后感覺有必要寫一下,后期遇見這種坑的小伙伴可以快速對應。話不多說,先弄一個簡單的分頁效果的UI,UI很丑,旨在實現功能。
AccountListController.cls:一個簡單的搜索list返回
public without sharing class AccountListController { public static final Integer DEFAULT_PAGE_SIZE = 100; @AuraEnabled(cacheable=false) public static List<Account> fetchAccountList(String serializedAccount){ Account account = (Account)JSON.deserialize(serializedAccount,Account.class); String fetchAccountSQL = 'SELECT Id,Name,Industry,AccountSource,Owner.Name FROM Account WHERE IsDeleted = false '; if(String.isNotBlank(account.name)) { fetchAccountSQL += 'AND Name like' + '\'%' + account.name + '%\''; } if(String.isNotBlank(account.industry)) { fetchAccountSQL += ' AND industry = \'' + account.industry + '\''; } if(String.isNotBlank(account.AccountSource)) { fetchAccountSQL += ' AND AccountSource = \'' + account.AccountSource + '\''; } Integer accountSize = DEFAULT_PAGE_SIZE; fetchAccountSQL += ' LIMIT ' + accountSize; List<Account> accountList = Database.query(fetchAccountSQL); return accountList; } }
accountSearchForm.html
<template> <!--use LDS to bind account --> <lightning-record-edit-form object-api-name='Account' onsubmit={handleRecordFormSubmit} > <!--used to show section close/open--> <lightning-accordion allow-multiple-sections-open active-section-name={activeSections}> <lightning-accordion-section name="basic" label="basic"> <lightning-layout multiple-rows="true"> <lightning-layout-item padding="around-small" flexibility='auto' size='4'> <lightning-input-field field-name='Name'></lightning-input-field> </lightning-layout-item> <lightning-layout-item padding="around-small" flexibility='auto' size='4'> <lightning-input-field field-name="AccountSource"></lightning-input-field> </lightning-layout-item> <lightning-layout-item padding="around-small" flexibility='auto' size='4'> <lightning-input-field field-name="Industry"></lightning-input-field> </lightning-layout-item> </lightning-layout> </lightning-accordion-section> </lightning-accordion> <!-- button group area --> <lightning-layout horizontal-align="center"> <lightning-layout-item > <lightning-button variant="brand" label="search" type="submit" name="search"></lightning-button> </lightning-layout-item> </lightning-layout> </lightning-record-edit-form> </template>
accountSearchForm.js
import { LightningElement,track } from 'lwc';
export default class AccountSearchForm extends LightningElement {
@track activeSections = ['basic'];
@track account;
handleRecordFormSubmit(event) {
event.preventDefault();
this.account = event.detail;
this.dispatchEvent(new CustomEvent('searchaccount',{detail:this.account}));
}
}
myAccountList.html
<template> <lightning-card title="list for account data" icon-name="standard:account"> <div style="height: 150px;"> <lightning-datatable data={accountListForCurrentPage} columns={columns} show-row-number-column key-field="id"> </lightning-datatable> </div> <lightning-layout horizontal-align="center"> <lightning-layout-item flexibility='auto' size='1'> <lightning-combobox value={perSize} options={entryList} placeholder="choose size" onchange={handlePerSizeChange} > </lightning-combobox> </lightning-layout-item> <lightning-layout-item flexibility='auto' size='9' class="slds-text-align_center"> totalSize: {totalSize} </lightning-layout-item> <lightning-layout-item flexibility="auto" size="2"> <lightning-button variant="brand" label="<--" disabled={isFirstPage} onclick={handlePreviousPageClick}></lightning-button> <lightning-button variant="brand" label="-->" disabled={isLastPage} title="next" onclick={handleNextPageClick}></lightning-button> </lightning-layout-item> </lightning-layout> </lightning-card> </template>
myAccountList.js
import { LightningElement,api, track } from 'lwc';
const columns = [
{label: 'Account Name', fieldName: 'Name'},
{label: 'Account Industry', fieldName: 'Industry'},
{label: 'Account Source', fieldName: 'AccountSource'},
{label: 'Owner Name', fieldName: 'OwnerName'}
];
export default class MyAccountList extends LightningElement {
//searched account list used to show in component table
@api accountList;
//indicator if table header checkbox check or not, true for check
@api totalChecked;
//list total entryList
@api totalSize;
//per size for show in table
@api perSize;
//current page index
@api currentPageIndex;
//list show in table
@api accountListForCurrentPage;
//indicator if current page is first page
@api isFirstPage;
//indicator if current page is last page
@api isLastPage;
@track columns = columns;
get entryList() {
return [
{ label: '3', value: '3' },
{ label: '5', value: '5' },
{ label: '10', value: '10' },
];
}
/**
* description: item checkbox check/uncheck, dispatch itemcheck custom event and parent component handle this
* @param event system event,used to get which item check/uncheck
*/
handleItemCheckboxClick(event) {
this.dispatchEvent(new CustomEvent('itemcheck',{detail:{id:event.currentTarget.value,checked:event.currentTarget.checked}}));
}
/**
* description: table header checkbox check/check, dispatch allcheck custom event
* @param event system event, used to get if table header checkbox check/uncheck
*/
handleAllCheckboxClick(event) {
this.dispatchEvent(new CustomEvent('allcheck',{detail:{checked:event.currentTarget.checked}}));
}
handleNextPageClick() {
this.dispatchEvent(new CustomEvent('nextpage'));
}
handlePreviousPageClick() {
this.dispatchEvent(new CustomEvent('previouspage'));
}
handlePerSizeChange(event) {
let currentSize = event.detail.value;
this.dispatchEvent(new CustomEvent('persizechange',{detail:{currentSize:currentSize}}));
}
}
accountListContainer.html
<template> <c-account-search-form onsearchaccount={handleSearchAccountEvent}></c-account-search-form> <c-my-account-list is-first-page={isFirstPage} is-last-page={isLastPage} account-list={accountList} account-list-for-current-page={accountListForCurrentPage} total-checked={totalChecked} onitemcheck={handleItemCheckEvent} onallcheck={handleAllCheckedEvent} onnextpage={handleNextPageEvent} onpreviouspage={handlePreviousPageEvent} total-size={totalSize} per-size={perSize} onpersizechange={handlePerSizeChangeEvent}></c-my-account-list> </template>
accountListContainer.js
import { LightningElement,track } from 'lwc';
import fetchAccountList from '@salesforce/apex/AccountListController.fetchAccountList';
export default class AccountListContainer extends LightningElement {
//account list used to show in account table
@track accountList = [];
//account form used to store form information user searched
@track accountForm;
//errors when fetch account list error
@track errors;
//indicator if table header total check box checked or not, true means checked
@track totalChecked = false;
//indicator if list modal close
@track showSelectedListModal = false;
@track data = [];
//list total size
@track totalSize;
//per size for show in table
@track perSize = 5;
//current page index
@track currentPageIndex;
//list show in table
@track accountListForCurrentPage;
@track isFirstPage = true;
@track isLastPage = true;
/**
* description: search account data by form information and set the result to account list to show in table
* @param event system event used to get form detail information
*/
handleSearchAccountEvent(event) {
this.totalChecked = false;
this.accountForm = event.detail;
fetchAccountList({serializedAccount:JSON.stringify(event.detail)})
.then(result => {
this.accountList = result;
this.totalSize = result.length;
this.currentPageIndex = 1;
if(this.totalSize > this.perSize * this.currentPageIndex) {
this.setPagination();
} else {
this.accountListForCurrentPage = this.accountList;
this.accountListForCurrentPage.forEach(item => {
if(item.Owner) {
item.OwnerName = item.Owner.Name;
}
});
this.isLastPage = true;
}
this.errors = undefined;
})
.catch(error =>{
this.errors = error;
this.accountList = undefined;
});
}
handlePreviousPageEvent() {
this.currentPageIndex = this.currentPageIndex - 1;
this.setPagination();
}
handlePerSizeChangeEvent(event) {
this.perSize = event.detail.currentSize;
this.currentPageIndex = 1;
this.setPagination();
}
handleNextPageEvent() {
this.currentPageIndex = this.currentPageIndex + 1;
this.setPagination();
}
setPagination() {
this.accountListForCurrentPage = [];
let tmpList = [];
for(let index = (this.perSize * (this.currentPageIndex - 1)); index < this.totalSize; index++) {
if(index < this.perSize * this.currentPageIndex) {
if(this.accountList[index].Owner) {
this.accountList[index].OwnerName = this.accountList[index].Owner.Name;
}
tmpList.push(this.accountList[index]);
}
}
this.accountListForCurrentPage = tmpList;
if(this.currentPageIndex === 1) {
this.isFirstPage = true;
} else {
this.isFirstPage = false;
}
if(this.perSize * this.currentPageIndex >= this.totalSize) {
this.isLastPage = true;
} else {
this.isLastPage = false;
}
}
}
結果展示:
看上去還可以是吧,但是有兩個潛在的問題。
按照以下操作步驟,第一頁有拖動條,選擇了第5條數據

點擊翻頁以后,兩個問題如下:
1)第二頁的第五條同樣的默認勾選了,盡管這個時候我們如果使用event.detail.selectedRows去獲取選中的選項,第二頁是沒有在這里的,但是UI撒謊了,這個是一個很嚴重的bug;
2)第二頁進去以后,滾動條沒有在最上面,即沒有默認展示第一條數據。

這兩個問題,第二個問題是小問題,可以忍;第一個問題會讓用戶有concern,屬於很嚴重的問題。但是什么原因呢???其實我也不太清楚是什么原因,datatable官方的設計中也沒有翻頁的demo,大部分都是loadMore當頁增加數據場景,所以可能針對每頁的index處選中效果有某個隱藏的bug。當我們盡管更新了list的數據,但是index給渲染了,很容易是render層的bug。所以我們想一下如何去處理這種問題。既然同步的渲染有問題,我們考慮其他方式,setTimeout弄成異步調用或者改成Promise實現。優化后的方案如下所示:
myAccountList.js新增一個方法
@api handleTableScrollTop() { this.template.querySelector('div').scrollTop = 0; }
accountListContainer.js修改一下 setPagination方法。新增了 setList這個Promise,js執行順序 : 同步代碼 > Promise > setTimeout這種異步方式。
setPagination() { this.accountListForCurrentPage = []; let tmpList = []; for(let index = (this.perSize * (this.currentPageIndex - 1)); index < this.totalSize; index++) { if(index < this.perSize * this.currentPageIndex) { if(this.accountList[index].Owner) { this.accountList[index].OwnerName = this.accountList[index].Owner.Name; } tmpList.push(this.accountList[index]); } } //this.accountListForCurrentPage = tmpList; if(this.currentPageIndex === 1) { this.isFirstPage = true; } else { this.isFirstPage = false; } if(this.perSize * this.currentPageIndex >= this.totalSize) { this.isLastPage = true; } else { this.isLastPage = false; } const setList = () => new Promise((resolve, reject) => { resolve(tmpList); }); setList() .then((result) => { this.accountListForCurrentPage = tmpList; this.template.querySelector('c-my-account-list').handleTableScrollTop(); }); }
通過以上的代碼就可以解決上述的兩個問題了。原理的話因為不清楚 datatable的渲染方式,只能找到解決這種問題的workaround的方式,同時作為sf開發人員在開發lightning的過程中,javascript真的是越來越重要了!!!
總結:篇中代碼實現了通過 lightning-datatable翻頁效果以及針對兩個潛在的bug的修復。偏中有錯誤歡迎指出,有不懂歡迎留言。有更好方式歡迎交流。
