サンプルコードからみる Slack モーダルフローの構築

## はじめに
Slack モーダルとモーダルフローは公式のドキュメントに大変参考になる解説があります。しかし実際に作成しはじめると、この値はどこからとるの？この値はどう使うの？といくつか試行錯誤しました。そこで簡単な Slack モーダルフローのサンプルコードを解説をします。これから作成する方の参考にしてもらえればと思います。

### よく参考にしたドキュメント
* [モーダルの開始](https://slack.dev/bolt-js/ja-jp/concepts#creating-modals)
* [Using modals in Slack apps](https://api.slack.com/surfaces/modals/using)

## サンプルで用意したモーダルとフローの説明

![display_comment_70.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743360/4015149f-6c2d-eab5-be44-4e14772b6d8f.png)
コマンド入力により "コメント表示モーダル" が開きます。このモーダルでは入力したコメントを表示するセクション（`未入力` と表示している部分） と "コメント入力モーダル" を表示させる "コメント更新" ボタンがあります。"コメント更新" ボタンは "コメント表示モーダル" を "コメント入力モーダル" に置き換える処理になります。またモーダルの処理を中断させる "Cancel" ボタンと、表示しているコメントを Slack アプリのメッセージ欄に出力する "コメント出力" ボタンがあります。

![input_comment_70.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743360/385ce87c-8434-f909-2a1d-c06fb0f42fe7.png)
"コメント入力モーダル" にはコメント入力をキャンセルする "Cancel" ボタンと、コメントに入力した内容を "コメント表示モーダル" に反映させる "更新" ボタンがあります。"更新" ボタンにより "コメント入力モーダル" を "コメント表示モーダル" に置き換えます。 

![display_comment2_70.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743360/121fef9f-cb02-7948-a5cf-9757db5776b2.png)
"コメント入力モーダル" でコメントを入力して "更新" ボタンを押すとコメント表示セクションにコメントが表示されます。

![output.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743360/cf6fd031-fb49-c00b-74be-2a55aebd375b.png)
"コメント出力" ボタンを押すと Slack apps のメッセージ欄にコメントを出力します。

## Step.1 コマンドによるモーダル起動

コマンドからコメント表示モーダルを開く処理です。

### コード

```javascript
app.command("/multi_modals", async ({ ack, body, client, logger }) => {
  await ack();
  const meta = {comment: ""};
  const metaStr = JSON.stringify(meta);
  try {
    await client.views.open({
      trigger_id: body.trigger_id,
      view: {
        "type": "modal",
        "title": {
          "type": "plain_text",
          "text": "コメント表示",
          "emoji": true
        },
        "callback_id": "output_comment",
        "submit": {
          "type": "plain_text",
          "text": "コメント出力",
          "emoji": true
        },
        "private_metadata": metaStr,
        "close": {
          "type": "plain_text",
          "text": "Cancel",
          "emoji": true
        },
        "blocks": [
          {
            "type": "section",
            "block_id": "comment_display_block",
            "text": {
              "type": "mrkdwn",
              "text": "未入力"
            },
            "accessory": {
              "type": "button",
              "text": {
                "type": "plain_text",
                "text": "コメント更新",
                "emoji": true
              },
              "value": metaStr,
              "action_id": "open_modal"
            }
          }
        ]
      }
    })
  } catch (error) {
    logger.error(error);
  }
});
```

### コード解説

- `app.command` メソッドで `/multi_modals` コマンドをリスニングします。このコマンドを受けてコメント表示モーダルを開く処理を行います。
- モーダルを開くには views.open を利用します。そのとき trigger_id を指定する必要があります。
- コメント表示モーダルには Input block がないため、"コメント出力" ボタンで表示している値を受け取るために `private_metadata` に入力している値（初期表示になるので空文字）を設定します。
- コメント表示セクションには "未入力" として入力がないことを分かりやすくしています。
- "コメント更新" ボタンの `value` に `private_metadata` と同じ値を設定します。ボタン押下時に "コメント入力モーダル" でコメント表示している値を取得できるようにします。


## Step.2 コメント表示モーダルのコメント更新ボタン処理

コメント表示モーダルをコメント入力モーダルに更新する処理です。

### コード

```javascript
app.action({action_id: "open_modal"}, async ({ ack, body, payload, client, logger }) => {
  try {
    await ack();

    const trigger_id = body.trigger_id;
    const metaStr = payload.value;
    const meta = JSON.parse(metaStr);

    await client.views.push({
      trigger_id: trigger_id,
      view: {
        "type": "modal",
        "title": {
          "type": "plain_text",
          "text": "コメント入力",
          "emoji": true
        },
        "callback_id": "update_comment",
        "submit": {
          "type": "plain_text",
          "text": "更新",
          "emoji": true
        },
        "private_metadata": metaStr,
        "close": {
          "type": "plain_text",
          "text": "Cancel",
          "emoji": true
        },
        "blocks": [
          {
            "type": "input",
            "block_id": "comment_block",
            "element": {
              "type": "plain_text_input",
              "action_id": "comment_action",
              "placeholder": {
                type: 'plain_text',
                text: "コメントを入力してください。",
                emoji: true
              },
              "initial_value": meta.comment
            },
            "label": {
              "type": "plain_text",
              "text": "コメント",
              "emoji": true
            }
          }
        ]
      }
    })
  } catch (error) {
    logger.error(error);
  }
});
```

### コード解説

- `app.action` メソッドを利用して "コメント更新" ボタンの処理をリスニングします。"コメント更新" ボタンの処理をハンドリングするため `action_id` に指定していた "open_modal" を指定します。
- "コメント更新" ボタンの `value` を `payload` から取得できます。
- `view.push` メソッドで表示していた "コメント入力モーダル" を "コメント表示モーダル" に置き換えます。
- `private_metadata` に JSON 文字列を渡す必要はありませんが、コメント入力モーダルを再表示したのちの設定値を簡易に用意するために渡しています。
- コメントの初期データとして `initial_value` に表示している値を設定しています。コメントがない場合には `placeholder` の値が表示されます。

## Step.3 コメント入力モーダルの更新ボタン処理

コメント入力モーダルをコメント表示モーダルに更新する（もとに戻す）処理です。また入力したコメントをコメント表示モーダルに反映させます。

### コード

```javascript
app.view("update_comment", async ({ ack, view, client}) => {
  const view_id = view.root_view_id ?? "";

  const comment = view.state.values["comment_block"]["comment_action"].value ?? "";

  try {
    await ack();
    const meta = JSON.parse(view.private_metadata);

    // meta.view_id = view_id;
    meta.comment = comment;
    const metaStr = JSON.stringify(meta);

    await client.views.update({
      view_id: view_id,
      view: {
        "type": "modal",
        "title": {
          "type": "plain_text",
          "text": "コメント表示",
          "emoji": true
        },
        "callback_id": "output_comment",
        "submit": {
          "type": "plain_text",
          "text": "コメント出力",
          "emoji": true
        },
        "private_metadata": metaStr,
        "close": {
          "type": "plain_text",
          "text": "Cancel",
          "emoji": true
        },
        "blocks": [
          {
            "type": "section",
            "block_id": "comment_display_block",
            "text": {
              "type": "mrkdwn",
              "text": meta.comment
            },
            "accessory": {
              "type": "button",
              "text": {
                "type": "plain_text",
                "text": "コメント更新",
                "emoji": true
              },
              "value": metaStr,
              "action_id": "open_modal"
            }
          }
        ]
      }
    });
  } catch (error) {
    logger.error(error);
  }
});

```

### コード解説

- `app.view` メソッドを利用して "コメント入力モーダル" の "更新" ボタンの処理をリスニングします。"更新" ボタンの処理をハンドリングするため `callback_id` の "update_comment" を指定します。
- "コメント表示モーダル" を再表示させるためには初期表示で割り振られた `view_id` が必要になります。初期表示の `view_id` は view から `root_view_id` で取得できます。
- 入力したコメントは view.state.values から `comment_block` `comment_action` を指定して取得します。
- `view.update` メソッドで、"コメント表示モーダル" に更新させます。ここで先ほど取得した view_id を指定します。
- view の部分はコマンドによるモーダル起動で使った view とほぼ同じで、コメント表示セクションを入力値にしたものになります。実際にコーディングする際には共通化したりすることが可能です。

## Step.4 コメント出力処理

コメント表示モーダルで表示している値をメッセージに出力します。

### コード

```javascript
app.view("output_comment", async ({ ack, body, view, client, logger }) => {
  try {
    await ack();

    const meta = JSON.parse(view.private_metadata);
    const out = meta.comment === "" ? "未入力" : meta.comment;

    await client.chat.postMessage({
      channel: body.user.id,
      text: out
    });
  } catch (error) {
    logger.error(error);
  }
});
```

### コード解説

- `app.view` メソッドを利用して "コメント表示モーダル" の "コメント出力" ボタンの処理をリスニングします。"コメント出力" ボタンの処理をハンドリングするため `callback_id` の "output_comment" を指定します。 
- `private_metadata` からコメントを取り出します。コメントがない場合には "未入力" を出力するようにします。
- `char.postMessage` を使ってチャンネルにコメントを出力します。

## 最後に

説明では省いてきましたが、private_metadata には 3000 文字の制限や button の value には 2000 文字の制限があるのでモーダルを実装する前にいろいろなコンポーネントの制限を確認しておくことをお勧めします。
以上、最後までよんでいただきありがとうございました。