最強のLightningページレイアウト #salesforce

(タイトルはネタですが)

テラスカイさんのブログ「Lightningページレイアウトについて考えてみる - TerraSkyBase」を読んで、以前から考えていたアイディアを試してみました。

それは、「組み込みで用意されているページレイアウトから選択する」ではなく、「どのパターンにも対応可能なレイアウトを用意すれば、迷うことがなくなるんじゃないか?」というものです。

きっかけ

Lightningページの困ることの一つは、一度作ったレイアウト(テンプレート)は、後から変更できないという制限(仕様)です。
なので、「いったん1カラムで作ってから、後で2カラムに変えたい」みたいなことができません。
設定画面(GUI)からではなく、メタデータ(XML)経由でなら変えられるんじゃないか?と思って試したりもしましたが、やっぱりNGでした。

通常の開発・運用であれば、新たに作り直せばいいのかもしれませんが、管理パッケージで配布するような場合、不用意にヘタなレイアウトで作ったLightningページをリリースしてしまうと、後からの差し替えが面倒になることが考えられます。

作ってみる

さて、Lightningページ作成時に選択するテンプレートですが、組み込みで用意されているもの以外に、カスタムで作ってそれを利用することもできます。

developer.salesforce.com

テンプレートの各リージョン(1つ1つの矩形のテンプレート領域を便宜上リージョンと呼んでいます)には、0個以上のLightningコンポーネントをスタックできるので、色んなグリッドのパターンを盛り込んだテンプレートを作ってあげれば、一つのレイアウトで色々なレイアウトのLightningページを作成できると考えました。

というわけで、作ったものがこちら。

f:id:jappy:20180718014824p:plain

(Lightningアプリケーションビルダーでの画面ですが、見やすさのために各リージョンにリッチテキストコンポーネントを配置しています)

タイトルで最強と言いつつも、以下の5タイプだけ対応しました。

  • 1カラム
  • 2カラム(等分割)
  • 3カラム(等分割)
  • 2カラム(右サイドバー形式)
  • 2カラム(左サイドバー形式)

また、順番の入れ替えもいったん未対応です。

コード

Component

カスタムのテンプレートはLightningコンポーネントバンドルとして実装します。
その際に必要なのはComponent(HTML)、Design(HTML)が必須で、後は必要に応じてSVGやStyleを作ります。今回は、Component、Design、Styleの3つを定義しました。

<aura:component implements="lightning:appHomeTemplate" description="All-in-one Template">
    <aura:attribute name="header" type="Aura.Component[]" />
    <aura:attribute name="leftOfBisection" type="Aura.Component[]" />
    <aura:attribute name="rightOfBisection" type="Aura.Component[]" />
    <aura:attribute name="leftOfTrisection" type="Aura.Component[]" />
    <aura:attribute name="centerOfTrisection" type="Aura.Component[]" />
    <aura:attribute name="rightOfTrisection" type="Aura.Component[]" />
    <aura:attribute name="leftOf2to1" type="Aura.Component[]" />
    <aura:attribute name="rightOf2to1" type="Aura.Component[]" />
    <aura:attribute name="leftOf1to2" type="Aura.Component[]" />
    <aura:attribute name="rightOf1to2" type="Aura.Component[]" />
    
    <div>
        <lightning:layout horizontalAlign="spread" multipleRows="true" pullToBoundary="medium">
            <lightning:layoutItem flexibility="grow" size="12" padding="horizontal-small">
                <div class="layout__col">{!v.header}</div>
            </lightning:layoutItem>
            <lightning:layoutItem flexibility="grow" size="6" padding="horizontal-small">
                <div class="layout__col">{!v.leftOfBisection}</div>
            </lightning:layoutItem>
            <lightning:layoutItem flexibility="grow" size="6" padding="horizontal-small">
                <div class="layout__col">{!v.rightOfBisection}</div>
            </lightning:layoutItem>
            <lightning:layoutItem flexibility="grow" size="4" padding="horizontal-small">
                <div class="layout__col">{!v.leftOfTrisection}</div>
            </lightning:layoutItem>
            <lightning:layoutItem flexibility="grow" size="4" padding="horizontal-small">
                <div class="layout__col">{!v.centerOfTrisection}</div>
            </lightning:layoutItem>
            <lightning:layoutItem flexibility="grow" size="4" padding="horizontal-small">
                <div class="layout__col">{!v.rightOfTrisection}</div>
            </lightning:layoutItem>
            <lightning:layoutItem flexibility="grow" size="8" padding="horizontal-small">
                <div class="layout__col">{!v.leftOf2to1}</div>
            </lightning:layoutItem>
            <lightning:layoutItem flexibility="grow" size="4" padding="horizontal-small">
                <div class="layout__col">{!v.rightOf2to1}</div>
            </lightning:layoutItem>
            <lightning:layoutItem flexibility="grow" size="4" padding="horizontal-small">
                <div class="layout__col">{!v.leftOf1to2}</div>
            </lightning:layoutItem>
            <lightning:layoutItem flexibility="grow" size="8" padding="horizontal-small">
                <div class="layout__col">{!v.rightOf1to2}</div>
            </lightning:layoutItem>
        </lightning:layout>
    </div>
    
</aura:component>

あんまり解説が必要なところがないのですが、レイアウトはLightningのnativeコンポーネントを使って楽にグリッドシステムを組み立てました。

Design

<design:component label="All-in-One Template">
    <flexipage:template>
        <flexipage:region name="header" defaultWidth="MEDIUM" />
        <flexipage:region name="leftOfBisection" defaultWidth="MEDIUM" />
        <flexipage:region name="rightOfBisection" defaultWidth="MEDIUM" />
        <flexipage:region name="leftOfTrisection" defaultWidth="MEDIUM" />
        <flexipage:region name="centerOfTrisection" defaultWidth="MEDIUM" />
        <flexipage:region name="rightOfTrisection" defaultWidth="MEDIUM" />
        <flexipage:region name="leftOf2to1" defaultWidth="MEDIUM" />
        <flexipage:region name="rightOf2to1" defaultWidth="MEDIUM" />
        <flexipage:region name="leftOf1to2" defaultWidth="MEDIUM" />
        <flexipage:region name="rightOf1to2" defaultWidth="MEDIUM" />
    </flexipage:template>
</design:component>

Style

CSS力が足らず少し時間がかかったのがこの部分。コンポーネントが存在しない行の場合に上下の空白の大きさを調節するため、CSS3のセレクタを使ってマージンを調整しています。

.THIS .layout__col > div:not(:empty) {
    margin-bottom: .75rem;
}

おわりに

ひとまず、目論んでいたようにカスタムテンプレートを作ることはできました。
ここで作ったテンプレートがどれくらい実用性があるのかは不明ですが、少なくとも、標準で用意されているテンプレートで対応できないケースの場合、カスタムのテンプレートを作るのは一つの手段として有力なので、こんなこともできるんだと触っておくとよいのかなと感じました。

Salesforce DXのforce:source:pushで「The "path" argument must be of type string.」エラーが出る時 #salesforce

ある時Salesforce DX (SFDX)の force:source:push で次のエラーが出ました。

$ sfdx force:source:push
ERROR:  The "path" argument must be of type string.

その時の対処法についてのメモです。

いきなり結論

*-meta.xml に対応するファイル(もしくはフォルダ)が正しく存在するか確認しましょう。

今回のケースでは、静的リソースフォルダ ( force-app/main/default/staticresources )に hoge.resource-meta.xml というファイルがあるのに、実際のリソースファイル(もしくはフォルダ)がないことが原因でした。

調査記録

原因を特定するために行ったことなどの記録です。

詳しいエラーメッセージを出してみる

一行だけのエラーメッセージでは原因に皆目検討がつかないので、 --json オプションをつけてもう少し詳細なエラーメッセージが表示されるようにします。 (出力形式をJSONにすることでなぜエラー出力が詳細になるのか?という疑問はさておき…)

するとこんな感じでした。

$ sfdx force:source:push --json
{"message":"The \"path\" argument must be of type string","status":1,"stack":"TypeError [ERR_INVALID_ARG_TYPE]: The \"path\" argument must be of type string\n    at assertPath (path.js:28:11)\n    at Object.resolve (path.js:1184:7)\n    at Object.copy (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/salesforce-alm/node_modules/fs-extra/lib/copy/copy.js:27:28)\n    at Object.copy (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/universalify/index.js:5:67)\n    at Object.tryCatcher (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/bluebird/js/release/util.js:16:23)\n    at Object.ret [as copyAsync] (eval at makeNodePromisifiedEval (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/bluebird/js/release/promisify.js:184:12), <anonymous>:15:23)\n    at Promise.resolve.then (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/salesforce-alm/dist/lib/sourceConvertApi.js:143:46)\n    at tryCatcher (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/bluebird/js/release/util.js:16:23)\n    at Promise._settlePromiseFromHandler (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/bluebird/js/release/promise.js:510:31)\n    at Promise._settlePromise (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/bluebird/js/release/promise.js:567:18)\n    at Promise._settlePromiseCtx (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/bluebird/js/release/promise.js:604:10)\n    at Async._drainQueue (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/bluebird/js/release/async.js:143:12)\n    at Async._drainQueues (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/bluebird/js/release/async.js:148:10)\n    at Immediate.Async.drainQueues [as _onImmediate] (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/bluebird/js/release/async.js:17:14)\n    at runCallback (timers.js:800:20)\n    at tryOnImmediate (timers.js:762:5)","name":"TypeError [ERR_INVALID_ARG_TYPE]","warnings":[]}

うーん、イマイチよくわかりませんね…。慣れている人ならピンとくるのかもしれませんが…。

sfdx-cliを最新にしてみる

最新バージョンにアップデートしました。

$ npm install --global sfdx-cli

(結果)状況変わらず…。

地道に原因を潰していく

どこのリビジョンからpushできなくなったのかを切り分けていき、原因を見極めていきます。
こういう時のためにも、意味のある最小単位で小さくコミットしていくことは重要ですね(自戒)

で、原因は、静的リソースのファイル不足であることがわかりました。

sfdxプロジェクトで開発をしている場合でも、webpackなどのNode.jsのエコシステムを使って静的リソースをビルドしてたので、ビルドの成果物をバージョン管理に含まれないようにするため、 .gitignore でこんなことを書いていたんですね…。

force-app/main/default/staticresources/**
!force-app/main/default/staticresources/*-meta.xml

そのため、ある人の環境できちんと動作でき、リポジトリに忘れずにpushされたはずの状態でも、別の人の環境で静的リソースファイルが存在せず、pushできなかった、という状況が起きていたようです。

Lightning Componentのコンポーネントライブラリが便利 #salesforce

Spring'18の時点でベータリリースはされていましたが、正式リリースされたっぽいです。

コンポーネントライブラリとは

developer.salesforce.com

実際のURLは https://developer.salesforce.com/docs/component-library/overview/components ですね。

上記のページを見てもらうのがよいですが、要するにLightningの基本コンポーネント(入力フォームとかグリッドシステムとか)のリファレンスページです。

どのようなコンポーネントがあるのか一目で分かるようになっており、各オプションの動作やコード表示も備わっているので、便利ですね。また、公式なので、リリースに伴う新しいコンポーネントやオプションもきちんと反映されていくことでしょう。

Lightningコンポーネントのライブエディタ

(ここからはただのこぼれ話です)

少し前(2017年11月頃)まではライブラリ集みたいなものはなく、ドキュメントもそこまで便利にはなっていなかったので、その不便さを解消するため、個人的に以下のようなプレビューアプリ(Lightningコンポーネントのライブエディタ)を作ったりしていました。

jappy.hatenablog.com

このアプリ、もうすぐ完成というところで、Spring'18が間近に迫り、その中でコンポーネントライブラリのベータ版 *1 もリリースされたことで、役割の被る拙作アプリの開発は止まっちゃっていました…。

当初目標に掲げていた以下のコンセプトも大体クリアできましたし、もう少しコードを整理して、別の形で活用法は考えたいなと思います。

  • Apex, Visualforceを一行も書かずに使えるアプリを作ること
  • 極力Lightning Design Systemを直に使わず、アプリ自身もLightning基本コンポーネントを徹底的に活用して作ること
  • $A.createComponents でどこまで柔軟なUIが作れるのか見極めること

*1:当時はhttps://<myDomain>.lightning.force.com/componentReference/suite.app みたいなURLにアクセスすると見ることができました。

Java10でLombokを使ったプロジェクトをGradleビルドする

業務プロジェクトのJavaのバージョンアップは8→11を目論んでいますが、新しいJavaの機能にも少しずつ慣れていかなくてはと思ってます。

というわけで、あるプロジェクトを試しにJava10でビルドしてみたところ、Lombokで躓いたのでその解消方法についてのメモです。
基本的には、エラーが出たらエラー内容をきちんと読んで、一つずつ解消していくしかない、という当たり前の結論になりました。

環境

元のプロジェクトのバージョンとバージョンアップ後の状況はこんな感じ。

バージョンアップ前 バージョンアップ後
Java 1.8.0_172 10.0.1
Gradle ※ 3.5.1 4.7
Lombok 1.16.10 1.16.22

※ Gradleは、Java10だと以下のエラーでビルドができないので、上げています。

FAILURE: Build failed with an exception.

* What went wrong:
Could not determine java version from '10.0.1'.

Lombokの最新Java対応状況

ちなみにLombokですが、今年の2月頃の状況としては、以下のページに書かれている通り、Java10 Early Access では動かなかったようです。

https://qiita.com/tmurakam99/items/b5ffe7f18bc06577f619qiita.com

LombokJDK の内部クラスにどっぷり依存しているので、JDKバージョンアップの度に追従が大変。JDKだけじゃなくてIDEの実装にも依存がある。
(略)
実際、OpenJDK10 Early Access ではすでに動かなくなってます。うはー

試したところ、Lombok 1.16.10のままで --stacktrace をつけてビルドすると、スタックトレースに以下のようなエラーが出力されました。

Caused by: java.lang.ClassNotFoundException: com.sun.tools.javac.code.TypeTags
        at lombok.launch.ShadowClassLoader.loadClass(ShadowClassLoader.java:418)
        at lombok.javac.JavacTreeMaker$SchroedingerType.getFieldCached(JavacTreeMaker.java:156)
        at lombok.javac.JavacTreeMaker$TypeTag.typeTag(JavacTreeMaker.java:244)
        at lombok.javac.Javac.<clinit>(Javac.java:154)
        ... 75 more

最新の1.16.22では、JDK10でコンパイルできるようになったようなので、試してみました。

Lombok1.16.22でビルドしてみる

早速ビルドすると、以下のようなコンパイルエラーが大量に出力…。

エラー: コンストラクタ Hoge()はすでにクラス Hogeで定義されています
@NoArgsConstructor
^

changelogを見ると、

FEATURE: Private no-args constructor for @Data and @Value to enable deserialization frameworks (like Jackson) to operate out-of-the-box. Use lombok.noArgsConstructor.extraPrivate = false to disable this behavior.

とあり、 @Data@Value アノテーションを付けた場合、privateな引数なしコンストラクタが作られるので、明示的に @NoArgsConstructor でpublicな引数なしコンストラクタを作ろうとすると、二重定義でエラーになってしまうみたいですね。

というわけで、メッセージに書かれている通り、プロジェクトの直下に lombok.config というファイルを作って、

lombok.noArgsConstructor.extraPrivate = false

と追記することで、今まで通りビルドできるようになりました。

Gradleのwarningも解消する

ビルドする過程でGradleをバージョンアップしたのですが、ビルド時にコンソールに

Deprecated Gradle features were used in this build, making it incompatible with Gradle 5.0.

Detecting annotation processors on the compile classpath is deprecated and Gradle 5.0 will ignore them.

という形の警告が表示されました。これを解消するには、dependenciesにannotationProcessorを追加すればOKです。

  dependencies {
      annotationProcessor "org.projectlombok:lombok:1.16.22"  // これを追加
      compileOnly "org.projectlombok:lombok:1.16.22"
  }

参考記事) java - Gradle deprecated annotation processor warnings for lombok - Stack Overflow

sfdx形式のソースとメタデータAPI形式のソースの違いについて #salesforce

メモ。

sfdxとスクラッチ組織を使った開発をしていく上で、従来のメタデータAPIに慣れている場合、sfdx形式のソースとメタデータAPI形式のソースでは違いがあることを知っておく必要があります。

「具体的にどんな違いがあるんだろう?」と疑問に思って、Salesforce CLIforce:(source|mdapi):convert コマンドで試したりしていたのですが、公式のドキュメントにきちんと記述されてましたw

developer.salesforce.com

カスタムオブジェクトや静的リソース(zip)については大胆に変わっているので、そのあたりの違いを抑えておくとよさそうです。
特に静的リソースについては、webpackやGulpなどを使って組んでいる自前の静的リソースのビルドルールに影響してきそう。

イマドキのSalesforce開発のプロジェクト雛形を作った #salesforce

ひとまず取り入れたかった技術要素が盛り込めたので、公開してみました。

開発環境の構築は最初に必要な作業ですが、得てしてハードルが高いので、そのあたりの敷居を下げるのが狙いです。

Lightning Testing Service (LTS) の使い方

LTSを使ってみようとしたわけですが、ドキュメント( https://forcedotcom.github.io/LightningTestingService/ )の情報量が少なく、独自のテストを追加してテストを走らせる方法が、ソースを見るまで分からなかったので、簡単に使い方をメモします。

LTSのインストール

$ sfdx force:lightning:test:install

対象のスクラッチ組織に LTS の非管理パッケージをインストールします。
パッケージ名は「Lightning Testing Service with Examples」です。
デフォルトでは最新バージョンがインストールされ、執筆時点で1.3でした。

これはスクラッチ組織ごとに必須な処理ですが、そんなに大きなパッケージではないため、おおよそ10秒未満でインストールが完了するようです。これくらいだったらCIを回す上でも許容範囲かなと。

独自のテストを作成

$ sfdx force:lightning:test:create -n {テスト名}

ここで作成されるテストクラスは、実体(メタデータ)は静的リソースです。

静的リソースといってもZIPファイルではなく単なるJavaScriptなので、ここにテストを記述していくことができます。

テストスイートとなるLightningアプリケーションを作成

追加した独自のテストを実行させるためには、別途Lightningアプリケーションが必要なようです。
これは、LTSパッケージに含まれているテストスイートでは、LTSパッケージ内に事前に用意されているテストしか実行されないためです。
例えばjasmineTestsテストスイートは以下のように記述されており、3つのテストを実行させるように指定されていますが、当然、こちらで新たに追加したテストは含まれていません。

<aura:application>

    <c:lts_jasmineRunner testFiles="{!join(',', 
      $Resource.jasmineHelloWorldTests,
      $Resource.jasmineExampleTests,
      $Resource.jasmineLightningDataServiceTests
    )}" />

</aura:application>

自分で追加するテストスイート用のアプリケーションでは、上記を真似て、testFiles 属性のところで、作成したテストクラス(静的リソース)の名前を渡してあげるよう変更すればOKです。

<aura:application >
    <c:lts_jasmineRunner testFiles="{!join(',', $Resource.<テスト名>)}" />
</aura:application>

テスト関連のメタデータをデプロイ

上記のLightningアプリケーションと静的リソースをスクラッチ組織にデプロイします。

$ sfdx force:source:push

テストの実行

$ sfdx force:lightning:test:run -a <テストアプリケーション名>

ちなみに、標準でJUnitのレポート出力も用意されているので、Travis CIなどのサービスでなくJenkinsでレポートすることも余計な手間なくできそうです。

テストの書き方

テストコードの文法は、JasmineやMochaの流儀にしたがっていけばよいわけですが、Lightningコンポーネントのテストをするのが目的なので、テストコード中でLightningコンポーネントへのアクセスが必要になります。

LTSの場合、テストコード中では $T というグローバルなオブジェクトが利用できるので、それを使うみたいです。

Jasmineの場合はこんな感じ。

describe("c:Hoge", function () {
  it("sets component attributes", function (done) {
    var that = this;

    $T.createComponent("c:Hoge", null)  // テスト対象となるコンポーネントを作成
      .then(function (component) {

        // 必要であれば、$T.waitFor を使って、条件を満たすまでウエイトを入れることができる
        return $T.waitFor(function () {
          that.component = component;
          return 1 === 1; // サンプルのためすぐに true が評価されるようにして、抜ける
        }, 10000);
      }).then(function () {
        // $T.createComponent のコールバックに作成されたコンポーネントが渡されるので、それを使ってassertionを記述する
        expect(that.component.get("v.attr1")).toBe("aaaaa");
        done();
      }).catch(function (e) {
        done.fail(e);
      });
  });
});

テストコードはバージョン管理上どこに置くべき?

どうすべきなんでしょうね…。

sfdxで素直に作ったプロジェクトであれば foce-app がパッケージディレクトリとなっていると思うので、今回は force-app/test/default 以下にテストコード(静的リソース)やテストスイート(Lightningアプリケーション)を置くようにしてみました。

Salesforce CLI (SFDX)のコマンドがわかる一枚絵 #salesforce

いっぱいあるsfdxのコマンドの勘所をつかむための一枚絵を作ってみました。

f:id:jappy:20180224114254p:plain

(探せばありそうな気もしますが…)

参考

qiita.com