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 Lightning Web Component (LWC) then don’t worry you can check my previous post and get the idea. I have added multiple post on Lightning Web Component (LWC) which you can refer and get hands on experience.

Today we will create a Lightning Web Components 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 Component ( 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

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

  1. 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.

    .html :

    >

    .js:

    import { LightningElement, track, wire,api } from ‘lwc’;

    /** getImpactedProducts() method in ImpactedProductController Apex class */
    import getImpactedProducts from ‘@salesforce/apex/ImpactedProductController.getImpactedProducts’;

    const columns = [
    { label: ‘Impacted Product Name’, fieldName: ‘name’ },
    { label: ‘Product’, fieldName: ‘product’ },
    { label: ‘Asset’, fieldName: ‘asset’ },
    { label: ‘Lot Number’, fieldName: ‘lot’},
    { label: ‘Batch Number’, fieldName: ‘batch’},
    ];

    export default class MvqRelatedImpactedProduct extends LightningElement {

    /** All available MVQ_Impacted_Product__c[]. */
    @track impactedProductsList = [];

    //@track caseId = “test”;
    @api recordId;
    @track totalItem = 50;

    //@track data = [];
    @track columns = columns;
    @track tableLoadingState = true;

    /**
    * Load the list of available impacted products.
    */
    @wire(getImpactedProducts, { caseId : ‘$recordId’ }) impactedProductsList;

    controller:

    public with sharing class ImpactedProductController {

    @AuraEnabled(Cacheable=true)
    public static List getImpactedProducts(String caseId) {
    system.debug(‘inside controller ‘+caseId);
    List iproductList = [select id,name,MVQ_Void__c,MVQ_Product__r.Name,Asset__r.Name,Lot_Number__r.Name,Batch_Number__r.Name FROM MVQ_Impacted_Product__c WHERE Complaint__c =:caseId LIMIT 100];
    system.debug(‘returnLIST ‘+iproductList);
    List response = new List();
    for(MVQ_Impacted_Product__c impProduct : iproductList){
    DataTableWrapper obj = new DataTableWrapper();
    obj.oppId = impProduct.Id;
    obj.name = impProduct.Name;
    obj.product = impProduct.MVQ_Product__r.Name;
    obj.asset = impProduct.Asset__r.Name;
    obj.lot = impProduct.Lot_Number__r.Name;
    obj.batch = impProduct.Batch_Number__r.Name;
    response.add(obj);
    }

    return response;

    }

    private class DataTableWrapper {
    @AuraEnabled
    public Id oppId {get;set;}
    @AuraEnabled
    public String name {get;set;}
    @AuraEnabled
    public String product {get;set;}
    @AuraEnabled
    public String asset {get;set;}
    @AuraEnabled
    public string lot {get;set;}
    @AuraEnabled
    public string batch {get;set;}
    }
    }

    Can you please help me on that ? Thanks in advance ..

      1. I am not able to add the actions (like edit) against each record by using a custom metadata.

Leave a Reply

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