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:
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:
callback_id
. We'll use it to define modal view handlers that react to view open/closed events later.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.
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
};
},
)
// ...
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.
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.