Lightning ComponentでLightning ContainerなしでD3.jsを使う #salesforce

凝ったチャートを作りたい場合、D3.jsを使いたいことがあります。

Lightning ComponentでD3.jsを使う場合、v3以下だと以下のようなエラーが起き、表示できません。

[Cannot read property 'document' of undefined] eval()@https://xxxxxx-dev-ed.lightning.force.com/resource/1537117226000/d3lib/d3lib/d3.v3.min.js:3:4187 Proxy.eval()@https://xxxxxx-dev-ed.lightning.force.com/resource/1537117226000/d3lib/d3lib/d3.v3.min.js:5:23548

Lightning Containerを使えば回避できそうですが、d3 v4を使えば、Lightning Containerなしでも表示できました(v5は未確認)。

サンプル

f:id:jappy:20180917154203p:plain

ChromeとIE11での動作を確認しています。

Component

D3のライブラリは静的リソースから読み込みます。バージョンは4.13.0です。

<aura:component implements="flexipage:availableForAllPageTypes">
    <ltng:require scripts="{!join(',',
                  $Resource.d3lib + '/d3lib/d3.min.js')}"
                  afterScriptsLoaded="{!c.initD3Charts}" />
    
    <lightning:card>
        <div id="chart"></div>
    </lightning:card>
</aura:component>

Controller

({
    initD3Charts : function(component, event, helper) {
        helper.renderGraph('#chart');
    }
})

Helper

({    
    renderGraph: function(selector) {
        // margin convention practice
        var margin = { top: 20, right: 20, bottom: 30, left: 50 };
        var width = 960 - margin.left - margin.right;
        var height = 400 - margin.top - margin.bottom;
        
        var num = 100;
        var yMax = 1000;
        
        var xScale = d3.scaleTime()
            .range([0, width])
            .domain([0, num-1]);
        
        var yScale = d3.scaleLinear()
            .range([height, 0])
            .domain([0, yMax]);
        
        // line generator
        var valueLine = d3.line()
            .x(function(d, i) { return xScale(i); })
            .y(function(d) { return yScale(d.value); });
        
        // create dataset
        var dataset = d3.range(num).map(function(d) { return {'value': d3.randomUniform(yMax)()}; });
        
        // add svg tag
        var svg = d3.select(selector).append('svg')
            .attr('width', width + margin.left + margin.right)
            .attr('height', height + margin.top + margin.bottom)
            .append('g')
            .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
        
        // X axis
        svg.append('g')
            .attr('transform', 'translate(0,' + height + ')')
            .call(d3.axisBottom(xScale));
        
        // Y axis
        svg.append('g')
            .call(d3.axisLeft(yScale));
        
        svg.append('path')
            .datum(dataset)
            .attr('class', 'line')
            .attr('d', valueLine);
    }
})

「アセットファイル」メタデータ #salesforce

あんまり馴染みがなかったのですが、Salesforceには「アセットファイル」というメタデータがあるんですね。

Salesforce Developers

  • 上記に書かれている通り、サフィックス.asset で、contentassets フォルダに保存されます。
  • package.xml においてはワイルドカードの利用も可。
  • 使い所
    • Lightningアプリケーションのブランド設定で、独自のロゴ画像を利用している場合の保管場所
    • 認証されていないユーザにファイルを共有する
    • (他には…?)
  • 権限まわりについては「アセットファイル」設定ではなく、「ファイル」タブの共有の設定で行う

画像などのリソースは静的リソースで管理するものだと思ってました。ファイルにまつわる設定としては、他にも、Files Connectなるものがあったり、まだまだ知らない機能があるんだなぁと思ったのでメモ。

最強の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などを使って組んでいる自前の静的リソースのビルドルールに影響してきそう。