[Salesforce LWC] — Lightning Data Table — proof of concept for get records, sort, custom validation, save, delete, row Action, wrap text, editable

Kousik Majilya
6 min readFeb 14, 2022

--

There are ample of business scenarios when we use data table or data grid for different use cases and struggle to find out what are the critical features salesforce lightning data table offers as part of standard component framework.

Below code will give a fair idea of different implementation using LDT.

Below code base is a workable version and can be leveraged as-is for any particular cases to implement with the mindset of certain LDT limitations that still under considerations from Salesforce platform end.

lightningDataTable.html

<template
<lightning-datatable
data={contacts}
columns={columnsContact}
key-field="Id"
onrowselection={getSelectedName}
sorted-by={sortBy}
sorted-direction={sortDirection}
onsort={sortLDT}
onrowaction={handleRowAction}
onsave={handleSave}
errors={tableErrors}
>
</lightning-datatable>
</template>

Java Script Piece of code

lightningDataTable.js

Few critical use case(s) frequently used to implement lightning data table case

(a) : define types of columns with different data types and properties Case

(b) : GET records via wire() and populate in LDT case

© : sort data in ascending or descending order for any particular column case

(d) : handle Row Action (Add/Delete) of a particular row case

(d.1) : SAVE updated records (ONLY single row) case

(d.2) : DELETE records (ONLY single row) case

(e) : SAVE updated records (can be multiple) case

(f) : Validate records for each row of a particular field or multiple fields

import { LightningElement, wire, track, api } from 'lwc';
import { updateRecord, deleteRecord } from 'lightning/uiRecordApi';
import getContact from '@salesforce/apex/lightningDataTable.getContact';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { refreshApex } from '@salesforce/apex';


const actions = [
{ label: 'save', name: 'save' },
{ label: 'Delete', name: 'delete' }
];


/**
* case (a) define types of columns with different data types and properties
* data type (i) : text
* data type (ii) : phone
* data type (iii): email
* data type (iv) : date
* data type (v) : button-icon
* data type (vi) : action
*
* property (i) : sortable
* property (ii) : editable
* property (iii) : wrapText
*/


const columnsContact = [
{ label: 'First Name', fieldName: 'FirstName', sortable: true, editable: true },
{ label: 'Last Name', fieldName: 'LastName', sortable: true, editable: true },
{ label: 'Title', fieldName: 'Title', wrapText: true, editable: true },
{ label: 'Phone', fieldName: 'Phone', type: 'phone', editable: true },
{ label: 'Email', fieldName: 'Email', type: 'email', editable: true },
{ label: 'Birthdate', fieldName: 'Birthdate', type: 'date', editable: true },
{
type: 'button-icon',
fixedWidth: 34,
typeAttributes:
{
iconName: 'utility:save',
name: 'save'
}
},
{
type: 'button-icon',
fixedWidth: 34,
typeAttributes:
{
iconName: 'utility:delete',
name: 'delete',
iconClass: 'slds-icon-text-error'
}
},
{ type: 'action', typeAttributes: { rowActions: actions, menuAlignment: 'left' } },
{ label: ' ', fieldName: 'DM', fixedWidth: 20 }
];



export default class DatatableExample extends LightningElement {
error;
@api selectedRows;
@api recordId;
@track contacts = [];
@track refreshcontacts;
@track sortBy;
@track sortDirection;
@track columnsContact = columnsContact;
@track tableErrors = { rows: {}, table: {} };


@api constant = {
ERROR_TITLE : 'Error Found',
ERROR_FIRSTNAME : 'first name can not be left blank',
ERROR_LASTNAME : 'last name can not be left blank',
FNAME_FIRSTNAME : 'FirstName',
FNAME_LASTNAME : 'LastName',
SORT_DIRECTION : 'asc',
ROWACTION_SAVE : 'save',
ROWACTION_DELETE : 'delete',
VAR_SUCCESS : 'Success',
VAR_ERROR : 'error',
MSG_DELETE : 'Record Deleted',
MSG_ERR_DEL : 'Error deleting record',
MSG_ERR_UPD_RELOAD : 'Error updating or reloading record',
MSG_UPD : 'Contact Updated',
DEL_CONFIRM : 'Want to delete?',
MODE_BATCH : 'batch',
MODE_SINGLE : 'single'
}

/**
* Case (b) : GET records and populate in LDT
* call getContact apex
* @param {function} getContact
* @returns
*/

@wire(getContact)
contacts(result) {
this.refreshcontacts = result;
if (result.data) {
this.contacts = result.data;
this.error = undefined;
} else if (result.error) {
this.error = result.error;
this.contacts = undefined;
}

}

/**
* Case (c) : sort data in asc or desc order for any particular column
* sort LDT column (asc or desc) using 2 functions (sortLDT and sortData)
* sortData() called from sortLDT() function
* @param {object} event
* @returns
*/

sortLDT(event) {
this.sortBy = event.detail.fieldName;
this.sortDirection = event.detail.sortDirection;
this.sortData(this.sortBy, this.sortDirection);
}


/**
* sort data
* called from sortLDT() function
* @param {String} fieldname
* @param {String>} direction
* @param {Integer} rowNumber
* @returns
*/

sortData(fieldname, direction) {

try {
let parseData = JSON.parse(JSON.stringify(this.contacts));
let keyValue = (a) => {
return a[fieldname];
};

let isReverse = direction === this.constant.SORT_DIRECTION ? 1: -1;
parseData.sort((x, y) => keyValue(x) > keyValue(y) ? (1 * isReverse) :
(keyValue(y) > keyValue(x) ? (-1 * isReverse):0));
this.contacts = parseData;
} catch (errorMsg) {
console.log ('error occured inside sortData() method.
See actual system message <' + errorMsg.message + '>');
}
}


/**
* case (d) : handle Row Action (Add/Delete) of a particular row
* (i) invoked against type: 'button-icon'
* (ii) invoked against type: 'action'
* called from html - onrowaction={handleRowAction}
* @param {object} event
* @returns
*/

handleRowAction(event) {

try {
const action = event.detail.action.name;

//check for save or delete action
switch (action) {
case this.constant.ROWACTION_SAVE:
this.rowactionSave(event);
break;
case this.constant.ROWACTION_DELETE:
this.rowactionDelete(event);
break;
}
} catch (errorMsg) {
console.log ('error occured inside handleRowAction() method.
See actual system message <' + errorMsg.message + '>');
}
}


/**
* case (e) : SAVE updated records (can be multiple)
* multiple records can be saved at one go
* called from html - onsave={handleSave}
* @param {object} event
* @returns
*/

handleSave(event) {

try {

//call validateError() to check for any errors
if (this.validateError (event.detail.draftValues,
this.constant.MODE_BATCH)) { return; };


const recordInputs = event.detail.draftValues.slice().map(draft => {
const fields = Object.assign({}, draft);
return { fields };
});

//invoke updaterecord() for batch update
const promises = recordInputs.map(recordInput =>
updateRecord(recordInput));
Promise.all(promises).then(contacts => {
this.dispatchEvent(
new ShowToastEvent({
title: this.constant.VAR_SUCCESS,
message: this.constant.MSG_UPD,
variant: this.constant.VAR_SUCCESS
})
);

//refresh data in the datatable
return refreshApex(this.refreshcontacts);
})

//display error message in case of errors
.catch(error => {
this.dispatchEvent(
new ShowToastEvent({
title: this.constant.MSG_ERR_UPD_RELOAD,
message: error.body.message,
variant: this.constant.VAR_ERROR
})
);
});
} catch (errorMsg) {
console.log ('error occured inside handleSave() method.
See actual system message <' + errorMsg.message + '>');
}
}


/**
* case (d.1) : SAVE updated records (ONLY single row)
* single record can be saved
* called from JS - handleRowAction()
* @param {object} event
* @returns
*/

rowactionSave(event) {

try {

//get the changed records using queryselector.draftValues

let qslct = this.template.querySelector('lightning-datatable').
draftValues.find(x => x.Id == event.detail.row.Id);

//call validateError() to check for any errors
if (this.validateError (qslct, this.constant.MODE_SINGLE)) { return;};

let row = [];
row = [...row, qslct];


const recordInputs = row.slice().map(draft => {
const fields = Object.assign({}, draft);
return { fields };
});

//invoke updaterecord() for single row update
const promises = recordInputs.map(recordInput =>
updateRecord(recordInput));
Promise.all(promises).then(contacts => {
this.dispatchEvent(
new ShowToastEvent({
title: this.constant.VAR_SUCCESS,
message: this.constant.MSG_UPD,
variant: this.constant.VAR_SUCCESS
})
);

//refresh data in the datatable
return refreshApex(this.refreshcontacts);
})

//display error message in case of errors

.catch(error => {
this.dispatchEvent(
new ShowToastEvent({
title: this.constant.MSG_ERR_UPD_RELOAD,
message: error.body.message,
variant: this.constant.VAR_ERROR
})
);
});
} catch (errorMsg) {
console.log ('error occured inside rowactionSave() method.
See actual system message <' + errorMsg.message + '>');
}
}


/**
* case (d.2): DELETE records (ONLY single row)
* (a) single record can be deleted
* called from JS - handleRowAction()
* @param {object} event
* @returns
*/

rowactionDelete(event) {

//confirm to delete & invoke deleteRecord()

if (window.confirm(this.constant.DEL_CONFIRM)) {

deleteRecord(event.detail.row.Id)
.then(() => {
this.dispatchEvent(
new ShowToastEvent({
title: this.constant.VAR_SUCCESS,
message: this.constant.MSG_DELETE,
variant: this.constant.VAR_SUCCESS
})
);

//refresh data in the datatable
return refreshApex(this.refreshcontacts);
})

//display error message in case of errors
.catch(error => {
this.dispatchEvent(
new ShowToastEvent({
title: this.constant.MSG_ERR_DEL,
message: error.body.message,
variant: this.constant.VAR_ERROR
})
);
});
}
}

/**
* case (f) : validate records for each row of a singlefield or multiple fields
* called from JS - rowactionSave()
* @param {object} event
* @returns
*/

validateError(rowToValidate, mode) {
try {
//set the default return type to false
let retType = false;

//set tableErrors and rows
this.tableErrors = {};
this.tableErrors.rows = {};

let errorMsgValue = {
title: this.constant.ERROR_TITLE,
messages: [this.constant.ERROR_FIRSTNAME],
fieldNames: [this.constant.FNAME_FIRSTNAME]
};

if (mode == this.constant.MODE_BATCH) {

//iterate to check for all changed rows

rowToValidate.forEach(rowToValidate => {

if ( rowToValidate.FirstName == null ||
rowToValidate.FirstName == '' ) {

this.tableErrors.rows[rowToValidate.Id] = errorMsgValue;

//change the return type to true if errors are there
retType = true;
}
});
}
else if (mode == this.constant.MODE_SINGLE) {

//validation will not work at the time of editing any other fields
//apart from firstname since it will be in undefined state

if (rowToValidate.FirstName != undefined) {

if ( rowToValidate.FirstName == null ||
rowToValidate.FirstName == '' ) {

this.tableErrors.rows[rowToValidate.Id] = errorMsgValue;

//change the return type to true if errors are there
retType = true;
}
}
}
return retType;
} catch (errorMsg) {
console.log ('error occured inside validateError() method. See actual system message <' + errorMsg.message + '>');
}
}
}

Apex Piece of Code

Fetch data from Contact Object

lightningDataTable.clspublic with sharing class lightningDataTable  {

@AuraEnabled(cacheable=true)
public static List<Contact> getContact() {
return [SELECT Id, FirstName, LastName, Title, Phone, Email,
Birthdate
FROM Contact LIMIT 30];
}
}

--

--

Kousik Majilya

Salesforce Account Director & Principal Architect with strong Industry knowledge & specialization in Salesforce Lightning Web Component (LWC) Architecture.