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

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>
lightningDataTable.js
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 + '>');
}
}
}
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];
}
}

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store