How to access Picklist field in Salesforce Lightning Datatable?

Salesforce made our life easier by building many standard Lightning UI components like lightning:input, lightning:card, lightning:datatable etc., to use in our custom component development. However, there are few limitations on few components which don't fit in all business scenarios, one of which is, picklist field support in it's standard lightning:datatable component. In standard lightning:datatable, if we add a picklist field and try to edit that field, it will show as text field instead of picklist field with it's values. So, I am writing this to share the dataTable component I have built, which supports the picklist field in editable data table.

Latest version: Here is the LWC version of the same which now supports lookup field as well along with picklist and checkbox functionality with pagination.


Okay, let's jump directly to the implementation. Go to your salesforce org and create below base reusable lightning aura components in the order mentioned below.

dataTableSaveEvent.evt

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

dataTableRowActionEvent.evt

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

dataTable.cmp

This takes the same type of "data" and "columns" as lightning:datatable and process it to serve different purposes like a field can be editable or not, sortable or not and resizable or not. This component supports all data types we can use in lightning:input component, anchor link data type and picklist data types. You can add remaining data types like button, lookup etc., data types same as the picklist data type implementation.
<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"/>
    <!--
        COLUMNS SHOULD BE IN BELOW FORMAT
        [{label: "Account Name", fieldName: "accountLink", type:"link", sortable: true, resizable:true, 
          attributes:{label:{fieldName:"Name"}, title:"Click to View(New Window)", target:"_blank"}},
         {label: "Created Date", fieldName: "CreatedDate", type:"date", editable: true},
         {label: "Active", fieldName: "Active__c", editable: true, type:"picklist", selectOptions:[{label:'Yes',value:'Yes'},{label:'No',value:'No'},]},
         {label: "Type", fieldName: "Type", editable: true, type:"picklist", selectOptions:types},            
         {label: "Shipping Street", fieldName: "ShippingStreet", sortable: true, },
         {label: "Shipping City", fieldName: "ShippingCity", editable: true},            
         {label: "Shipping State", fieldName: "ShippingState"},
         {label: "Shipping PostalCode", fieldName: "ShippingPostalCode"},
         {label: "Shipping Country", fieldName: "ShippingCountry"}], 
    -->
    <!-- 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>
    
    <div class="slds-table_edit_container slds-is-relative">
        <aura:if isTrue="{!v.isLoading}">
            <lightning:spinner alternativeText="Loading" />
        </aura:if>
        <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" role="grid">
            <thead>
                <tr class="slds-line-height_reset">
                    <aura:if isTrue="{!v.showRowNumberColumn}">
                        <th scope="col" style="width:50px;max-width:60px;text-align:center;">#</th>
                    </aura:if>
                    <aura:iteration items="{!v.columns}" var="col">
                        <th 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>
                        </th>
                    </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}"/>
                                        </aura:if>
                                        <aura:if isTrue="{!!field.isViewSpecialType}">
                                            <span class="slds-truncate" 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>
        <aura:if isTrue="{!v.isEditModeOn}">
            <div class="ctFooter slds-modal__footer">
                <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>
        </aura:if>        
    </div>
</aura:component>


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;
            }
            var 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;
        }
        var 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: JSON.stringify(component.get("v.modifiedRecords"))
            });
            saveEvent.fire();
        }, 0);
    },
    
    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;
            
            if(result === "SUCCESS"){//success
                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)));                    
                }
                component.set("v.isEditModeOn", false);
            }else{
                if(message) component.set("v.error", message);
            }
        }
        component.set("v.isLoading", false);
        component.set("v.buttonsDisabled", false);
        component.set("v.buttonClicked", "");
    }    
})


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){
            /*COLUMNS SHOULD BE IN BELOW FORMAT
            [{label: "Oppotunity Name", fieldName: "oppLink", type:"link", sortable: true, resizable: true, 
              attributes:{label:{fieldName:"Name"}, title:"Click to View(New Window)", target:"_blank"}},
             {label: "Type", fieldName: "Type", editable: true, type:"picklist", selectOptions:types},            
             {label: "Stage", fieldName: "StageName", sortable: true},                                
             {label: "Close Date", fieldName: "CloseDate", type:"date", editable: true, resizable: true}, 
             {label: "Amount", fieldName: "Amount", type:"number", editable: true, attributes:{formatter:"currency"}},
             {label: "Owner", fieldName: "ownerLink", type:"link", sortable: true, resizable: true, 
              attributes:{label:{fieldName:"ownerName"}, title:"Click to View(New Window)", target:"_blank"}}],
            */
            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;" : "";
                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 === "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;
                            }                                
                            field.target = col.attributes.target;
                        }
                    }
                    field.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 records = component.get("v.modifiedRecords");
        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);
        }
        component.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));
        };
    },
    
})


dataTable.css

.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;
}
.THIS .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;
}
.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;
}


Now, let's go and use the above components wherever you need. Here, I am going to show how can you use them with a simple example where it displays list of accounts.

AccountsTableController.apxc

public with sharing class AccountsTableController {
    
    @AuraEnabled
    public static List<Account> getRecords() {
        List<Account> accs = [SELECT Id, Name, Type, ShippingStreet, ShippingCity, ShippingState, ShippingPostalCode, ShippingCountry, CreatedDate
                              FROM Account ORDER BY Name LIMIT 10];
        return accs;
    }
    
    @AuraEnabled
    public static void updateRecords(String jsonString){
        try{
            List<Account> records = (List<Account>) JSON.deserialize(jsonString, List<Account>.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;
    }
}


accountsTable.cmp

<aura:component implements="force:appHostable" controller="AccountsTableController">
    <aura:attribute name="data" type="Object" />
    <aura:attribute name="columns" type="List" />
    <aura:attribute name="isLoading" type="Boolean" default="false"/>
    
    <aura:handler name="init" value="{!this}" action="{!c.doInit}" />
    <aura:handler name="dataTableSaveEvent" event="c:dataTableSaveEvent" action="{!c.saveTableRecords}"/>    
    
    <aura:if isTrue="{!v.data.length > 0}">
        <lightning:card title="Data Table">
            <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:component>

accountsTableController.js

({
    doInit : function(component, event, helper) {
        component.set("v.isLoading", true);
        helper.setupTable(component);
    },
    
    saveTableRecords : function(component, event, helper) {
        var recordsData = event.getParam("recordsString");
        var tableAuraId = event.getParam("tableAuraId");
        var action = component.get("c.updateRecords");
        action.setParams({
            jsonString: recordsData
        });
        action.setCallback(this,function(response){
            var datatable = component.find(tableAuraId);
            datatable.finishSaving("SUCCESS");
        });
        $A.enqueueAction(action);        
    }
})

accountsTableHelper.js

({
    setupTable : function(component) {
        var action = component.get("c.getPicklistValues");
        action.setParams({
            objectAPIName: "Account",
            fieldAPIName: "Type"
        });
        action.setCallback(this,function(response){
            if(response.getState() === "SUCCESS"){
                var types = [];
                Object.entries(response.getReturnValue()).forEach(([key, value]) => types.push({label:key,value:value}));
                var cols = [
                    {label: "Account Name", fieldName: "accountLink", type:"link", sortable: true, resizable:true, 
                     attributes:{label:{fieldName:"Name"}, title:"Click to View(New Window)", target:"_blank"}},
                    {label: "Created Date", fieldName: "CreatedDate", type:"date", sortable: true},
                    {label: "Type", fieldName: "Type", editable: true, type:"picklist", selectOptions:types},            
                    {label: "Shipping Street", fieldName: "ShippingStreet", sortable: true, },
                    {label: "Shipping City", fieldName: "ShippingCity", editable: true},            
                    {label: "Shipping State", fieldName: "ShippingState"},
                    {label: "Shipping PostalCode", fieldName: "ShippingPostalCode"},
                    {label: "Shipping Country", fieldName: "ShippingCountry"},                                
                    
                ];
                component.set("v.columns", cols);
                this.loadRecords(component);
            }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);
    },
                    
    loadRecords : function(component) {
        var action = component.get("c.getRecords");
        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);
    },
})

PS: Thanks for reading, hope this helps. By the way, this is my first blog post, your suggestions/feedback is most welcome.

Most wanted: Here is the long waited LWC version which now supports lookup field as well along with picklist and checkbox functionality with pagination.

66 Comments

  1. Thanks a lot for sharing, Nice Works..!!
    and coming to the blog, It is Well designed.
    This is the best blog I've ever seen...

    ReplyDelete
  2. That's great! Thanks for posting it! Do you know how can I enable the click event for each row of the table?

    ReplyDelete
    Replies
    1. Create dataTableRowAction.evt and uncomment the line where we register this event in dataTable.cmp (post updated with these changes). And then pass the actionName:"some action" in the attributes of column. It will now work as action link, and fires the row action event. You have to capture this component event and perform next steps based on the actionName and row data coming from this event parametes. Let me know if that works!

      Delete
    2. This comment has been removed by the author.

      Delete
  3. Can we have a similar component using Lightning web component

    ReplyDelete
    Replies
    1. here is an example something similar for LWC https://blog.lkatney.com/2019/11/13/picklist-in-lightning-datatable/

      Delete
    2. Here is the long waited LWC version. Please check it out.
      https://vkambham.blogspot.com/2021/01/lwc-datatable.html

      Delete
  4. This is a very nice & clean solution of adding picklist into lightning data table. I have implemented this for one of my personal projects. I have moved all this code here (https://www.playg.app/play/picklist-in-lightning-datatable-aura) so that it can be imported directly into any salesforce org for future purposes. This will also make it reusable for other users.

    @Venky, I hope this is fine with you.

    ReplyDelete
  5. Venky, thanks so much for this--I've tried several other implementations and none work as well. That being said, the only issue I'm having with your implementation is that I'm unable to get the table to horizontally scroll. All my columns are automatically given equal width on one page. I've tried various overrides, editing your code to higher minimum width, without improvement. I'd appreciate any pointers. Thanks again!

    ReplyDelete
    Replies
    1. Please try by removing the class 'slds-table_fixed-layout' from table tag in dataTable.cmp and let me know if it is not working. Thanks!

      Delete
  6. Do you know how is it possible to create a method to add a new row?

    ReplyDelete
    Replies
    1. We can, but it may take lot of effort to include it in this component. I would suggest, better create a new component for new row addition and display it just below this table when user clicks on the new row button.

      Delete
  7. I am trying to get this to work but the data is not setting in the table. I have an alert statement on the Accounts Table Helper that shows the data is there. However, the table is not showing any records. Has anyone else run into this issue?

    ReplyDelete
    Replies
    1. Do you see any errors in the browser console? If not, please check once the column names are correct for the data you are fetching.

      Delete
    2. Hello V K, This is good block and work. I like it, but I have question how to use custom lookup in data table i have also pass from data table "Datatable.cmp" type:'lookup' and also use custom component over there like




      It's looking fine and also working for selecting record but when i have change lookup value and save the record but still not change because onInputChange() not calling so i have a doubt

      How to i use this thing in my custom component like
      name="{!rowIndex+'-'+fieldIndex}" value="{!field.value}" onchange="{!c.onInputChange}"
      when i change lookup value and save the record then how to call onchange="{!c.onInputChange}" and also how to set name="{!rowIndex+'-'+fieldIndex}" value="{!field.value}".

      Please look at this and let me know. I'm still waiting your reply. Thanks!

      Delete
  8. I have used in cmp
    aura:if isTrue="{!field.type == 'lookup'}"
    c:customLookup objectAPIName="Part__c" IconName="standard:account" selectedRecord="{!v.selectedRecord}"/
    /aura:if

    How i use name="{!rowIndex+'-'+fieldIndex}" value="{!field.value}" onchange="{!c.onInputChange}" in that. because this field value not getting in onInputChange() method.

    ReplyDelete
    Replies
    1. Hey Rahul, please check my latest post about lookup component and I guess it will resolve your problem.

      Delete
  9. Hello, very useful article, I have a use case extending to this scenario. Can you please help with Checkbox and Lookup fields as well along with Picklist fields.
    1. For Checkbox, view displaying checkbox but not with default value( Record Value ).
    2. How we can handle Lookups

    THanks

    ReplyDelete
    Replies
    1. Thanks for checking. Please check my post about the lookup and use the same in your table same as picklist. Before you do it, please read about "How to use lwc in aura component?". The checkbox thing, I am not able to understand whats your requirement is.

      Delete
  10. Hi VK, very nice component and extremely useful!

    I have two (kind of 3) questions about building upon your current implementation.

    1. Is there a way to include checkboxes for each row and the select all rows check box? Similar to how it works with standard lightning:datatable

    2. You have to click on the edit icon to enter the picklist each time. Is it possible to show a default value and make it so the user can edit upon clicking the value?

    Thank you for releasing this!!


    Best,

    Jake

    ReplyDelete
    Replies
    1. Hello, please keep an eye on this blog. I will soon add the checkbox functionality. For your second question, you can customize the select component as you like and add an attribute like defaultValue: and use it to achieve your requirement.

      Delete
    2. Here is the long waited LWC version with the checkbox functionality you're looking for. Please check it out.
      https://vkambham.blogspot.com/2021/01/lwc-datatable.html

      Delete
  11. I want to add a column to this table with Picklist values(Read,Write) and functionality of it inline editing should be disabled when I choose Read and enabled when I choose write.any suggestion on how to do it

    ReplyDelete
  12. Can you do this with LWC datatable?

    ReplyDelete
    Replies
    1. I am on it, you will see it soon.

      Delete
    2. Here is the long waited LWC version. Please check it out.
      https://vkambham.blogspot.com/2021/01/lwc-datatable.html

      Delete
  13. Hi Venky,
    This post helped me a lot, it is really nice.
    I have requirement to add button on each row can you please send me the code snippet?

    Note: I have added button but i am confusing to get particular record when button clicked.

    ReplyDelete
  14. Hi Venky,

    Thank you for this post i learned a lot from it.
    But can you help me with pagination? I am able to fetch records piece by piece and i can see it in the log but the datatable is not displaying any changes any new data.

    Best regards
    Herry

    ReplyDelete
  15. This is quite amazing, thank you! I've adapted this solution and have a question about the link. I'm using this to link to child records - so my link field renders incorrectly. Work Plan is the Parent Object - ObjectiveActivity is the Child Object. Any guidance?
    Example:
    This is where my link goes --- Work_Plan__c/a023h000001drfhAAA/a083h000001B8zJAAS
    This is where my link should go --- ObjectiveActivity__c/a083h000001B8zJAAS/view

    ReplyDelete
    Replies
    1. It is about passing right data to the fieldName in the columns you prepare.
      For example, in the below one, you have pass correct link data to the 'accountLink' field.
      {label: "Account Name", fieldName: "accountLink", type:"link", attributes:{label:{fieldName:"Name"}, target:"_blank"}

      In your case, populate your link like yourLink = '/ObjectiveActivity__c/'+recordId+'/view';

      Delete
    2. Thank you so much! I found my error - I hadn't updated the loadRecords method with the proper variable name.

      Delete
  16. Hello! I am new in components and I copied and pasted the example of accounts and ir does not appear in the custom components, I think I am missing something, Can you help me?

    Thanks!!

    ReplyDelete
  17. Hello!

    I have the following error, when i tried to add in a landing page
    Uncaught Action failed: c:dataTable$controller$doInit [types is not defined]
    Callback failed: apex://AccountsTableController/ACTION$getRecords

    Could you help me please?
    Thanks!!

    ReplyDelete
    Replies
    1. Please make sure you have created all the components and methods with correct names.

      Delete
  18. Is there a way to include checkboxes for each row and the select all rows check box? Similar to how it works with standard lightning:datatable

    ReplyDelete
    Replies
    1. I have implemented it in lwc and will post it here soon.

      Delete
    2. Here is the long waited LWC version with the checkbox functionality you're looking for. Please check it out.
      https://vkambham.blogspot.com/2021/01/lwc-datatable.html

      Delete
  19. Hi Venky, Do you have github repo? I did some small changes to your dataTable. And now I can use a picklist as a lookup field and i show Name instead of Id of this field, when the table in view mode, which is really cool. And one more thing, your code is awesome, very clean and intuitively readable! It was really easy to understand the functionality!

    ReplyDelete
    Replies
    1. Hi Bodgan, thanks and I appreciate your effort. I don't have any repo. May be I will create one and share the details soon.

      Delete
  20. To have it so the table will add rows upon datachange from the parent component, add the following to the top of your dataTable.cmp:



    Good luck! looking forward to the LWC version :)

    ReplyDelete
    Replies
    1. sorry! did not include copy paste of this event

      Delete
    2. handler change, value v.data, recall doInit. doesnt let me post it

      Delete
    3. Thanks for your suggestion. Please copy it as text and post it here. May be it helps for people who might have the same scenario.

      Delete
    4. Here is the long waited LWC version. Please check it out.
      https://vkambham.blogspot.com/2021/01/lwc-datatable.html

      Delete
  21. Hi Venky,

    Thanks for this article. Can you please let me know how can I add search filter to this?

    Thanks
    Sachin

    ReplyDelete
    Replies
    1. hi did you ever get the search feature working? i tried by adding a text input above the table and passing the value over to the apex class to filter records. however not sure how to handle the structure and caching of the dataTable component and controllers to basically refresh it?

      Delete
  22. How can I apply mass update with selected row in this table?
    Thank and best regards.

    ReplyDelete
  23. Hi this is very slick thank you! However, I need for multiple picklists to show in the table. Yet it appears I can only have 1? the method only takes an object string and 1 field name. then the result has the map for the value. i have about 3-4 picklist values i need to add to 1 table. am i missing something?

    ReplyDelete
    Replies
    1. You can have as many picklist fields as you can. You just need to pass other picklist fields the same way.

      Delete
  24. Here is the long waited LWC version. Please check it out.
    https://vkambham.blogspot.com/2021/01/lwc-datatable.html

    ReplyDelete
  25. Hi Venky,

    Thanks for this article, its helpful.

    ReplyDelete
    Replies
    1. Thank you. Please have a look at the lwc version also.
      https://vkambham.blogspot.com/2021/01/lwc-datatable.html

      Delete
  26. HI VK,
    Good Work. Can you please let me know how to implement pagination in this component?

    ReplyDelete
    Replies
    1. Please check below post.
      https://vkambham.blogspot.com/2021/01/lwc-datatable.html

      Delete
  27. it's very strong what you did
    I would like to know if you have done a class_test for the class_apex

    my email adress is : hamid.benchikh@gmail.com

    thank you very much

    ReplyDelete
  28. Hello
    In the data table you accessed only one picklist field type. I need one more picklist field to get on data table how to add that field.

    ReplyDelete
    Replies
    1. Just add another picklist field to your columns like first one and get/add picklist options for that.

      Delete
  29. Hello,

    Can we use this for inline editing of multiple records with data table having multiple picklist fields for a custom object?

    ReplyDelete
    Replies
    1. Hey Anusha, Yes! we can do that. Do you see any difficulties while doing that?

      Delete
  30. Hi, this is perfect for my project! Thank you so much for posting! I am fairly new at Aura development, and, the project I'm using is completely Aura components, so, I'm trying to use this for my data table, but, when I copy the code in, add to dataTable AuraDefinitionBundle to my deployment, deploy, but I get:

    Error dataTable Aura Definition Bundle dataTable Aura Definition Event content cannot be empty.
    Error CommunityPlatformContacts The attribute "data" was not found on the COMPONENT markup://c:dataTable

    Where "CommunityPlatformContacts" is my Aura component using the datatable. What am I doing wrong?

    I'm using VSCode, I created using "Create Aura Component" under ./aura folder, then copied code in as you posted, and created 2 new files in this same component for the two events (should I have done "Create Aura Event" outside of this component for these?)

    Again, thanks for posting this!

    ReplyDelete
    Replies
    1. Please create Aura Events first through VS Code or Developer Console and then create Aura Components.

      Delete
  31. Dude, you save my life with this !
    There an easier way to hard code the picklist values if you have an object with more than one picklist field:

    var types = [{label: '--None--', value: ''}, {label: 'Yes', value: 'Yes'}, {label: 'No', value: 'No'}];
    var types2 = [{label: '--None--', value: ''}, {label: 'Positive', value: 'Positive'}, {label: 'Neutral', value: 'Neutral'}, {label: 'Negative', value: 'Negative'}];

    {label: 'Customer impression', fieldName: 'Customer_impression__c', type: 'picklist' ,editable: true, selectOptions: types2},
    {label: 'PSM installed?', fieldName: 'PSM_installed__c', type: 'picklist' ,editable: true, selectOptions:types },

    Example !!!

    Thank you SO much

    ReplyDelete
  32. Hi Venky, Thank you so much for this blog , can you please help me how to add columns dynamically from apex class with the dynamic label not static.

    ReplyDelete
Post a Comment
Previous Post Next Post