
Salesforce Spring ’26 Apex Cursors give us a server-side pointer over SOQL results – without relying on OFFSET or forcing Batch Apex patterns. For UI lists, the PaginationCursor for stable paging either infinite scroll or load more and it works cleanly with LWC because cursors support @AuraEnabled serialisation so the client can hold cursor state and send it. back for the next page.
Cursor vs PaginationCursor:
- Cursor – up to 50M rows across all Apex cursors per transaction, 10,000 cursors per day
- PaginationCursor – UI concurrency + consistent pages: 100K records, 2000 rows max per page – 200k instances per day.
What that means: Cursor is for deep service-side processing; PaginationCursor is for lots of users paging smoothly through lists without “weird” page sizes.
Also, you can offer “Export CSV” directly in the UI while still staying within limits by repeatedly fetching pages via PaginationCursor and appending rows into a CSV string so the heavy work happens in the browser.


In the example below, We’ll load 50k Account records in a paged chunks (10,200,300 per fetch), keeping the browser fast while still allowing deep navigation.
- Create a PaginationCursor from a deterministic SOQL.
- Fetch pages using fetchpage(startIndex,pageSize)
- Return {records, nextIndex,cursor) to LWC
- LWC appends rows and request next page on scrolls
- Download CSV button to skim through 50k records into CSV
Generate 50K records Batch:
public without sharing class AccountDataGeneratorBatch implements Database.Batchable<Integer>, Database.Stateful { private final Integer totalRecords; private Integer successCount = 0; private Integer errorCount = 0; private List<String> errorSamples = new List<String>(); private static final List<String> ADJECTIVES = new List<String>{ 'Summit','Pioneer','Nimbus','Evergreen','Blue','Bright','Golden','Silver','Clear','Rapid', 'Liberty','Crest','Oak','River','Canyon','Sun','Lunar','Stellar','Aurora','Vertex', 'Iron','Apex','Atlas','Harbor','Beacon','Sierra','Redwood','Stone','Sky','Noble' }; private static final List<String> NOUNS = new List<String>{ 'Solutions','Dynamics','Systems','Holdings','Ventures','Labs','Group','Partners','Networks','Works', 'Logistics','Energy','Healthcare','Foods','Capital','Industries','Retail','Digital','Analytics','Manufacturing', 'Resources','Aerospace','Media','Communications','Engineering','Software','Consulting','Finance','Supply','Services' }; private static final List<String> SUFFIXES = new List<String>{ 'Inc','LLC','Ltd','Co','Corp','Group','PLC','GmbH' }; private static final List<String> STREET_NAMES = new List<String>{ 'Maple','Oak','Pine','Cedar','Elm','Lake','Hill','Park','View','Washington', 'Madison','Jackson','Franklin','Lincoln','Adams','Jefferson','Main','Highland','Sunset','Ridge' }; private static final List<String> STREET_SUFFIXES = new List<String>{ 'St','Ave','Blvd','Rd','Ln','Way','Dr','Ct','Pl','Terrace' }; private static final List<String> CITIES = new List<String>{ 'Austin','Denver','Seattle','Boston','Phoenix','Dallas','Atlanta','Chicago','San Diego','Nashville', 'Portland','Raleigh','Tampa','Orlando','Columbus','Cleveland','Charlotte','San Jose','Detroit','Sacramento' }; private static final List<String> STATES = new List<String>{ 'CA','NY','TX','FL','WA','IL','GA','NC','CO','AZ','MA','OH','MI','OR','TN','PA','NJ','VA','MN','MO' }; private static final List<String> SITES = new List<String>{ 'HQ','West','East','North','South','Central' }; private static Map<Schema.SObjectField, List<String>> picklistCache = new Map<Schema.SObjectField, List<String>>(); public AccountDataGeneratorBatch(Integer totalRecords) { this.totalRecords = totalRecords == null || totalRecords <= 0 ? 0 : totalRecords; } public Iterable<Integer> start(Database.BatchableContext bc) { return new AccountCounterIterable(totalRecords); } public void execute(Database.BatchableContext bc, List<Integer> scope) { if (scope.isEmpty()) { return; } List<Account> accounts = new List<Account>(); for (Integer ignored : scope) { accounts.add(buildAccount()); } Database.SaveResult[] results = Database.insert(accounts, false); for (Database.SaveResult sr : results) { if (sr.isSuccess()) { successCount++; } else { errorCount++; if (errorSamples.size() < 5) { errorSamples.add(sr.getErrors()[0].getMessage()); } } } } public void finish(Database.BatchableContext bc) { System.debug('AccountDataGeneratorBatch finished. Success=' + successCount + ', Errors=' + errorCount); if (!errorSamples.isEmpty()) { System.debug('Sample errors: ' + String.join(errorSamples, ' | ')); } } private static Account buildAccount() { String adjective = randomFrom(ADJECTIVES); String noun = randomFrom(NOUNS); String suffix = randomFrom(SUFFIXES); String name = adjective + ' ' + noun + ' ' + suffix; String city = randomFrom(CITIES); String state = randomFrom(STATES); String street = String.valueOf(randInt(100, 9999)) + ' ' + randomFrom(STREET_NAMES) + ' ' + randomFrom(STREET_SUFFIXES); String postal = String.valueOf(randInt(10000, 99999)); String website = 'https://www.' + (adjective + noun).toLowerCase() + '.com'; Account acct = new Account(); acct.Name = name; acct.Type = randomPicklistValue(Account.Fields.Type); acct.Industry = randomPicklistValue(Account.Fields.Industry); acct.Rating = randomPicklistValue(Account.Fields.Rating); acct.Ownership = randomPicklistValue(Account.Fields.Ownership); acct.AccountSource = randomPicklistValue(Account.Fields.AccountSource); acct.Phone = randomPhone(); acct.Fax = randomPhone(); acct.Website = website; acct.NumberOfEmployees = randInt(5, 20000); acct.AnnualRevenue = randInt(50000, 1000000000); acct.BillingStreet = street; acct.BillingCity = city; acct.BillingState = state; acct.BillingPostalCode = postal; acct.BillingCountry = 'United States'; acct.ShippingStreet = street; acct.ShippingCity = city; acct.ShippingState = state; acct.ShippingPostalCode = postal; acct.ShippingCountry = 'United States'; acct.Description = 'Generated account data for testing and analytics.'; acct.AccountNumber = String.valueOf(randInt(10000000, 99999999)); acct.Sic = String.valueOf(randInt(1000, 9999)); acct.TickerSymbol = randomTicker(randInt(3, 4)); acct.Site = randomFrom(SITES); acct.Created_From_Batch__c = true; return acct; } private static String randomFrom(List<String> values) { if (values == null || values.isEmpty()) { return null; } return values[randInt(0, values.size() - 1)]; } private static Integer randInt(Integer min, Integer max) { if (min == null || max == null || max < min) { return min; } Integer range = max - min + 1; Integer random = Math.abs(Crypto.getRandomInteger()); Integer offset = Math.mod(random, range); return min + offset; } private static String randomPhone() { Integer area = randInt(200, 999); Integer prefix = randInt(200, 999); Integer line = randInt(0, 9999); return String.valueOf(area) + '-' + String.valueOf(prefix) + '-' + String.valueOf(line).leftPad(4, '0'); } private static String randomTicker(Integer len) { Integer length = len == null || len <= 0 ? 3 : len; String letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; String out = ''; for (Integer i = 0; i < length; i++) { Integer idx = randInt(0, letters.length() - 1); out += letters.substring(idx, idx + 1); } return out; } private static String randomPicklistValue(Schema.SObjectField field) { if (field == null) { return null; } List<String> values = picklistCache.get(field); if (values == null) { values = new List<String>(); for (Schema.PicklistEntry entry : field.getDescribe().getPicklistValues()) { if (entry.isActive()) { values.add(entry.getValue()); } } picklistCache.put(field, values); } if (values.isEmpty()) { return null; } return values[randInt(0, values.size() - 1)]; } public class AccountCounterIterable implements Iterable<Integer> { private final Integer total; public AccountCounterIterable(Integer total) { this.total = total == null || total < 0 ? 0 : total; } public Iterator<Integer> iterator() { return new AccountCounterIterator(total); } } public class AccountCounterIterator implements Iterator<Integer> { private final Integer total; private Integer index = 0; public AccountCounterIterator(Integer total) { this.total = total == null || total < 0 ? 0 : total; } public Boolean hasNext() { return index < total; } public Integer next() { return index++; } }}
PageCursorContronller:
public with sharing class PageCursorController { private static final Integer DEFAULT_PAGE_SIZE = 10; private static final Integer MAX_PAGE_SIZE = 2000; public class PersonRow { @AuraEnabled public Id id; @AuraEnabled public String name; @AuraEnabled public String phone; @AuraEnabled public String website; @AuraEnabled public String type; @AuraEnabled public String industry; @AuraEnabled public String billingCity; @AuraEnabled public String billingCountry; @AuraEnabled public Datetime createdDate; public PersonRow(Account account) { id = account.Id; name = account.Name; phone = account.Phone; website = account.Website; type = account.Type; industry = account.Industry; billingCity = account.BillingCity; billingCountry = account.BillingCountry; createdDate = account.CreatedDate; } } public class CursorPageResult { @AuraEnabled public Database.PaginationCursor cursor; @AuraEnabled public List<PersonRow> records; @AuraEnabled public Integer totalRecords; @AuraEnabled public Integer currentPage; @AuraEnabled public Integer totalPages; @AuraEnabled public Integer startIndex; @AuraEnabled public Integer pageSize; @AuraEnabled public Integer nextIndex; @AuraEnabled public Integer deletedRows; @AuraEnabled public Boolean hasMore; } @AuraEnabled public static CursorPageResult getPersonPageWithCursor(Database.PaginationCursor cursor, Integer pageNumber, Integer pageSize) { system.debug(cursor + ' Cursorrrrrrrrr ===== '); Integer safeSize = normalizePageSize(pageSize); Integer safePage = normalizePageNumber(pageNumber); Database.PaginationCursor effectiveCursor = getOrCreateCursor(cursor); Integer totalRecords = effectiveCursor.getNumRecords(); Integer totalPages = calcTotalPages(totalRecords, safeSize); if (totalPages > 0 && safePage > totalPages) { safePage = totalPages; } Integer startIndex = (safePage - 1) * safeSize; return fetchPageByIndex(effectiveCursor, startIndex, safeSize, totalRecords, safePage, totalPages); } @AuraEnabled public static CursorPageResult getPersonPageWithCursorByIndex(Database.PaginationCursor cursor, Integer startIndex, Integer pageSize) { system.debug(cursor + ' Cursorrrrrrrrr ===== '); Integer safeStart = normalizeStartIndex(startIndex); Integer safeSize = normalizePageSize(pageSize); Database.PaginationCursor effectiveCursor = getOrCreateCursor(cursor); Integer totalRecords = effectiveCursor.getNumRecords(); Integer totalPages = calcTotalPages(totalRecords, safeSize); Integer currentPage = totalRecords == 0 ? 0 : (Integer)Math.floor((Decimal)safeStart / safeSize) + 1; return fetchPageByIndex(effectiveCursor, safeStart, safeSize, totalRecords, currentPage, totalPages); } private static CursorPageResult fetchPageByIndex( Database.PaginationCursor cursor, Integer startIndex, Integer pageSize, Integer totalRecords, Integer currentPage, Integer totalPages ) { Integer safeStart = normalizeStartIndex(startIndex); Integer safeSize = normalizePageSize(pageSize); System.debug('PageCursorController fetch startIndex=' + safeStart + ', pageSize=' + safeSize + ', totalRecords=' + totalRecords + ', currentPage=' + currentPage + ', totalPages=' + totalPages); if (safeStart >= totalRecords) { return buildEmptyResult(cursor, totalRecords, currentPage, totalPages, safeStart, safeSize); } Integer remaining = totalRecords - safeStart; Integer effectiveSize = remaining < safeSize ? remaining : safeSize; if (effectiveSize <= 0) { return buildEmptyResult(cursor, totalRecords, currentPage, totalPages, safeStart, safeSize); } Database.CursorFetchResult fetchResult = cursor.fetchPage(safeStart, effectiveSize); List<PersonRow> rows = buildRows((List<Account>) fetchResult.getRecords()); CursorPageResult result = new CursorPageResult(); result.cursor = cursor; result.records = rows; result.totalRecords = totalRecords; result.currentPage = totalPages == 0 ? 0 : currentPage; result.totalPages = totalPages; result.startIndex = safeStart; result.pageSize = safeSize; Integer rawNextIndex = fetchResult.getNextIndex(); result.nextIndex = rawNextIndex != null && rawNextIndex > totalRecords ? totalRecords : rawNextIndex; result.deletedRows = fetchResult.getNumDeletedRecords(); result.hasMore = rows != null && !rows.isEmpty() && result.nextIndex != null && result.nextIndex < totalRecords; System.debug('PageCursorController fetch nextIndex=' + result.nextIndex + ', deletedRows=' + result.deletedRows + ', hasMore=' + result.hasMore); return result; } private static CursorPageResult buildEmptyResult( Database.PaginationCursor cursor, Integer totalRecords, Integer currentPage, Integer totalPages, Integer startIndex, Integer pageSize ) { CursorPageResult emptyResult = new CursorPageResult(); emptyResult.cursor = cursor; emptyResult.records = new List<PersonRow>(); emptyResult.totalRecords = totalRecords; emptyResult.currentPage = totalPages == 0 ? 0 : currentPage; emptyResult.totalPages = totalPages; emptyResult.startIndex = startIndex; emptyResult.pageSize = pageSize; emptyResult.nextIndex = totalRecords; emptyResult.deletedRows = 0; emptyResult.hasMore = false; System.debug('PageCursorController fetch empty nextIndex=' + emptyResult.nextIndex + ', totalRecords=' + totalRecords); return emptyResult; } private static Integer normalizePageSize(Integer pageSize) { Integer safeSize = pageSize == null || pageSize <= 0 ? DEFAULT_PAGE_SIZE : pageSize; if (safeSize > MAX_PAGE_SIZE) { safeSize = MAX_PAGE_SIZE; } return safeSize; } private static Integer normalizeStartIndex(Integer startIndex) { return startIndex == null || startIndex < 0 ? 0 : startIndex; } private static Integer normalizePageNumber(Integer pageNumber) { return pageNumber == null || pageNumber < 1 ? 1 : pageNumber; } private static Integer calcTotalPages(Integer totalRecords, Integer pageSize) { if (totalRecords == null || totalRecords == 0) { return 0; } return (Integer)Math.ceil((Decimal)totalRecords / pageSize); } private static Database.PaginationCursor getOrCreateCursor(Database.PaginationCursor cursor) { if (cursor != null) { System.debug('Using existing cursor with ' + cursor.getNumRecords() + ' records.'); return cursor; } return Database.getPaginationCursor(buildPersonSoql()); } private static String buildPersonSoql() { return 'SELECT Id, Name, Phone, Website, Type, Industry, BillingCity, BillingCountry, CreatedDate ' + 'FROM Account ' + 'ORDER BY Name ASC, CreatedDate DESC'; } private static List<PersonRow> buildRows(List<Account> accounts) { List<PersonRow> rows = new List<PersonRow>(); if (accounts == null) { return rows; } for (Account account : accounts) { rows.add(new PersonRow(account)); } return rows; }}
PersonTablePagination:
css:
.range-label { display: inline-block; padding-top: 0.25rem;}.toolbar { gap: 0.75rem;}.toolbar-actions { display: inline-flex; align-items: center; gap: 0.75rem; justify-content: flex-end; width: 100%;}.export-status { display: block;}.table-panel { border: 1px solid #e5e5e5; border-radius: 0.25rem; background: #ffffff; overflow: hidden; height: 520px;}.table-panel lightning-datatable { height: 100%;}.pagination { padding: 0.5rem 0;}.pagination-panel { background: #f3f6f9; border: 1px solid #d8dde6; border-radius: 0.5rem; padding: 0.5rem 0.75rem; gap: 0.5rem;}.pagination-controls { align-items: center;}.page-control { --slds-c-button-neutral-color-background: #ffffff; --slds-c-button-neutral-color-border: #d8dde6; --slds-c-button-neutral-text-color: #1b2638; --slds-c-button-neutral-color-background-hover: #eef2f7; --slds-c-button-neutral-color-border-hover: #c9d2e0; --slds-c-button-brand-color-background: #0176d3; --slds-c-button-brand-color-border: #0176d3; --slds-c-button-brand-text-color: #ffffff;}.pagination .ellipsis { display: inline-flex; align-items: center; padding: 0.25rem 0.5rem; color: #6b6d70;}
HTML:
<template> <lightning-card title="Person Accounts (Paged)"> <div class="slds-p-horizontal_medium slds-p-top_small"> <div class="toolbar slds-grid slds-wrap slds-grid_vertical-align-center slds-var-m-bottom_small"> <div class="slds-col slds-size_1-of-1 slds-medium-size_4-of-12 slds-var-m-bottom_x-small"> <lightning-combobox label="Rows per page" value={pageSizeValue} options={pageSizeOptions} onchange={handlePageSizeChange} ></lightning-combobox> </div> <div class="slds-col slds-size_1-of-1 slds-medium-size_8-of-12 slds-text-align_right"> <div class="toolbar-actions"> <lightning-badge label={rangeLabel}></lightning-badge> <lightning-button label="Download CSV" variant="brand" icon-name="utility:download" onclick={handleExport} disabled={isExporting} ></lightning-button> </div> </div> </div> <template if:true={errorMessage}> <div class="slds-text-color_error slds-var-m-bottom_small">{errorMessage}</div> </template> <template if:true={showExportStatus}> <div class="export-status slds-text-color_weak slds-var-m-bottom_small"> {exportMessage} </div> </template> <div class="table-panel"> <lightning-datatable key-field="id" data={rows} columns={columns} show-row-number-column hide-checkbox-column is-loading={isLoading} ></lightning-datatable> </div> <template if:true={showPagination}> <div class="pagination slds-m-top_small"> <div class="pagination-panel slds-grid slds-wrap slds-grid_vertical-align-center slds-align_absolute-center"> <div class="pagination-info slds-text-color_weak slds-m-right_small"> Page {currentPage} of {totalPages} </div> <div class="pagination-controls slds-button-group" role="group"> <lightning-button label="First" variant="neutral" onclick={handleFirst} disabled={disablePrev} class="page-control" ></lightning-button> <lightning-button label="Prev" variant="neutral" onclick={handlePrev} disabled={disablePrev} class="page-control" ></lightning-button> <template for:each={pageItems} for:item="item"> <template if:true={item.isEllipsis}> <span key={item.key} class="ellipsis">…</span> </template> <template if:false={item.isEllipsis}> <lightning-button key={item.key} label={item.label} data-page={item.page} variant={item.variant} onclick={handlePageClick} class="page-control" ></lightning-button> </template> </template> <lightning-button label="Next" variant="neutral" onclick={handleNext} disabled={disableNext} class="page-control" ></lightning-button> <lightning-button label="Last" variant="neutral" onclick={handleLast} disabled={disableNext} class="page-control" ></lightning-button> </div> </div> </div> </template> </div> </lightning-card></template>
JS
import { LightningElement } from 'lwc';import getPersonPageWithCursor from '@salesforce/apex/PageCursorController.getPersonPageWithCursor';const PAGE_SIZE_OPTIONS = [ { label: '10', value: '10' }, { label: '200', value: '200' }, { label: '300', value: '300' }];const COLUMNS = [ { label: 'Name', fieldName: 'name', type: 'text', wrapText: true }, { label: 'Phone', fieldName: 'phone', type: 'phone' }, { label: 'Website', fieldName: 'website', type: 'url' }, { label: 'Type', fieldName: 'type', type: 'text' }, { label: 'Industry', fieldName: 'industry', type: 'text' }, { label: 'Billing City', fieldName: 'billingCity', type: 'text' }, { label: 'Billing Country', fieldName: 'billingCountry', type: 'text' }, { label: 'Created', fieldName: 'createdDate', type: 'date' }];const EXPORT_PAGE_SIZE = 2000;const CSV_FIELDS = [ { label: 'Id', fieldName: 'id' }, { label: 'Name', fieldName: 'name' }, { label: 'Phone', fieldName: 'phone' }, { label: 'Website', fieldName: 'website' }, { label: 'Type', fieldName: 'type' }, { label: 'Industry', fieldName: 'industry' }, { label: 'Billing City', fieldName: 'billingCity' }, { label: 'Billing Country', fieldName: 'billingCountry' }, { label: 'Created', fieldName: 'createdDate' }];export default class PersonTablePagination extends LightningElement { rows = []; errorMessage; exportMessage; isLoading = false; isExporting = false; pageSize = 10; totalRecords = 0; currentPage = 1; nextIndex = 0; cursor; columns = COLUMNS; pageSizeOptions = PAGE_SIZE_OPTIONS; get pageSizeValue() { return String(this.pageSize); } connectedCallback() { this.loadPage(1); } get totalPages() { if (this.totalRecords === 0) { return 0; } return Math.ceil(this.totalRecords / this.pageSize); } get disablePrev() { return this.currentPage <= 1 || this.totalPages === 0; } get disableNext() { return this.totalPages === 0 || this.currentPage >= this.totalPages; } get showPagination() { return this.totalPages > 1; } get showExportStatus() { return this.isExporting || this.exportMessage; } get rangeLabel() { if (this.totalRecords === 0) { return 'No records'; } if (!this.rows || this.rows.length === 0) { return `Showing 0 of ${this.totalRecords}`; } const start = (this.currentPage - 1) * this.pageSize + 1; const end = Math.min(start + this.rows.length - 1, this.totalRecords); return `Showing ${start}-${end} of ${this.totalRecords}`; } get pageItems() { const total = this.totalPages; if (total <= 1) { return []; } const current = this.currentPage; const items = []; const addPage = page => { items.push({ key: `page-${page}`, label: `${page}`, page, isEllipsis: false, variant: page === current ? 'brand' : 'neutral' }); }; const addEllipsis = key => { items.push({ key, isEllipsis: true }); }; if (total <= 9) { for (let i = 1; i <= total; i += 1) { addPage(i); } return items; } addPage(1); let start = Math.max(2, current - 2); let end = Math.min(total - 1, current + 2); if (start > 2) { addEllipsis('ellipsis-left'); } for (let i = start; i <= end; i += 1) { addPage(i); } if (end < total - 1) { addEllipsis('ellipsis-right'); } addPage(total); return items; } async loadPage(pageNumber) { const targetPage = Math.max(1, pageNumber || 1); this.isLoading = true; this.errorMessage = undefined; try { const result = await getPersonPageWithCursor({ cursor: this.cursor, pageNumber: targetPage, pageSize: this.pageSize }); this.rows = result?.records || []; this.totalRecords = result?.totalRecords || 0; this.nextIndex = result?.nextIndex; this.currentPage = result?.currentPage || targetPage; this.cursor = result?.cursor || this.cursor; } catch (error) { this.errorMessage = this.reduceError(error); this.rows = []; this.totalRecords = 0; } finally { this.isLoading = false; } } async handleExport() { this.exportMessage = 'Starting export...'; this.isExporting = true; try { const csv = await this.buildCsvExport(); if (!csv) { this.exportMessage = 'No records to export.'; return; } this.downloadCsv(csv); this.exportMessage = 'Export complete.'; } catch (error) { this.exportMessage = this.reduceError(error); } finally { this.isExporting = false; } } async buildCsvExport() { const lines = [this.buildCsvHeader()]; let exportCursor; let pageNumber = 1; let totalRecords; let fetched = 0; while (true) { // eslint-disable-next-line no-await-in-loop const result = await getPersonPageWithCursor({ cursor: exportCursor, pageNumber, pageSize: EXPORT_PAGE_SIZE }); exportCursor = result?.cursor || exportCursor; const records = result?.records || []; if (totalRecords === undefined && result && typeof result.totalRecords === 'number') { totalRecords = result.totalRecords; } let pageRecords = records; if (totalRecords !== undefined) { const remaining = totalRecords - fetched; if (remaining <= 0) { break; } if (records.length > remaining) { pageRecords = records.slice(0, remaining); } } for (const record of pageRecords) { lines.push(this.buildCsvRow(record)); } fetched += pageRecords.length; if (totalRecords && fetched <= totalRecords) { this.exportMessage = `Exporting ${fetched} of ${totalRecords}...`; } else { this.exportMessage = `Exporting ${fetched} records...`; } if (!result?.hasMore || records.length === 0) { break; } if (totalRecords !== undefined && fetched >= totalRecords) { break; } pageNumber += 1; } if (lines.length <= 1) { return ''; } return lines.join('\n'); } buildCsvHeader() { return CSV_FIELDS.map(field => this.escapeCsv(field.label)).join(','); } buildCsvRow(record) { return CSV_FIELDS.map(field => this.escapeCsv(record?.[field.fieldName])).join(','); } escapeCsv(value) { if (value === null || value === undefined) { return ''; } let text = String(value); text = text.replace(/"/g, '""'); if (/[",\n\r]/.test(text)) { return `"${text}"`; } return text; } downloadCsv(csv) { const csvWithBom = `\uFEFF${csv}`; const encoded = encodeURIComponent(csvWithBom); const url = `data:text/csv;charset=utf-8,${encoded}`; const link = document.createElement('a'); link.href = url; link.download = this.buildExportFilename(); link.target = '_self'; document.body.appendChild(link); link.click(); link.remove(); } buildExportFilename() { const dateStamp = new Date().toISOString().slice(0, 10); return `person-export-${dateStamp}.csv`; } handlePageSizeChange(event) { this.pageSize = parseInt(event.detail.value, 10); this.currentPage = 1; this.cursor = undefined; this.loadPage(1); } handlePageClick(event) { const page = parseInt(event.target.dataset.page, 10); if (Number.isNaN(page)) { return; } this.loadPage(page); } handlePrev() { if (this.disablePrev) { return; } this.loadPage(this.currentPage - 1); } handleNext() { if (this.disableNext) { return; } this.loadPage(this.currentPage + 1); } handleFirst() { if (this.disablePrev) { return; } this.cursor = undefined; this.loadPage(1); } handleLast() { if (this.disableNext) { return; } this.cursor = undefined; this.loadPage(this.totalPages); } reduceError(error) { if (error && error.body && error.body.message) { return error.body.message; } if (error && error.message) { return error.message; } return 'Unexpected error.'; }}
XML
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata"> <apiVersion>66.0</apiVersion> <isExposed>true</isExposed> <targets> <target>lightning__AppPage</target> <target>lightning__HomePage</target> <target>lightning__RecordPage</target> </targets></LightningComponentBundle>