Blog

Inline Edit with Dynamic Picklist

In order to display Salesforce data in a table, use the lightning-datatable component. Inline
editing lets users quickly edit the field value right on a record’s detail pages.
You can edit multiple rows and save them instantly. Also, if any error occurs, it will be visible
instantly as Toast Message. This component supports multiple salesforce data types and all
necessary data validations are available.
Pros and cons of the component are :

Pros :

  • Single Unit Component.
  • Data Validations are Handled.
  • Horizontal scroll bar with single column freezing.
  • Supports major SF datatypes like Text, Picklist, Number, Percent, Currency, Date,

Cons :

  • Lookup, Rich Text Area, Geo-Location are yet in view mode only

Inline Edit – Case Details :

Inline Edit with Dynamic Picklist

Freezing Column :

Inline Edit with Dynamic Picklist

Dynamic Picklist:

Inline Edit with Dynamic Picklist

Data Validation :

Inline Edit with Dynamic Picklist

Lightning Component :

Parent Component:

InlineEditParentComponent.cmp

<aura:component
    implements="force:appHostable,flexipage:availableForAllPageTypes,flexipage:availableForRecordHo
me,force:hasRecordId,forceCommunity:availableForAllPageTypes,force:lightningQuickAction,lightning
:isUrlAddressable"
    access="global"
    controller="InlineEditController"
>
    <aura:attribute name="recordId" type="Id" />
    <aura:attribute name="data" type="Object" />
    <aura:attribute name="columns" type="List" />
    <aura:attribute name="originList" type="List" />
    <aura:attribute name="reasonList" type="List" />
    <aura:attribute name="isLoading" type="Boolean" default="false" />
    <aura:attribute name="viewAll" type="Boolean" default="true" /><aura:handler name="init" value="{!this}" action="{!c.doInit}" />
    <!--Events-->
    <aura:handler name="dataTableSaveEvent" event="c:dataTableSaveEvent" action="{!c.saveTableRecords}" />
    <!--Start of Headers-->
    <div class="slds-page-header">
        <div class="slds-page-header__row">
            <div class="slds-page-header__col-title">
                <div class="slds-media">
                    <div class="slds-media__figure">
                        <span class="slds-icon_container slds-icon-standard-case" title="case">
                            <lightning:icon iconName="standard:case" alternativeText="Case" title="Case" />
                            <span class="slds-assistive-text">Case</span>
                        </span>
                    </div>
                    <div class="slds-media__body">
                        <div class="slds-card__header-link baseCard__header-title-container">
                            <h5>
                                <span
                                    class="slds-page-header__title slds-truncate"
                                    title="Optimisation
Requests"
                                >
                                    Case Details
                                </span>
                            </h5>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <!--End of Headers-->
    <aura:if isTrue="{!v.data.length > 0}">
        <lightning:card>
            <c:dataTable aura:id="datatableId" auraId="datatableId" columns="{!v.columns}" data="{!v.data}" showRowNumberColumn="true" />
        </lightning:card>
    </aura:if>
    <aura:if isTrue="{!v.isLoading}">
        <lightning:spinner alternativeText="Loading.." variant="brand" />
    </aura:if>
    <aura:if isTrue="{!v.viewAll}">
        <a class="slds-align_absolute-center" label="View all" title="View All" onclick="{!c.gotoRelatedList}">View All</a>
    </aura:if>
</aura:component>
Controll

Controller:

InlineEditParentComponentController.js

({
    doInit: function (component, event, helper) {
        component.set("v.isLoading", true);
        helper.setupTable(component, event, helper);
    },
    gotoRelatedList: function (component, event, helper) {
        helper.relatedList(component, event, helper);
        component.set("v.viewAll", false);
    },
    saveTableRecords: function (component, event, helper) {
        var recordsData = event.getParam("recordsString");
        var tableAuraId = event.getParam("tableAuraId");
        var action = component.get("c.updateRecords");
        var conditionCheck = true;
        var test = true;
        if (conditionCheck == true) {
            for (var i = 0; i < recordsData.length; i++) {
                var rate = recordsData[i].Rate__c;
                var fee = recordsData[i].Fee__c;
                var origin = recordsData[i].Origin;
                var reason = recordsData[i].Reason;
                var notes = recordsData[i].Notes__c;
                if ((rate != undefined && isNaN(rate)) || (fee != undefined && isNaN(fee))) {
                    test = false;
                    var toastEvent = $A.get("e.force:showToast");
                    toastEvent.setParams({ title: "Error!", type: "error", message: "Please enter a valid number" });
                    toastEvent.fire();
                    console.log("v.isLoading" + component.get("v.isLoading"));
                    component.set("v.isLoading", false);
                }
            }
        }
        if (test == true) {
            action.setParams({ jsonString: JSON.stringify(recordsData) });
            action.setCallback(this, function (response) {
                var datatable = component.find(tableAuraId);
                datatable.finishSaving("SUCCESS");
            });
            $A.enqueueAction(action);
        }
    },
});

Helper :

InlineEditParentComponentHelper.js

({
    setupTable: function (component, event, helper) {
        // Calling apex method to get picklist values dynamically
        var action = component.get("c.getPicklistValues");
        action.setParams({
            objectAPIName: "Case",
            fieldAPIName: "Reason",
        });
        action.setCallback(this, function (response) {
            if (response.getState() === "SUCCESS") {
                var winReason = [];
                Object.entries(response.getReturnValue()).forEach(([key, value]) => winReason.push({ label: key, value: value }));
                component.set("v.reasonList", winReason);
                this.setupTableStatus(component, event, helper);
            } else {
                var errors = response.getError();
                var message = "Error: Unknown error";
                if (errors && Array.isArray(errors) && errors.length > 0) message = "Error: " + errors[0].message;
                component.set("v.error", message);
            }
        });
        $A.enqueueAction(action);
    },
    setupTableStatus: function (component, event, helper) {
        var action = component.get("c.getPicklistValues");
        action.setParams({
            objectAPIName: "Case",
            fieldAPIName: "Origin",
        });
        action.setCallback(this, function (response) {
            if (response.getState() === "SUCCESS") {
                var status = [];
                Object.entries(response.getReturnValue()).forEach(([key, value]) => status.push({ label: key, value: value }));
                component.set("v.originList", status);
                this.columns(component, event, helper);
            } else {
                var errors = response.getError();
                var message = "Error: Unknown error";
                if (errors && Array.isArray(errors) && errors.length > 0) message = "Error: " + errors[0].message;
                component.set("v.error", message);
            }
        });
        $A.enqueueAction(action);
    },
    columns: function (component, event, helper) {
        var originOption = [];
        var reasonOption = [];
        originOption = component.get("v.originList");
        reasonOption = component.get("v.reasonList");
        var cols = [
            { label: "Case Number", fieldName: "accountLink", type: "link", sortable: true, resizable: true, attributes: { label: { fieldName: "CaseNumber" }, title: "Click to View(New Window)", target: "_blank" } },
            { label: "Origin", fieldName: "Origin", editable: true, type: "picklist", selectOptions: originOption, resizable: true },
            { label: "Reason", fieldName: "Reason", editable: true, type: "picklist", selectOptions: reasonOption, resizable: true },
            { label: "Notes", fieldName: "Notes__c", editable: true, resizable: true },
            { label: "Fee", fieldName: "Fee__c", type: "currency", editable: true, resizable: true },
            { label: "Rate", fieldName: "Rate__c", type: "percent", sortable: true, editable: true, resizable: true },
            { label: "Email", fieldName: "ContactEmail", type: "text", editable: true, resizable: true },
            { label: "Mobile", fieldName: "ContactMobile", type: "number", editable: true, resizable: true },
            { label: "Comments", fieldName: "Comments", type: "text", editable: true, resizable: true },
        ];
        component.set("v.columns", cols);
        this.loadRecords(component);
    },
    relatedList: function (component, event, helper) {
        var evt = $A.get("e.force:navigateToComponent");
        evt.setParams({
            componentDef: "c:accountsTable",
            isredirect: false,
            componentAttributes: {
                recordId: component.get("v.recordId"),
            },
        });
        evt.fire();
        component.set("v.viewAll", false);
    },
    loadRecords: function (component) {
        var action = component.get("c.getRecords");
        action.setParams({
            recordId: component.get("v.recordId"),
        });
        action.setCallback(this, function (response) {
            if (response.getState() === "SUCCESS") {
                var allRecords = response.getReturnValue();
                allRecords.forEach((rec) => {
                    rec.accountLink = "/" + rec.Id;
                });
                component.set("v.data", allRecords);
                component.set("v.isLoading", false);
            } else {
                var errors = response.getError();
                var message = "Error: Unknown error";
                if (errors && Array.isArray(errors) && errors.length > 0) message = "Error: " + errors[0].message;
                component.set("v.error", message);
                console.log("Error: " + message);
            }
        });
        $A.enqueueAction(action);
    },
});


Child Component:

dataTable.cmp

<aura:component>
    <aura:attribute name="auraId" type="String" />
    <aura:attribute name="data" type="Object" />
    <aura:attribute name="columns" type="List" />
    <aura:attribute name="sortBy" type="String" />
    <aura:attribute name="sortDirection" type="String" />
    <aura:attribute name="showRowNumberColumn" type="Boolean" default="false" />
    <!-- LOCAL VARIABLES -->
    <aura:attribute name="dataCache" type="Object" />
    <aura:attribute name="tableData" type="Object" />
    <aura:attribute name="tableDataOriginal" type="Object" />
    <aura:attribute name="updatedTableData" type="Object" />
    <aura:attribute name="modifiedRecords" type="List" />
    <aura:attribute name="isEditModeOn" type="Boolean" default="false" />
    <aura:attribute name="isLoading" type="Boolean" default="false" />
    <aura:attribute name="error" type="String" default="" />
    <aura:attribute name="startOffset" type="String" />
    <aura:attribute name="buttonClicked" type="String" />
    <aura:attribute name="buttonsDisabled" type="Boolean" />
    <aura:handler name="init" value="{!this}" action="{!c.doInit}" />
    <aura:registerEvent name="dataTableSaveEvent" type="c:dataTableSaveEvent" />
    <!-- EDITABLE
TABLE SAVE COMP EVENT -->
    <aura:registerEvent name="dataTableRowActionEvent" type="c:dataTableRowActionEvent" />
    <!--
ROW ACTION COMP EVENT -->
    <aura:method
        name="finishSaving"
        action="{!c.finishSaving}"
        description="Update table and clode
edit mode"
    >
        <aura:attribute name="result" type="String" />
        <aura:attribute name="data" type="Object" /><aura:attribute name="message" type="String" default="" />
    </aura:method>
    <aura:if isTrue="{!v.isLoading}">
        <lightning:spinner alternativeText="Loading" variant="brand" />
    </aura:if>
    <div class="slds-table_edit_container slds-is-relative table-container" style="width: 100%; overflow: auto;">
        <table aria-multiselectable="true" class="cTable slds-table slds-no-cell-focus slds-table_bordered slds-table_edit slds-table_fixed-layout slds-table_resizable-cols table-freeze-style" role="grid">
            <thead>
                <tr class="slds-line-height_reset header-style">
                    <aura:if isTrue="{!v.showRowNumberColumn}">
                        <th scope="col" style="width: 80px; text-align: center;"></th>
                    </aura:if>
                    <aura:iteration items="{!v.columns}" var="col">
                        <td name="{!col.sortBy}" aria-label="{!col.label}" aria-sort="none" class="{!col.thClassName}" scope="col" style="{!col.style}">
                            <span class="{!!col.sortable ? 'slds-truncate slds-p-horizontal_x-small' : 'slds-hide'}" title="{!col.label}">{!col.label}</span>
                            <a class="{!col.sortable ? 'slds-th__action slds-text-link_reset' : 'slds-hide'}" href="javascript:void(0);" role="button" tabindex="0" onclick="{!c.sortTable}">
                                <span class="slds-assistive-text">Sort by: {!col.label}</span>
                                <div class="slds-grid slds-grid_vertical-align-center slds-has-flexi-truncate" title="{!'Sorty by: '+col.label}">
                                    <span class="slds-truncate" title="{!col.label}">{!col.label}</span>
                                    <span class="slds-icon_container slds-icon-utility-arrowdown">
                                        <lightning:icon iconName="{!v.sortDirection=='asc'?'utility:arrowup':'utility:arrowdown'}" size="xx-small" class="{!v.sortBy==col.sortBy? 'slds-m-left_x-small':'slds-is-sortable__icon'}" />
                                    </span>
                                </div>
                            </a>
                            <div class="{!col.resizable ? 'slds-resizable' : 'slds-hide' }" onmousedown="{!c.calculateWidth}">
                                <input type="range" min="50" max="1000" class="slds-resizable__input slds-assistive-text" tabindex="-1" />
                                <span class="slds-resizable__handle" ondrag="{!c.setNewWidth}" style="will-change: transform;">
                                    <span class=""></span>
                                </span>
                            </div>
                        </td>
                    </aura:iteration>
                </tr>
            </thead>
            <tbody>
                <aura:iteration items="{!v.tableData}" var="row" indexVar="rowIndex">
                    <tr aria-selected="false" class="slds-hint-parent">
                        <aura:if isTrue="{!v.showRowNumberColumn}"><td scope="col" style="width: 50px; max-width: 60px; text-align: center;">{!rowIndex+1}</td> </aura:if>
                        <aura:iteration items="{!row.fields}" var="field" indexVar="fieldIndex">
                            <td class="{!field.tdClassName}" role="gridcell">
                                <span class="slds-grid slds-grid_align-spread">
                                    <aura:if isTrue="{!field.mode == 'view'}">
                                        <aura:if isTrue="{!field.type == 'link'}">
                                            <a class="slds-truncate" id="{!rowIndex+'-'+fieldIndex}" href="{!field.value}" title="{!field.title}" target="{!field.target}">{!field.label}</a>
                                        </aura:if>
                                        <aura:if isTrue="{!field.type == 'link-action'}">
                                            <a class="slds-truncate" id="{!rowIndex+'-'+fieldIndex+'-'+field.actionName}" title="{!field.title}" onclick="{!c.onRowAction}">{!field.label}</a>
                                        </aura:if>
                                        <aura:if isTrue="{!field.type == 'date'}">
                                            <lightning:formattedDateTime class="slds-truncate" value="{!field.value}" year="numeric" month="numeric" day="numeric" timeZone="UTC" />
                                        </aura:if>
                                        <aura:if isTrue="{!field.type == 'number'}">
                                            <lightning:formattedNumber
                                                class="slds-truncate"
                                                value="{!field.value}"
                                                style="{!field.formatter}"
                                                currencyCode="{!field.currencyCode}"
                                                minimumFractionDigits="{!field.minimumFractionDigits}"
                                                maximumFractionDigits="{!field.maximumFractionDigits}"
                                            ></lightning:formattedNumber>
                                        </aura:if>
                                        <!--Start of Percent-->
                                        <aura:if isTrue="{!field.type == 'percent'}">
                                            <aura:if isTrue="{!field.value}">{!field.value}% </aura:if>
                                            <lightning:formattedNumber style="percent" minimumFractionDigits="2" maximumFractionDigits="2" />
                                        </aura:if>
                                        <!--End of Percent-->
                                        <!--Start of currency-->
                                        <aura:if isTrue="{!field.type == 'currency'}">
                                            <lightning:formattedNumber value="{!field.value}" style="currency" id="testId" />
                                        </aura:if>
                                        <!--End of currency-->
                                        <aura:if isTrue="{!!field.isViewSpecialType}">
                                            <span class="slds-truncate fix" title="{!field.value}">{!field.value}</span>
                                        </aura:if>
                                        <aura:if isTrue="{!field.editable}">
                                            <lightning:buttonIcon
                                                iconName="utility:edit"
                                                variant="bare"
                                                name="{!rowIndex+'-'+fieldIndex}"
                                                onclick="{!c.editField}"
                                                alternativeText="{! 'Edit: '+field.value}"
                                                class="slds-cell-edit__button slds-m-left_x-small"
                                                iconClass="slds-button__icon_hint
slds-button__icon_edit"
                                            />
                                        </aura:if>
                                        <aura:set attribute="else">
                                            <!--EDIT MODE-->
                                            <aura:if isTrue="{!field.isEditSpecialType}">
                                                <aura:if isTrue="{!field.type == 'picklist'}">
                                                    <lightning:select label="Hidden" variant="label-hidden" class="slds-truncate ctInput" name="{!rowIndex+'-'+fieldIndex}" value="{!field.value}" onchange="{!c.onInputChange}">
                                                        <aura:iteration items="{!field.selectOptions}" var="pl">
                                                            <option value="{!pl.value}">{!pl.label}</option>
                                                        </aura:iteration>
                                                    </lightning:select>
                                                </aura:if>
                                                <aura:set attribute="else">
                                                    <lightning:input
                                                        name="{!rowIndex+'-'+fieldIndex}"
                                                        type="{!field.type}"
                                                        value="{!field.value}"
                                                        variant="label-hidden"
                                                        onchange="{!c.onInputChange}"
                                                        class="ctInput"
                                                        formatter="{!field.formatter}"
                                                    />
                                                </aura:set>
                                            </aura:if>
                                        </aura:set>
                                    </aura:if>
                                </span>
                            </td>
                        </aura:iteration>
                    </tr>
                </aura:iteration>
            </tbody>
        </table>
        <aura:if isTrue="{!v.tableData.length == 0}">
            <div class="slds-p-left_x-small slds-p-vertical_xx-small slds-border_bottom">
                No records found to display!
            </div>
        </aura:if>
    </div>
    <aura:if isTrue="{!v.isEditModeOn}">
        <div class="ctFooter slds-modal__footer">
            <div class="slds-grid_align-center">
                <div
                    class="slds-text-color_error slds-p-bottom_small"
                    style="
                         {
                            !v.error?'display:block': 'display:none';
                        }
                    "
                >
                    {!v.error}
                </div>
                <div class="slds-grid slds-grid_align-center">
                    <lightning:button label="Cancel" onclick="{!c.closeEditMode}" />
                    <lightning:button label="Save" variant="brand" onclick="{!c.saveRecords}" />
                </div>
            </div>
        </div>
    </aura:if>
</aura:component>

Controller:

dataTableController.js

({
	doInit: function (component, event, helper) {
		helper.setupTable(component);
	},
	sortTable: function (component, event, helper) {
		component.set("v.isLoading", true);
		setTimeout(function () {
			var childObj = event.target;
			var parObj = childObj.parentNode;
			while (parObj.tagName != 'TH') {
				parObj = parObj.parentNode;
			}
			v
			ar sortBy = parObj.name, //event.getSource().get("v.name"),
				sortDirection = component.get("v.sortDirection"),
				sortDirection = sortDirection === "asc" ? "desc" : "asc"; //change the direction for next time
			component.set("v.sortBy", sortBy);
			component.set("v.sortDirection", sortDirection);
			helper.sortData(component, sortBy, sortDirection);
			component.set("v.isLoading", false);
		}, 0);
	},
	calculateWidth: function (component, event, helper) {
		var childObj = event.target;
		var parObj = childObj.parentNode;
		var startOffset = parObj.offsetWidth - event.pageX;
		component.set("v.startOffset", startOffset);
	},
	setNewWidth: function (component, event, helper) {
		var childObj = event.target;
		var parObj = childObj.parentNode;
		while (parObj.tagName != 'TH') {
			parObj = parObj.parentNode;
		}
		v
		ar startOffset = component.get("v.startOffset");
		var newWidth = startOffset + event.pageX;
		parObj.style.width = newWidth + 'px';
	},
	editField: function (component, event, helper) {
		var field = event.getSource(),
			indexes = field.get("v.name"),
			rowIndex = indexes.split('-')[0],
			colIndex = indexes.split('-')[1];
		var data = component.get("v.tableData");
		data[rowIndex].fields[colIndex].mode = 'edit';
		data[rowIndex].fields[colIndex].tdClassName = 'slds-cell-edit slds-is-edited';
		component.set("v.tableData", data);
		component.set("v.isEditModeOn", true);
	},
	onInputChange: function (component, event, helper) {
		var field = event.getSource(),
			value = field.get("v.value"),
			indexes = field.get("v.name"),
			rowIndex = indexes.split('-')[0],
			colIndex = indexes.split('-')[1];
		helper.updateTable(component, rowIndex, colIndex, value);
	},
	onRowAction: function (component, event, helper) {
		var actionEvent = component.getEvent("dataTableRowActionEvent"),
			indexes = event.target.id, //rowIndex-colIndex-actionName
			params = indexes.split('-'),
			data = component.get("v.dataCache");
		actionEvent.setParams({
			actionName: params[2],
			rowData: data[params[0]]
		});
		actionEvent.fire();
	},
	closeEditMode: function (component, event, helper) {
		component.set("v.buttonsDisabled", true);
		component.set("v.buttonClicked", "Cancel");
		component.set("v.isLoading", true);
		setTimeout(function () {
			var dataCache = component.get("v.dataCache");
			var originalData = component.get("v.tableDataOriginal");
			component.set("v.data", JSON.parse(JSON.stringify(dataCache)));
			component.set("v.tableData", JSON.parse(JSON.stringify(originalData)));
			component.set("v.isEditModeOn", false);
			component.set("v.isLoading", false);
			component.set("v.error", "");
			component.set("v.buttonsDisabled", false);
			component.set("v.buttonClicked", "");
		}, 0);
	},
	saveRecords: function (component, event, helper) {
		component.set("v.buttonsDisabled", true);
		component.set("v.buttonClicked", "Save");
		component.set("v.isLoading", true);
		setTimeout(function () {
			var saveEvent = component.getEvent("dataTableSaveEvent");
			saveEvent.setParams({
				tableAuraId: component.get("v.auraId"),
				recordsString: component.get("v.modifiedRecords")
			});
			saveEvent.fire();
		}, 0);
		component.set("v.isLoading", false);
	},
	finishSaving: function (component, event, helper) {
		var params = event.getParam('arguments');
		if (params) {
			var result = params.result, //Valid values are "SUCCESS" or "ERROR"
				data = params.data, //refreshed data from server
				message = params.message;
			console.log('message---' + JSON.stringify(message));
			if (result === "SUCCESS") {
				var toastEvent = $A.get("e.force:showToast");
				toastEvent.setParams({
					"title": "Success!",
					"type": 'success',
					"message": "Records has been updated Successfully!"
				});
				toastEvent.fire();
				if (data) {
					helper.setupData(component, data);
				} else {
					var dataCache = component.get("v.dataCache"),
						updatedData = component.get("v.updatedTableData");
					component.set("v.data", JSON.parse(JSON.stringify(dataCache)));
					component.set("v.tableDataOriginal", JSON.parse(JSON.stringify(updatedData)));
					component.set("v.tableData", JSON.parse(JSON.stringify(updatedData)));
				}
				c
				omponent.set("v.isEditModeOn", false);
			}
			e
			lse {
				if (message) component.set("v.error", message);
				var toastEvent = $A.get("e.force:showToast");
				toastEvent.setParams({
					"title": "Error!",
					"type": 'error',
					"message": "Error in Updating Records!" + message
				});
				toastEvent.fire();
			}
		}
		c
		omponent.set("v.isLoading", false);
		component.set("v.buttonsDisabled", false);
		component.set("v.buttonClicked", "");
	}
})

Helper :

dataTableHelper.js

({
    setupTable: function (component, data) {
        var cols = component.get("v.columns"),
            data = component.get("v.data");
        this.setupColumns(component, cols);
        this.setupData(component, data);
        component.set("v.isLoading", false);
    },
    setupColumns: function (component, cols) {
        var tempCols = [];
        if (cols) {
            cols.forEach(function (col) {
                //set col values
                col.thClassName = "slds-truncate";
                col.thClassName += col.sortable === true ? " slds-is-sortable" : "";
                col.thClassName += col.resizable === true ? " slds-is-resizable" : "";
                col.style = col.width ? "width:" + col.width + "px;" : "";
                col.style += col.minWidth ? "min-width:" + col.minWidth + "px;" : "";
                col.style += col.maxWidth ? "max-width:" + col.maxWidth + "px;" : "";
                col.style += "position:sticky; top:0; z-index: 1;";
                if (col.sortable === true) {
                    col.sortBy = col.fieldName;
                    if (col.type === "link" && col.attributes && typeof col.attributes.label === "object") col.sortBy = col.attributes.label.fieldName;
                } //
                if (!tableData) col.thClassName = "";
                tempCols.push(col);
            });
            component.set("v.columns", JSON.parse(JSON.stringify(tempCols)));
        }
    },
    setupData: function (component, data) {
        var tableData = [],
            cols = component.get("v.columns");
        component.set("v.dataCache", JSON.parse(JSON.stringify(data)));
        if (data) {
            data.forEach(function (value, index) {
                var row = {},
                    fields = [];
                cols.forEach(function (col) {
                    //set data values
                    var field = {};
                    field.name = col.fieldName;
                    field.value = value[col.fieldName];
                    field.type = col.type ? col.type : "text";
                    if (field.type === "percent") {
                        field.isViewSpecialType = true;
                        if (col.attributes) {
                            field.formatter = col.attributes.formatter;
                            field.style = col.attributes.formatter;
                            field.minimumFractionDigits = col.attributes.minimumFractionDigits ? col.attributes.minimumFractionDigits : 2;
                            field.maximumFractionDigits = col.attributes.maximumFractionDigits ? col.attributes.maximumFractionDigits : 2;
                            field.currencyCode = col.attributes.currencyCode ? col.attributes.currencyCode : "USD";
                        }
                    }
                    if (field.type === "currency") {
                        field.isViewSpecialType = true;
                    }
                    if (field.type === "date") {
                        field.isViewSpecialType = true;
                    }
                    if (field.type === "number") {
                        field.isViewSpecialType = true;
                        if (col.attributes) {
                            field.formatter = col.attributes.formatter;
                            field.style = col.attributes.formatter;
                            field.minimumFractionDigits = col.attributes.minimumFractionDigits ? col.attributes.minimumFractionDigits : 0;
                            field.maximumFractionDigits = col.attributes.maximumFractionDigits ? col.attributes.maximumFractionDigits : 2;
                            field.currencyCode = col.attributes.currencyCode ? col.attributes.currencyCode : "USD";
                        }
                    }
                    if (field.type === "picklist") {
                        field.isEditSpecialType = true;
                        field.selectOptions = col.selectOptions;
                    }
                    if (field.type === "link") {
                        field.isViewSpecialType = true;
                        if (col.attributes) {
                            if (typeof col.attributes.label === "object") field.label = value[col.attributes.label.fieldName];
                            else field.label = col.attributes.label;
                            if (typeof col.attributes.title === "object") field.title = value[col.attributes.title.fieldName];
                            else field.title = col.attributes.title;
                            if (col.attributes.actionName) {
                                field.type = "link-action";
                                field.actionName = col.attributes.actionName;
                            }
                            f;
                            ield.target = col.attributes.target;
                        }
                    }
                    f;
                    ield.editable = col.editable ? col.editable : false;
                    field.tdClassName = field.editable === true ? "slds-cell-edit" : "";
                    field.mode = "view";
                    fields.push(field);
                });
                row.id = value.Id;
                row.fields = fields;
                tableData.push(row);
            });
            component.set("v.tableData", tableData);
            component.set("v.tableDataOriginal", JSON.parse(JSON.stringify(tableData)));
            component.set("v.updatedTableData", JSON.parse(JSON.stringify(tableData)));
        }
    },
    updateTable: function (component, rowIndex, colIndex, value) {
        //Update Displayed Data
        var data = component.get("v.tableData");
        data[rowIndex].fields[colIndex].value = value;
        component.set("v.tableData", data);
        //Update Displayed Data Cache
        var updatedData = component.get("v.updatedTableData");
        updatedData[rowIndex].fields[colIndex].value = value;
        updatedData[rowIndex].fields[colIndex].mode = "view";
        component.set("v.updatedTableData", updatedData);
        //Update modified records which will be used to update corresponding salesforce records
        //
        var newList = [];
        // component.set("v.modifiedRecords", newList);
        var records = component.get("v.modifiedRecords");
        console.log("records---" + JSON.stringify(records));
        var recIndex = records.findIndex((rec) => rec.id === data[rowIndex].id);
        if (recIndex !== -1) {
            records[recIndex]["" + data[rowIndex].fields[colIndex].name] = value;
        } else {
            var obj = {};
            obj["id"] = data[rowIndex].id;
            obj["" + data[rowIndex].fields[colIndex].name] = value;
            records.push(obj);
        }
        c;
        omponent.set("v.modifiedRecords", records);
        //Update Data Cache
        var dataCache = component.get("v.dataCache");
        var recIndex = dataCache.findIndex((rec) => rec.Id === data[rowIndex].id);
        var fieldName = data[rowIndex].fields[colIndex].name;
        dataCache[recIndex][fieldName] = value;
        component.set("v.dataCache", dataCache);
    },
    sortData: function (component, sortBy, sortDirection) {
        var reverse = sortDirection !== "asc",
            data = component.get("v.dataCache");
        if (!data) return;
        var data = Object.assign([], data.sort(this.sortDataBy(sortBy, reverse ? -1 : 1)));
        this.setupData(component, data);
    },
    sortDataBy: function (field, reverse, primer) {
        var key = primer
            ? function (x) {
                  return primer(x[field]);
              }
            : function (x) {
                  return x[field];
              };
        return function (a, b) {
            var A = key(a);
            var B = key(b);
            return reverse * ((A > B) - (B > A));
        };
    },
});

Style :

.THIS .cTable.slds-table_fixed-layout tbody {
     transform: none !important;
}
.THIS .cTable thead th {
     background-color:#f9f9fa;
    min-height: 1.3rem;
}
.THIS .cTable tbody td {
     padding-top: .25rem;
     padding-bottom: .25rem;
}
.THIS .cTable .slds-th__action {
     padding: .5rem;
     height: 1.75rem;
}
.T HIS .ctInput {
     width: 100%;
     height: 1.5rem;
     min-height: 1.5rem;
     line-height: 1.5rem;
}
.THIS .ctInput .slds-input {
     height: 1.5rem;
     min-height: 1.5rem;
     line-height: 1.5rem;
}
.THIS .ctInput .slds-select {
     min-height: 1rem !important;
     height: 1.5rem;
     padding-left:.3rem !important;
     padding-right:1.2rem !important;
}
.THIS .ctInput .slds-select_container::before {
     border-bottom: 0 !important;
}
.THIS .ctInput .slds-form-element__label {
     display: none;
}
.THIS .ctFooter.slds-modal__footer {
     padding: .5rem;
     text-align: center;
     width: 165rem;
}
.THIS .slds-datepicker table {
     table-layout: fixed;
     width: 300px;
}
.THIS .slds-datepicker .slds-day{
     width: auto !important;
     min-width: auto !important;
     height: auto !important;
     line-height: inherit !important;
}
.THIS .slds-shrink-none {
     margin-top: 5px;
}
.THIS .cTable thead th {
     width: 130px;
}
 .THIS .table-container {
     width: 100%;
     overflow: auto;
}
.THIS .spinclass {
     position: relative;
     display: inline-block;
}
 / *Leads Column Freeze Style*/
 .THIS .table-freeze-style .header-style td {
     text-align:left !important;
     background:#f4f4f4 !important;
     font-weight:bold;
}
 .T HIS .table-freeze-style .header-style td span {
     padding-left:0px !important;
}
 .THIS .table-freeze-style tr td {
     width:130px !important;
}
.THIS .table-freeze-style tr td:nth-child(1) .THIS .table-freeze-style tr td:nth-child(2), .THIS .table-freeze-style tr td:nth-child(3) {
     background : white !important;
     position: sticky !important;
     left: 0 !important;
     z-index: 2 !important;
}

Events :

dataTableSaveEvent :

<aura:event type="COMPONENT" description="Event template">
    <aura:attribute name="tableAuraId" type="String" />
    <aura:attribute name="recordsString" type="List" /> 
<!-- Records JSON String -->
</aura:event>

dataTableRowActionEvent :

<aura:event type="COMPONENT" description="Event template">
    <aura:attribute name="actionName" type="String" />
    <aura:attribute name="rowData" type="Object" /> 
<!-- sObject record -->
</aura:event>

Apex Controller:

InlineEditController.apxc

public with sharing class InlineEditController {
@AuraEnabled
public static List<Case> getRecords(String recordId) {
    List<Case> caseList = new List<Case>();
    caseList = [ SELECT Id, CaseNumber, Reason, Origin
        FROM Case
        WHERE AccountId =: recordId
        ];
        return caseList;
    }
@AuraEnabled
public static void updateRecords(String jsonString){
    try{
            List<Case> records = (List<Case>) JSON.deserialize(jsonString,     List<Case>.class);
            update records;
        }
    catch(Exception e){
            throw new AuraHandledException(e.getMessage());
        }
    }
@AuraEnabled
public static Map<String,String> getPicklistValues(String objectAPIName, String fieldAPIName){
    Map<String,String> pickListValuesMap = new Map<String,String>();
    Schema.SObjectType convertToObj = Schema.getGlobalDescribe().get(objectAPIName);
    Schema.DescribeSObjectResult descResult = convertToObj.getDescribe();
    Schema.DescribeFieldResult fieldResult = descResult.fields.getMap().get(fieldAPIName).getDescribe();
    Boolean isFieldNotRequired = fieldResult.isNillable();
    List<Schema.PicklistEntry> ple = fieldResult.getPicklistValues();
    for(Schema.PicklistEntry pickListVal : ple){
        if(isFieldNotRequired)
            pickListValuesMap.put('--None--', '');
        if(pickListVal.isActive())
            pickListValuesMap.put(pickListVal.getLabel(), pickListVal.getValue());
    }
    return pickListValuesMap;
   }
}