draftValues array until the user clicks Save or Cancel on the native footer banner.
Inline editing provides a fast, Excel-like user experience for updating multiple records without opening separate record detail pages or modal forms. By combining lightning-datatable with Lightning Data Service (updateRecord) and refreshApex, you can build a scalable, performant inline-editable grid.
Prerequisites
- A Salesforce Developer Edition, Sandbox, or Scratch Org.
- Salesforce CLI (
sf) installed and authenticated to your org. - Basic understanding of LWC lifecycle hooks, wire adapters, and Lightning Data Service.
Step 1: Create the Server-Side Apex Controller
Create an Apex class named AccountTableController.cls to fetch account records. The method must be annotated with @AuraEnabled(cacheable=true) so it can be wired and refreshed reactively:
public with sharing class AccountTableController {
@AuraEnabled(cacheable=true)
public static List<Account> getAccounts() {
return [
SELECT Id, Name, Phone, Industry, AnnualRevenue
FROM Account
WITH USER_MODE
ORDER BY CreatedDate DESC
LIMIT 15
];
}
}
Step 2: Set Up the LWC Component
sf lightning generate component -n inlineEditDatatable -d force-app/main/default/lwc --type lwc
Step 3: Build the HTML Template
In inlineEditDatatable.html, declare the lightning-datatable. Set key-field="Id", bind draft-values to track edited cells, and attach the onsave event handler:
<template>
<lightning-card title="Inline Editable Account Grid" icon-name="standard:account">
<div class="slds-p-around_medium">
<template lwc:if={isLoading}>
<lightning-spinner alternative-text="Saving updates..." size="small"></lightning-spinner>
</template>
<div style="height: 380px;">
<lightning-datatable
key-field="Id"
data={accounts}
columns={columns}
draft-values={draftValues}
onsave={handleSave}
oncancel={handleCancel}
hide-checkbox-column>
</lightning-datatable>
</div>
</div>
</lightning-card>
</template>
Step 4: Implement Save & Draft Value Logic in JavaScript
In inlineEditDatatable.js, configure columns with editable: true. Use updateRecord from lightning/uiRecordApi inside Promise.all() to persist changes, and invoke refreshApex() to refresh the cached data:
import { LightningElement, wire, track } from 'lwc';
import { updateRecord } from 'lightning/uiRecordApi';
import { refreshApex } from '@salesforce/apex';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import getAccounts from '@salesforce/apex/AccountTableController.getAccounts';
const COLUMNS = [
{ label: 'Account Name', fieldName: 'Name', type: 'text', editable: true },
{ label: 'Phone', fieldName: 'Phone', type: 'phone', editable: true },
{ label: 'Industry', fieldName: 'Industry', type: 'text', editable: true },
{ label: 'Annual Revenue', fieldName: 'AnnualRevenue', type: 'currency', editable: true }
];
export default class InlineEditDatatable extends LightningElement {
columns = COLUMNS;
accounts = [];
@track draftValues = [];
isLoading = false;
wiredAccountsResult;
@wire(getAccounts)
wiredAccountList(result) {
this.wiredAccountsResult = result;
const { data, error } = result;
if (data) {
this.accounts = data;
} else if (error) {
this.showToast('Error Loading Records', error?.body?.message || 'Failed to fetch accounts', 'error');
}
}
async handleSave(event) {
this.isLoading = true;
// Convert draftValues into record inputs formatted for updateRecord
const recordInputs = event.detail.draftValues.map(draft => {
const fields = { ...draft };
return { fields };
});
try {
// Execute parallel DML updates via Lightning Data Service
const promises = recordInputs.map(recordInput => updateRecord(recordInput));
await Promise.all(promises);
this.showToast('Success', 'Records updated successfully!', 'success');
// Clear draft values and refresh the wired dataset
this.draftValues = [];
await refreshApex(this.wiredAccountsResult);
} catch (error) {
this.showToast('Update Error', error?.body?.message || 'Failed to save changes.', 'error');
} finally {
this.isLoading = false;
}
}
handleCancel() {
// Clear all pending edits from the UI banner
this.draftValues = [];
}
showToast(title, message, variant) {
this.dispatchEvent(new ShowToastEvent({ title, message, variant }));
}
}
key-field property on lightning-datatable is case-sensitive and must match the unique key in your dataset (typically Id for Salesforce sObjects). If you set key-field="id" (lowercase) while your record payload contains Id, inline edits will fail to map back to the correct row and throw runtime errors.
- Column Configuration: Add
editable: trueto enable inline edit inputs for text, numbers, dates, and phone types. - Draft Value Management: The datatable automatically stores unsaved edits in
event.detail.draftValueswith their matchingId. - Security & FLS: Using
uiRecordApi.updateRecordenforces Field-Level Security (FLS) and Object Permissions automatically. - Cache Invalidation: Always store the entire wired provisioning result and pass it to
refreshApex()after successful updates.
Step 5: Configure Metadata and Deploy
Update inlineEditDatatable.js-meta.xml to expose the component to Lightning App Builder:
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>60.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__AppPage</target>
<target>lightning__RecordPage</target>
<target>lightning__HomePage</target>
</targets>
</LightningComponentBundle>
# Deploy the Apex class and LWC component
sf project deploy start
# Open target org in browser
sf org open
- Open Setup > Lightning App Builder.
- Drag
inlineEditDatatableonto any Record or App page, then save and activate. - Double-click any editable cell to modify data, click Save on the bottom banner, and verify real-time database updates.
draftValues alongside updateRecord and refreshApex delivers clean, Excel-like batch inline editing without writing manual DML Apex classes.