Skip to main content

How to Implement Inline Editing in Salesforce LWC Datatable (Full Working Code)

In plain words: Inline editing in an LWC datatable lets users edit table cells directly on screen by double-clicking or clicking the pencil icon, batching the modified values into a 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

Generate Component via Salesforce CLI:
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 }));
    }
}
Warning Trap: The 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.
360 Architecture Summary:
  • Column Configuration: Add editable: true to enable inline edit inputs for text, numbers, dates, and phone types.
  • Draft Value Management: The datatable automatically stores unsaved edits in event.detail.draftValues with their matching Id.
  • Security & FLS: Using uiRecordApi.updateRecord enforces 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>
Deployment & Verification:
# 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 inlineEditDatatable onto 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.
Core Takeaway: Managing draftValues alongside updateRecord and refreshApex delivers clean, Excel-like batch inline editing without writing manual DML Apex classes.