$A.createComponentsでちょっと複雑なコンポーネントを動的に作成してみる #salesforce

Lightningコンポーネントには $A.createComponent$A.createComponents メソッドを利用して、コントローラ(or ヘルパー)側で動的にコンポーネントを作成する手段が用意されています。

developer.salesforce.com

2つのメソッドの違いは単一のコンポーネントを作るか、複数の(特にネストした)コンポーネントを作るか、にあります。
$A.createComponents メソッドを使って、どれくらい複雑なコンポーネントが作れるものなのか確認してみました。

試したサンプルは、lightning:layoutにあるものです(↓)。

<aura:component>
    <div class="c-container">
        <lightning:layout horizontalAlign="space">
            <lightning:layoutItem flexibility="auto" padding="around-small">
                1
            </lightning:layoutItem>
            <lightning:layoutItem flexibility="auto" padding="around-small">
                2
            </lightning:layoutItem>
            <lightning:layoutItem flexibility="auto" padding="around-small">
                3
            </lightning:layoutItem>
            <lightning:layoutItem flexibility="auto" padding="around-small">
                4
            </lightning:layoutItem>
        </lightning:layout>
    </div>
</aura:component>

そこまで複雑というわけではないですね。。
ただ、公式の例( ui:messageui:outputText )と比べ、複数の子要素( lightning:layoutItem )を含んでいたりするので、これくらいのものができれば、後は応用が効きそうだということで選定しました。

コンポーネント : DynamicNestedComponents.cmp

Lightningコンポーネントはこんな感じになりました。

コントローラ(後述)で作成した lightning:layout, lightning:layoutItem{! v.contents} のところで出力させるようにします。

<aura:component  implements="flexipage:availableForAllPageTypes" access="global">
    <aura:attribute name="contents" type="String" />
    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
    
    <div class="c-container">
    {! v.contents }
    </div>
</aura:component>

なお、 implements="flexipage:availableForAllPageTypes" access="global" の部分に関しては、動作確認を適当なLightningページでするためのものなので、今回の本筋ではないです。

コントローラ : DynamicNestedComponentsController.js

コンポーネントを作成する肝心のコントローラの doInit は次のようになりました。

({
    doInit : function(component, event, helper) {        
        $A.createComponents([
            ["lightning:layout", {
                "horizontalAlign" : "space"
            }],
            ["lightning:layoutItem", {
                "flexibility" : "auto",
                "padding" : "around-small"
            }],
            ["ui:outputText",{
                "value" : "1"
            }],
            ["lightning:layoutItem", {
                "flexibility" : "auto",
                "padding" : "around-small"
            }],
            ["ui:outputText",{
                "value" : "2"
            }],
            ["lightning:layoutItem", {
                "flexibility" : "auto",
                "padding" : "around-small"
            }],
            ["ui:outputText",{
                "value" : "3"
            }],
            ["lightning:layoutItem", {
                "flexibility" : "auto",
                "padding" : "around-small"
            }],
            ["ui:outputText",{
                "value" : "4"
            }],
        ],
            function(components, status, errorMessage){
                if (status === "SUCCESS") { 
                    var layout = components[0];
                    var layoutItems = [
                        components[1],
                        components[3],
                        components[5],
                        components[7] ];
                    var outputTexts = [
                        components[2],
                        components[4],
                        components[6],
                        components[8] ];
                    
                    // それぞれの lightning:layoutItem に値をセット
                    for (var i=0; i<4; i++) {
                        layoutItems[i].set("v.body", outputTexts[i]);
                    }
                    
                    // lightning:layout の下に lightning:layoutItem をセット
                    var layoutBody = layout.get("v.body");
                    for (var i =0; i<4; i++) {
                        layoutBody.push(layoutItems[i]);
                    }
                    layout.set("v.body", layoutBody);
                    
                    component.set("v.contents", layout);
                }
                else if (status === "INCOMPLETE") {
                    console.log("No response from server or client is offline.")
                    // Show offline error
                }
                else if (status === "ERROR") {
                    console.log("Error: " + errorMessage);
                    // Show error message
                }
            }
        );
    }
})

長いですね…。
子要素が4つあるサンプルのため冗長であることをを抜きにしても、結構長いです。

注意する点として、 set("v.body", ...) で中身をセットしたい場合、セットする値もコンポーネントである必要があります。
なので、lightning:layoutItem の中身は表示したい文字列ではなく、 ui:outputText コンポーネントになっています *1
もし、

layoutItems[i].set("v.body", "" + i);

のようにしてしまうと、

Uncaught Assertion Failed!: Descriptor for Config required for registration : undefined

といったエラーが起きます。

というわけで、複数の子要素を含んだLightningコンポーネントでも一応は動的に作成可能であることが確認できました。

*1:実はこのせいで、出力されるHTMLはオリジナルのものとは微妙に異なります。ui:outputText はspanタグを作ってしまうので。