The new Slack platform is in beta. Your feedback is most welcome.

Modal View Interactivity

As you build your new Slack app or even update an existing app, you might be asking yourself - how can I display or collect information in a focused space within Slack? Look no further than utilizing the power of modals. Modals provide a space for your app to request and collect data from users or display dynamic and interactive data.

What's a modal without some rich interactive components or better yet, Block Kit interactive components. Vast possibilities lie ahead when you begin to add levels of interactivity into your Slack app. This page will look at the ways to add and handle modal interactivity events throughout your app.

Say you want to collect some feedback from your team or display data from a 3rd party API; you can build modal views to help you accomplish those tasks. Views within your app are either submitted, closed, or updated. Become familiar with the modal functionality here.

This page will provide information about how to handle view_submission, view_closed, view_update events within your app. You will also learn how each view can be triggered at different places in your app. Modal views within your app can be triggered within your function or within a Block Kit action.

Adding interactivity in a function definition

Before your app is able to recognize interactivity, you must have the below requirements:

  1. Have a complete Function definition
  2. Include interactivity as an input parameter in your function with type: Schema.slack.types.interactivity

This is an example function definition with the interactivity input parameter. This will exist in your manifest.ts file.

import { DefineFunction, Schema } from "deno-slack-sdk/mod.ts";

export const DiaryFunction = DefineFunction({
  callback_id: "diary",
  title: "Diary",
  description: "Write a diary entry",
  source_file: "functions/diary_function.ts", // <-- important! Make sure this is where the logic for your function 
  input_parameters: {
    properties: {
      interactivity: { // <-- important! This gives Slack a hint that your function will create interactive elements like views
        type: Schema.slack.types.interactivity,
      },
      channel_id: {
        type: Schema.slack.types.channel_id,
      },
    },
    required: ["interactivity"],
  },
  output_parameters: {
    properties: {},
    required: [],
  },
});

Next we will look at how to open a modal within your app.

Opening a modal view

Modal views utilize the views.open API method and views.push API method to control when a modal is being triggered by the user.

When you create an input parameter named interactivity of type Schema.slack.types.interactivity, your implementation can leverage an interactivity pointer that serves as a unique identifier for the interactivity event. This is accessed in your functions via inputs.interactivity.interactivity_pointer.

Opening a View from a Function

Here's an example of a views.open API call within a function implementation. This code could exist under functions/diary_function.ts.

import type { SlackFunctionHandler } from "deno-slack-sdk/types.ts";
import { SlackAPI } from "deno-slack-api/mod.ts";
// DiaryFunction is the function we defined in the previous section
import { DiaryFunction } from "../manifest.ts";

const diary: SlackFunctionHandler<
  typeof DiaryFunction.definition
> = async ({ inputs, token }) => {
  console.log('Someone might want to write a diary entry...');
  // A Slack API client, so that we can make API calls to Slack
  const client = SlackAPI(token);

  await client.views.open({
    interactivity_pointer: inputs.interactivity.interactivity_pointer
    
    view: {
      "type": "modal", //Be sure to specify the view type
      "title": {
        "type": "plain_text",
        "text": "Modal title",
      },
      "blocks": [
        {
          "type": "input",
          "block_id": "section1",
          "element": {
            "type": "plain_text_input",
            "action_id": "diary_input",
            "multiline": true,
            "placeholder": {
              "type": "plain_text",
              "text": "What is on your mind today?",
            },
          },
          "label": {
            "type": "plain_text",
            "text": "Diary Entry",
          },
          "hint": {
            "type": "plain_text",
            "text": "Don't worry, no one but you will see this.",
          },
        },
      ],
      "submit": {
        "type": "plain_text",
        "text": "Save",
      },
      "callback_id": "view_diary", // <-- remember this ID, we will use it to route events to handlers!
      "notify_on_close": true, // <-- this must be defined in order to trigger `view_closed` events!
    },
  });
  // Important to set completed: false! We will set the function's complete
  // status later - in our view submission handler
  return {
    completed: false,
  };
};

In this code sample keep in mind of a few things.

  • We defined a callback_id which will we will use to route our views.open event. More on this below.
  • Set notify_on_close to true in order to trigger the view_closed event.
  • Set the return to completed: false

The above are important to remember to ensure your modal isn't left suspended in the modal wasteland.

Opening a view using Block Kit

Similarly to opening a view within a Function, you can also open a view using a Block Kit Action Handler.

Below is an example of what your code might look like:

import type { SlackFunctionHandler } from "deno-slack-sdk/types.ts";
import { SlackAPI } from "deno-slack-api/mod.ts";
// DiaryFunction is the function we defined in the previous section
import { DiaryFunction } from "../manifest.ts";

export const blockActions = BlockActionsRouter(DiaryFunction).addHandler(
  'open_diary',
  async ({ action, body, token }) => {
    const client = SlackAPI(token);
    await client.views.open({
      interactivity_pointer: inputs.interactivity.interactivity_pointer,
      view: { /* your view object goes here */ },
    });
  });

Next, we will look at how to handle view events sent to your app.

Routing events using ViewRouter

Now with your defined modal view equipped with a callback_id, you can implement a ViewsRouter. This is a helpful utility to "route" different view events to specific view handlers inside your application.

With our Diary function above, we will use ViewRouter to call either the addSubmissionHandler or addClosedHandler method. These methods are available to the ViewRouter to determine the next steps in your modal view.

Here is an example of both methods:

import { ViewsRouter } from "deno-slack-sdk/mod.ts";
import { DiaryFunction } from "../manifest.ts";
// We must pass in the function definition for our main function when creating a new `ViewsRouter`
const ViewRouter = ViewsRouter(DiaryFunction);
// Now can use the router's `addSubmissionHandler` and `addClosedHandler` methods
// to register different handlers based on the `callback_id` view property
export const { viewSubmission, viewClosed } = ViewRouter
  .addSubmissionHandler(
    "view_diary", // The first argument to any of the add*Handler methods can accept a string, array of strings, or RegExp.
    // This first argument will be used to match the view's `callback_id`
    // Check the API reference at the end of this document for the full list of supported options
    async ({ view, body, token }) => { // The second argument is the handler function itself
      console.log('Incoming view submission handler invocation', body);
    }
  )
  .addClosedHandler(
    "view_diary",
    async({ view, body, token }) => {
      console.log('Incoming view closed handler invocation', body);
    }
  );

Updating and pushing a view

Become familiar with the ways you can update a view, push a new view and close a view. In order to update or push a new view you will use the addSubmissionHandler method with either the views.update API, views.push API or via the returned view handler called response_action.

Following is an example of addSubmissionHandler method that identical behavior of updating a view:

export const { viewSubmission, viewClosed } = ViewRouter
  // A view submission handler that pushes a new view using the API
  .addSubmissionHandler(/view/, async ({ inputs, token, body }) => {
    const client = SlackAPI(token);
    await client.views.push({
      interactivity_pointer: inputs.interactivity.interactivity_pointer,
      view: { /* your view object goes here */ },
    });
  });
  // A view submission handler that pushes a new view using the `response_action`
  .addSubmissionHandler(/view/, async () => {
    return {
      response_action: "push", 
      view: { /* your view object goes here */ },
    });
  });

Onward

Hooray! You now have some cool new views weaved within your app. You are on a path to providing a keen user experience. Keep on reading to learn how to trigger your workflows and include even more rich Block Kit components.

Have 2 minutes to provide some feedback?

We'd love to hear about your experience with the new Slack platform. Please complete our short survey so we can use your feedback to improve.