Lightning Experience流のUIを作る4つの方法と比較 #salesforce

アプリをSalesforce ClassicからLightning Experience(LEX)に移行するにあたり、検討・整理した事項についてのメモです。

Salesforceの新しいUIであるLEXですが、開発する画面のLook & FeelをLEXに合わせるためには、私が知る範囲では少なくとも以下の4つの方法があります。

まぁ、どれも根底のスタイル装飾はSLDSなわけですが、作り方・扱い方の違いという観点で、4つを分けました。

主観でメリット、デメリットを整理するとこんな感じです(デメリットと言ってよいものか迷うものもありますが…)。

方式 メリット デメリット
SLDSスクラッチ 開発方式や表現力の自由度が高い マークアップが辛い。
SLDSのバージョンアップに追随する場合、コストをかけて対応が必要。
Reactライブラリ 充実したフロントエンド開発のエコシステムに乗っかって開発ができる SLDSのバージョンアップにライブラリが対応していく必要がある(利用については自己責任)
Visualforce 表現範囲が限られる 完璧ではないので、SLDSを使って細かい調整が必要になることもある。
Visualforceなので、表示領域の高さを固定にしておく必要がある(必要なコンテンツの高さだけ確保する、というのが難しい)
Lightning基本コンポーネント 継続的なアップデートと後方互換性が見込まれる(多分) 利用可能なコンポーネントには限りがある

汎用性(適用範囲)と生産性の高さのバランスがよいので、総合的には2番目がベターかなー、という気がしています。
もう少し掘り下げると、それぞれの方式が適切な用途というのは、それぞれ次のような時かなと思っています。

方式 あてはまるケース
SLDSスクラッチ 慣れているSPA方式で開発したい場合(React以外のライブラリ利用時。Vue.jsとか…?)
Reactライブラリ 慣れているSPA方式で開発したい場合(React利用時)
Visualforce Visualforceの標準コンポーネントを駆使して作られた既存のVisualforceページをLEX対応したい場合
Lightning基本コンポーネント 新規で作る画面であり、比較的シンプルな場合

また、4つの方式は排他的なものではなく、複数を組み合わせて開発することもあるかもしれません。
分かりにくい表ですが、その場合の組み合わせ方は下表のような手段を利用することになりそうです。 (1番目と2番めは、成果物はSPA方式の静的リソースであるという観点では一緒なので、同一のものと見なしています)

取り込む側\取り込まれる側 SLDSスクラッチ
Reactライブラリ
(=HTML,JS,CSS)
Visualforce Lightningコンポーネント
SLDSスクラッチ
Reactライブラリ
BrowserifyやWebpack等、普通のJSエコシステムの世界 無理 Lightning Out
Visualforce 静的リソースとして読み込み 必要であればVisualforceコンポーネント Lightning Out
Lightningコンポーネント lightning:container, ltng:require iframe <c:コンポーネント名 />

$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タグを作ってしまうので。

Salesforce開発デザインパターン集 #salesforce

Salesforce Platformの上でApexクラス・トリガやLightningコンポーネントを開発していく上でのデザインパターンについてのまとめ。
他にもあるかもしれないです(知っていたら教えてください)

先人の知恵、大事。

トリガ

Trigger Frameworks and Apex Trigger Best Practices - developer.force.com

適用していないプロジェクトの方が少ないかもしれないくらい定番かもしれないですね。
複数のオブジェクトを操作するような混み入ったDMLを扱う時に、 bypass, clearBypass が役立ちます。

スケジュールバッチ

tyoshikawa1106.hatenablog.com

Lightning Component

広範囲のパターン集(全部はまだ追えていません…)

Salesforce Lightning Component Design Patterns and Development Best Practices - Mike Topalovich | Chicago Salesforce Architect and CTO

ApexCallだとこちら。

qiita.com

早速今年のAdvent Calendarからですね。 なるほどー!と思いました(小並感)

番外編(?)HTTP コールアウト

github.com

パターンというよりもガッツリしたライブラリですが。使い所は選びそう。。


以上。
あとは、Apex言語そのものの進化にも期待したいところです。

AWS CodeBuildでいつの間にかDependency Cacheがサポートされてた

re:Inventの流れが早くて見逃していたのですが、AWS CodeBuildで依存関係のキャッシュ機構が使えるようになっていました。

aws.amazon.com

昔、自前で似たようなことをするためのやり方を試行錯誤してたのですが、もう不要ですね。

jappy.hatenablog.com

あとあるとすれば、ビルドにカスタムDockerイメージを使っている場合に、イメージのダウンロードに時間がかかってしまう課題が解消できると、最強な気がします。

SOAP API(wsc)で地理位置情報型や時間型を扱う #salesforce

Advent Calendarの季節になりましたね。世のエンジニアの1年間のノウハウが放出されるイベント、楽しみです。

さてそんな中、Advent Calendarとまったく関係のない記事になりますw(エントリを逃したとも言う…)

SFDC謹製のJavaAPIクライアントであるWSC (Force.com Web Service Connector)を使って、ちょっとクセのある(?)「地理地情報型(Location)」や、Summer'17でパイロットリリースされた「時間データ型(Time)」を操作する時のやり方についてのメモです。

地理位置情報型・時間型の扱い方

先に結論ですが、それぞれ以下のように扱うことで、利用できます。

  • 地理位置情報型
    • 緯度と経度を別々に扱えばOK。例えば項目のAPI参照名が FooLocation__c の場合は、 緯度が FooLocation__Latitude__s, 経度が FooLocation__Longitude__s のようになる
  • 時間型
    • "00:00:00.000Z" の形式

地理位置情報型、以下のIssueにある通り、少し前まではダウンロードしてきたWSDLに手を加えないと使えなかったのですが、今は直っているみたいですね*1

github.com

また、時間データ型についてはベータなので、今後変更になる可能性もある気もしますが、現時点での結果ということで…。

サンプルコード

レコード1件をINSERTする場合の例。

public class Sample {

    private static final Logger log = LoggerFactory.getLogger(Sample.class);

    public static void main(String... args) throws Exception {
        ConnectorConfig config = new ConnectorConfig();
        config.setUsername("[ログイン名]");
        config.setPassword("[パスワード]");

        PartnerConnection connection = Connector.newConnection(config);
        
        SObject obj = new SObject("Hoge__c");
        obj.setField("Name", "test");
        obj.setField("Time1__c", "12:00:00.000Z"); // これで 12:00 として時間が登録される
        obj.setField("Location1__Latitude__s", "35.1");
        obj.setField("Location1__Longitude__s", "135.2");

        SaveResult[] results = connection.create(new SObject[]{obj});

        for (SaveResult result : results) {
            log.info("{} {} {}", result.getId(), result.getSuccess(), result.getErrors());
        }
    }
}

実行環境

  • Java 8
  • WSC : "42.0.0" タグ
  • Partner WSDL : API Version 41.0

*1:しれっと会社のGitHubアカウントで +1 していますが。

Java8のLambda式とGroovy

Spockでテストコードを書く時の備忘録。

Consumer : void accept(T)

メソッド

public void method1(List<String> list, Consumer<List<String>> consumer)

Java

hoge.method1(foo, list -> list.add("aaa"));

Groovy

hoge.method1(foo, { list -> list.add('aaa') })

Predicate : boolean test(T)

メソッド

public void method2(String text, Predicate<String> predicate)

Java

hoge.method2("test", s -> s.length() < 5);

Groovy

hoge.method2('test', { s -> s.length() < 5 }) // size() でもOK

Supplier : T get()

メソッド

public void method3(Supplier<String> supplier)

Java

hoge.method3(() -> "test");

Groovy

hoge.method3({ 'aaa' }) // -> 'aaa'
hoge.method3({ -> 'bbb' }) // -> 'bbb'

Function : R apply(T t)

メソッド

public void method4(String text, Function<String, String> function)

Java

hoge.method4("abcde", s -> s.toUpperCase()); // -> "ABCDE"
hoge.method4("abcde", String::toUpperCase); // -> "ABCDE"

Groovy

 hoge.method4('abcde', { s -> s.toUpperCase() }) // -> 'ABCDE'

ちなみに、Groovy2.6からは、オプションとして -Dgroovy.antlr4=true を指定すると、GroovyでもLambda式がそのまま使えるみたいです(このブログを書いた直後に偶然知りました)

Trailheadはじめました(2) #salesforce

10月も終わろうとしていますが、依然とほぼ同じペースで時間を確保することができ、大分進みました。

  • ランク: Ranger
  • バッジ: 202(内1つはハロウィーンのイベントバッヂ)
  • ポイント: 165,325
  • トレイル: 32

50個ごとにSuperbadgeにチャレンジするという計画は、100個取った時点で軌道修正しました。
そもそも今ある全部のバッヂを足しても300行かなそうなので…。というわけで、週末を利用してSuperbadgeはコンプリートしました。

必然的に日本語訳されていないモジュールに取り組むことも多くなりましたが、英語と日本語で内容が違っているのとかは相変わらずでしたね。
特に、Lightningコンポーネント関連のモジュールは多かった気がします。それだけ発展途上ってことなのかなと思いました。

今後は、ちょっとペースを落としつつ、お気に入りしているボリュームのあるモジュール(とプロジェクト)を消化していこうかなぁと思います。