Aura コンポーネント開発 TIPS: モーダル画面の作り方

この記事では、Aura プログラミングモデルにおけるカスタムモーダル画面の作成方法をご紹介します。



## モーダルの作成方法の種類
Aura でモーダル画面を作成するにはいくつかの方法がありますが、今回はよく使うであろう２通りをご紹介します。
それぞれに利点がありますが、実装方法が大きく異なるのでご注意ください。

- [lightning:overlayLibrary](https://developer.salesforce.com/docs/component-library/bundle/lightning:overlayLibrary/documentation) を使った方法
    - 動的にモーダル画面を作成し、都度コンポーネントインスタンスの作成と削除を行う
    - ヘッダー、ボディ、フッターをコンポーネント化するので、同様のモーダルを大量に作成する場合は再利用性が高まる
- [Salesforce Lightning Design System](https://lightningdesignsystem.com/components/modals/#site-main-content) を使った方法
    - 静的なモーダル画面を作成し、SLDS のクラスの付け替えで表示を制御する
    - 一つのコンポーネントで完結するので、ちょっとしたモーダルを作りたいときに便利


## [lightning:overlayLibrary](https://developer.salesforce.com/docs/component-library/bundle/lightning:overlayLibrary/documentation) を使った方法

![overlay.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26378/bb8b2e5d-6238-456a-8071-850901d47479.gif)

#### ボディとフッターが連携する
通常のモーダルの使い方として、ボディにフォームを配置しフッターに保存ボタンを配置することがあります。
しかし、lightning:overlayLibrary ではボディとフッターが別コンポーネントとなるため、互いに連携する手段が必要です。

> 例: フッターで「保存」が押されたら、ボディで保存処理をする

今回は **アプリケーションイベント** を使った連携方法も合わせてご紹介します。

### イベントを作成
```html:CustomModalEvent.evt
<aura:event type="APPLICATION" description="Custom Modal Event">
  <aura:attribute type="String" name="type" /> <!-- SAVE/ERROR -->
</aura:event>
```

### フッターを作成
```html:CustomModalFooter.cmp
<aura:component>
  <!-- Private Attributes -->
  <aura:attribute access="private" type="Boolean" name="isDisabled" default="false" />

  <!-- Registered Events -->
  <aura:registerEvent type="c:CustomModalEvent" name="customModalEvent" />

  <!-- Overlay Library -->
  <lightning:overlayLibrary aura:id="overlayLib" />

  <!-- User Interface -->
  <div>
    <lightning:button label="キャンセル" onclick="{!c.onCancelClicked}" disabled="{!v.isDisabled}" />
    <lightning:button label="保存" variant="brand" onclick="{!c.onSaveClicked}" disabled="{!v.isDisabled}" />
  </div>
</aura:component>
```

```js:CustomModalFooterController.js
({
  onCancelClicked: function(c, e, h) {
    c.find('overlayLib').notifyClose();
  },
  onSaveClicked: function(c, e, h) {
    c.set('v.isDisabled', true);
    $A.get(`e.c:CustomModalEvent`)
      .setParams({
        type: 'SAVE'
      }).fire();
  }
});
```

### ボディを作成
```html:CustomModalBody.cmp
<aura:component>
  <!-- Public Attributes -->
  <aura:attribute access="public" type="String" name="lastName" />
  <aura:attribute access="public" type="String" name="firstName" />
  <aura:attribute access="public" type="String" name="email" />

  <!-- Event Handlers -->
  <aura:handler event="c:CustomModalEvent" action="{!c.onCustomModalEvent}" />

  <!-- Overlay Library -->
  <lightning:overlayLibrary aura:id="overlayLib" />

  <!-- User Interface -->
  <div>
    <lightning:input type="text" label="姓" value="{!v.lastName}" />
    <lightning:input type="text" label="名" value="{!v.firstName}" />
    <lightning:input type="email" label="メール" value="{!v.email}" />

    <lightning:spinner aura:id="spinner" class="slds-hide" variant="brand" size="large" />
  </div>
</aura:component>

```

```js:CustomModalBodyController.js
({
  onCustomModalEvent: function(c, e, h) {
    const eventType = e.getParams().type;
    if (eventType === 'SAVE') {
      h.showSpinner(c);

      // TODO: ここで値の保存処理をする

      // 保存が完了したらモーダルを閉じる
      setTimeout($A.getCallback(function() {
        h.hideSpinner(c);
        c.find('overlayLib').notifyClose();
      }), 3000);
    }
  }
});
```

```js:CustomModalBodyHelper.js
({
  showSpinner: function(c) {
    const spinner = c.find('spinner');
    $A.util.removeClass(spinner, 'slds-hide');
  },
  hideSpinner: function(c) {
    const spinner = c.find('spinner');
    $A.util.addClass(spinner, 'slds-hide');
  }
});
```

```css:CustomModalBody.css
.THIS {
  position: relative;
}
```


### モーダルを呼び出すには...

```html:CustomModalButton.cmp
<aura:component access="global" implements="flexipage:availableForAllPageTypes,forceCommunity:availableForAllPageTypes">
  <!-- Overlay Library -->
  <lightning:overlayLibrary aura:id="overlayLib" />

  <!-- User Interface -->
  <lightning:button variant="brand" label="モーダルを開く" onclick="{!c.onButtonClick}" />
</aura:component>
```

```js:CustomModalButtonController.js
({
  onButtonClick: function(c, e, h) {
    // モーダルボディのコンポーネントを作成
    const createCustomModalBodyPromise = h.createComponent(c, h, 'c:CustomModalBody', {
      lastName: '林',
      firstName: '恵美',
      email: 'ehayashi@example.com'
    });

    // モーダルフッターのコンポーネントを作成
    const createCustomModalFooterPromise = h.createComponent(c, h, 'c:CustomModalFooter', {});

    Promise.all([createCustomModalBodyPromise, createCustomModalFooterPromise])
      .then(
        $A.getCallback(function([customModalBody, customModaFooter]) {
          c.find('overlayLib').showCustomModal({
            header: '顧客情報の編集',
            body: customModalBody,
            footer: customModaFooter
          });
        })
      )
      .catch(function(reason) {
        console.error(reason);
      });
  }
});
```

```js:CustomModalButtonHelper.js
({
  createComponent: function(c, h, componentDef, componentAttributes) {
    return new Promise(function(resolve, reject) {
      $A.createComponent(componentDef, componentAttributes, function(
        newComponent,
        status,
        errorMessage
      ) {
        if (status === "SUCCESS") resolve(newComponent);
        else if (status === "ERROR") reject(errorMessage);
      });
    });
  }
});
```



## [Salesforce Lightning Design System](https://lightningdesignsystem.com/components/modals/#site-main-content) を使った方法

![static.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26378/c91f44fe-7a77-7171-eb10-84d6f9156b9d.gif)

### モーダル本体を作成
```html:CustomModal.cmp
<aura:component access="global">
    
    <!-- Public Attributes -->
    <aura:attribute access="public" type="String" name="header"  />
    <aura:attribute access="public" type="String" name="lastName"  />
    <aura:attribute access="public" type="String" name="firstName"  />
    <aura:attribute access="public" type="String" name="email"  />
    
    <!-- Privte Attributes -->
    <aura:attribute access="private" type="Boolean" name="showModal" default="false" />
    
    <!-- Component Method -->
    <aura:method name="open" action="{!c.onOpenCalled}" />
    
    <!-- User Interface -->
    <div>
        <section role="dialog" tabindex="-1" class="{!'slds-modal' + if(v.showModal, ' slds-fade-in-open', '')}">
            <div class="slds-modal__container">
                <header class="slds-modal__header">
                    <lightning:buttonIcon size="large" class="slds-modal__close" iconName="utility:close" variant="bare-inverse" title="close" alternativeText="Close window" onclick="{!c.onCloseClicked}"/>
                    <h2 class="slds-text-heading_medium slds-hyphenate">{!v.header}</h2>
                </header>
                <div class="slds-modal__content slds-p-around_medium">
                    <lightning:input type="text"  label="姓"     value="{!v.lastName}"/>
                    <lightning:input type="text"  label="名"     value="{!v.firstName}"/>
                    <lightning:input type="email" label="メール" value="{!v.email}"/>
                </div>
                <footer class="slds-modal__footer">
                    <lightning:button label="キャンセル" onclick="{!c.onCancelClicked}" />
                    <lightning:button label="保存" variant="brand" onclick="{!c.onSaveClicked}" />
                </footer>
            </div>
             <lightning:spinner aura:id="spinner" class="slds-hide" variant="brand" size="large" />
        </section>
        <div class="{!'slds-backdrop' + if(v.showModal, ' slds-backdrop_open', '')}"></div>
    </div>
</aura:component>
```

```js:CustomModalController.js
({
    onOpenCalled: function(c, e, h) {
        c.set('v.showModal', true);
    },
    onCloseClicked: function(c, e, h) {
        c.set('v.showModal', false);
    },
    onCanceClicked: function(c, e, h) {
        c.set('v.showModal', false);
    },
    onSaveClicked:  function(c, e, h) {
        h.showSpinner(c);
        
        // TODO: ここで値の保存処理をする
        
        // 保存が完了したらモーダルを閉じる
        setTimeout($A.getCallback(function() {
            h.hideSpinner(c);
            c.set('v.showModal', false);
        }), 3000);
    },
})
```

```js:CustomModalHelper.js
({
  showSpinner: function(c) {
    const spinner = c.find('spinner');
    $A.util.removeClass(spinner, 'slds-hide');
  },
  hideSpinner: function(c) {
    const spinner = c.find('spinner');
    $A.util.addClass(spinner, 'slds-hide');
  }
});
```

### モーダルを呼び出すには...

```html:CustomModalButton.cmp
<aura:component access="global" implements="flexipage:availableForAllPageTypes,forceCommunity:availableForAllPageTypes">
  <!-- User Interface -->
  <lightning:button variant="brand" label="モーダルを開く" onclick="{!c.onButtonClick}" />

  <c:CustomModal aura:id="modal" header="顧客情報の編集" lastName="林" firstName="恵美" email="ehayashi@example.com" />
</aura:component>
```

```js:CustomModalButtonController.js
({
  onButtonClick: function(c, e, h) {
    c.find('modal').open();
  }
});
```