モーダル
モーダルは、ユーザからデータを収集したり、動的でインタラクティブな表示を見せることに特化したインターフェースです。モーダルは Slack 内でユーザがアプリとの端的でありながらも深いインタラクションを行うことができるインターフェースです。モーダルは Block Kit の視覚的でインタラクティブなコンポーネントを使って構成されます。
Slack アプリの設定
モーダルを使うための最初のステップは、インタラクティブコンポーネントを有効にすることです。Slack アプリ管理画面にアクセスし、開発中のアプリを選択、左ペインの Features > Interactivity & Shortcuts へ遷移します。このページで以下の設定を行います。
- Interactivity を Off から On にする
https://{あなたのドメイン}/slack/events
を Request URL に設定 (ソケットモードの場合、この手順は不要です)- 最下部にある Save Changes ボタンをクリック
Bolt アプリがやること
モーダルのハンドリングには 3 つのパターンがあります。いつものように、Bolt アプリは Slack API サーバーからのリクエストに対して 3 秒以内に ack()
メソッドで応答する必要があります。3 秒以内に応答しなかった場合、コマンドを実行したユーザーに対して Slack 上でタイムアウトした旨が通知されます。
"block_actions"
リクエスト
ユーザーがモーダル内のインタラクティブなコンポーネントを使用して何かアクションを起こしたとき、アプリは "block_actions"
という type のペイロードを受信します。このリクエストを処理するためにやらなければならないことは以下の通りです。
- Slack API からのリクエストを検証
- リクエストボディをパースして
type
が"block_actions"
かつaction_id
が処理対象か確認 - views.* API を使って書き換えたり、上に新しく追加したりする and/or 必要に応じて送信された情報を private_metadata に保持
- 受け取ったことを伝えるために Slack API へ 200 OK 応答
"view_submission"
リクエスト
ユーザーがモーダル最下部の Submit ボタンを押してフォームの送信を行ったとき、"view_submission"
という type のペイロードを受信します。このリクエストを処理するためにやらなければならないことは以下の通りです。
- Slack API からのリクエストを検証
- リクエストボディをパースして
type
が"view_submission"
かつcallback_id
が処理対象かを確認 view.state.values
からフォーム送信された情報 を抽出- 入力バリデーション、データベースへの保存、外部サービスとの連携など任意の処理
- 以下のいずれかによって受け取ったことを伝えるために Slack API へ 200 OK 応答:
- 空のボディで応答してモーダルを閉じる
response_action
(可能な値は"errors"
,"update"
,"push"
,"clear"
) を指定して応答する
"view_closed"
リクエスト (notify_on_close
が true
のときのみ)
ユーザーがモーダルの Cancel ボタンや x ボタンを押したとき、"view_closed"
という type のペイロード を受信する場合があります。これらのボタンは blocks ではなく、標準で配置されているものです。このリクエストを受信するためには views.open や views.push の API メソッドでモーダルを生成したときに notify_on_close
を true
に設定しておく必要があります。このリクエストを処理するためにやらなければならないことは以下の通りです 。
- Slack API からのリクエストを検証
- リクエストボディをパースして
type
が"view_closed"
かつcallback_id
が処理対象かを確認 - 任意のこのタイミングでやるべきこと
- 受け取ったことを伝えるために Slack API へ 200 OK 応答
モーダル開発 Tips
一般に Slack のモーダルを使って開発する上で知っておくべきことがいくつかあります。
- モーダルを開始するには、ユーザーインタラクションのペイロードに含まれる
trigger_id
が必要です "type": "input"
のブロックに含まれる入力項目だけが"view_submission"
のview.state.values
に含まれます"section"
,"actions"
等の"input"
の type ではないブロックでの入力・セレクトメニュー選択は"block_actions"
として個別に送信されます- モーダルを特定するには
callback_id
を使用し、view.states
内で入力値を特定するにはblock_id
とaction_id
のペアを使用します - モーダルの内部状態や
"block_actions"
での入力結果はview.private_metadata
に保持することができます "view_submission"
のリクエストは、その応答 (=ack()
) でresponse_action
を指定することでモーダルの次の状態を指示します- views.update、views.push API メソッドはモーダル内での
"block_actions"
リクエストを受信したときに使用するものであり、"view_submission"
時にモーダルを操作するための API ではありません
コード例
注: もし Bolt を使った Slack アプリ開発にまだ慣れていない方は、まず「Bolt 入門」を読んでください。
まずはモーダルを新しく開くところから始めましょう。ここでは以下のようなモーダルを開いてみることにします。
{
"type": "modal",
"callback_id": "meeting-arrangement",
"notify_on_close": true,
"title": { "type": "plain_text", "text": "Meeting Arrangement" },
"submit": { "type": "plain_text", "text": "Submit" },
"close": { "type": "plain_text", "text": "Cancel" },
"private_metadata": "{\"response_url\":\"https://hooks.slack.com/actions/T1ABCD2E12/330361579271/0dAEyLY19ofpLwxqozy3firz\"}",
"blocks": [
{
"type": "section",
"block_id": "category-block",
"text": { "type": "mrkdwn", "text": "Select a category of the meeting!" },
"accessory": {
"type": "static_select",
"action_id": "category-selection-action",
"placeholder": { "type": "plain_text", "text": "Select a category" },
"options": [
{ "text": { "type": "plain_text", "text": "Customer" }, "value": "customer" },
{ "text": { "type": "plain_text", "text": "Partner" }, "value": "partner" },
{ "text": { "type": "plain_text", "text": "Internal" }, "value": "internal" }
]
}
},
{
"type": "input",
"block_id": "agenda-block",
"element": { "action_id": "agenda-action", "type": "plain_text_input", "multiline": true },
"label": { "type": "plain_text", "text": "Detailed Agenda" }
}
]
}
slack-api-client は blocks や views を構築するための扱いやすい DSL を提供しています。以下のコード例は型安全に View オブジェクトを生成している例です。
import com.slack.api.model.view.View;
import static com.slack.api.model.block.Blocks.*;
import static com.slack.api.model.block.composition.BlockCompositions.*;
import static com.slack.api.model.block.element.BlockElements.*;
import static com.slack.api.model.view.Views.*;
View buildView() {
return view(view -> view
.callbackId("meeting-arrangement")
.type("modal")
.notifyOnClose(true)
.title(viewTitle(title -> title.type("plain_text").text("Meeting Arrangement").emoji(true)))
.submit(viewSubmit(submit -> submit.type("plain_text").text("Submit").emoji(true)))
.close(viewClose(close -> close.type("plain_text").text("Cancel").emoji(true)))
.privateMetadata("{\"response_url\":\"https://hooks.slack.com/actions/T1ABCD2E12/330361579271/0dAEyLY19ofpLwxqozy3firz\"}")
.blocks(asBlocks(
section(section -> section
.blockId("category-block")
.text(markdownText("Select a category of the meeting!"))
.accessory(staticSelect(staticSelect -> staticSelect
.actionId("category-selection-action")
.placeholder(plainText("Select a category"))
.options(asOptions(
option(plainText("Customer"), "customer"),
option(plainText("Partner"), "partner"),
option(plainText("Internal"), "internal")
))
))
),
input(input -> input
.blockId("agenda-block")
.element(plainTextInput(pti -> pti.actionId("agenda-action").multiline(true)))
.label(plainText(pt -> pt.text("Detailed Agenda").emoji(true)))
)
))
);
}