この記事は Salesforce Platform Advent Calendar 2019 - Qiita 第2日目の投稿です。
事の発端
とある業務にて、Aura コンポーネント で、 Salesforceの設定メニューから生成した項目セットを動的に追加する方法を調べたので、今回はそちらをメモのためにも投稿します。
そもそも Aura コンポーネント って何?という人向けに補足
Salesforceのフロントサイド側のフレームワークの一つです。旧い名称では「Lightning コンポーネントフレームワーク」ともいいます。
新しく Lightning Web Components(LWC) が生まれて、呼称がAuraコンポーネントになりました。
最近Salesforceを始めたばかり、Vueを少し触ったことがあるだけの私自身の感覚では少しVueが近い感覚です。
詳細は、Salesforce の 無料で利用できる Web学習サイト Trailhead に記載されております。
Trailhead - Aura コンポーネントの基本
そもそも 項目セット って何?という人向けの補足
項目セットは、オブジェクトの各項目を利用するにあたって、好きにグループ化することができる機能です。
普通は、一つのオブジェクトを触る時は、ページレイアウトでカスタマイズすれば良いじゃない・・・となりますが。
 
例えば、複数のオブジェクトの更新がまとめて出来るようにカスタマイズしたい。など。
Webの閲覧画面(View)や編集画面(Edit)、新規画面(New)をフロントサイドで作り込むとき等に、項目セットは便利な機能です。
項目セットについて - Salesforce Help
改めて 目的
以下のような関係のオブジェクトが存在しておりました。
(※Salesforce上のオブジェクトはデータベース上のテーブルと同じような感じで考えてください)

今回、オブジェクトCの新規(New)、編集(Edit)を実行する際に、取引先オブジェクトの情報も同時に編集したいという要望です。
すること整理
改めて、以下のことをする必要がある。
 1.項目セットの作成
  新規(New)・編集(Edit)の時、取引先オブジェクトのどの項目を更新したいのかをリストアップして、それに合わせて項目セットを作成する。
 2.項目セットを取得するApexクラスを作成
 3.専用入力フォームをAura コンポーネントで作成
 4.標準ボタンの上書き
1.項目セットの作成
Salesforceの設定画面を開き、オブジェクトマネージャから、今回、項目セットを作成したいオブジェクトを選択して、追加できます。

新規ボタンを押すと、新規項目セットを入力する画面に移ります。
 
- 項目セットラベル
- 簡単な一言ラベルを入力するフィールド
- 項目セット名
- Apexやデータ取得時に利用するAPI参照キーを入力するフィールド
- 使用場所
- 実際に使用する場所についてフリーテキストで、解説を入力するフィールド
それぞれ、適切な値を入力して、保存ボタンをクリックする。
 
あとは、必要な項目をドラック&ドロップで入れて、保存を押す。
以上で、項目セットの作成ができます。
以下、独断と偏見にみまれた・・・どうでも良い独り言ですが・・・
 
こちらの解説に” Visualforce ページ・・・”という記載があるため、あくまで私個人の感想では、「項目セット」って・・・、もう古い機能であり、今後は進化しない機能なのかな?と一瞬かんじることがありました。とはいえ、ページの更新漏れかもしれないし、実際に、気軽に使える項目セットならではのメリットもあるため、気のせいかもしれないです。
ただ、もう時代はページレアウトに変わっているのですかね?
ここら辺が勉強不足だなぁ・・・と感じている次第です。
なお、Visualforceページ は、Classic時代に生まれたフロントサイドフレームワークという知識です。Salesforceのフロントサイドフレームワークは、Visualforce → Aura コンポーネント → Lightning Web Components(LWC) という流れでここ数年は、新しいフレームワークが提供されております。Visualforceページ は、まだまだ普通に使えるけど、最近は、LWCが人気で勢いがあるイメージを持ってます。
2.項目セットを取得するApexクラスを作成
項目セットを取得する方法は、以下の通りです。
FieldSet クラスの使用方法
何個かサンプルソースがあってわかりやすいですね!
では実際のApexクラスのイメージです。
public with sharing class ObjCFormController {
    public class FieldSetMember {
        public FieldSetMember(Schema.FieldSetMember f) {
            this.fieldPath = f.getFieldPath();
            this.label = f.getLabel();
            this.required = (f.getDBRequired() || f.getRequired());
            this.type = '' + f.getType();
        }
        @AuraEnabled
        public String fieldPath { get;set; }
        @AuraEnabled
        public String label { get;set; }
        @AuraEnabled
        public Boolean required { get;set; }
        @AuraEnabled
        public String type { get; set; }
    }
    @AuraEnabled
    public static List<FieldSetMember> getFieldSet(String typeName, String fieldSetName) {
        // 指定オブジェクトのAPI 参照名(typeName)を利用して登録済みの項目セット情報を全取得
        Schema.SObjectType targetType = Schema.getGlobalDescribe().get(typeName);
        Schema.DescribeSObjectResult describe = targetType.getDescribe();
        Map<String, Schema.FieldSet> fsMap = describe.fieldSets.getMap();
        // 取得したい項目セット名(fieldSetName)を指定してセット内容を取得
        Schema.FieldSet fs = fsMap.get(fieldSetName);
        List<Schema.FieldSetMember> fieldSet = fs.getFields();
        // 取得した項目セットをaura コンポーネントで利用出来るようにデータ形式の調整
        List<FieldSetMember> fields = new List<FieldSetMember>();
        for (Integer i = 0; i < fieldSet.size(); i++) {
            Schema.FieldSetMember f = fieldSet[i];
            fields.add( new FieldSetMember(f) );
        }
        return fields;
    }
}
コードはほぼJavaにそっくりですが、各メソッドの解説はApex開発者ガイドにクラスリファレンスが存在します。
今回、あえて、FieldSetMemberクラスを用意しているのは、Schema.FieldSetMemberがAuraコンポーネント上で利用できないからです。
 エラーメッセージ:AuraEnabled methods do not support return type of List<Schema.FieldSetMember>
当初は List<Schema.FieldSetMember> fieldSet = fs.getFields();の値を、そのまま返却して利用しようとしましたが、上記のエラーメッセージが出てきてしまい、内部クラス FieldSetMember を用意しました。
項目セットを様々なシーンにて、よく呼び出すような実装する方は、FieldSetMemberを別クラスで実装するのが良いのだろうと思ってますが、そうでもない場合は、必要なものは必要な場所だけに・・・という精神のもと、シンプルにクラス内にまとめました。
 
 
なお、当メソッドの利用イメージですが、例えば、以下条件に当てはまる項目セットを取得したい場合は
- 取引先オブジェクト(API参照名=Account)の
- 項目セットラベル:Obj-C用入力項目(項目セット名=Obj_C_InputField)
getFieldSet メソッドの引数にtypeName="Account", fieldSetName="Obj_C_InputField"を指定するイメージで作りました。
(※注意※上記コードは項目セット名が存在しない場合のロジックを、あえて除外してますのでコピペの際はご注意を・・・)
3.専用入力フォームをAura コンポーネントで作成
さて、次は、専用入力フォームをAura コンポーネントで作成する必要があります。
なお、大前提として、Auraコンポーネント(Lightning Component)の作成やApexクラスの作成については、SFDXコマンドを実行するなり、開発者コンソールから新規作成するなり通常通りです。
以下、Auraコンポーネント側のイメージです。(一部抜粋です)
<aura:component implements="lightning:actionOverride"
                controller="ObjCFormController">
    <aura:attribute name="recordId" type="Id" />
    <aura:attribute name="sObjectName" type="String" />
    <aura:attribute name="accountId" type="Id" />
    <aura:attribute name="accountFields" type="FieldSetMember[]" />
    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
    
    <lightning:card iconName="custom:custom46">
        <aura:set attribute="title">
            <aura:if isTrue="{!v.recordId}">
                オブジェクトCの編集
                <aura:set attribute="else">新規オブジェクトC</aura:set>
            </aura:if>
        </aura:set>
        <aura:set attribute="actions">
            <lightning:button variant="brand" label="保存" title="保存" onclick="{!c.save}" />
            <lightning:button label="キャンセル" title="キャンセル" onclick="{!c.cancel}"/>
        </aura:set>
        <div class="slds-section slds-is-open">
            <div class="slds-section slds-is-open">
                <h3 class="slds-section__title slds-theme_shade slds-m-horizontal_x-small">
                    <span class="slds-truncate slds-p-horizontal_small">オブジェクト情報</span>
                </h3>          
                <div aria-hidden="false" class="slds-section__content slds-m-horizontal_x-large">
                    <!-- 省略中 -->
                    <lightning:recordEditForm
                      aura:id="recordEditForm"
                      objectApiName="{!v.sObjectName}"
                      recordTypeId="{!v.recordTypeId}"
                      onsuccess="{!c.handleSuccess}">
                      <lightning:messages />
                      <lightning:inputField aura:id="objcInput" fieldName="Name" />
                      <lightning:inputField aura:id="objcInput" fieldName="Account__c" />
                    </lightning:recordEditForm>
                </div>
            </div>
            <div class="slds-section slds-is-open">
                <h3 class="slds-section__title slds-theme_shade slds-m-horizontal_x-small">
                    <span class="slds-truncate slds-p-horizontal_small">編集可能な関連Account情報</span>
                </h3>          
                <div aria-hidden="false" class="slds-section__content slds-m-horizontal_x-large">
                    <lightning:recordEditForm 
                                              recordId="{!v.accountId}" 
                                              objectApiName="Account">
                        <lightning:messages />
                        <aura:iteration items="{! v.accountFields }" var="f">
                            <div class="slds-form__row">
                                <div class="slds-form__item" role="listitem">
                                    <lightning:inputField aura:id="accInput" fieldName="{! f.fieldPath }" required="{! f.required }" />
                                </div>
                            </div>
                        </aura:iteration>
                    </lightning:recordEditForm>
                </div>
            </div>
        </div>
    </lightning:card>
</aura:component>
※aura:componentのcontroller属性に"ObjCFormController"を付与しています。
またaura:componentのimplements属性にlightning:actionOverride インターフェースを追加しています。
こちらは、利用するApexクラス名の定義、またこの後に関連するのですが標準ボタンを上書きにするにあたって必要なインターフェースです。
補足は アクション上書きとして使用する Aura コンポーネントの作成 に記載あります。
そのほか、関連するController.jsとHelper.jsは以下のようになります。
({
  doInit: function (cmp, event, helper) {
    // 取引先の項目セットを取得する
    helper.callAction(cmp, event, 'getFieldSet', {
      typeName: 'Account',
      fieldSetName: 'Obj_C_InputField'
    })
    .then(function (fields) {
      console.log(fields);
      cmp.set('v.accountFields', fields);
    })
    .catch(function (e) {
      console.error(e); // コピペ注意,エラー時の処理は除外
    });
    // 関連するaccountIDやオブジェクトCの情報などを取得する処理は除外
  }
})
({
    callAction: function(cmp, event, name, params, opts) {
        var _this = this;
        return new Promise(function(resolve, reject) {
            if (!opts) opts = {};
            var action = cmp.get("c." + name);
            if (opts.storable) action.setStorable();
            if (params) action.setParams(params);
            action.setCallback(_this, function(response) {
                var state = response.getState();
                if (cmp.isValid() && state === "SUCCESS") {
                    resolve(response.getReturnValue());
                } else if (state === "ERROR") {
                    var errors = response.getError();
                    if (errors) {
                        console.error(errors);
                        var message = errors[0] && (
                            errors[0].message || 
                            (errors[0].pageErrors && errors[0].pageErrors[0] && errors[0].pageErrors[0].message)
                        );
                        reject(message || 'Unexpected error occured.');
                    } else {
                        console.error("Unknown error occured.");
                        reject("Unknown error occured.");
                    }
                }
            });
            $A.enqueueAction(action);
        });
    }
})
以上で、項目セットに関係するロジック の実装は完了です。
4.標準ボタンの上書き
最後に、作成したコンポーネントを使えるように標準ボタンの上書きです。
場所は、Salesforceの設定画面を開き、オブジェクトマネージャから、今回、標準ボタンの処理を上書きしたいオブジェクト選択して、「ボタン、リンク、およびアクション」を選択します。
例えば、新規ボタンをクリック時の処理を変更するときは、右側の逆三角形をクリックして、「編集」をクリックします。

 
次に、今回作成したコンポーネントで上書きするために、オプションを変更します。
今回は、[Lightning Experienceの上書き]からLightningコンポーネントで、作成したコンポーネントを指定して保存をクリックします。
(注意ですが、改めて、aura:componentのimplements属性にlightning:actionOverride インターフェースを追加しないと、こちらには表示されません。私の中の忘れがちポイントです。)

 
設定が保存されて、Webページをリロードすると、
実際にオブジェクトCのタブで新規でデータを作成しようとした時に、以下のようにカスタマイズされた画面を表すことができます。

以上です!
最後に、改めて、今回の調べて思ったことは、、、
今回は項目セットって結構自由に使えるんだなぁ・・・と感じたのがでかい学びになりました。
固定入力項目と比べても、項目セットを使うことで、メンテナンス性の高いLightningコンポーネントを作る事ができます。
しかし、頭の片隅にあるのは、もしかすると・・・今の時代は「ページレイアウトを活用しよう!」という時代なのかもしれないです。
なぜならば、ただ項目を並べるだけまとめるだけの項目セットと比べて、2列表示やセクションを分けてレイアウトを柔軟に作り込むことができるからです。
しかし、私の知識不足もあり、パッと見てページレイアウトにはAPI参照名という存在が見当たらず、ソース上から複数のオブジェクトのページレイアウトを柔軟に表示する方法が簡単に開発者ガイドからも見当たらないのが気になるところです。
今度良きタイミングがあれば、こちらをもうちょっと調べてみたいなと感じております。
項目セットだけでも、メンテナンス性が高いもの作れますが、ページレイアウトも使いこなせるようになれば、更に自由度がさらに広がりそうです。
私自身、まだまだSalesforceでの開発について学び中の身ではありますが、参考になればと思い記事にしました。
以上です。ではでは!
