Slackのマルチステップモーダルの画面遷移を試してみた

はじめに

Slack Advent Calendar 2019 2日目の記事です。

Slackの公式ブログで紹介されていた「2.マルチステップモーダル:情報の表示・収集がもっと簡単に」について、モーダルの画面遷移を実装して試してみました。
実際に、検索機能を設けたSlackアプリケーションのUIを作るために使っています(まだ開発中ですけど)。

大きく進化した Slack アプリツールキットのご紹介
https://slackhq.com/intl-ja-jp-introducing-a-dramatically-upgraded-slack-app-toolkit

上記紹介ページより、引用

モーダルとは Slack インターフェースの上に出てくるウィンドウのことです。このツールはさまざまなフォームでデータを集めたり、インタラクティブな選択肢や結果を表示したりするときにとても便利なものです。今回、開発者はカスタム情報やより複雑なものに対応できるよう、必要な数の画面を追加表示できるようになりました。これで長々とした bot とのやり取りをしなくて済むようになります!

マルチステップモーダルを試す際に参考にしたドキュメント

Slackアプリでのモーダルの使用 - Slack API
https://api.slack.com/surfaces/modals/using

Slackアプリケーションのモーダルはどんなものなのか。まず機能とAPIを知る上で読むと良いです。

モーダルビューの更新と多重表示 - Bolt API
https://slack.dev/bolt/ja-jp/concepts#updating-pushing-views

Bolt入門ガイドです。Boltフレームワークを使って、モーダルの実装をする際に、日本語による説明とサンプルコードから学べます。

Slack API 新機能を使ってアプリのホーム・ヴューを活用しよう🏡
https://qiita.com/girlie_mac/items/fae66bcc2ec3ccf25db6

日本語のApp Homeチュートリアルです。チュートリアルを通して、ノートアプリの開発をします。
Slackアプリの設定、モーダル(シングル)の実装を学べます。

マルチステップモーダルの画面遷移を試してみた

モーダルのオープンと view.push, view.update による画面遷移のコードについて紹介します。説明のために、画面遷移以外のフォームの値の参照などのコードは省いています。
なお、紹介するコードはこちらで全体を公開しています。
https://github.com/shoito/slack-app-examples

マルチステップの動きとしては、このようになっています(アニメーションGIFだとカクカクしてる...)。

ファイル構成としては、分かりやすいように以下のようにしています。

$ tree -L 2          
.
├── README.md
├── appHome.js - App Homeを構成するUI定義
├── index.js - アプリのメイン実装
├── modals
│   ├── detail.js - ステップ3を構成するUI定義
│   ├── detail_expand.js - ステップ3のview.updaet後のUIを構成するUI定義
│   ├── input.js - モーダルのステップ1を構成するUI定義
│   └── result.js - ステップ2を構成するUI定義
├── package-lock.json
└── package.json

まずは、App Home画面を表示するコードですが、以下のように app.event("app_home_opened", ...) app_home_openedイベントをハンドリングします。 appHome() ファンクションで appHome.js に定義されているブロックを返しています。

app.event("app_home_opened", async ({ event, context, payload }) => {
  try {
    const result = await app.client.apiCall("views.publish", {
      token: context.botToken,
      user_id: event.user,
      view: appHome()
    });
  } catch (e) {
    console.log(e);
    app.error(e);
  }
});

App HomeはBlock Kit Builderで作ったJSONを返すようにしています。
18行目の action_id はOpen modalボタンをクリックしたときのアクションの定義先のIDです。

module.exports = () => {
  return {
    type: "home",
    callback_id: "home_view",
    title: {
      type: "plain_text",
      text: "Modal"
    },
    blocks: [
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: "*Welcome!*"
        },
        accessory: {
          type: "button",
          action_id: "open_modal_clicked",
          text: {
            type: "plain_text",
            text: "Open modal"
          }
        }
      }
    ]
  };
};

Open modalボタンがクリックされたときに、こちらのアクションがフックされます。
views.open している部分で、modals/input.js で定義されているモーダルを表示します。

app.action("open_modal_clicked", async ({ ack, body, context }) => {
  ack();

  try {
    const result = await app.client.views.open({
      token: context.botToken,
      trigger_id: body.trigger_id,
      view: inputModal()
    });
  } catch (e) {
    console.log(e);
    app.error(e);
  }
});

モーダルを閉じずに、画面遷移する場合は、 views.push で新たなレイヤを構築します。
ここでは modals/result.js で定義した画面が、前のモーダルの上に表示されます。

app.action("show_result_clicked", async ({ ack, body, context }) => {
  ack();

  try {
    const result = await app.client.views.push({
      token: context.botToken,
      trigger_id: body.trigger_id,
      view: resultModal()
    });
  } catch (e) {
    console.log(e);
    app.error(e);
  }
});

modals/details.js では、 views.push による画面の遷移ではなく、 views.update による画面内の更新を行っています。

app.action("expand_clicked", async ({ ack, body, context }) => {
  ack();

  try {
    const result = await app.client.views.update({
      token: context.botToken,
      view_id: body.view.id,
      view: detailExpandModal()
    });
  } catch (e) {
    console.log(e);
    app.error(e);
  }
});

さいごに

今回 view.push を使ったマルチステップモーダルのコードがなかなか見当たらなかったので、参考にシンプルなコードを紹介しました。
マルチステップモーダルにより、これまでSlackアプリケーションではできないと諦めていたインタラクションが実現できそうなので、個人的にとても嬉しい機能です。