skosaka's blog

Study note

Recreate modal component for force:lightningQuickActionWithoutHeader

Intro

Lightning component action is a handy option to create custom function called from record detail page. We can launch a lightning component from a custom action by just implementing either force:lightningQuickAction or force:lightningQuickActionWithoutHeader. However, lightning component action has serious user interface problems. I will introduce them and propose a workaround.

Problems

A component implemented force:lightningQuickAction is shown on the auto-created modal and its header and footer are outside of the component. So we can neither customize the header text nor put an another button next to the cancel button on the footer.
f:id:shunkosa:20171212181307p:plain

It's a natural to think about recreating everything in a component implemented force:lightningQuickActionWithoutHeader. However, it has another problem. Extra margin is generated.
f:id:shunkosa:20171212181318p:plain

Workaround

Notice: The following workaround is not compatible with Salesforce mobile.
We can overwrite the style of the auto-created modal by inline CSS to disable extra margin. Another possibility is calling a custom modal from lightning:button tag but it requires you to recreate a highlight panel and it would be a tough work.

.cuf-content{
    padding:0 !important;
}
.slds-p-around--medium{
    padding:0 !important;
}
.slds-modal__content{
    overflow-y:hidden !important;
    height:unset !important;
    max-height:unset !important;
}

f:id:shunkosa:20171212181336p:plain
Now we get a modal which is similar to standard action.
f:id:shunkosa:20171212181350p:plain:w500

Reusable component

I created a template modal component which can be used in a component implemented force:lightningQuickActionWithoutHeader. It is available on this repository.

Usage
<aura:component implements="force:lightningQuickActionWithoutHeader">
	<c:LightningActionModal title="Hello world!" percentWidth="85" bodyHeight="300">
        <aura:set attribute="body">
            //awesome modal body
        </aura:set>
        <aura:set attribute="footer">
            <lightning:button label="Cancel" variant="neutral"/>
            <lightning:button label="Save" variant="brand" />
        </aura:set>
    </c:LightningActionModal>
</aura:component>
Attributes
Attribute Type Description
title String Text shown on the modal header.
percentWidth String Designate the width of the modal by percent. Default value is 50.
bodyHeight Integer Designate the height of the modal body by pixel. Default value is 250. The value in the custom action setting is neglected due to the overwritten CSS.
body Object Content in the modal body.
footer Object Content in the modal footer.

Lightning Data Serviceで取得したレコードをApexに渡す

Lightning Data Serviceの登場により,Id以外の項目の取得が容易になりました.CRUDが容易に実現できることはもちろんですが,Lightning Data Serviceで取得したレコードをそのままApexに渡せば不要なクエリを削減できて便利です.ここで注意が必要なのは,initイベントではtargetRecordはnullとなるため,targetRecordのchangeイベントを利用しなくてはいけない点です.これはLightning Data Serviceが非同期でレコード情報を取得しているためです.最初にinitイベントで渡してしまいうまく動かなくてハマってしまいました.

簡易な例として、取引先責任者レコードをLightning Data Serviceで取得し、それをApexに渡し(わざわざSOQLで)取引先を取得してみます.

<aura:component controller="MyController" implements="flexipage:availableForRecordHome,force:hasRecordId" access="global" >
    <aura:attribute name="contact" type="Object" />
    <aura:attribute name="account" type="Account" />
    <force:recordData recordId="{!v.recordId}" targetRecord="{!v.contact}" layoutType="FULL"/>
    <aura:handler name="init" value="{!this}" action="{!c.init}" />
    <ui:outputText value="{!v.account.Name}" />
</aura:component>
  • クライアントサイドコントローラ
({
	onInit : function(component, event, helper) {
        var action = component.get("c.getAccount");
        action.setParams({ AccountId : component.get("v.contact.AccountId") });
        action.setCallback(this, function(response) {
            var state = response.getState();
            if(state === "SUCCESS"){
                console.log('success');
                component.set("v.account",response.getReturnValue());
            } else {
                console.log(response.getError());
            }
        });
        $A.enqueueAction(action);   
	},
})
  • Apexコントローラ
public class AccountController {
    @AuraEnabled
    public static String getAccount(Id AccountId) {
        List<Account> result = [SELECT Name From Account Where Id = :AccountId];
        if(result.size() > 0){
            return result[0];
        } else {
            return null;
        }
    }
}

onInitが呼び出される時点では,{!v.contact}はnullであるため,取引先を取得することはできません.initイベントではなく,

<aura handler="change" value="{!v.contact}" ... />

とすると,取引先責任者のレコードがロードされたタイミングで,すなわち,{!v.contact}にレコード情報が含まれた状態でクライアントサイドコントローラのメソッドを呼び出せます.

Lightning Componentをリネームする

作り直さなくても,AuraDefinitionBundleをクエリすれば良いです.

SELECT Id, MasterLabel, DeveloperName FROM AuraDefinitionBundle

数が多い場合はデータローダで.依存関係も自動で修正されますが,コンポーネントを動的に生成している場合はリファクタリングが必要です.

Force.com IDE 2 (SalesforceDX)を導入する

Salesforce DXのオープンベータが始まり,IDEも新しくなったようです.Eclipseプラグインをインストールする旧IDEより軽量で快適ですが,DXの使用が前提となっているため,今までのdeploy to serverは使えません.コマンドを覚えなくても良いという点では便利かもしれません.

インストール・起動

  • インストーラ形式ではないので,zipを解凍すればforceide.exeをすぐに起動できます.

プロジェクトを作成しScratch Orgとつなぐ

  1. File -> New -> SFDX Project
    • force:project:createと同じ.
  2. Salesforce DX -> Authorize a Dev Hub
    • ログイン画面が開きます。force:auth:web:loginと同じ.
  3. Salesforce DX -> Create Scratch Org...
    • 1.で作成したproject-defを指定する.force:org:createと同じ.

f:id:shunkosa:20170706194429p:plain
うまく接続できるとプロジェクト名の隣にScratch OrgのユーザIDが表示され,以降はSalesforce DX -> push source / pull sourceでScratch Orgとメタデータを連携できます.

備考

  • ソースコードはデフォルトだとforce-app/default配下で,ここには初期状態ではauraしかフォルダがないので,ApexクラスやVisualforceページを作成する場合は別途classes,pagesフォルダの作成が必要です.適切なフォルダに資源が無いと,push sourceしてもコンソールにはfinished successfullyと表示されますが,Scratch Orgには反映されません.メタデータを新規作成する際は自動で適切なフォルダに配置してくれれば便利なのですが.

プラットフォームイベント(Platform Events)を試す

Summer '17で正式リリースされたプラットフォームイベントを試してみました.

プラットフォームイベントとは
  • Pub/Subのメッセージングシステム (e.g., Apache Kafka, Cloud Pub/Sub)
  • メッセージの配信者(Publisher)は,購読者(Subscriber)を意識せずメッセージを送信できる.そのため,コンポーネント同士が疎結合になる点がメリット.
  • プラットフォームイベントはImmutableであり,クエリ不可.
  • プラットフォームイベントは配信してから24時間レコードが保持される.
  • ちなみに,標準オブジェクトのEvent(行動)とは無関係.
プラットフォームイベントの登録
  • [設定] - [プラットフォームイベント]
  • カスタムオブジェクトのように,プラットフォームイベントはAPI参照名の末尾に__eが付与される.
  • 受信者は,replayID(再実行ID)を利用して過去のイベントを取得可.

f:id:shunkosa:20170601173052p:plain

プラットフォームイベントの配信(Publisher: Salesforce Internal)
  • Apexでは,EventBus.publish()メソッドでイベントを配信できる.
  • EventBus.publish()メソッドの呼び出しは,レコードのInsertと同等のため,通常のDMLと同じガバナ制約が適用される.また,このメソッドはList<Database.SaveResult>を返す.
//ケースがエスカレーションされていたらイベントを配信するトリガ
trigger CaseTrigger on Case (after update) {
    List<Case_Event__e> CaseEvents = new List<Case_Event__e>();
    for(Case c : Trigger.new)
        if(c.isEscalated){
            CaseEvents.add(
            	new Case_Event__e(Type__c = c.Type, 
                                  CaseNumber__c = c.CaseNumber, 
                                  Description__c = c.Description
                                  )
            );
        }
    }
    EventBus.publish(CaseEvents);
}
プラットフォームイベントの購読(Subscriber: External)

Force.comのGithubCometDプロトコルを実装したJavaのクライアントが公開されています.まずは,これを用いてイベントを購読してみます.

  • exampleパッケージ内のLoginExampleを実行.コマンドライン引数に ログインID パスワード トピック を順に指定
  • イベントを購読する際のトピックのフォーマットは /event/(イベントのAPI参照名)

下記は,先のトリガを有効化し,ケースをエスカレーションした際のJava側の標準出力例.うまく購読できています.

{schema=X1gI8VTM2N-J2F6sVaOWVA, 
 payload={
	CaseNumber__c=00001026, 
	CreatedById=005B0000000P1Vz, 
	Description__c=Test Description, 
	Type__c=Electrical, 
	CreatedDate=2017-06-01T07:39:02Z},
 event={replayId=1}}
プラットフォームイベントの配信(Publisher: External)
  • 通常のsObjectのレコード挿入と同様にAPI経由でイベントを配信(=イベントレコードを挿入).

先の例だと, /services/data/v40.0/sobjects/Case_Event__e/

プラットフォームイベントの購読(Subscriber: Internal Salesforce)
  • イベントのafter insertトリガを用いて,イベントを購読可能.
  • イベントレコードはImmutableであるため,beforeトリガやafter updateトリガには非対応.
trigger CaseEventTrigger on Case_Event__e (after insert) {
    for(Case_Event__e cEvent : Trigger.new){
    //例えば,ケース番号で元のケースをクエリして,元のケースを更新する等
    }
}
主な制約事項
  • 1時間あたりにpublishできるイベントレコード数の上限:100000 (DEは1000)
ポイント
  • イベントのPublish = イベントレコードのInsert