Today we will create a Lightning Web Components Datatable component that supports Inline Edit, Lazy Loading, Dynamic Row Actions, and Multiple selections using the checkbox. We will also use the Event to pass data between Lightning Web Component ( LWC ) and Lightning Component.
If you are following the Salesforce ecosystem, and you are a developer then you are already aware of Lightning Web Components ( LWC ). If you left behind and don’t know much about Lightning Web Component (LWC) then don’t worry you can check my previous post and get the idea. I have added multiple posts on Lightning Web Component (LWC) which you can refer to and get hands-on experience.
We will also use NavigationMixin from 'lightning/navigation';
to redirect user to record detail page. In our code we have also called Apex using both approach Wire method and Imperative method. As we need to perform DML operations, so its must to call them Imperatively. We will also use refreshApex
to refresh the data.
I have also created a similar component in Lightning as well so the apex part is similar in both. This is how our final UI will look like.

Here for Toast, I have used the standard method, but if you want you can use custom components as well. The benefit of using custom component is it support all UI experience.
Now we will check the code part.
lwcDataTable.html
<template> <div style="height: 450px;"> <lightning-datatable key-field="id" data={data} columns={columns} onrowselection={getSelectedName} enable-infinite-loading onloadmore={loadMoreData} onrowaction={handleRowAction} is-loading={tableLoadingState} onsave={handleSave} draft-values={draftValues} onsort={updateColumnSorting} > </lightning-datatable> {loadMoreStatus} </div> </template>
lwcDataTable .js
import { LightningElement, wire, api, track } from 'lwc';
import initRecords from '@salesforce/apex/LWCDataTableController.initRecords';
import updateRecords from '@salesforce/apex/LWCDataTableController.updateRecords';
import deleteSObject from '@salesforce/apex/LWCDataTableController.deleteSObject';
import { NavigationMixin } from 'lightning/navigation';
import { refreshApex } from '@salesforce/apex';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
export default class LwcDataTable extends NavigationMixin(LightningElement) {
@api objectApiName;
@api fieldNamesStr;
@api inlineEdit = false;
@api colAction = false;
@track data;
@track columns;
@track loadMoreStatus;
@api totalNumberOfRows;
wiredsObjectData;
@wire(initRecords, { ObjectName: '$objectApiName', fieldNamesStr: '$fieldNamesStr', recordId: '' , Orderby: 'Id', OrderDir: 'ASC',inlineEdit:'$inlineEdit' , enableColAction:'$colAction' })
wiredSobjects(result) {
this.wiredsObjectData = result;
if (result.data) {
this.data = result.data.sobList;
this.columns = result.data.ldwList;
this.totalNumberOfRows = result.data.totalCount;
}
}
getSelectedName(event) {
var selectedRows = event.detail.selectedRows;
var recordIds=[];
if(selectedRows.length > 0) {
for(var i =0 ; i< selectedRows.length; i++) {
recordIds.push(selectedRows[i].Id);
}
const selectedEvent = new CustomEvent('selected', { detail: {recordIds}, });
// Dispatches the event.
this.dispatchEvent(selectedEvent);
}
}
loadMoreData(event) {
//Display a spinner to signal that data is being loaded
//Display "Loading" when more data is being loaded
this.loadMoreStatus = 'Loading';
const currentRecord = this.data;
const lastRecId = currentRecord[currentRecord.length - 1].Id;
initRecords({ ObjectName: this.objectApiName, fieldNamesStr: this.fieldNamesStr, recordId: lastRecId , Orderby: 'Id', OrderDir: 'ASC',inlineEdit:this.inlineEdit , enableColAction:this.colAction })
.then(result => {
const currentData = result.sobList;
//Appends new data to the end of the table
const newData = currentRecord.concat(currentData);
this.data = newData;
if (this.data.length >= this.totalNumberOfRows) {
this.loadMoreStatus = 'No more data to load';
} else {
this.loadMoreStatus = '';
}
})
.catch(error => {
console.log('-------error-------------'+error);
console.log(error);
});
}
handleRowAction(event) {
const actionName = event.detail.action.name;
const row = event.detail.row;
switch (actionName) {
case 'edit':
this.editRecord(row);
break;
case 'view':
this.viewRecord(row);
break;
case 'delete':
this.deleteRecord(row);
break;
default:
this.viewRecord(row);
break;
}
}
//currently we are doing client side delete, we can call apex tp delete server side
deleteRecord(row) {
const { id } = row;
const index = this.findRowIndexById(id);
if (index !== -1) {
this.data = this.data
.slice(0, index)
.concat(this.data.slice(index + 1));
}
}
findRowIndexById(id) {
let ret = -1;
this.data.some((row, index) => {
if (row.id === id) {
ret = index;
return true;
}
return false;
});
return ret;
}
editRecord(row) {
this[NavigationMixin.Navigate]({
type: 'standard__recordPage',
attributes: {
recordId: row.Id,
actionName: 'edit',
},
});
}
viewRecord(row) {
this[NavigationMixin.Navigate]({
type: 'standard__recordPage',
attributes: {
recordId: row.Id,
actionName: 'view',
},
});
}
//When save method get called from inlineEdit
handleSave(event) {
var draftValuesStr = JSON.stringify(event.detail.draftValues);
updateRecords({ sobList: this.data, updateObjStr: draftValuesStr, objectName: this.objectApiName })
.then(result => {
this.dispatchEvent(
new ShowToastEvent({
title: 'Success',
message: 'Records updated',
variant: 'success'
})
);
// Clear all draft values
this.draftValues = [];
return refreshApex(this.wiredsObjectData);
})
.catch(error => {
console.log('-------error-------------'+error);
console.log(error);
});
}
// The method will be called on sort click
updateColumnSorting(event) {
var fieldName = event.detail.fieldName;
var sortDirection = event.detail.sortDirection;
}
}
LWCDataTableController.apxc
public with sharing class LWCDataTableController {
//init method to fetch initial records
@AuraEnabled(cacheable=true)
public static DataTableWrapper initRecords(String ObjectName, String fieldNamesStr, String recordId, String Orderby, String OrderDir, boolean inlineEdit, boolean enableColAction) {
DataTableWrapper dtw = new DataTableWrapper();
List<LabelDescriptionWrapper> labelList = new List<LabelDescriptionWrapper>();
List<String> fieldSet = new List<String>();
System.debug(fieldNamesStr);
Set<String> fieldNameSet = new Set<String>(fieldNamesStr.split(','));
if(Schema.getGlobalDescribe().containsKey(ObjectName) ) {
sObject sObj = Schema.getGlobalDescribe().get(ObjectName).newSObject() ;
//get all the labels for Opportunity fields and put them in a map, keyed to the field api name
Map<String, Schema.SObjectField> fieldMap = Schema.getGlobalDescribe().get(ObjectName).getDescribe().fields.getMap();
Map<Schema.SObjectField,String> fieldToAPIName = new Map<Schema.SObjectField,String>();
Map<String, String> apiNameToLabel = new Map<String, String>();
for(String fieldName : fieldNameSet){
if(fieldMap.containsKey(fieldName)) {
fieldSet.add(fieldName);
labelList.add(new LabelDescriptionWrapper(fieldMap.get(fieldName).getDescribe().getLabel(), fieldName, fieldMap.get(fieldName).getDescribe().getType().name().toLowerCase(), true, inlineEdit, null ));
}
}
if(enableColAction) {
List<Actions> actionList = new List<Actions>();
actionList.add(new Actions('Edit','edit'));
actionList.add(new Actions('View','view'));
actionList.add(new Actions('Delete','delete'));
TypeAttributes tAttribute = new TypeAttributes(actionList);
labelList.add(new LabelDescriptionWrapper('', '', 'action', false, false, tAttribute ));
}
//call method to query
List<sObject> sObjectRecords = getsObjectRecords(ObjectName, fieldSet, 50, recordId, Orderby, OrderDir);
dtw.ldwList = labelList;
dtw.sobList = sObjectRecords;
dtw.fieldsList = fieldSet;
dtw.totalCount = Database.countQuery('SELECT count() FROM '+ObjectName);
}
return dtw;
}
@AuraEnabled(cacheable=true)
public static List<sObject> getsObjectRecords(String ObjectName, List<String> fieldNameSet, Integer LimitSize, String recId, String Orderby, String OrderDir) {
OrderDir = String.isBlank(OrderDir) ? 'asc' : OrderDir;
String query = 'SELECT '+String.join(fieldNameSet, ',')+' FROM '+ObjectName;
if(String.isNotBlank(recId)) {
recId = String.valueOf(recId);
query += ' WHERE ID >: recId ';
}
query += ' ORDER BY '+Orderby+' '+OrderDir+' NULLS LAST';
if(LimitSize != null && Integer.valueOf(LimitSize) > 0) {
LimitSize = Integer.valueOf(LimitSize);
query += ' Limit '+LimitSize;
}
return Database.query(query);
}
//metho if we want to delete records
@AuraEnabled
public static sObject deleteSObject(sObject sob) {
delete sob;
return sob;
}
//Method to save records in inline edit
@AuraEnabled
public static void updateRecords(List<sObject> sobList, String updateObjStr, String objectName) {
schema.SObjectType sobjType = Schema.getGlobalDescribe().get(ObjectName);
Map<String, Schema.sObjectField> sObjectFields = sobjType.getDescribe().fields.getMap();
List<sObject> updateList = new List<sObject>();
List<Object> obList = (List<object>) json.deserializeUntyped(updateObjStr);
for(object ob : obList) {
Map<String, object> obmap = (Map<String, object>)ob;
String rowKey = (String)obmap.get('id');
Integer rowKeyInt = Integer.ValueOf(rowKey.removeStart('row-'));
sobject sObj = sobList[rowKeyInt];
for(string fieldName : obmap.keySet()) {
if(fieldName != 'id') {
Object value = obmap.get(fieldName);
Schema.DisplayType valueType = sObjectFields.get(fieldName).getDescribe().getType();
if (value instanceof String && valueType != Schema.DisplayType.String)
{
String svalue = (String)value;
if (valueType == Schema.DisplayType.Date)
sObj.put(fieldName, Date.valueOf(svalue));
else if(valueType == Schema.DisplayType.DateTime) {
try{
System.debug( (DateTime)value);
}catch(exception ex) {
system.debug(ex.getmessage());
}
String d1 = svalue;
list<String> d2 = d1.split('-');
list<integer> timeComponent = new list<integer>();
timeComponent.add(Integer.valueOf(d2[0]));
timeComponent.add(Integer.valueOf(d2[1]));
timeComponent.add(Integer.valueOf(d2[2].left(2)));
String t = d2[2].substringBetween('T','.');
list<String> time1 = t.split(':');
timeComponent.add(Integer.valueOf(time1[0]));
timeComponent.add(Integer.valueOf(time1[1]));
timeComponent.add(Integer.valueOf(time1[2]));
Datetime dt = Datetime.newInstance(timeComponent[0],timeComponent[1],timeComponent[2],timeComponent[3],timeComponent[4],timeComponent[5]);
sObj.put(fieldName, dt);
}
//
else if (valueType == Schema.DisplayType.Percent || valueType == Schema.DisplayType.Currency)
sObj.put(fieldName, svalue == '' ? null : Decimal.valueOf(svalue));
else if (valueType == Schema.DisplayType.Double)
sObj.put(fieldName, svalue == '' ? null : Double.valueOf(svalue));
else if (valueType == Schema.DisplayType.Integer)
sObj.put(fieldName, Integer.valueOf(svalue));
else if (valueType == Schema.DisplayType.Base64)
sObj.put(fieldName, Blob.valueOf(svalue));
else
sObj.put(fieldName, svalue);
}
else
sObj.put(fieldName, value);
}
}
updateList.add(sObj);
}
update updateList;
}
//wrapper class for data table
public class DataTableWrapper {
@AuraEnabled
public List<LabelDescriptionWrapper> ldwList;
@AuraEnabled
public List<sObject> sobList;
@AuraEnabled
public List<String> fieldsList;
@AuraEnabled
public Integer totalCount;
}
//Wrapper class to store Field details
public class LabelDescriptionWrapper {
@AuraEnabled
public String label;
@AuraEnabled
public String fieldName;
@AuraEnabled
public String type;
@AuraEnabled
public boolean sortable;
@AuraEnabled
public boolean editable;
@AuraEnabled
public TypeAttributes typeAttributes;
public LabelDescriptionWrapper(String labelTemp, String fieldNameTemp, String typeTemp, boolean sortableTemp, boolean editableTemp,TypeAttributes typeAttributesTemp) {
label = labelTemp;
fieldName = fieldNameTemp;
type = typeTemp;
sortable = false;//sortableTemp;
editable = editableTemp;
typeAttributes = typeAttributesTemp;
}
}
//Wrapper class to bind dropdown action with data row
public class TypeAttributes {
@AuraEnabled
public List<Actions> rowActions;
public typeAttributes(List<Actions> rowActionsTemp) {
rowActions = rowActionsTemp;
}
}
//Wrapper class to store dropdown action
public class Actions {
@AuraEnabled
public String label;
@AuraEnabled
public String name;
public Actions(String labelTemp, String nameTemp) {
label = labelTemp;
name = nameTemp;
}
}
}
So now you can just add this code in your org and lightning Web Components Datatable ready to use. You can find complete code in my GitHub repo as well.
So far if you are following my blog then you already have a good idea about LWC, as we have cover most points in detail. In case if you have any question here or want to add anything, let me know in comments, Happy programming 🙂
could you please provide sorting functionality on it?
I have already handle the controller code. Just bind updateColumnSorting method and enable sorting. You can take reference from Lightning datatable post.
Hi,I am very noob to LWC and I have a requirement very similar to this.I have one custom object call Impacted Product which is a related list of Case. Now I have to add some actions against each Impacted Product record.(e.g- edit,assign code etc) better it would be from metadata. I don’t need to show this record in sorting order. Below is my .html, .js and controller.
Can you please help me on that ? Thanks in advance ..
You didn’t mentioned what challenge you are facing here.
I am not able to add the actions (like edit) against each record by using a custom metadata.
I have added dynamic action from controller. You can query metadata in apex and then use them. But you need to handle lightning controller for this.
Dude!!
Much Appreciation.
And the LWC DataTable is barely documented.
Thanks for sharing your hard work.
I need to color code a cell based on value, what are the options.
I don’t think that’s supported.
Manually we can do it by defining cellAttributes, so can we do it by modifying the current wrapper ? something like
public class LabelDescriptionWrapper {
.
.
.
.
@AuraEnabled
public TypeAttributes typeAttributes;
@AuraEnabled
—->> public cellAttributes cellAttributes;
public LabelDescriptionWrapper(String labelTemp, String fieldNameTemp, String typeTemp, boolean sortableTemp, boolean editableTemp,TypeAttributes typeAttributesTemp,—->>cellAttributes cellAttributesTemp) {
label = labelTemp;
fieldName = fieldNameTemp;
type = typeTemp;
sortable = false;//sortableTemp;
editable = editableTemp;
typeAttributes = typeAttributesTemp;
—->> cellAttributes = cellAttributes Temp;
}
}
and so on ?
I checked this. In docs cellAttribute we can set icon and other property. But I don’t find any support for color. You can give it a try
Yes I guess icon will work if not color. Have you tried icon ?
For color we will need to define the class and use it.
Nope didn’t try. Give it a try in case face any issue let me know I will check.
Sure, will share my updates by tomorrow, i am working on it
I achieved custom and logic based colro coding in two steps – one i created the column
{
label: ‘Age’,
fieldName: ‘Case_Aging__c’,
type: ‘number’,
cellAttributes:
{ class: { fieldName: ‘trendicon__c’ }}
}
Second based on the logic i populated the field with CSS class like slds-icon-custom-custom5 or slds-icon-custom-custom15 these classes change the cell background
Glad to know you made it work. Thanks for sharing here.
Could you guide how can we create a column like this in the code you have {
label: ‘Age’,
fieldName: ‘Case_Aging__c’,
type: ‘number’,
cellAttributes:
{ class: { fieldName: ‘trendicon__c’ }}
}
i need to pass the cellAttribute and class values
We need to use nested wrapper. I will try to add this but wont be able to provide quickly.
I still have two tasks I am trying to do
1. Making a clickable filed like Case Id , which takes the click to the record view.
2. Bring record owner names instead of ID, like i want to show the case owner in the case row, I get case ownerid, but not the name
any suggestions
@tushar Any idea about how to make field clickable to open the record and also bring ownerid name instead of Id. When i try to attempt by passing the ownerid.name or opportunity__r.name/opportunity.name its not working.
I was aware that there are an issue related to this I need to check if that is fixed or not.
Hi Tushar,
Do you have update on updesh Requirement?
I am displaying the Custom object details which is a child of Account.
Basically, I need to do below 2 functionalities in datatable of LWC
1.Display the name of the custom object record in first column.Make this as clickable.On click on this,it should take me to the record detail page.
2.Display of the related Account Name.
Thanks in Advance.
Reagrds,
Harsha
For both use a formula field. For Link something similar to
Field name link__c,
type text
value = IF( Opportunity__c !=”,”../r/Opportunity/”&Opportunity__c&”/view”,”)
And then display it by using the below code in .js
{ label: ‘Name’, fieldName: ‘Link__c’, type: ‘url’,initialWidth: 100,sortable: true,
typeAttributes: {label: { fieldName: ‘Opportunity Name’ },
target: ‘_self’},
}
Hi Updesh,
Thanks for your response.
But,I need to create additional formula fields to display values.Is there any other workaround?
Regards,
Harsha BR
Well will like to see if there are other options ? One more is to code the wrapper in APEX class.
Hi Updesh,
Do you have the sample Apex code for the same?
In my requirement,I need to display more than 5 columns associated to the related object.Creating formula field for each might not fit well for my requirement.
Thanks & Regards,
Harsha BR
How to execute this? am passing the Object name and the fieldName with “comma separeted” but stil show nothing.
Check if you are getting any error in console or in debug.
Hi, Tushar. This is awesome. But how do I make this appear on my Opportunity page? I’d like to show Opportunity Line records in the data table. Which values should I change in your code?
I tried:
@api objectApiName = ‘Opportunity__c’;
Is this correct? What do I put in fieldNamesStr?
Appreciate your guidance here. Thanks.
Refer this sample. You don’t need to make changes in actual code: https://github.com/tushar30/LWCDataTable-LazyLoading/blob/master/LWCDataTableDemo/LWCDataTableDemo.cmp
Hi Tushar,
I needed help i’m new to lwc, how do you code a dynamic row actions in data table from server side?
We are already doing that in the code. We have added action using server side code.
Hi, Thanks for sharing.
However, I think there is an issue if the loadMoreData is called and you try to edit the last records. In that case, the call to refreshApex doesn’t work because wiredsObjectData doesn’t contain the last records.
We have to refresh the web page to actually see the update.
Hey! Did you get how sorting and lazy loading both can be incorporated?