LWC_lightning-datatableのインライン編集で即時データ更新(保存ボタン押下不要)

lightning-datatableを使用して、保存ボタン押下せず、インライン編集で即時データ保存できる方法を紹介するよ！
ユーザビリティ意識すると、今のご時世「保存」ボタン押下しないで直接更新させたいよね！

というわけで、今回は、取引先レコード詳細ページに配置するLWCを紹介するよ！
内容は、取引先に紐付いている取引先責任者一覧をリストで表示し、インライン編集して即時データ更新・保存するよ！
また、名前はリンクになっていて、ページ遷移も出来るよ！<br/><br/><br/>

以下、まずはサンプルコードだよ！VSCodeで作成してね！

```ContactController.cls
public with sharing class ContactController {
    @AuraEnabled(cacheable=true)
    public static List<Contact> getContactsByAccount(Id accountId) {
        // 引数が空なら空リストを返す
        if (String.isBlank(accountId)) {
            return new List<Contact>();
        }
        return [
            SELECT Id, Name, Title, Phone, Email
            FROM Contact
            WHERE AccountId = :accountId
            ORDER BY CreatedDate DESC
        ];
    }

    @AuraEnabled
    public static void updateContacts(List<Contact> updatedContacts) {
        if (updatedContacts == null || updatedContacts.isEmpty()) {
            return;
        }
        update updatedContacts;
    }
}
```
```inline.html
<template>
    <lightning-card title="取引先責任者">
        <div class="hide-buttons">
            <lightning-datatable
                key-field="Id"
                data={contacts}
                columns={columns}
                draft-values={draftValues}
                editable
                oncellchange={handleCellChange}
                hide-checkbox-column
                suppress-bottom-bar
            >
            </lightning-datatable>
        </div>
    </lightning-card>
</template>
```
```inline.js
import { LightningElement, api, track, wire } from 'lwc';
import getContactsByAccount from '@salesforce/apex/ContactController.getContactsByAccount';
import updateContacts from '@salesforce/apex/ContactController.updateContacts';
import { refreshApex } from '@salesforce/apex';

export default class AutoSaveContacts extends LightningElement {
    @api recordId;
    @track contacts = [];
    @track draftValues = [];
    wiredResult;

    // データテーブル列定義
    columns = [
        {
            label: '氏名',
            fieldName: 'NameUrl',     // 実際のリンク先URL
            type: 'url',
            typeAttributes: {
                label: { fieldName: 'Name' }, // 表示する文字列
                target: '_self',             // 新規タブを開く場合は '_blank'
                tooltip: { fieldName: 'Name' }
            },
            editable: false
        },
        {
            label: '役職',
            fieldName: 'Title',
            type: 'text',
            editable: true
        },
        {
            label: '電話',
            fieldName: 'Phone',
            type: 'phone',
            editable: true
        },
        {
            label: 'メールアドレス',
            fieldName: 'Email',
            type: 'email',
            editable: true
        }
    ];

    // 取引先責任者を取得
    @wire(getContactsByAccount, { accountId: '$recordId' })
    wiredContacts(result) {
        this.wiredResult = result;
        const { data, error } = result;
        if (data) {
            // データにリンク先URLを付与
            this.contacts = data.map(contact => ({
                ...contact,
                NameUrl: `/lightning/r/Contact/${contact.Id}/view`
            }));
        } else if (error) {
            console.error('取引先責任者取得エラー: ', error);
        }
    }

    // セル変更時の保存処理
    handleCellChange(event) {
        const changedValues = event.detail.draftValues;

        updateContacts({ updatedContacts: changedValues })
            .then(() => {
                // Apex更新成功後、最新データにリフレッシュ
                return refreshApex(this.wiredResult);
            })
            .then(() => {
                // 保存後、draftValuesをクリアして編集モードをリセット
                this.draftValues = [];
            })
            .catch(error => {
                console.error('更新エラー: ', error);
            });
    }
}
```
```inline.css
.hide-buttons .slds-cell-edit__button,
.hide-buttons .slds-button.slds-button_icon.slds-button_icon-border-filled {
    display: none !important;
}
```
```inline.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>62.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordPage</target>
    </targets>
</LightningComponentBundle>

```

<h1>VSCodeを使用した作成手順</h1>
1．ContactController.clsを作成、デプロイ<br/>
2．inline.js、html、css、xmlを作成、デプロイ

<h1>全体の流れ</h1>
<dt>1．画面を開くと @wire デコレーターが実行される
    <dd>- getContactsByAccount が Apexメソッドとして呼び出され、recordId（取引先ID）に紐づく取引先責任者(Contact)の一覧が取得される。<br/>
    - 取得結果は wiredContacts(result) の result に渡される。</dd>
<br/></dt>
<dt>2．取得データを整形し、contacts プロパティへ格納
    <dd>- wiredContacts(result) 内で、data.map() を使って NameUrl というリンク先フィールドを追加。<br/>
    - その結果を this.contacts に代入し、画面の lightning-datatable に表示される。</dd>
<br/></dt>
<dt>3．ユーザーが画面上のデータテーブルをインライン編集
    <dd>- 「役職」や「電話」などを編集すると、handleCellChange イベントが発火。<br/>
    - event.detail.draftValues に編集された項目と値が格納される。</dd>
<br/></dt>
<dt>4．編集内容を Apex メソッド（updateContacts）へ送って更新
    <dd>- updateContacts({ updatedContacts: changedValues }) が呼ばれ、Salesforce上のContact情報を更新。<br/>
- 更新成功後に .then() 内の処理が継続。</dd>
<br/></dt>

<dt>5．更新直後のデータを画面に反映するため refreshApex を呼ぶ
    <dd>- refreshApex(this.wiredResult) によって、先ほどの @wire(getContactsByAccount, { accountId: '$recordId' }) が再実行される。<br/>
    - これにより最新の取引先責任者情報を再取得し、画面へ反映。</dd>
<br/></dt>

<dt>6．編集完了後、draftValues をクリア
    <dd>- this.draftValues = [] でリセットし、テーブルのインライン編集ハイライトなどを元の状態に戻す。</dd>
</dt>

<h1>JavaScriptのポイント</h1>
<dt>1．インライン編集時のdraftValues
    <dd>- draftValues はデータテーブルの「まだ保存されていない変更情報」を一時的に保持するもの。<br/>
    - ユーザーがセルを編集すると、自動的に event.detail.draftValues に編集されたフィールドや値が配列形式で入る。</dd>
<br/></dt>
<dt>2．レコード更新 & データリフレッシュ
    <dd>- updateContacts() の Apex メソッドが成功すると、更新が完了した最新の状態を画面に反映させたい。<br/>
    - そこで refreshApex() を利用し、最初に登録した this.wiredResult（@wireの戻り値）を再取得する。<br/>
    - 再取得成功後、ユーザーが見ているデータテーブルにも最新のContact情報が映る</dd>
<br/></dt>
<dt>3．編集モードのクリア
    <dd>- 編集が完了したので draftValues = [] にする。<br/>
    - これで編集途中の状態をクリアして、また新たに編集できるようにする。</dd>
</dt>
    <h3><b>※2、3がないと最新データでの画面描画がされないんだよね！</b></h3>

<h1>HTMLのポイント</h1>
<dt>1．key-field="Id"
    <dd>レコードの一意識別子を指定する属性です。<br/>
    lightning-datatable がテーブルの行を認識するために必要で、"Id" フィールドを使うのが一般的です。</dd>
<br/></dt>
<dt>2．draft-values={draftValues}
    <dd>インライン編集でまだ保存されていない値を一時的に保持するためのプロパティです。<br/>
    ユーザーがセルを編集すると、その変更内容は自動的に {draftValues} に反映されます。<br/>
    Apex保存後などのタイミングでクリアしてあげることで、編集モードをリセットできます。</dd>
<br/></dt>
<dt>3．editable
    <dd>データテーブルをインライン編集可能な状態にするための属性です。<br/>
    これを指定することで、テーブルのセルをクリックした時に編集が可能になります。</dd>
<br/></dt>
<dt>4．suppress-bottom-bar
    <dd>インライン編集時(※)にテーブル下部に表示される「保存」「キャンセル」ボタンを非表示にする属性です。<br/>
    非表示にすることで、ユーザーは編集したら自動的に保存処理（または独自の保存処理）が行われるようなUIにできます。<br/><br/>
    ※保存時に一瞬保存ボタンが見えてしまうので、そこはcssを使用して無理矢理非表示にしているよ！<br/>今回はサンプルコード＆インラインが本題だから、強引なやり方だよ！</dd>
<br/></dt>
<br/>
インライン編集で保存ボタン使用せず保存することは、ユーザビリティ向上になるよね！<br/>
何か参考になれば嬉しいな！<br/>
今後も初心者～中級者向けにどんどんアップしていくので、フォロー＆いいねよろしくね！！！