🙆‍♀️

【LWC】Datatableでインライン編集機能を実装する(バリデーション付き)

2023/12/20に公開

はじめに

LWC(Lightning Web Component)でDatatableのインライン編集機能を実装しました。バリデーションチェックもしてくれます。

完成形


実装方法

コード

HTML

datatable.html
<template>
    <lightning-card title="取引先一覧">
        <lightning-button
                lwc:if={draftAccounts.length} 
                label="キャンセル" 
                slot="actions"
                onclick={handleCancel}
                class="slds-var-m-right_x-small">
        </lightning-button>
        <lightning-button
                label="保存" 
                slot="actions" 
                variant="brand"
                onclick={handleSave}
                disabled={isNotEdited}>
        </lightning-button>
        <div class="slds-card__body slds-card__body_inner">
            <lightning-datatable
                    key-field="Id"
                    data={accounts}
                    columns={columns}
                    draft-values={draftAccounts}
                    errors={tableError}
                    oncellchange={handleCellChange}
                    hide-checkbox-column
                    suppress-bottom-bar>
            </lightning-datatable>
        </div>
    </lightning-card>
</template>

JavaScript

datatable.js
import { LightningElement } from 'lwc';
import fetchAccounts from '@salesforce/apex/DatatableController.fetchAccounts';
import updateAccounts from '@salesforce/apex/DatatableController.updateAccounts';
import Toast from 'lightning/toast';

const columns = [
    { label: '取引先名', fieldName: 'Name', editable: true },
    { label: '訪問回数', fieldName: 'NumberOfVisits__c', type: 'number', editable: true },
    { label: 'Webサイト', fieldName: 'Website', type: 'url', editable: true }
];

export default class Datatable extends LightningElement {
    accounts = [];      // Datatableに表示する取引先
    draftAccounts = []; // インライン編集で変更された取引先
    tableError = {};    // インライン編集後のエラー
    columns = columns;

    get isNotEdited() {
        return !this.draftAccounts.length;
    }

    connectedCallback() {
        this.fetchTableData();
    }

    showSuccessToast(label) {
        Toast.show({
            label: label,
            mode: 'dismissible',
            variant: 'success'
        });
    }

    showErrorToast(message) {
        Toast.show({
            label: 'エラーが発生しました。',
            message: message,
            mode: 'dismissible',
            variant: 'error'
        });
    }

    async fetchTableData() {
        try {
            this.accounts = await fetchAccounts();
        } catch {
            this.showErrorToast('取引先を正しく取得できませんでした。');
        }
    }

    handleCellChange(event) {
        // event.target.draftValuesに変更後の値が格納されている
        this.draftAccounts = [...event.target.draftValues];
    }

    async prepareValidationError() {
        const rowsError = {};
        // 行でループ
        for (const account of this.draftAccounts) {
            let errorMessages = [];
            let errorFieldNames = new Set();

            // 列でループ
            for (const column of Object.keys(account)) {
                switch (column) {
                    case 'Name':
                        if (!account[column]) {
                            errorMessages.push('取引先名の入力は必須です。');
                            errorFieldNames.add(column);
                        }
                        break;
                    case 'NumberOfVisits__c':
                        if (Number(account[column]) < 0) {
                            errorMessages.push('訪問回数は正の値で入力してください。');
                            errorFieldNames.add(column);
                        }
                        if (!Number.isInteger(Number(account[column]))) {
                            errorMessages.push('訪問回数は整数で入力してください。');
                            errorFieldNames.add(column);
                        }
                        break;
                    case 'Website':
                        if (!account[column].startsWith('https://')) {
                            errorMessages.push('Webサイトは「https://」で始まる文字列を入力してください。');
                            errorFieldNames.add(column);
                        }
                        break;
                }
            }

            if (errorMessages.length) {
                rowsError[account.Id] = {
                    title: errorMessages.length + '件のエラーがあります。',
                    messages: errorMessages,
                    fieldNames: [...errorFieldNames]
                }
            }
        }
        return rowsError;
    }

    async handleSave() {
        try {
            const rowsError = await this.prepareValidationError();
            if (Object.keys(rowsError).length) {
                // バリデーションエラー出力
                this.tableError = { rows: rowsError };
            } else {
                this.tableError = {};
                await updateAccounts({ records: this.draftAccounts });
                this.draftAccounts = [];

                this.showSuccessToast('取引先が更新されました。');
            }
        } catch(error) {
            console.error(error);
            this.showErrorToast('取引先を正しく更新できませんでした。');
        } finally {
            await this.fetchTableData();
        }
    }

    handleCancel() {
        this.tableError = {};
        this.draftAccounts = [];
    }
}

Apex

DatatableController.cls
public with sharing class DatatableController {
    @AuraEnabled
    public static List<Account> fetchAccounts() {
        // 取引先レコードを、新しく作成された順に3件取得
        return [
            SELECT Name, NumberOfVisits__c, Website
            FROM Account
            ORDER BY CreatedDate DESC
            LIMIT 3
        ];
    }

    @AuraEnabled
    public static void updateAccounts(List<Accounts> records) {
        update records;
    }
}

解説

<lightning-datatable
        key-field="Id"
        data={data}
        columns={columns}
        draft-values={draftValues}
        errors={errors}
        oncellchange={handleCellChange}>
</lightning-datatable>

インライン編集を有効化する

インライン編集したい列にedatable: trueを設定し、columnsに受け渡します。

変更後の値を上書きする

oncellchangeに指定したメソッドは、あるセルのインライン編集が終わり、別の場所へフォーカスしたときに実行されます。
引数のeventから、event.target.draftValuesとたどることでインライン編集後のデータが取得できます。this.template.querySelector('lightning-datatable').draftValuesとHTML要素を直接指定して取得することも可能です。
このとき、その変数の値は

[
    {
        Id: 'xxxxxxxxxxxxxxx',
        Name: 'xxx株式会社_new',
    },
    {
        Id: 'yyyyyyyyyyyyyyy',
        Website: 'https://yyy-new-example.com' 
    }
]

のように、Idkey-fieldで指定された値)と変更された列の値で配列として構成されています。(変更されていない行は含まれません。)
これをそのままdraft-valuesに渡してあげることで、変更後の値がセルに反映され、色が変わるようになります。

バリデーションエラーメッセージを表示する

{
    rows: {
        xxxxxxxxxxxxxxx: {
            title: '2件のエラーがあります。'
            messages: [
                '○○は××で入力してください。',
                'hogehogeの入力は必須です。'
            ],
            fieldNames: ['Name', 'NumberOfVisits__c']
        },
        yyyyyyyyyyyyyyy: {
            title: '1件のエラーがあります。'
            messages: [
                '○○は××で入力してください。',
            ],
            fieldNames: ['Name']
        }
    }
}

上記のような変数を用意してerrorsに渡せば、特定の行(Id)の特定の列(fieldNames)で指定されたセルに赤枠をつけ、Datatable左端にエラーを表示することができます。
Datatable左端以外にもDatatable全体としてのエラーを表示できるらしいですが、ここでは割愛します。

参考

https://developer.salesforce.com/docs/component-library/bundle/lightning-datatable/documentation
https://base.terrasky.co.jp/articles/81Ugy
https://salesforcegirl.in/2022/11/30/add-dynamic-validation-in-lwc-datatable/

終わりに

インライン編集まわりは意外とよく使う?にもかかわらず、融通が利かないところが多々あるので、また何か見つけたら更新したいと思います。

Discussion