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.
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.
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.
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.
Thanks a lot for sharing, Nice Works..!!
ReplyDeleteand coming to the blog, It is Well designed.
This is the best blog I've ever seen...
Thanks for the feedback!
DeleteThat's great! Thanks for posting it! Do you know how can I enable the click event for each row of the table?
ReplyDeleteCreate 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!
DeleteThis comment has been removed by the author.
DeleteCan we have a similar component using Lightning web component
ReplyDeletehere is an example something similar for LWC https://blog.lkatney.com/2019/11/13/picklist-in-lightning-datatable/
DeleteHere is the long waited LWC version. Please check it out.
Deletehttps://vkambham.blogspot.com/2021/01/lwc-datatable.html
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.
ReplyDelete@Venky, I hope this is fine with you.
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!
ReplyDeletePlease 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!
DeleteDo you know how is it possible to create a method to add a new row?
ReplyDeleteWe 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.
DeleteI 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?
ReplyDeleteDo you see any errors in the browser console? If not, please check once the column names are correct for the data you are fetching.
DeleteHello 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
DeleteIt'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!
I have used in cmp
ReplyDeleteaura: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.
Hey Rahul, please check my latest post about lookup component and I guess it will resolve your problem.
DeleteHello, 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.
ReplyDelete1. For Checkbox, view displaying checkbox but not with default value( Record Value ).
2. How we can handle Lookups
THanks
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.
DeleteHi VK, very nice component and extremely useful!
ReplyDeleteI 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
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.
DeleteHere is the long waited LWC version with the checkbox functionality you're looking for. Please check it out.
Deletehttps://vkambham.blogspot.com/2021/01/lwc-datatable.html
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
ReplyDeleteCan you do this with LWC datatable?
ReplyDeleteI am on it, you will see it soon.
DeleteHere is the long waited LWC version. Please check it out.
Deletehttps://vkambham.blogspot.com/2021/01/lwc-datatable.html
Hi Venky,
ReplyDeleteThis 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.
Hi Venky,
ReplyDeleteThank 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
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?
ReplyDeleteExample:
This is where my link goes --- Work_Plan__c/a023h000001drfhAAA/a083h000001B8zJAAS
This is where my link should go --- ObjectiveActivity__c/a083h000001B8zJAAS/view
It is about passing right data to the fieldName in the columns you prepare.
DeleteFor 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';
Thank you so much! I found my error - I hadn't updated the loadRecords method with the proper variable name.
DeleteHello! 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?
ReplyDeleteThanks!!
Hello!
ReplyDeleteI 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!!
Please make sure you have created all the components and methods with correct names.
DeleteIs 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
ReplyDeleteI have implemented it in lwc and will post it here soon.
DeleteHere is the long waited LWC version with the checkbox functionality you're looking for. Please check it out.
Deletehttps://vkambham.blogspot.com/2021/01/lwc-datatable.html
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!
ReplyDeleteHi Bodgan, thanks and I appreciate your effort. I don't have any repo. May be I will create one and share the details soon.
DeleteBest Code work bro!
ReplyDeleteThanks!
DeleteTo have it so the table will add rows upon datachange from the parent component, add the following to the top of your dataTable.cmp:
ReplyDeleteGood luck! looking forward to the LWC version :)
sorry! did not include copy paste of this event
Deletehandler change, value v.data, recall doInit. doesnt let me post it
DeleteThanks for your suggestion. Please copy it as text and post it here. May be it helps for people who might have the same scenario.
DeleteHere is the long waited LWC version. Please check it out.
Deletehttps://vkambham.blogspot.com/2021/01/lwc-datatable.html
Hi Venky,
ReplyDeleteThanks for this article. Can you please let me know how can I add search filter to this?
Thanks
Sachin
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?
DeleteHow can I apply mass update with selected row in this table?
ReplyDeleteThank and best regards.
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?
ReplyDeleteYou can have as many picklist fields as you can. You just need to pass other picklist fields the same way.
DeleteHere is the long waited LWC version. Please check it out.
ReplyDeletehttps://vkambham.blogspot.com/2021/01/lwc-datatable.html
Hi Venky,
ReplyDeleteThanks for this article, its helpful.
Thank you. Please have a look at the lwc version also.
Deletehttps://vkambham.blogspot.com/2021/01/lwc-datatable.html
HI VK,
ReplyDeleteGood Work. Can you please let me know how to implement pagination in this component?
Please check below post.
Deletehttps://vkambham.blogspot.com/2021/01/lwc-datatable.html
it's very strong what you did
ReplyDeleteI 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
Hello
ReplyDeleteIn 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.
Just add another picklist field to your columns like first one and get/add picklist options for that.
DeleteHello,
ReplyDeleteCan we use this for inline editing of multiple records with data table having multiple picklist fields for a custom object?
Hey Anusha, Yes! we can do that. Do you see any difficulties while doing that?
DeleteHi, 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:
ReplyDeleteError 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!
Please create Aura Events first through VS Code or Developer Console and then create Aura Components.
DeleteDude, you save my life with this !
ReplyDeleteThere 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
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