LWC Datatable with Bulk Delete & Export to CSV

Key Features
  1. Bulk delete
  2. Export selected records to CSV
  3. Revert delete action if record is associate with another records
Technical Highlights
  1. Apex Method to delete record 
Demo
Code Files
Apex Controller
public with sharing class BulkDeleteController {

    @AuraEnabled(cacheable=true)
    public static List<sObject> getRecords(String objectName) {
        String query = 'SELECT Id, Name, AccountNumber, Rating, CreatedDate FROM ' + objectName +' limit 200';

        return  Database.query(query);
    }

    @AuraEnabled(cacheable=true)
    public static List<sObject> getRecordsForLstIds(List<String> recordIds, String objectName) {
        try {
            List<SObject> records;
            if(String.isNotBlank(objectName)) {
                // Handle Case Object
                String fields = 'Id, Name';
                if(objectName == 'Case') { 
                    fields = 'Id, CaseNumber';
                } else if(objectName == 'Task') { 
                    fields = 'Id';
                }
                String query = 'SELECT ' + fields + ' FROM ' + objectName + ' WHERE Id IN :recordIds';
                System.debug('query ' + query);
                records= Database.query(query);
            }
            System.debug('records ' + records);
            return records;
        }
        catch (Exception exc) {
            return null;
        }
    }

    @AuraEnabled
    public static String deleteLstRecords(List <String> recordIds, String objectName) {
        try {
            Schema.DescribeSObjectResult checkObjectAccess = Schema.getGlobalDescribe().get(objectName).getDescribe();	
            if (checkObjectAccess.isDeletable()) {
                List <Id> lstRecordsToDelete = new List <Id>();
                Set <Id> deletedRecordsIds = new Set <Id>();
                for (Id  recordId: recordIds) {
                    lstRecordsToDelete.add(recordId);
                }
                Database.DeleteResult[] deleteResults = Database.delete(lstRecordsToDelete, true);
                for (Database.DeleteResult deletedId: deleteResults) {
                    if (deletedId.isSuccess())
                        deletedRecordsIds.add(deletedId.getId());
                }
            }
            return 'success';
        } catch (Exception e) {
            return 'Error deleting records: ' + e.getMessage();
        }
    }
}
HTML
<template>
    <div>
        <div class="slds-card slds-p-around_large">
            <div>
                <div class="slds-p-bottom_large">
                    <lightning-button label="Delete records" variant="destructive"  onclick={openModel} disabled={deleteButtonReadOnly}></lightning-button>
                    <lightning-button class="slds-m-left_large"  icon-name="utility:download" label="Download Records" variant="brand"  onclick={downloadRecords} disabled={deleteButtonReadOnly}></lightning-button>
                </div>
            </div>
        
            <div style="height: 18.75rem;">
                <lightning-datatable
                    data={lstRecords}
                    columns={columns}
                    key-field="Id"
                    show-checkbox-column="true"
                    onrowselection={handleRecordSelection}
                   sorted-direction={sortDirection}
                   sorted-by={sortedBy}
                     onsort={handleSortAccountData}
                     onrowaction={rowActionHandler}
            ></lightning-datatable>
            </div>
        </div>

        <template if:true={isModelOpen}>
            <section role="dialog" tabindex="-1" aria-labelledby="modal-heading-01" aria-modal="true" class="slds-modal slds-fade-in-open slds-modal_small">
                <div class="slds-modal__container slds-is-relative">
                    <lightning-modal-header label='Delete records'></lightning-modal-header>
                    <lightning-modal-body>
                        <div if:false={isLoaded} class="slds-is-relative spinner-margin">
                            <lightning-spinner alternative-text="Loading..." variant="brand"> </lightning-spinner>
                        </div>
                        <div if:true={isLoaded}>
                            <div if:true={havingRecordsToDelete}>
                                <p class="slds-align_absolute-center">Delete records</p>
                            </div>
                        
                            <div class="slds-modal__content slds-p-around_medium" id="modal-content-id-1">
                                <div if:true={recordToDelete}>
                                    <lightning-layout multiple-rows>
                                        <template  for:each={recordsLst} for:item="record" for:index="index" >
                                            <lightning-layout-item  class="slds-truncate_container_60" key={record} size="3">
                                                <lightning-pill data-record-id={record.Id} data-one="value for some"  data-two="some value for two" label={record.Name}  href={record.recordLink}  target="_blank" onremove={removeItem} ></lightning-pill>
                                            </lightning-layout-item>
                                        </template>
                                    </lightning-layout>
                                </div>
                                <div if:false={recordToDelete}>
                                    <h1>Please select record to delete.</h1>
                                </div>
                            </div>
                        </div>
                    </lightning-modal-body>
                    <lightning-modal-footer>
                        <lightning-button variant="neutral" label="Cancel" onclick={closeModel}></lightning-button>
                        <lightning-button class="slds-m-left_x-small" variant="brand" label='Delete' onclick={deleteRecords} disabled={deleteDisabled}></lightning-button>
                    </lightning-modal-footer>
                </div>
            </section>
            <div class="slds-backdrop slds-backdrop_open" role="presentation"></div>
        </template>
    </div>
</template>
Java Script
import { LightningElement, wire,track } from 'lwc';
import getRecords from '@salesforce/apex/BulkDeleteController.getRecords';
import getRecordsForLstIds from '@salesforce/apex/BulkDeleteController.getRecordsForLstIds';
import deleteLstRecords from '@salesforce/apex/BulkDeleteController.deleteLstRecords';
import { refreshApex } from '@salesforce/apex';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { NavigationMixin } from 'lightning/navigation';

export default class DatatableBulkDelete extends NavigationMixin(LightningElement)  {
    
    objectName = 'Account';
    lstRecords;
    selectRecordLst = [];
    updatedselectRecordLst = [];
    recordsLst = [];
    selectedRows = [];
    isModelOpen = false;
    isLoaded = false;
    recordToDelete = false;
    deleteButtonReadOnly = true;
    orgUrl;
    isRemoveClicked  = false;
    isAcs = false;
    defaultSortDirection = 'asc';
    @track sortDirection = 'asc';
    sortedBy = 'Name';
    @track record = [];


    @track recordsCount = 0;
    columns = [
        { label: 'Id', fieldName: 'Id', type: 'text' },
        { label: 'Name', fieldName: 'Name', type: 'text',sortable: true },
        { label: 'Account Number', fieldName: 'AccountNumber', type: 'picklist' },
        { label: 'Rating', fieldName: 'Rating', type: 'text' },
        { label: 'Created Date', fieldName: 'CreatedDate', type: 'date' , typeAttributes:{day:'numeric',month:'short',year:'numeric', hour:'2-digit',minute:'2-digit',hour12:true}},
        
    ];

    headers ={
        Id:"Id",
        Name:"Name",
        AccountNumber:"Account Number",
        Rating:"Rating",
        CreatedDate:"Created Date",

    }

    connectedCallback() { 
        this.orgUrl = window.location.origin;
    }

    @wire(getRecords, { objectName: 'Account' })
    getRecords(result) {
        this.wiredRecords = result;
        if (result.data) {
            if(result.data.length){
                this.lstRecords = result.data;
            } else{
                console.error('Error fetching records', result);
            } 
        } else if (result.error) {
            console.error('Error fetching records', result.error);
        }
    }

    handleRecordSelection(event){
        const selectedRows = event.detail.selectedRows;
        let selectedRecords = [];
        this.recordsCount = event.detail.selectedRows.length;
        this.selectedRows = selectedRows
        for(const row of selectedRows){
           selectedRecords.push( row.Id);
        }
        if(selectedRecords.length) { 
            this.deleteButtonReadOnly = false;
            this.selectRecordLst = selectedRecords.join(','); 
        } else { 
            this.deleteButtonReadOnly = true;
        }
    }

    getRecordsForLstIds() { 
        let lstRecordIds = this.selectRecordLst.split(','); 
        getRecordsForLstIds({ recordIds: lstRecordIds, objectName: this.objectName })
            .then(result => {
                if (result) {
                    this.recordToDelete = true;
                    result.forEach(item => {
                    let keys = Object.keys(item);
                    let nameKey = keys.find(key => key !== 'Id');
                    if (nameKey) {
                        this.recordsLst.push({Id: item['Id'],Name: item[nameKey], recordLink: this.orgUrl + '/' + item['Id']
                    });
                    this.isLoaded = true;
                }
            });}
        })
        .catch(error => {
            console.log('Error in getting ids ', error);
        });
    } 

    openModel() { 
        this.isModelOpen = true;
        this.getRecordsForLstIds();
    } 
    
    closeModel() { 
        this.recordsLst = [];
        this.isModelOpen = false;
        this.recordToDelete = false;
        this.isLoaded = false;
    }

    deleteRecords() { 
        let lstRecordIds = this.selectRecordLst.split(','); 
        if(this.updatedselectRecordLst.length ) { 
            lstRecordIds = this.updatedselectRecordLst
        } else if(this.isRemoveClicked && this.updatedselectRecordLst.length == 0) { 
            this.showtoast('No record selected');
            return;
        }
        
        deleteLstRecords({ recordIds: lstRecordIds, objectName: this.objectName })
        .then(result => {
            if(result == 'success') { 
                this.showtoast(this.recordsCount + ' records deleted successfully.', 'success');
                this.selectRecordLst = [];
                this.recordsLst = [];
                this.refreshData();
                this.deleteButtonReadOnly = true;
                this.isModelOpen = false;
                this.recordToDelete = false;
            } else { 
                this.deleteButtonReadOnly = true;
                this.showtoast('error in deleting records : ' + result, 'error');
            }
            this.template.querySelector('lightning-datatable').selectedRows = [];

            this.recordsCount = 0;
        })
        .catch(error => {
            this.isModelOpen = false;
            this.deleteButtonReadOnly = true;
            console.log('An error occurred while deleting. ', error)
        });
    }

    refreshData() {
        refreshApex(this.wiredRecords);
    }

    showtoast(message, variant) {
        this.dispatchEvent(
            new ShowToastEvent({
                title: message,
                variant: variant
            })
        );
    }
    removeItem(event){
        const recordId = event.target.dataset.recordId;
        let updatedRecordList = this.recordsLst.filter(function(item) {
                return item.Id != recordId;
        });
        this.recordsLst = updatedRecordList;
        this.lstRecordIds = updatedRecordList;
        this.updatedselectRecordLst = [];
        for(let i =0 ; i< this.lstRecordIds.length; i++){
            this.updatedselectRecordLst.push(this.lstRecordIds[i].Id); 
    
        }
        this.isRemoveClicked = true;
    
    }

    downloadRecords(){
        this.exportCSVFile(this.headers, this.selectedRows, "Record Sheet")
    }

    exportCSVFile(headers, totalData, fileTitle){
        if(!totalData || !totalData.length){
            return null
        }
        const jsonObject = JSON.stringify(totalData)
        const result = this.convertToCSV(jsonObject, headers)
        if(result === null) return
        const blob = new Blob([result])
        const exportedFilename = fileTitle ? fileTitle+'.csv' :'export.csv'
        if(navigator.msSaveBlob){
            navigator.msSaveBlob(blob, exportedFilename)
        } else if (navigator.userAgent.match(/iPhone|iPad|iPod/i)){
            const link = window.document.createElement('a')
            link.href='data:text/csv;charset=utf-8,' + encodeURI(result);
            link.target="_blank"
            link.download=exportedFilename
            link.click()
        } else {
            const link = document.createElement("a")
            if(link.download !== undefined){
                const url = URL.createObjectURL(blob)
                link.setAttribute("href", url)
                link.setAttribute("download", exportedFilename)
                link.style.visibility='hidden'
                document.body.appendChild(link)
                link.click()
                document.body.removeChild(link)
            }
        }
        
  
    }
    convertToCSV(objArray, headers){
        const columnDelimiter = ','
        const lineDelimiter = '\r\n'
        const actualHeaderKey = Object.keys(headers)
        const headerToShow = Object.values(headers) 
        let str = ''
        str+=headerToShow.join(columnDelimiter) 
        str+=lineDelimiter
        const data = typeof objArray !=='object' ? JSON.parse(objArray):objArray
  
        data.forEach(obj=>{
            let line = ''
            actualHeaderKey.forEach(key=>{
                if(line !=''){
                    line+=columnDelimiter
                }
                let strItem = obj[key]+''
                line+=strItem? strItem.replace(/,/g, ''):strItem
            })
            str+=line+lineDelimiter
        })
        return str
    }

    handleSortAccountData(event) {
        this.sortBy = event.detail.fieldName;
        this.sortDirection = event.detail.sortDirection;
        this.sortAccountData(event.detail.fieldName, event.detail.sortDirection);

    }
    sortAccountData(fieldname, sortDirection) {

        let parseData = JSON.parse(JSON.stringify(this.lstRecords));
        
        let keyValue = (a) => {
        
        return a[fieldname];
        
        };
        let isReverse = sortDirection === 'asc' ? 1: -1;

        parseData.sort((x, y) => {

        x = keyValue(x) ? keyValue(x) : '';

        y = keyValue(y) ? keyValue(y) : '';
        if(x > y){
            return sortDirection === 'asc' ? -1 : 1;}
        else if(y > x){
            return sortDirection === 'asc' ? 1 : -1;}
            else return 0;
        });
        this.lstRecords = parseData;
    }
    rowActionHandler(event) {
        const actionName = event.detail.action.name;
        const row = event.detail.row;
        switch ( actionName ) {
            case 'view':
                this[NavigationMixin.Navigate]({
                    type: 'standard__recordPage',
                    attributes: {
                        recordId: row.Id,
                        objectApiName: 'Account',
                        actionName: 'view'
                    }
                });
                break;
            case 'edit':
                this.refreshData();
                this[NavigationMixin.Navigate]({
                    type: 'standard__recordPage',
                    attributes: {
                        recordId: row.Id,
                        objectApiName: 'Account',
                        actionName: 'edit'
                    }
                });
                break;
            default:
        }
    }

}
Watch Demo

Leave a Reply