Modals & Views
Pumble's modals and views allow apps to create rich, interactive user interfaces beyond simple messages.
Modals
Modals in Pumble are interactive, pop-up windows that provide a focused space for user interaction, ideal for collecting input or guiding multistep processes. They allow apps to present dynamic forms and information, creating richer, more app-like experiences within Pumble.
Key Concepts
- Views: Every modal is built using a view, which is a JSON object defining the UI layout and content. This allows you to arrange various UI components like text, input fields, buttons, and more.
- Focus & Interactivity: Modals are designed to capture user focus until they are submitted or dismissed. This makes them ideal for forms, confirmations, and any scenario requiring dedicated user interaction.
- View Stack: Modals can have a modal stack, allowing your app to push up to three modals. This enables multistep forms or wizards without closing the initial modal.
When to Use Modals
Modals are best suited for situations where you need to:
- Collect User Input: Create forms with various input types (text fields, dropdowns, etc.) to gather data from users.
- Guide Multi-Step Workflows: Break down complex processes into digestible steps using the modal stack.
- Display Focused Information: Present details that require the user's undivided attention, such as summaries or configuration options.
- Confirm Actions: Prompt users for confirmation before performing critical operations.
- Present Dynamic Content: Show information that changes based on user input or external data.
How Modals Work (Basic Flow)
- Triggering the modal: A modal is typically opened in response to a user action, such as:
- Clicking a button in a message (or interacting with any interactive component)
- Using a slash command, global shortcut or message shortcut
- Composing the view: Your app constructs a JSON object defining the modal's content and layout using blocks. This includes all the blocks and input elements.
- Opening the modal: Your app makes call to
context.spawnModalView
to display the constructed view as a modal to the user. - User interaction: The user interacts with the elements within the modal (e.g., filling out forms, clicking buttons).
- Handling submissions/closures: When the user submits the form or dismisses the modal, Pumble sends an interaction payload to your app's configured Request URL.
- For submissions, payload includes all the submitted data in
state
field - For dismissals, your app can define if it will handle such event by setting
notifyOnClose
property
- For submissions, payload includes all the submitted data in
- Responding to interactions: Your app processes the block interaction payload. Based on the input, it can:
- Update the current view by using
context.updateView
- Push a new view onto the stack (by using
context.pushModalView
) for the next step - Present an error message within the modal if validation fails
- Send a message to a channel based on the submission
- Whatever you need to do 😉
- Update the current view by using
Let's write some code 🚀
Spawn modal
globalShortcuts: [
{
name: 'Open modal',
description: 'Open interactive modal',
handler: async (ctx) => {
await ctx.spawnModalView({
callbackId: "modal_callback_id_1",
type: "MODAL",
title: {
type: "plain_text",
text: "Title"
},
submit: {
type: "plain_text",
text: "Submit"
},
notifyOnClose: false,
blocks: [
{
type: "input",
blockId: "input_static_1",
label: {
text: "Select menu",
type: "plain_text"
},
element: {
type: "static_select_menu",
placeholder: {text: "text", type: "plain_text"},
onAction: "static_select_menu_input_action",
options: [
{text: {type: "plain_text", text: "Option 1"}, value: "1"},
{text: {type: "plain_text", text: "Option 2"}, value: "2"} // value that's preselected in modal state
]
},
dispatchAction: true
},
{
type: "actions",
elements: [
{
type: "button",
onAction: "info_btn_action",
value: `test metadata`,
text: {
text: 'Read info',
type: "plain_text"
},
style: "danger",
}
]
}
]
});
}
}
]
WARNING
Don't call ctx.ack()
in case you're handling modals (opening, updating or pushing new modals).
Handle submission
viewAction: {
onSubmit: {
modal_callback_id_1: async (ctx) => {
await ctx.ack();
// block needs to be wrapped with block of type = "input" in order to be available in a view state
// in our case only static_select_menu_input_action (that's inside input_static_1) is available
console.log(`Modal state ${JSON.stringify(ctx.payload.view.state)}. Do whatever with it :)`);
}
},
onClose: {
// will be triggered only if notifyOnClose=true
modal_callback_id_1: async (ctx) => {
await ctx.ack();
console.log("Modal closed");
}
}
}
WARNING
For input values to be included in a view state
, they must be wrapped within an input
block. Furthermore, the modal_callback_id_1
handler name must correspond to the value specified in the modal's callbackId
property.
TIP
You can also receive block interaction events for modal inputs by setting dispatchAction
to true
in input block.
Handle block interactions
blockInteraction: {
interactions: [
{
sourceType: "MESSAGE",
handlers: {
bttn_1_action: async (ctx) => {
await ctx.ack();
await ctx.say("Button 1 pressed", "ephemeral");
}
}
},
{
sourceType: 'VIEW',
handlers: {
static_select_menu_input_action: async (ctx) => {
await ctx.ack();
console.log("Static select from input in modal triggered, do something :)");
},
info_btn_action: async (ctx) => {
// no need to call ctx.ack when pushing new modal
await ctx.pushModalView(
{ parentViewId: ctx.payload.sourceId, ...}
);
},
},
}]
}
NOTE
When pushing a new modal over an existing one, you must specify parentViewId
, which represents the parent modal's id
.
Take a look at detailed code examples 💡 interactivity-example.
Home views
The Home tab (in app channel), is a private, customizable one-to-one space between a user and an app. It acts as a persistent dashboard where your app can display dynamic, personalized content and provide entry points for core functionality using blocks.
Publish home view
The Home view can be updated at any time an app deems appropriate, for instance, three seconds after a user authorizes the app:
redirect: {
enable: true, path: "/redirect",
onSuccess: async (result, req, res) => {
if (result.botToken && result.botId && result.userId) {
setTimeout(async () => {
try {
const addonManifest = await addonManifestPromise;
const botClient = await addonManifest.getBotClient(result.workspaceId);
// publish addon home view when user authorizes
botClient?.v1.app.publishHomeView(result.userId, {callbackId: '', title: {...}, ...});
} catch (err) {
console.error(err);
}
}, 3000);
}
}
}