Datatableを使いこなすためのヒント

## はじめに

LWC（Lightning Web Components）のDatatableは、Salesforceでデータを表形式で表示するための便利なコンポーネントです。高い拡張性が特徴ですが、それゆえ使いこなすには少しコツが必要です。この記事では、LWCのDatatableを効果的に利用するための実装サンプルを紹介します。
なお、簡略のためエラーハンドリング等は省略しているので、参考にされる際はご注意下さい。

## データ型

### 標準データ型

列で使用できるデータ型が標準でいくつか用意されています。列の定義をcolumns属性に設定します。columns属性が変更されると表は再描画されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125621/77d5e0e1-1c12-4b9d-8e7c-85bd68d7f461.png)

<details><summary>サンプルコード・詳細</summary>

Datatableは標準で下記のデータ型をサポートします（最新の標準データ型は[こちら](https://developer.salesforce.com/docs/component-library/bundle/lightning-datatable/documentation)を確認してください）。


| データ型 | コンポーネント | typeAttributesで設定可能 |
|-|-|-|
| action | [lightning-button-menu](https://developer.salesforce.com/docs/component-library/bundle/lightning-button-menu) |  rowActions(必須), menuAlignment |
| boolean |||
| button | [lightning-button](https://developer.salesforce.com/docs/component-library/bundle/lightning-button) | disabled, iconName, iconPosition, label, name, title, variant |
| button-icon| [lightning-button-icon](https://developer.salesforce.com/docs/component-library/bundle/lightning-button-icon) | alternativeText, class, disabled, iconClass, iconName, name, title, variant |
| currency | [lightning-formatted-number](https://developer.salesforce.com/docs/component-library/bundle/lightning-formatted-number) | currencyCode, currencyDisplayAs, minimumIntegerDigits, minimumFractionDigits, maximumFractionDigits, minimumSignificantDigits, maximumSignificantDigits, step |
| date | [lightning-formatted-date-time](https://developer.salesforce.com/docs/component-library/bundle/lightning-formatted-date-time) | day, era, hour, hour12, minute, month, second, timeZone, timeZoneName, weekday, year |
| date-local| [lightning-formatted-date-time](https://developer.salesforce.com/docs/component-library/bundle/lightning-formatted-date-time) | day, month, year |
| email | [lightning-formatted-email](https://developer.salesforce.com/docs/component-library/bundle/lightning-formatted-email) ||
| location | [lightning-formatted-location](https://developer.salesforce.com/docs/component-library/bundle/lightning-formatted-location) | latitude, longitude |
| number | [lightning-formatted-number](https://developer.salesforce.com/docs/component-library/bundle/lightning-formatted-number) | minimumIntegerDigits, minimumFractionDigits, maximumFractionDigits, minimumSignificantDigits, maximumSignificantDigits |
| percent | [lightning-formatted-number](https://developer.salesforce.com/docs/component-library/bundle/lightning-formatted-number) | minimumIntegerDigits, minimumFractionDigits, maximumFractionDigits, minimumSignificantDigits, maximumSignificantDigits, step |
| phone | [lightning-formatted-phone](https://developer.salesforce.com/docs/component-library/bundle/lightning-formatted-phone) ||
| text | [lightning-formatted-text](https://developer.salesforce.com/docs/component-library/bundle/lightning-formatted-text) | linkify |
| url | [lightning-formatted-url](https://developer.salesforce.com/docs/component-library/bundle/lightning-formatted-url) | label, target, tooltip |

action、button、button-iconのデータ型はonrowactionに設定したメソッドを呼び出します。

```html
<template>
  <lightning-datatable
    key-field="Id"
    columns={columns}
    data={data}
    hide-checkbox-column
    onrowaction={handleRowAction}
  ></lightning-datatable>
</template>
```

typeAttributeの値を行ごとに使用したい場合は`{fieldName: "xxxxx"}`のように設定します。

```js
import { LightningElement, wire } from "lwc";
import fetchRecords from "@salesforce/apex/DatatableSampleCtrl.fetchRecords";

const actions = [
    { label: "Show details", name: "show_details" },
    { label: "Delete", name: "delete" }
];

const columns = [
    { label: "action", type: "action", typeAttributes: { rowActions: actions } },
    { label: "boolean", type: "boolean", fieldName: "Checkbox__c" },
    {
        label: "button",
        type: "button",
        typeAttributes: {
            iconName: "utility:alert", // 全ての行で同じ値を使用する場合はリテラルで指定します。
            label: "label", title: "title", name: "name", value: "value"
        }
    },
    { label: "button-icon", type: "button-icon", typeAttributes: {
        iconName: { fieldName: "buttonIconName" }  // fieldNameを渡すことで行ごとに値を設定できます。
    } },
    { label: "currency", type: "currency", fieldName: "Currency__c" },
    { label: "date", type: "date", fieldName: "Date__c" },
    { label: "date-local", type: "date-local", fieldName: "Date__c" },
    { label: "email", type: "email", fieldName: "Email__c" },
    { label: "location", type: "location", fieldName: "Location__c" },
    { label: "number", type: "number", fieldName: "Number__c" },
    { label: "percent", type: "percent", fieldName: "percent" },
    { label: "phone", type: "phone", fieldName: "Phone__c" },
    { label: "text", type: "text", fieldName: "Text__c" },
    { label: "url", type: "url", fieldName: "Url__c" }
];

export default class DatatableSample extends LightningElement {
    data = [];
    columns = columns;

    @wire(fetchRecords)
    getData({ data }) {
        if (data) {
            // この例ではmap関数を使用して「buttonIconName」に値を設定しています。
            this.data = data.map((record) => {
                const buttonIconName = record.isAlarmEnabled__c ? "utility:favorite" : "utility:favorite_off";
                return { ...record, percent: record.Percent__c / 100, buttonIconName };
            });
        }
    }

    handleRowAction(event) {
        console.log(JSON.stringify(event.detail));
        // 処理
    }
}
```

</details>

### カスタムデータ型（表示）

標準のデータ型では実現できないデータ入力形式でも、カスタムで定義することができます。
たとえばリッチテキストエリアは標準データ型では扱うことができませんが、独自に実装することでdatatableに表示することが可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125621/81bafb7e-2472-4a1d-92b0-7383bc33945c.png)

<details><summary>サンプルコード・詳細</summary>

カスタムデータ型を使用するには、LWCのコンポーネントを新たに作成して、`LightningDatatable`を継承するクラスを作成する必要があります。
カスタムデータ型のhtmlを作成して、インポートし、`customTypes`を宣言します。
テンプレートにはJavaScriptを含めることはできませんが、htmlに他のLWCコンポーネントを使用することはできます。

```shell
customDatatable
   ├──richTextarea.html
   ├──customDatatable.js
   └──customDatatable.js-meta.xml
```

```html:richTextarea.html
<template>
    <lightning-formatted-rich-text value={value}></lightning-formatted-rich-text>
</template>
```

```js:customDatabase.js
import LightningDatatable from "lightning/datatable";
import richTextarea from "./richTextarea.html";

export default class CustomDatatable extends LightningDatatable {
    static customTypes = {
        richTextarea: {
            template: richTextarea,
            standardCellLayout: true
        }
    };
}
```

`lightning-datatable`のかわりに作成したカスタムデータテーブルコンポーネントを使用します。

```html:datatableSample.html
<template>
  <c-custom-datatable
    key-field="Id"
    columns={columns}
    data={data}
    hide-checkbox-column
  ></c-custom-datatable>
</template>
```

カスタムデータタイプの使い方は標準データタイプと同じです。

```js:datatableSample.js
import { LightningElement, wire } from "lwc";
import fetchRecords from "@salesforce/apex/DatatableSampleCtrl.fetchRecords";

const columns = [
    { label: "name", fieldName: "Name" },
    { label: "rich_textarea", type: "richTextarea", fieldName: "Html__c" },
];

export default class DatatableSample extends LightningElement  {
    data = [];
    columns = columns;

    @wire(fetchRecords)
    getData({ data }) {
        if (data) {
            this.data = data;
        }
    }
}
```

- [カスタムデータ型の作成 | Developers](https://developer.salesforce.com/docs/platform/ja-jp/lwc/guide/data-table-custom-types.html)

</details>

### カスタムデータ型（インライン編集）

カスタムデータ型をインライン編集に対応させることができます。
たとえば下記のデータ型は標準で対応していませんが、独自に実装することで使用することができます。

- 主従関係・参照関係
- リッチテキストエリア
- ロングテキストエリア
- 時間
- 選択リスト
- 複数選択リスト

下記は選択リストの編集が可能なカスタムデータ型の例です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125621/84ac11db-0bc2-4465-9830-cff6af635787.png)

<details><summary>サンプルコード・詳細</summary>

テンプレートファイルにはhtmlしか指定できませんが、下記のように自作のコンポーネントを使用することができます。

```shell
customDatatable
   ├──template
   │  ├──picklist.html
   │  └──picklistEdit.html
   ├──customDatatable.js
   └──customDatatable.js-meta.xml
```

```html:picklistEdit.html
<template>
  <c-custom-datatable-combobox
    label={columnLabel}
    options={typeAttributes.options}
    value={editedValue}
    required={required}
    variant="label-hidden"
    data-inputable="true"
  ></c-custom-datatable-combobox>
</template>

```

```js:customDatatable.js
import LightningDatatable from "lightning/datatable";
import picklistTemplate from "./template/picklist.html";
import picklistEditTemplate from "./template/picklistEdit.html";

export default class CustomDatatable extends LightningDatatable {
    static customTypes = {
        picklist: {
            template: picklistTemplate,
            editTemplate: picklistEditTemplate,
            standardCellLayout: true,
            typeAttributes: ["options", "placeholder"]
        }
    };
}
```

上記では下記のコンポーネントを使用しています。
typeAttributesはリストがオブジェクトに変換されてしまうため、lightning-comboboxをそのまま使用することができません。そのため、customDatatableComboboxを作成して、optionsをリストに変換してから使用します。

```html:customDatatableCombobox.html
<template>
  <lightning-combobox
    lwc:ref="combobox"
    label={label}
    options={options}
    value={value}
    required={required}
    variant={variant}
    onchange={handleChange}
  ></lightning-combobox>
</template>
```

```js:customDatatableCombobox
import { LightningElement, api } from "lwc";

export default class CustomDatatableCombobox extends LightningElement {
    @api label;
    @api required;
    @api variant;
    _options;
    _value;

    @api
    get value() {
        return this._value;
    }

    set value(value) {
        this._value = value;
    }

    @api
    get options() {
        return this._options;
    }

    set options(value) {
        // ここでオブジェクトをリストに変換しています。
        const keys = Object.keys(value)
            .map(Number)
            .sort((a, b) => a - b);
        this._options = keys.map((key) => value[key]);
    }

    @api
    get validity() {
        return this.refs.combobox.validity;
    }

    @api
    blur() {
        return this.refs.combobox.blur();
    }

    @api
    checkValidity() {
        return this.refs.combobox.checkValidity();
    }

    @api
    focus() {
        return this.refs.combobox.focus();
    }

    @api
    reportValidity() {
        return this.refs.combobox.reportValidity();
    }

    @api
    setCustomValidity(message) {
        return this.refs.combobox.setCustomValidity(message);
    }

    @api
    showHelpMessageIfInvalid() {
        this.refs.combobox.showHelpMessageIfInvalid();
    }

    handleChange(event) {
        event.stopPropagation();
        this._value = event.detail.value;
        this.dispatchEvent(
            new CustomEvent("change", {
                bubbles: true,
                composed: true,
                detail: {
                    value: this._value
                }
            })
        );
    }
}
```

```js:datatableSample
import { LightningElement, wire } from "lwc";
import fetchRecords from "@salesforce/apex/DatatableSampleCtrl.fetchRecords";
import fetchOptions from "@salesforce/apex/DatatableSampleCtrl.fetchOptions";

const columns = [
    { label: "name", fieldName: "Name" },
    {
        label: "picklist",
        type: "picklist",
        columnKey: "picklist",
        fieldName: "Picklist__c",
        editable: true,
        typeAttributes: {
            options: []
        }
    }
];

const blankOption = { label: "--なし--", value: "" };

export default class DatatableSample extends LightningElement {
    data = [];
    columns = [];
    initialized = false;

    async connectedCallback() {
        if (this.initialized) {
            return;
        }
        this.columns = await this.generateColumn();
        this.initialized = true;
    }

    async generateColumn() {
        const options = await fetchOptions();
        return columns.map((column) => {
            if (column.columnKey !== "picklist") {
                return column;
            }
            return {
                ...column,
                typeAttributes: {
                    ...column.typeAttributes,
                    options: [blankOption, ...options]
                }
            };
        });
    }

    @wire(fetchRecords)
    getData({ data }) {
        if (data) {
            this.data = data;
        }
    }

    handleSave(event) {
        console.log(event.detail.draftValues);
        // 処理
    }
}
```

- [カスタムデータ型の作成 | Developers](https://developer.salesforce.com/docs/platform/ja-jp/lwc/guide/data-table-custom-types.html)
- [カスタムデータ型を編集可能にする | Developers](https://developer.salesforce.com/docs/platform/ja-jp/lwc/guide/data-table-custom-types-editable.html)
- [Combobox | Developers](https://developer.salesforce.com/docs/component-library/bundle/lightning-combobox)
- [LWCで選択リスト項目を扱う (lightning-combobox) - Qiita](https://qiita.com/TaaaZyyy/items/cd6a3af6c73813636cd0)
- [Lightning Web Component 間の通信の基礎 - Qiita](https://qiita.com/TaaaZyyy/items/ec82c0f048440e904d5e)
- [LWCで入力コンポーネントを作成する - Qiita](https://qiita.com/TaaaZyyy/items/2effdfdcbbf69d85708c)
- `map(Number)`について→[Array.prototype.map() | mdn web docs](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/map)
- [customInlineEditDatatable | github](https://github.com/liumiaowilson/customInlineEditDatatable)

</details>

<!-- ## セルのスタイル -->

## レコードページへのリンク

Datatableでリンクを表示するには、typeにurl、typeAttributes.labelに表示名の属性名を設定します。
レコード詳細ページへのURLはNavigationMixin.GenerateUrlで取得できます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125621/f2d93e9e-af55-4545-a788-e11abcc96853.png)

<details><summary>サンプルコード・詳細</summary>

```js:datatableSample.js
import { LightningElement } from "lwc";
import { NavigationMixin } from "lightning/navigation";
import fetchAccounts from "@salesforce/apex/DatatableSampleCtrl.fetchAccounts";

// リンクにはurlタイプを使用します。
const columns = [{
    label: "取引先名",
    fieldName: "url",
    type: "url",
    typeAttributes: { label: { fieldName: "Name" } }
}];

export default class DatatableSample extends NavigationMixin(LightningElement) {
    data = [];
    columns = columns;

    async connectedCallback() {
        const accounts = await fetchAccounts();
        this.data = await Promise.all(
            accounts.map(async (account) => {
                return {
                    ...account,
                    url: await this.generateUrl(account.Id)
                };
            })
        );
    }

    generateUrl(recordId) {
        // NavigationMixin.GenerateUrlでURLを作成します。
        return this[NavigationMixin.GenerateUrl]({
            type: "standard__recordPage",
            attributes: {
                recordId,
                actionName: "view"
            }
        });
    }
}
```

- [Formatted Url | Developers](https://developer.salesforce.com/docs/component-library/bundle/lightning-formatted-url/example)
- [基本的なナビゲーション | Developers](https://developer.salesforce.com/docs/platform/ja-jp/lwc/guide/use-navigate-basic.html)
- [JavaScriptの配列のmapでasync/awaitを使う方法 | Qiita](https://qiita.com/kwbt/items/6c0fe424c89a9f7553c1)

</details>

## 無限スクロール

無限スクロールを使用すると、テーブルの下端までスクロールした際に追加のデータを読み込むことができます。データの取得を分割することで、パフォーマンスの向上や Salesforce のガバナ制限の回避に役立ちます。

無限スクロールを有効にするには、`enable-infinite-loading`属性を追加します。テーブルの末尾までスクロールすると`onloadmore`に指定したメソッドが呼び出されます。

データの読み込みをトリガーするスクロール位置は、`load-more-offset`属性で調整可能です。デフォルトでは、テーブルの下端から20ピクセルの位置で`onloadmore`イベントが発火します。なお、テーブルの高さを指定しない場合、すべてのデータが一度に表示されてしまうため注意が必要です。

![movie3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125621/b9413925-b6cb-4af8-a0db-fc87fc3888e6.gif)

<details><summary>サンプルコード・詳細</summary>

```html:datatableSample.html
<template>
  <div style="height: 200px">
    <lightning-datatable
      lwc:ref="datatable"
      key-field="Id"
      columns={columns}
      data={data}
      hide-checkbox-column
      show-row-number-column
      enable-infinite-loading
      onloadmore={handleLoadMore}
    >
    </lightning-datatable>
  </div>
</template>
```

SOQLのOFFSETとLIMITを使用してデータを取得する方法もありますが、OFFSETはデータが2000件以上あるとエラーになります。下記の例でははじめに全件のIdを取得して、表示する分のデータをApexで取得します。

```js:datatableSample.js
import { LightningElement, track } from "lwc";
import fetchRecordIds from "@salesforce/apex/DatatableSampleCtrl.fetchRecordIds";
import fetchRecords from "@salesforce/apex/DatatableSampleCtrl.fetchRecords";

const columns = [
    { label: "取引先名", fieldName: "Name"},
    { label: "業種", fieldName: "Industry"},
    { label: "メモ", fieldName: "Memo__c"}
];

export default class DatatableSample extends LightningElement {
    @track data = [];
    recordIds = [];
    columns = columns;
    displayLimit = 10; // 一度に表示する件数
    currentIndex = 0;  // 現在の表示位置

    async connectedCallback() {
        this.recordIds = await fetchRecordIds();
        this.loadMoreData();
    }

    handleLoadMore() {
        this.loadMoreData();
    }

    async loadMoreData() {
        // スピナーを表示
        this.refs.datatable.isLoading = true;

        // 追加分のデータを取得
        const nextRecordIds = this.recordIds.slice(this.currentIndex, this.currentIndex + this.displayLimit);
        const nextData = await fetchRecords({recordIds: nextRecordIds});
        this.data = [...this.data, ...nextData];
        this.currentIndex += this.displayLimit;

        // データが全て表示されたら infinite loading を無効化
        if (this.currentIndex >= this.recordIds.length) {
            this.refs.datatable.enableInfiniteLoading = false;
        }

        // スピナーを非表示
        this.refs.datatable.isLoading = false;
    }
}
```

</details>

<!-- ## ページネーション -->

## 検索

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125621/1a328ee5-f20d-46d4-85d4-c079bd736c40.png)

表示データをフィルターする機能はDatatableには備わっていないため、独自に機能を追加する必要があります。

<details><summary>サンプルコード・詳細</summary>

下記は検索窓でエンター押した時、検索窓からフォーカスを外した時に検索を実行する例です。
lightning-inputには`type=search`を使用します。

```html:datatableSample.html
<template>
  <lightning-card title="取引先" icon-name="standard:account">
    <lightning-input
      type="search"
      label="search"
      placeholder="検索..."
      variant="label-hidden"
      onkeyup={handleKeyUp}
      onblur={handleBlur}
      slot="actions"
    ></lightning-input>
    <div class="slds-p-horizontal_small" style="height: 200px">
      <lightning-datatable key-field="Id" columns={columns} data={data} hide-checkbox-column></lightning-datatable>
    </div>
  </lightning-card>
</template>
```

```js:datatableSample.js
import { LightningElement, wire } from "lwc";
import fetchAccounts from "@salesforce/apex/DatatableSampleCtrl.fetchAccounts";

const columns = [
    { label: "取引先名", fieldName: "Name" },
    { label: "業種", fieldName: "Industry" },
    { label: "メモ", fieldName: "Memo__c" }
];
export default class DatatableSample extends LightningElement {
    allData = [];
    data = [];
    columns = columns;
    searchTerm;

    @wire(fetchAccounts)
    getData({ data }) {
        if (data && !this.allData.length) {
            this.allData = data;
            this.filterRows();
        }
    }

    handleKeyUp(event) {
        if (event.key === "Enter" && !event.isComposing) {
            this.searchTerm = event.target.value;
            this.filterRows();
        }
    }

    handleBlur(event) {
        if (this.searchTerm !== event.target.value) {
            this.searchTerm = event.target.value;
            this.filterRows();
        }
    }

    filterRows() {
        if (!this.searchTerm || this.searchTerm === "") {
            this.data = [...this.allData];
            return;
        }

        this.data = this.allData.filter((row) => {
            return Object.values(row).some((value) => String(value).toLowerCase().includes(this.searchTerm.toLowerCase()));
        });
    }
}
```

- [lightning-input | Developers](https://developer.salesforce.com/docs/component-library/bundle/lightning-input/example)
- [isComposing | mdn web docs](https://developer.mozilla.org/ja/docs/Web/API/KeyboardEvent/isComposing)

</details>

## 並び替え

columnで`sortable=true`を設定すると、列ラベルをクリックしてソートを行うことができます。列ラベルをクリックすると、onsortイベントハンドラに設定されたメソッドが呼び出されます。
`sorted-by`要素は並び替えの方向を指定します。`asc`（昇順）または`desc`（降順）が設定できます。
`default-sort-direction`は列ラベルをクリックした初回の並び替えの方向を指定します。

![movie1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125621/c603ce34-ccb3-4435-9e50-d7e1529e5550.gif)

<details><summary>サンプルコード・詳細</summary>

```html:datatableSample.html
<template>
  <lightning-datatable
    key-field="Id"
    columns={columns}
    data={data}
    hide-checkbox-column
    default-sort-direction={defaultSortDirection}
    sorted-direction={sortDirection}
    sorted-by={sortedBy}
    onsort={onHandleSort}
  >
  </lightning-datatable>
</template>

```

`event.detail`にはfieldNameとsortDirectionが含まれます。

```js:datatableSample.js
import { LightningElement, wire } from "lwc";
import fetchAccounts from "@salesforce/apex/DatatableSampleCtrl.fetchAccounts";

const columns = [
    { label: "取引先名", fieldName: "Name", sortable: true },
    { label: "電話", fieldName: "Phone", type: "phone", sortable: true },
    { label: "評価", fieldName: "Rating", sortable: true },
    { label: "メモ", fieldName: "Memo__c", sortable: true }
];

export default class DatatableSample extends LightningElement {
    data = [];
    columns = columns;
    defaultSortDirection = "asc";
    sortDirection = "asc";
    sortedBy;

    @wire(fetchAccounts)
    getData({ data }) {
        if (data) {
            this.data = data;
        }
    }

    onHandleSort(event) {
        const { fieldName: sortedBy, sortDirection } = event.detail;
        const cloneData = [...this.data];

        cloneData.sort(this.sortBy(sortedBy, sortDirection));
        this.data = cloneData;
        this.sortDirection = sortDirection;
        this.sortedBy = sortedBy;
    }

    /**
     * 特定のフィールドでオブジェクトの配列をソートするための関数を生成します。
     *
     * @param {string} fieldName - ソートの基準となるフィールド名。
     * @param {'asc' | 'desc'} sortDirection - ソートの方向。'asc'（昇順）または 'desc'（降順）を指定します。
     * @param {function} [valueTransformer] - ソート前にフィールド値を変換するためのオプションの関数。
     * @returns {function(Object, Object): number} - 配列の Array.prototype.sort メソッドで使用できる比較関数。
     *   - 比較関数のパラメータ:
     *     - {Object} item1 - 比較対象の最初のオブジェクト。
     *     - {Object} item2 - 比較対象の2番目のオブジェクト。
     *   - 比較関数の戻り値:
     *     - {number} - item1 が item2 より小さい場合は負の値、item1 が item2 より大きい場合は正の値、等しい場合は 0 を返します。
     */
    sortBy(fieldName, sortDirection, valueTransformer) {
        const getValue = valueTransformer
            ? function (item) {
                  return valueTransformer(item[fieldName]);
              }
            : function (item) {
                  return item[fieldName];
              };

        const directionMultiplier = sortDirection === "asc" ? 1 : -1;

        return (item1, item2) => {
            const value1 = getValue(item1);
            const value2 = getValue(item2);

            // `undefined` の場合の特別な処理
            if (value1 === undefined || value2 === undefined) {
                if (value1 === undefined && value2 !== undefined) {
                    return directionMultiplier * 1;
                }
                if (value1 !== undefined && value2 === undefined) {
                    return directionMultiplier * -1;
                }
                return 0;
            }

            // 文字列の比較には `localeCompare` を使用
            if (typeof value1 === "string" && typeof value2 === "string") {
                return directionMultiplier * value1.localeCompare(value2);
            }

            return directionMultiplier * ((value1 > value2) - (value2 > value1));
        };
    }
}
```

- [Array.prototype.sort() | mdn web docs](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)

</details>

## インライン編集が可能なデータテーブル

### Datatableの保存ボタンを使用する

Datatableではインライン編集に対応しています。columnで`editable=true`に設定した項目はセルに鉛筆マークが表示され、クリックすると入力欄が表示されます。
変更を加えるとボトムにキャンセルボタンと保存ボタンが表示されます。それぞれクリックすると`oncancel`と`onsave`に割り当てたメソッドが実行されます。
`onsave`では`draft-values`に空の配列を代入することでボトムのボタンが非表示になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125621/5c3d9f77-9ef1-4e91-8303-9f0a48cf04fe.png)

<details><summary>サンプルコード・詳細</summary>

```html:datatableSample.html
<template>
  <lightning-datatable
    key-field="Id"
    data={data}
    draft-values={draftValues}
    columns={columns}
    show-row-number-column
    hide-checkbox-column
    onsave={handleSave}
    oncancel={handleCancel}
  >
  </lightning-datatable>
</template>
```

```js:datatableSample.js
import { LightningElement, wire } from "lwc";
import { refreshApex } from "@salesforce/apex";
import { notifyRecordUpdateAvailable } from "lightning/uiRecordApi";
import { ShowToastEvent } from "lightning/platformShowToastEvent";
import fetchAccounts from "@salesforce/apex/DatatableSampleCtrl.fetchAccounts";
import saveAccounts from "@salesforce/apex/DatatableSampleCtrl.saveAccounts";

const columns = [
    { label: "取引先名", fieldName: "Name", displayReadOnlyIcon: true },
    { label: "Phone", fieldName: "Phone", type: "phone", displayReadOnlyIcon: true },
    { label: "評価", fieldName: "Rating", editable: true },
    { label: "メモ", fieldName: "Memo__c", editable: true }
];

export default class DatatableSample extends LightningElement {
    data = [];
    columns = columns;
    draftValues = [];
    wiredValue;

    @wire(fetchAccounts)
    getData(value) {
        this.wiredValue = value;
        const { data } = value;
        if (data) {
            this.data = data;
        }
    }

    async connectedCallback() {
        this.data = await fetchAccounts();
    }

    async handleSave(event) {
        // 保存
        const draftValues = event.detail.draftValues;
        await saveAccounts({ accounts: draftValues });
        // LDSに変更を通知
        const changedRecordIds = draftValues.map((account) => { return { recordId: account.Id } });
        await notifyRecordUpdateAvailable(changedRecordIds);
        // 画面を更新
        await refreshApex(this.wiredValue);
        this.draftValues = [];
        this.dispatchEvent(new ShowToastEvent({ title: "保存しました。", variant: "success" }));
    }

    handleCancel(event) {
        // キャンセルボタンをクリックした時の処理
    }
}
```

onsaveの`event.detail`ではインライン編集で編集した箇所の情報が取得できます。

```json
{
  "draftValues": [
    { "Rating": "Cold", "Memo__c": "123", "Id": "001IS000002LZALYA4" },
    { "Rating": "Hot", "Id": "001IS000002LZAMYA4" }
  ]
}
```

- [Apex メソッド結果のクライアント側キャッシュ | Developers](https://developer.salesforce.com/docs/platform/ja-jp/lwc/guide/apex-result-caching.html)
- [notifyRecordUpdateAvailable(recordIds) | Developers](https://developer.salesforce.com/docs/platform/ja-jp/lwc/guide/reference-notify-record-update.html)
- [トースト通知 | Developers](https://developer.salesforce.com/docs/platform/ja-jp/lwc/guide/use-toast.html)
- [インライン編集を使用したテーブルでのデータの表示 | Developers](https://developer.salesforce.com/docs/platform/ja-jp/lwc/guide/data-table-inline-edit.html)
- (参考)[Salesforceのタブ切替え時で未保存時の警告を出す | Qiita](https://qiita.com/TaaaZyyy/items/70667d2eb282050def85)
- (参考)[window.alert、window.confirm、window.promptの代替 | Qiita](https://qiita.com/TaaaZyyy/items/76b55f54050a4d1f0f2b)

</details>

### カスタムの保存ボタンを使用する

`suppress-bottom-bar`をつけることでインライン編集時にボトムの保存・キャンセルボタンが表示されなくなります。
Datatableの保存ボタンを使用するときはonchangeのeventから変更値を取得していましたが、カスタムの保存ボタンを使用するときは`this.refs.{uniqueId}.draftValue`で変更値を取得します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125621/7094194c-0013-4a6b-b7ed-1d2bdc407266.png)

<details><summary>サンプルコード・詳細</summary>

```html:datatableSample.html
<template>
  <lightning-button label="保存" onclick={handleSave} disabled={disableButton}></lightning-button>
  <lightning-datatable
    lwc:ref="datatable"
    key-field="Id"
    data={data}
    draft-values={draftValues}
    columns={columns}
    show-row-number-column
    hide-checkbox-column
    suppress-bottom-bar
    oncellchange={handleCellChange}
  >
  </lightning-datatable>
</template>
```

```js:datatableSample.js
async handleSave() {
    // 保存
    const draftValues = this.refs.datatable.draftValues;
    await saveAccounts({ accounts: draftValues });
    // LDSに変更を通知
    const changedRecordIds = draftValues.map((account) => { return { recordId: account.Id } });
    await notifyRecordUpdateAvailable(changedRecordIds);
    // 画面を更新
    await refreshApex(this.wiredValue);
    this.draftValues = [];
    this.disableButton = true;
    this.dispatchEvent(new ShowToastEvent({ title: "保存しました。", variant: "success" }));
}

handleCellChange(event) {
    // セルに変更があるたびに呼び出されます。
    // event.detailに含まれる情報は変更されたセルとkey-fieldのみです。
    console.log("change", JSON.stringify(event.detail));
    this.disableButton = !this.hasChange();
}

hasChange() {
    return !!this.refs.datatable.draftValues?.length;
}
```

:::note

**draft-values属性について**
`draft-values`属性は、インライン編集内容をプログラムから設定するために使用します。ユーザーが入力した内容を取得することはできません。インライン編集で入力された値を取得するには、Datatableのプロパティを直接参照します。

```html
    <lightning-datatable key-field="Id" draft-values={draftValues}
```

```js
draftValues = [];

doSomething() {
    // インライン編集の値の設定
    this.draftValues = [{"Memo__c":"345","Id":"001IS000002LZAMYA4"}];
    // インライン編集状況のリセット
    this.draftValues = [];
    // インライン編集の値の取得
    const draftValues = this.template.querySelector('lightning-datatable').draftValue;
}
```

:::

- [HTML テンプレートディレクティブ | Developers](https://developer.salesforce.com/docs/platform/ja-jp/lwc/guide/reference-directives.html#lwccolonrefuniqueid)
- [Apex メソッド結果のクライアント側キャッシュ | Developers](https://developer.salesforce.com/docs/platform/ja-jp/lwc/guide/apex-result-caching.html)
- [notifyRecordUpdateAvailable(recordIds) | Developers](https://developer.salesforce.com/docs/platform/ja-jp/lwc/guide/reference-notify-record-update.html)
- [トースト通知 | Developers](https://developer.salesforce.com/docs/platform/ja-jp/lwc/guide/use-toast.html)
- [インライン編集を使用したテーブルでのデータの表示 | Developers](https://developer.salesforce.com/docs/platform/ja-jp/lwc/guide/data-table-inline-edit.html)
- (参考)[Salesforceのタブ切替え時で未保存時の警告を出す | Qiita](https://qiita.com/TaaaZyyy/items/70667d2eb282050def85)
- (参考)[window.alert、window.confirm、window.promptの代替 | Qiita](https://qiita.com/TaaaZyyy/items/76b55f54050a4d1f0f2b)

</details>

<!-- ### エラーの表示 -->

## 参考

Datatableの公式ドキュメントは下記になります。

- [Datatableのリファレンスガイド | Developers](https://developer.salesforce.com/docs/component-library/bundle/lightning-datatable)
- [Datatableに関するデベロッパーガイド | Developers](https://developer.salesforce.com/docs/platform/lwc/guide/data-table-intro.html)

Datatableの拡張例はUNOFFICIAL SFがかなり優れています。

- [Datatableドキュメント | unofficial sf](https://unofficialsf.com/datatable-lightning-web-component-for-flow-screens-2/)
- 上記のソースコード（[github](https://github.com/alexed1/LightningFlowComponents/tree/master/flow_screen_components/datatable)）

<!--
## memo

- 選択した行の管理
- Apex コントローラを使用してデータを取得する
- 列プロパティの操作
    - *label
    - *fieldName
    - *type
    - action: アクションのドロップダウン メニュー
    - cellAttributes
        - alignment: 水平方向の揃え
        - iconName
        - iconLabel
        - iconPosition
        - iconAlternativeText
        - class
    - editable: インライン編集をサポート
    - fixedWidth: 列の幅をピクセル単位で固定
    - hideDefaultActions: 列のデフォルトのヘッダー アクションを非表示
    - hideLabel: 列のラベルを非表示
    - iconName: ヘッダーラベルの左側に追加するアイコン
    - initialWidth: 初期化時の列の幅
    - sortable: 列を並べ替えることができるか
    - typeAttributes: データ型のコンポーネント属性を使用してカスタム書式設定を提供
    - wrapText
    - columnKey: 2 つの列に同じフィールド名を使用する場合に列に一意の ID を付与するには、列定義の 1 つで fieldName ではなく、属性 columnKey を使用します。
- カスタム書式設定の例: 対応する Lightning 基本コンポーネントの属性を渡します。
- 通貨とパーセンテージの表示
- 型属性を使用して日付と時刻を表示する
- コンテンツを列に揃える
- 列ラベルの代わりにアイコンを表示する
- 列データにアイコンを追加する
- 列にスタイルを追加する
- 読み取り専用フィールドのインジケーターを表示する
- 無限スクロールを使用してより多くの行を読み込む
- ヘッダーアクションの作成
- 静的な行レベルのアクションの作成
- 動的な行レベルのアクションの作成
- テーブルと列のサイズ変更
    - 列幅の操作
    - 列幅のサイズ変更の管理
    - 固定幅モードの実装
    - 自動幅モードの実装
- 選択行の最大数の指定
- 単一行選択を有効にする
- プログラムによる行の選択
- 列によるデータの並べ替え
- インライン編集の操作
- エラーの表示
- 行にエラーを表示する
- セルのカスタムエラーメッセージを表示する
- テーブルの表示エラー
- テキストの折り返しとクリッピング
    - 列ヘッダーのテキスト折り返し
- アクセシビリティ
    - テーブルにアクセシブルなラベルを付ける
    - 表にアクセシブルなキャプションを付ける
    - ナビゲーションモードとアクションモードを切り替える
- 使用上の考慮事項
- カスタムイベント
    - cancel
    - headeraction
    - loadmore
    - resize
    - rowselection
    - save
    - sort
-->