Lightning Web Components: Datatable with Lazy loading, Inline Edit and Dynamic Row Action

If you are following Salesforce eco system, 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 LWC then don’t worry you can check my previous post and get the idea. I have shared multiple post on LWC which you can refer and get hands on experience.

Today we will create a LWC Data Table component which supports Inline Edit, Lazy Loading, Dynamic Row Actions and Multiple selections using checkbox. We will also use Event to pass data between Lightning Web Components ( LWC ) and Lightning Component.

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 the similar component in Lightning as well so the apex part is similar in both. This is how our final UI will look like.

Lightning Web Components ( LWC ) : Datatable with Lazy loading, Inline Edit and Dynamic Row Action

Here for Toast I have used standard method, but if you want you can use custom component as well.

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 its 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 good idea about LWC, as we have cover most points in that. In case if you have any question here or want to add anything, let me know in comments, Happy programming 🙂

Advertisements

One thought on “Lightning Web Components: Datatable with Lazy loading, Inline Edit and Dynamic Row Action

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.