【Salesforce】LWC で作るオブジェクト項目一覧

## はじめに

Salesforceの管理者やデベロッパーにとって、オブジェクトの項目構成を把握することは日常的な作業です。特に多くのカスタム項目が追加されたオブジェクトでは、項目の全体像を素早く確認できると便利です。

この記事では、Lightning Web Components (LWC) を使って、任意のオブジェクトの項目一覧を表示するコンポーネントの作成方法を解説します。このコンポーネントは以下の機能を持ちます：

- 任意のSalesforceオブジェクトの項目一覧を表示
- 項目のラベル、API参照名、データ型、必須かどうかを表示
- レコードページで使用すると、そのレコードの項目値も表示
- 各列でのソート機能
- 項目をクリックすると詳細情報をモーダルで表示
- CSVエクスポート機能

## 完成イメージ

![スクリーンショット 2025-02-25 22.38.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492812/e0fbf919-3913-4aab-b011-e02b216956d1.png)

出力したCSVです

![スクリーンショット 2025-02-25 22.38.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492812/be1f5477-ef93-4994-bf07-809881238c2d.png)

## 前提条件

- Salesforce Developer Edition または Sandbox 環境
- Visual Studio Code と Salesforce Extension Pack
- SFDX CLI のインストール

## 実装手順

### 1. プロジェクトの準備

まず、VS Code で新しい LWC コンポーネントを作成します。

```bash
sfdx force:lightning:component:create --type lwc --componentName objectFieldsList --outputDir force-app/main/default/lwc
```

### 2. メタデータファイルの作成

`objectFieldsList.js-meta.xml` ファイルを以下のように編集します：

```xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>58.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>オブジェクト項目一覧</masterLabel>
    <description>任意のオブジェクトの項目一覧を表示します</description>
    <targets>
        <target>lightning__AppPage</target>
        <target>lightning__RecordPage</target>
        <target>lightning__HomePage</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__RecordPage,lightning__AppPage,lightning__HomePage">
            <property name="objectApiName" type="String" label="オブジェクトAPI名" 
                      description="表示するオブジェクトのAPI名（例：Account, Contact, Opportunity）" 
                      default="Account" required="true" />
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>
```

このメタデータファイルでは、コンポーネントをアプリページ、レコードページ、ホームページで使用できるように設定し、表示するオブジェクトのAPI名を設定できるようにしています。

### 3. JavaScript ファイルの作成

`objectFieldsList.js` ファイルを以下のように作成します：

```javascript
import { LightningElement, wire, api } from 'lwc';
import { getObjectInfo } from 'lightning/uiObjectInfoApi';
import { getRecord } from 'lightning/uiRecordApi';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

export default class ObjectFieldsList extends LightningElement {
    @api objectApiName = 'Account'; // デフォルトはAccount、設定で変更可能
    @api recordId; // レコードページで使用する場合のレコードID
    objectFields = [];
    recordData = {};
    error;
    fieldsToRetrieve = [];
    defaultSortDirection = 'asc';
    sortDirection = 'asc';
    sortedBy;
    isExporting = false;
    
    // データテーブルの列定義
    columns = [
        { 
            label: '項目ラベル', 
            fieldName: 'label', 
            type: 'button',
            typeAttributes: {
                label: { fieldName: 'label' },
                name: 'navigate_to_field',
                variant: 'base'
            },
            sortable: true
        },
        { label: 'API参照名', fieldName: 'apiName', type: 'text', sortable: true },
        { label: 'データ型', fieldName: 'dataType', type: 'text', sortable: true },
        { label: '必須', fieldName: 'isRequired', type: 'boolean', sortable: true },
        { label: '値', fieldName: 'value', type: 'text', sortable: true }
    ];
    
    // オブジェクト情報の取得
    @wire(getObjectInfo, { objectApiName: '$objectApiName' })
    wiredObjectInfo({ error, data }) {
        if (data) {
            // オブジェクト情報からフィールド情報を取得
            const fields = data.fields;
            this.fieldsToRetrieve = Object.keys(fields);
            this.objectFields = this.fieldsToRetrieve.map(fieldApi => {
                return {
                    apiName: fieldApi,
                    label: fields[fieldApi].label,
                    dataType: fields[fieldApi].dataType,
                    isRequired: fields[fieldApi].required,
                    value: ''
                };
            });
            // デフォルトでラベルでソート
            this.sortData('label', 'asc');
            this.error = undefined;
            
            // オブジェクト名を保存（表示用）
            this.objectLabel = data.label;
        } else if (error) {
            this.error = error;
            this.objectFields = [];
            console.error(`Error loading ${this.objectApiName} fields`, error);
        }
    }
    
    // レコードデータの取得（レコードページで使用時）
    @wire(getRecord, { recordId: '$recordId', optionalFields: '$fieldsForWire' })
    wiredRecord({ error, data }) {
        if (data) {
            // レコードデータを取得したら、項目値を更新
            this.recordData = data;
            this.updateFieldValues();
        } else if (error) {
            console.error(`Error loading ${this.objectApiName} data`, error);
        }
    }
    
    // 取得する項目のリストを生成
    get fieldsForWire() {
        if (this.recordId && this.objectApiName && this.fieldsToRetrieve && this.fieldsToRetrieve.length > 0) {
            // 全ての項目を取得するように指定
            return this.fieldsToRetrieve.map(field => `${this.objectApiName}.${field}`);
        }
        return [];
    }
    
    // カードのタイトルを生成
    get cardTitle() {
        return `${this.objectLabel || this.objectApiName} 項目一覧`;
    }
    
    // オブジェクトのアイコンを取得
    get objectIcon() {
        // 一般的なオブジェクトのアイコン名をマッピング
        const iconMap = {
            'Account': 'standard:account',
            'Contact': 'standard:contact',
            'Opportunity': 'standard:opportunity',
            'Lead': 'standard:lead',
            'Case': 'standard:case',
            'Campaign': 'standard:campaign',
            'Contract': 'standard:contract',
            'Product2': 'standard:product',
            'Order': 'standard:orders',
            'Task': 'standard:task',
            'Event': 'standard:event',
            'User': 'standard:user'
        };
        
        return iconMap[this.objectApiName] || 'standard:custom_object';
    }
    
    // レコードデータから項目値を更新
    updateFieldValues() {
        if (this.recordData && this.recordData.fields) {
            this.objectFields = this.objectFields.map(field => {
                const fieldData = this.recordData.fields[field.apiName];
                return {
                    ...field,
                    value: fieldData ? this.formatFieldValue(fieldData) : ''
                };
            });
            // 値を更新した後、現在のソート順を維持
            if (this.sortedBy) {
                this.sortData(this.sortedBy, this.sortDirection);
            }
        }
    }
    
    // 項目値のフォーマット
    formatFieldValue(fieldData) {
        if (fieldData.value === null || fieldData.value === undefined) {
            return '';
        }
        
        // 日付や日時の場合はフォーマットする
        if (fieldData.displayValue) {
            return fieldData.displayValue;
        }
        
        // 参照項目の場合
        if (typeof fieldData.value === 'object' && fieldData.value !== null) {
            return fieldData.value.displayValue || '';
        }
        
        return String(fieldData.value);
    }
    
    // ソート処理
    sortData(fieldName, direction) {
        const cloneData = [...this.objectFields];
        
        cloneData.sort((a, b) => {
            let valueA = a[fieldName];
            let valueB = b[fieldName];
            
            // 文字列の場合は小文字に変換
            if (typeof valueA === 'string') {
                valueA = valueA.toLowerCase();
            }
            if (typeof valueB === 'string') {
                valueB = valueB.toLowerCase();
            }
            
            // null/undefined値の処理
            if (valueA === null || valueA === undefined) valueA = '';
            if (valueB === null || valueB === undefined) valueB = '';
            
            return direction === 'asc' ? this.sortBy(valueA, valueB) : this.sortBy(valueB, valueA);
        });
        
        this.objectFields = cloneData;
        this.sortDirection = direction;
        this.sortedBy = fieldName;
    }
    
    // ソート比較関数
    sortBy(a, b) {
        // null値は常に最後に
        if (a === '' && b !== '') return 1;
        if (a !== '' && b === '') return -1;
        
        // 通常のソート
        if (a > b) return 1;
        if (b > a) return -1;
        return 0;
    }
    
    // ソート方向の切り替え
    handleSort(event) {
        const { fieldName, sortDirection } = event.detail;
        this.sortData(fieldName, sortDirection);
    }
    
    // 行アクション処理
    handleRowAction(event) {
        const actionName = event.detail.action.name;
        const row = event.detail.row;
        
        if (actionName === 'navigate_to_field') {
            this.showFieldDetails(row.apiName);
        }
    }
    
    // 項目詳細表示
    showFieldDetails(fieldApiName) {
        // 項目の詳細情報を取得
        const selectedField = this.objectFields.find(field => field.apiName === fieldApiName);
        
        if (selectedField) {
            // モーダルで表示するための詳細情報を設定
            this.selectedFieldDetails = {
                label: selectedField.label,
                apiName: selectedField.apiName,
                dataType: selectedField.dataType,
                isRequired: selectedField.isRequired ? 'はい' : 'いいえ',
                value: selectedField.value || '(空白)',
                isOpen: true
            };
        }
    }

    // モーダル関連
    selectedFieldDetails = {
        isOpen: false
    };

    closeModal() {
        this.selectedFieldDetails = {
            isOpen: false
        };
    }
    
    // CSVエクスポート
    exportToCSV() {
        this.isExporting = true;
        
        try {
            // CSVヘッダー行
            let csvContent = '項目ラベル,API参照名,データ型,必須,値\r\n';
            
            // データ行を追加
            this.objectFields.forEach(field => {
                const row = [
                    field.label || '',
                    field.apiName || '',
                    field.dataType || '',
                    field.isRequired ? 'はい' : 'いいえ',
                    field.value || ''
                ];
                
                // エスケープ処理
                const escapedRow = row.map(cell => {
                    if (cell.includes(',') || cell.includes('"') || cell.includes('\n')) {
                        return `"${cell.replace(/"/g, '""')}"`;
                    }
                    return cell;
                });
                
                csvContent += escapedRow.join(',') + '\r\n';
            });
            
            // BOMを追加してUTF-8として認識されるようにする
            const BOM = '\uFEFF';
            
            // データURIを使用してダウンロード
            const encodedUri = 'data:text/csv;charset=utf-8,' + encodeURIComponent(BOM + csvContent);
            const link = document.createElement('a');
            link.setAttribute('href', encodedUri);
            link.setAttribute('download', `${this.objectApiName}_Fields.csv`);
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            
            this.showToast('成功', 'CSVファイルのエクスポートが完了しました', 'success');
        } catch (error) {
            console.error('CSV export error:', error);
            this.showToast('エラー', `CSVエクスポート中にエラーが発生しました: ${error.message || error}`, 'error');
        } finally {
            this.isExporting = false;
        }
    }
    
    // 現在の日時を取得（ファイル名用）
    getFormattedDate() {
        const now = new Date();
        const year = now.getFullYear();
        const month = String(now.getMonth() + 1).padStart(2, '0');
        const day = String(now.getDate()).padStart(2, '0');
        const hours = String(now.getHours()).padStart(2, '0');
        const minutes = String(now.getMinutes()).padStart(2, '0');
        
        return `${year}${month}${day}_${hours}${minutes}`;
    }
    
    // トースト通知を表示
    showToast(title, message, variant) {
        const event = new ShowToastEvent({
            title: title,
            message: message,
            variant: variant
        });
        this.dispatchEvent(event);
    }
}
```

### 4. HTML ファイルの作成

`objectFieldsList.html` ファイルを以下のように作成します：

```html
<!--
  @description       : 任意のオブジェクトの項目一覧を表示するコンポーネント
  @author            : Your Name
  @group             : 
  @last modified on  : 2023-05-25
  @last modified by  : Your Name
-->
<template>
    <lightning-card title={cardTitle} icon-name={objectIcon}>
        <div slot="actions">
            <lightning-button 
                label="CSVエクスポート" 
                icon-name="utility:download" 
                onclick={exportToCSV} 
                disabled={isExporting}
                variant="brand">
            </lightning-button>
        </div>
        
        <div class="slds-m-around_medium">
            <template if:false={recordId}>
                <div class="slds-text-color_weak slds-m-bottom_small">
                    レコードページで使用すると、項目値も表示されます。
                </div>
            </template>
            
            <template if:true={objectFields.length}>
                <lightning-datatable
                    key-field="apiName"
                    data={objectFields}
                    columns={columns}
                    hide-checkbox-column
                    sorted-by={sortedBy}
                    sorted-direction={sortDirection}
                    onsort={handleSort}
                    onrowaction={handleRowAction}>
                </lightning-datatable>
            </template>
            
            <template if:true={error}>
                <div class="slds-text-color_error">
                    エラーが発生しました: {error.body.message}
                </div>
            </template>
        </div>
    </lightning-card>

    <!-- 項目詳細モーダル -->
    <template if:true={selectedFieldDetails.isOpen}>
        <section role="dialog" tabindex="-1" aria-labelledby="modal-heading-01" aria-modal="true" aria-describedby="modal-content-id-1" class="slds-modal slds-fade-in-open">
            <div class="slds-modal__container">
                <header class="slds-modal__header">
                    <button class="slds-button slds-button_icon slds-modal__close slds-button_icon-inverse" title="閉じる" onclick={closeModal}>
                        <lightning-icon icon-name="utility:close" alternative-text="閉じる" variant="inverse" size="small"></lightning-icon>
                        <span class="slds-assistive-text">閉じる</span>
                    </button>
                    <h2 id="modal-heading-01" class="slds-modal__title slds-hyphenate">項目詳細: {selectedFieldDetails.label}</h2>
                </header>
                <div class="slds-modal__content slds-p-around_medium" id="modal-content-id-1">
                    <dl class="slds-dl_horizontal">
                        <dt class="slds-dl_horizontal__label slds-truncate">項目ラベル:</dt>
                        <dd class="slds-dl_horizontal__detail">{selectedFieldDetails.label}</dd>
                        <dt class="slds-dl_horizontal__label slds-truncate">API参照名:</dt>
                        <dd class="slds-dl_horizontal__detail">{selectedFieldDetails.apiName}</dd>
                        <dt class="slds-dl_horizontal__label slds-truncate">データ型:</dt>
                        <dd class="slds-dl_horizontal__detail">{selectedFieldDetails.dataType}</dd>
                        <dt class="slds-dl_horizontal__label slds-truncate">必須:</dt>
                        <dd class="slds-dl_horizontal__detail">{selectedFieldDetails.isRequired}</dd>
                        <dt class="slds-dl_horizontal__label slds-truncate">値:</dt>
                        <dd class="slds-dl_horizontal__detail">{selectedFieldDetails.value}</dd>
                    </dl>
                </div>
                <footer class="slds-modal__footer">
                    <button class="slds-button slds-button_neutral" onclick={closeModal}>閉じる</button>
                </footer>
            </div>
        </section>
        <div class="slds-backdrop slds-backdrop_open"></div>
    </template>
</template>
```

### 5. コードの解説

#### 主要な機能

1. **オブジェクト情報の取得**
   - `getObjectInfo` ワイヤアダプターを使用して、指定されたオブジェクトの項目情報を取得します。
   - 取得した情報から、項目のラベル、API名、データ型、必須かどうかを抽出します。

2. **レコードデータの取得**
   - レコードページで使用される場合、`getRecord` ワイヤアダプターを使用してレコードデータを取得します。
   - 取得したデータを項目リストに反映させます。

3. **ソート機能**
   - `lightning-datatable` の `onsort` イベントを処理して、指定された列でデータをソートします。
   - デフォルトでは項目ラベルで昇順ソートされます。

4. **項目詳細表示**
   - 項目ラベルをクリックすると、その項目の詳細情報をモーダルで表示します。
   - モーダルには項目のラベル、API名、データ型、必須かどうか、現在の値が表示されます。

5. **CSVエクスポート**
   - 「CSVエクスポート」ボタンをクリックすると、表示されている項目情報をCSVファイルとしてダウンロードします。
   - CSVファイルには、項目ラベル、API名、データ型、必須かどうか、値の列が含まれます。

#### 重要なポイント

- **リアクティブプロパティ**
  - `@api` デコレータを使用して、外部から設定可能なプロパティを定義しています。
  - `$recordId` や `$objectApiName` のように、リアクティブな変数を使用してワイヤアダプターのパラメータを動的に変更できます。

- **エラーハンドリング**
  - データ取得時のエラーを適切に処理し、ユーザーに表示します。
  - CSVエクスポート時のエラーも捕捉し、トースト通知で表示します。

- **パフォーマンス考慮**
  - 大量の項目がある場合でも効率的に処理できるよう、データのクローンを作成してからソートしています。

## 使用方法

### コンポーネントのデプロイ

作成したコンポーネントをSalesforce組織にデプロイします：

```bash
sfdx force:source:deploy -p force-app/main/default/lwc/objectFieldsList
```

### コンポーネントの配置

1. Salesforce組織にログインします。
2. 設定 > Lightning アプリケーションビルダー から、任意のページを編集します。
3. コンポーネントパレットから「オブジェクト項目一覧」を探し、ページにドラッグ＆ドロップします。
4. コンポーネントの設定で、表示したいオブジェクトのAPI名を指定します（例：Account, Contact, Opportunity）。
5. 「保存」をクリックしてページを保存します。

### 使用例

#### ホームページでの使用

ホームページに配置すると、指定したオブジェクトの項目構造を素早く確認できます。

#### レコードページでの使用

レコードページに配置すると、そのレコードの実際の値も含めて項目情報を確認できます。これは、特定のレコードのデータを詳細に分析する際に役立ちます。

#### アプリページでの使用

カスタムアプリページに配置すると、管理者向けのダッシュボードとして使用できます。

## カスタマイズのヒント

### 表示する列のカスタマイズ

`columns` 配列を編集することで、表示する列をカスタマイズできます。例えば、特定の列を非表示にしたり、新しい列を追加したりできます。

### フィルタリング機能の追加

検索ボックスを追加して、項目名や値でフィルタリングする機能を実装できます：

```javascript
// JavaScript に追加
searchTerm = '';

handleSearch(event) {
    this.searchTerm = event.target.value.toLowerCase();
    this.filterFields();
}

filterFields() {
    if (!this.searchTerm) {
        // 検索語がない場合は全ての項目を表示
        this.objectFields = [...this.allObjectFields];
    } else {
        // 検索語でフィルタリング
        this.objectFields = this.allObjectFields.filter(field => 
            field.label.toLowerCase().includes(this.searchTerm) ||
            field.apiName.toLowerCase().includes(this.searchTerm) ||
            field.dataType.toLowerCase().includes(this.searchTerm) ||
            (field.value && field.value.toLowerCase().includes(this.searchTerm))
        );
    }
    
    // 現在のソート順を維持
    if (this.sortedBy) {
        this.sortData(this.sortedBy, this.sortDirection);
    }
}
```

```html
<!-- HTML に追加 -->
<div class="slds-m-bottom_small">
    <lightning-input 
        type="search" 
        label="項目を検索" 
        onchange={handleSearch}
        value={searchTerm}>
    </lightning-input>
</div>
```

### 項目グループの表示

標準項目とカスタム項目を分けて表示するなど、項目をグループ化して表示することもできます：

```javascript
// JavaScript に追加
get standardFields() {
    return this.objectFields.filter(field => !field.apiName.endsWith('__c'));
}

get customFields() {
    return this.objectFields.filter(field => field.apiName.endsWith('__c'));
}
```

```html
<!-- HTML を修正 -->
<lightning-tabset>
    <lightning-tab label="すべての項目">
        <lightning-datatable
            key-field="apiName"
            data={objectFields}
            columns={columns}
            hide-checkbox-column
            sorted-by={sortedBy}
            sorted-direction={sortDirection}
            onsort={handleSort}
            onrowaction={handleRowAction}>
        </lightning-datatable>
    </lightning-tab>
    <lightning-tab label="標準項目">
        <lightning-datatable
            key-field="apiName"
            data={standardFields}
            columns={columns}
            hide-checkbox-column
            sorted-by={sortedBy}
            sorted-direction={sortDirection}
            onsort={handleSort}
            onrowaction={handleRowAction}>
        </lightning-datatable>
    </lightning-tab>
    <lightning-tab label="カスタム項目">
        <lightning-datatable
            key-field="apiName"
            data={customFields}
            columns={columns}
            hide-checkbox-column
            sorted-by={sortedBy}
            sorted-direction={sortDirection}
            onsort={handleSort}
            onrowaction={handleRowAction}>
        </lightning-datatable>
    </lightning-tab>
</lightning-tabset>
```

## まとめ

この記事では、Lightning Web Components を使用して、任意のSalesforceオブジェクトの項目一覧を表示するコンポーネントを作成する方法を解説しました。このコンポーネントは、オブジェクトの構造を理解したり、特定のレコードのデータを確認したりするのに役立ちます。

また、ソート機能、項目詳細表示、CSVエクスポートなどの機能を追加することで、より使いやすいコンポーネントになりました。

このコンポーネントをベースに、さらに機能を追加したり、組織の要件に合わせてカスタマイズしたりすることができます。例えば、検索フィルター、項目グループ化、項目編集機能などを追加することで、より強力なツールになるでしょう。

Salesforceの管理者やデベロッパーにとって、このようなツールは日常業務の効率化に大いに役立つはずです。ぜひ、自分の組織に合わせてカスタマイズして活用してみてください。

## 参考リンク

- [Lightning Web Components 開発者ガイド](https://developer.salesforce.com/docs/component-library/documentation/ja-jp/lwc)
- [Lightning データサービス](https://developer.salesforce.com/docs/component-library/documentation/ja-jp/lwc/lwc.data_wire_service_about)
- [Lightning デザインシステム](https://www.lightningdesignsystem.com/)