[Slack API] スラックアプリのモーダルでバリデーションエラーを表示する方法


はじめに

モーダルを使ったスラックbotを作っていると、やはり入力値に対してバリデーションをかけて、期待しない入力値であればエラーを表示したくなりませんか?こんな風に。(画像はドキュメントより抜粋)

こうすることで後続する処理に渡す値を制限することができて超助かります。
自分が実装する時に結構調べたのですが、なかなかエラー文言出せずハマったのでまとめてみました!(実はドキュメントをちゃんと読解できてれば難なくできたのですが。。)

方法

基本的にはこれだけ!

view_submission イベントを受け取った時のレスポンスに以下を含める!

このオブジェクトのうち、block_id の部分にバリデーションエラーを表示したいブロックのidを渡します。そして,その値としてエラーメッセージを記述します。他の部分は固定です。

{
  "response_action": "errors",
  "errors": {
    "block_id": "Errorだよ!"
  }
}

↓↓公式のドキュメントにも書いてあります!↓↓

複数のブロックにエラ-メッセージを出したいとき

以下のように複数のblock_idと文言の組み合わせをerrorsオブジェクトに設定します。

{
  "response_action": "errors",
  "errors": {
    "block_id_A": "Errorだよ!",
    "block_id_B": "Errorじゃん"
  }
}

実際の使い方 with Bolt ⚡️

ここではSlackの提供するJS用ライブラリBoltを用いた場合の実装をお見せします。
Boltではview関数を使ってview_submission イベントを拾うので以下のように記述します。

app.view('modal_hoge',async ({ ack, body, view, client }) => {
    //入力値を取得
    const title = view['state']['values']['block_id_a']['action_id_a'].value;
    const userSelection = view['state']['values']['block_id_b']['action_id_b'].selected_users
    //空のエラーオブジェクトを作成
    const errors = {};
    if (title.length <= 5 ){
        // ブロックIDがblock_id_aのブロックにエラーメッセージを出す。
        errors['block_id_a'] = '5文字以上入力してください'
    }

    if (userSelection.length <= 2 ){
        // ブロックIDがblock_id_bのブロックにエラーメッセージを出す。
        errors['block_id_b'] = '少なくとも3人はユーザーを選択してください'
    }

    //errorsオブジェクトが空オブジェクト(エラーが一件もない)か確認
    if (Object.keys(errors).length === 0 ){
    // エラーを返す
        await ack({
            response_action: 'errors',
            errors:errors
        });
    }else{
        await ack()
        //処理を書く。
    }
});

上記を試したのに動かないときにチェックすること2点!

その1: 渡してるブロックIDはinputブロックのものになってる?

{
  "response_action": "errors",
  "errors": {
    "block_id": "Errorだよ!"
  }
}

上のblock_idの部分で渡すブロックIDはエラーを表示したいブロックのものを指定すると言いましたが、このブロックの種類がinputブロックになっているか確認してください!

inputブロックの例(ドキュメントより抜粋)

typeがinputになってるものがinputブロックです。input blockのドキュメント

{
  "type": "input",
  "element": {
    "type": "plain_text_input"
  },
  "label": {
    "type": "plain_text",
    "text": "Label",
    "emoji": true
  }
}

inputブロックでのマルチセレクタ、シングルセレクタ、ユーザーセレクタをBlockKitBuilderで組んでみたのでよかったら参考にしてください
Inputブロックでの各種セレクタの表現例 in Block Kit Builder

間違えやすいsectionブロック(ドキュメントより抜粋)

typeがsectionになってるのがわかると思います。

{
  "type": "section",
  "text": {
    "text": "*Sally* has requested you set the deadline for the Nano launch project",
    "type": "mrkdwn"
  },
  "accessory": {
    "type": "datepicker",
    "action_id": "datepicker123",
    "initial_date": "1990-04-28",
    "placeholder": {
      "type": "plain_text",
      "text": "Select a date"
    }
  }
}

inputブロックでもsectionブロックでもセレクターやユーザーセレクターなど同様の入力を作れてしまうので、見た目上は違いがないんです。ブロックキットビルダーで見た目だけ揃えて作ってしまうとこのミスに陥りやすいです!(自分がまさにこれでした)

実はこの話、公式のドキュメントにはちゃんと明記してあるんです。

自分はBlockKitの理解が甘いために、このinput blockという記述が特定のブロックの種類でなく、ユーザーの入力を受け取るブロックと思っていたためなかなか間違いに気づけませんでした。。

ただ、結構他にも同じ勘違いをしていた方はいたみたいで、これが今回記事書くモチベにもなりました。
What is the recommended way to obtain selected options when handling_submissions? #303
Way to get Multi-select Menu values in app.view #550

おまけ:sectionブロックとinputブロックの違いの例

  • sectionブロックでは入力のたびにイベントが発火するが、inputブロックはsubmitしたときのみイベントが発火する
  • inputブロックのlabelキーのtypeはplain_textでないといけない。mrkdwnは選べません!

その2: エラーを返すためのオブジェクトのキー、値でタイポしてない?

{
  "response_action": "errors",
  "errors": {
    "block_id": "Errorだよ!"
  }
}

レスポンスで返す上のオブジェクトではユーザーが任意の値を設定するのは
errorsキーの値のオブジェクトだけです!
その他の部分はドキュメントにある値通りかかないと動きません。
自分はerrosをerrorと書いていてうまく動かない!ってなったこともありました。

おわりに

今回のハマりどころ、結局ドキュメントを正確に読み解けば書いてあったことで、、とほほ。ドキュメント読むときは、ドキュメントの中の用語が一般的な名詞だった場合、その言葉がそのドキュメント内で特別な意味を持っているかどうか見極められるかが重要だなと思いました。

参考

Slack アプリフレームワーク Bolt for JavaScript を Cloud Functions for Firebase で動かそう