Modal View Interactivity

Modals are equivalent to alert, pop-up, or dialog boxes; they capture and maintain focus within Slack until the user submits or dismisses them. Modals consist of one or more views that you create, update, or close based on user interaction.

To learn more about how modals work behind-the-scenes, check out Understanding modal flows.

This page will guide you through adding modal interactivity to your app. We'll walk through the following:

  1. Adding interactivity to your function definition
  2. Creating a function that builds your modal (alternatively, you can use Block Kit interactive components)
  3. Creating a top-level function to open the modal we just built
  4. Adding modal view event handling to your top-level function
  5. Updating and pushing a modal view

Adding interactivity to your function definition

The first step in getting your app set up for modal interactivity is to add the interactivity property to an existing function definition as an input parameter. Once added, your function implementation can leverage the interactivity pointer provided by the interactivity input that serves as a unique identifier for the interactivity event. This can be accessed within your function using inputs.interactivity.interactivity_pointer.

For example, see how we added interactivity in the configure_events.ts function from our Simple Survey sample app:

// configure_events.ts

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

import {
  createReactionTriggers,
  findReactionTriggers,
  getReactionTriggerChannelIds,
  getReactionTriggerSurveyorIds,
  updateReactionTriggers,
} from "./utils/trigger_operations.ts";

export const ConfigureEventsFunctionDefinition = DefineFunction({
  callback_id: "configure_events",
  title: "Manage channel and user filters for the reaction event triggers",
  source_file: "functions/configure_events.ts",
  input_parameters: {
    properties: {
      interactivityPointer: { type: Schema.types.string },
    },
    required: ["interactivityPointer"],
  },
  output_parameters: { properties: {}, required: [] },
});
// To be continued ...

Next, we're ready to define how your modal will appear.

In order to open a modal view, first we need to build out how we actually want our modal to look. In the following example from configure_events.ts, we'll create two interal functions: buildModalView and buildModalUpdateResponse that define how our modal will appear:

// configure_events.ts

function buildModalView(channelIds: string[], surveyorIds: string[]) {
  return {
    "type": "modal",
    "callback_id": "configure-workflow",
    "title": {
      "type": "plain_text",
      "text": "Simple Survey",
    },
    "notify_on_close": true,
    "submit": {
      "type": "plain_text",
      "text": "Confirm",
    },
    "blocks": [
      {
        "type": "input",
        "block_id": "channel_block",
        "element": {
          "type": "multi_channels_select",
          "placeholder": {
            "type": "plain_text",
            "text": "Select channels to survey in",
          },
          "initial_channels": channelIds,
          "action_id": "channels",
        },
        "label": {
          "type": "plain_text",
          "text": "Channels to survey",
        },
      },
      {
        "type": "input",
        "block_id": "user_block",
        "element": {
          "type": "multi_users_select",
          "placeholder": {
            "type": "plain_text",
            "text": "Select users that can create surveys",
          },
          "initial_users": surveyorIds,
          "action_id": "users",
        },
        "label": {
          "type": "plain_text",
          "text": "Surveying users",
        },
      },
      {
        "type": "context",
        "elements": [
          {
            "type": "plain_text",
            "text":
              "Spreadsheets will be created using the external auth tokens added from the CLI",
          },
        ],
      },
    ],
  };
}

function buildModalUpdateResponse(modalMessage: string) {
  // To be filled in later
}

Some important considerations to note so that you can ensure your modal isn't left floating in the vast sea of suspended modals:

  1. Take note of the callback_id. We'll use it to define modal view handlers that react to view open/closed events later.
  2. Set notify_on_close to true in order to trigger a view_closed event.

Now that we have defined how modals will appear, we can call those functions from our top-level function to open them. The views that comprise your modal utilize the views.open and views.push API methods to control when a modal is being triggered by the user.

There are two methods of opening a modal view:

In our top-level function, we'll use the views.open API call as in the following example from configure_events.ts:

// configure_events.ts

export default SlackFunction(
  ConfigureEventsFunctionDefinition,
  async ({ inputs, client }) => {
    const { error, triggers } = await findReactionTriggers(client);
    if (error) return { error };

    // Retreive existing channel and surveyor info
    const channelIds = triggers != undefined
      ? getReactionTriggerChannelIds(triggers)
      : [];

    const surveyorIds = triggers != undefined
      ? getReactionTriggerSurveyorIds(triggers)
      : [];

    // Open the modal to configure the channel list for the workflows
    const response = await client.views.open({
      interactivity_pointer: inputs.interactivityPointer,
      view: buildModalView(channelIds, surveyorIds),
    });
    if (!response.ok) {
      return { error: `Failed to open configurator modal: ${response.error}` };
    }

    // Set this to continue the interaction with this user
    return { completed: false };
  },
).addViewSubmissionHandler(["configure-workflow"], async ({ view, client }) => {

  // Gather input from the modal
  const channelIds = view.state.values.channel_block.channels
    .selected_channels as string[];
  const reactorIds = view.state.values.user_block.users
    .selected_users as string[];
  const filters = { channelIds, reactorIds };

  // Search for existing reaction triggers
  const { error, triggers } = await findReactionTriggers(client);
  if (error) {
    return { error: `Failed to collect trigger information: ${error}` };
  }

  // Create new event reaction triggers or update existing ones
  if (triggers === undefined || triggers.length === 0) {
    const { error } = await createReactionTriggers(client, filters);
    if (error) {
      return { error: `Failed to create new event triggers: ${error}` };
    }
  } else {
    const { error } = updateReactionTriggers(client, triggers, filters);
    if (error) {
      return { error: `Failed to update existing event triggers: ${error}` };
    }
  }

  // Join all selected channels as the bot user
  channelIds.forEach(async (channel) => {
    const response = await client.conversations.join({ channel });
    if (!response.ok) {
      return {
        error: `Failed to join channel <#${channel}>: ${response.error}`,
      };
    }
  });

  // Update the modal with a notice of successful configuration
  const modalMessage =
    "*You're all set!*\n\nAdd a :clipboard: reaction to messages in these channels to create a survey";
  return buildModalUpdateResponse(modalMessage);
}).addViewClosedHandler(["configure-workflow"], () => {
  return { outputs: {}, completed: true };
});

Make sure to set the return to completed: false in the top-level function, and then set it to true later in your modal view event handler.

Similarly to opening a modal view from a function, you can open a modal view using a Block Kit Action Handler. Below is an example of what that code might look like:

export default SlackFunction(ConfigureEventsFunctionDefinition, async ({ inputs, client }) => {

).addBlockActionsHandler(["configure_events"], async ({ body, client }) => {
  const openingModal = await client.views.open({
    interactivity_pointer: body.interactivity.interactivity_pointer,
    view,
  });
  if (openingModal.error) {
    const error = `Failed to open a modal - ${openingModal.error}`;
    return { error };
  }
});

With your defined modal view equipped with a callback_id, you can implement a modal view event handler to respond to interactions with your modal view. In our top-level function, we can "chain" additional calls to either the addViewSubmissionHandler or addViewClosedHandler methods. These methods allow you to use specific function handlers to respond to either view submission or view closed events, respectively.

You can also add a call to the client.functions.completeSuccess endpoint to explicitly mark the function as complete like this:

const response = await client.functions.completeSuccess({
  function_execution_id: body.function_data.execution_id,
  outputs,
});

Or, if the function execution was not successful and you'd like to raise an error, like this:

const response = await client.functions.completeError({
  function_execution_id: body.function_data.execution_id,
  error: "Error completing function",
});

Note that both functions.completeSuccess and functions.completeError are private methods.

Updating and pushing a modal view

In order to update a modal view or to push a new modal view, your modal view event handlers can use the views.update or views.push APIs.

Or, as in our configure_events.ts example, modal views can return an object with a special response_action property:

// configure_events.ts

function buildModalUpdateResponse(modalMessage: string) {
  return {
    response_action: "update",
    view: {
      "type": "modal",
      "callback_id": "configure-workflow",
      "notify_on_close": true,
      "title": {
        "type": "plain_text",
        "text": "Simple survey",
      },
      "blocks": [
        {
          "type": "section",
          "text": {
            "type": "mrkdwn",
            "text": modalMessage,
          },
        },
      ],
    },
  };
}

To learn more about updating and pushing views, check out Understanding modal flows for details about how the view stack works. It may also be helpful to read up on updating a view, pushing a new view, and closing a view.

In the examples below, the addViewSubmissionHandler method registers a handler to push a new view on to the view stack. The first code snippet shows how to push a new view:

// ...
.addViewSubmissionHandler(
  "configure_events",
  async ({ inputs, client, body }) => {
    const response = await client.views.push({
      interactivity_pointer: inputs.interactivity.interactivity_pointer,
      view, // The view defined earlier
    });
  },
)
// ...

The second code snippet shows how to use response_action to do the same thing. Both result in identical behavior!

// ...
.addViewSubmissionHandler(
  "configure_events",
  async () => {
    return {
      response_action: "push",
      view, // The view defined earlier
    };
  },
)
// ...

Data validation

Once you have opened a modal and handled your modal views, you may decide that you'd like to display any potential data validation error messages to your users. It is important to validate the inputs you receive from the user: first, that the user is authorized to pass the input, and second, that the user is passing a value you expect to receive and nothing more.

To display any potential validation error messages, you can apply functionality from the view-related APIs you know and love. As long as your submission handler returns an error object defined on this page, the error messages you include in that object will be displayed right next to the relevant form fields based on their field IDs.

Onward

You now have some shiny new modal views weaved within your app, and are on a course to providing a wonderful user experience.

To learn more about Block Kit interactive components, check out the Block Kit interactive components page.

Have 2 minutes to provide some feedback?

We'd love to hear about your experience building modular Slack apps. Please complete our short survey so we can use your feedback to improve.