Skip to content

Latest commit

 

History

History
333 lines (288 loc) · 13.9 KB

functions-view-handlers.md

File metadata and controls

333 lines (288 loc) · 13.9 KB

View Handlers

Your application's functions can do a wide variety of interesting things: post messages, create channels, or anything available to developers via the Slack API. They can even include interactive components or pop up a Modal. Modals are composed of up to three Views. These Views can contain form inputs or interactive components. Views themselves may also trigger events. This document explores the APIs available to app developers building Run-On-Slack applications to create modals composed of views and how applications can respond to the view submission and closed events they can trigger.

If you're already familiar with the main concepts underpinning View Handlers, then you may want to skip ahead to the API Reference.

Requirements

This functionality requires at least version 0.2.0 of the deno-slack-sdk.

Your app needs to have an existing function defined, implemented and working before you can add interactivity handlers like View Handlers or Block Kit Action Handlers to them. Make sure you have followed our functions documentation and have a function in your app ready that we can expand with a View Handler.

As part of exploring how View Handlers work, we'll walk through a simple diary flow example. It is nothing more than a contrived example aimed at showing off the APIs. A user would trigger our app's function, which would open a view with a single text input. If the view is submitted with content, the application will send the user a DM with their inputted content. If the view is closed, the application will send the user a DM encouraging them not to give up on their diarying habit.

For the purposes of walking through this approval flow example, let us assume the following function definition (that we will store in a file called definition.ts under the functions/diary/ subdirectory inside your app):

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/mod.ts", // <-- important! Make sure this is where the logic for your function - which we will write in the next section - exists.
  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: [],
  },
});

Opening a view

Opening a view via the views.open API and pushing a new view onto the view stack via the views.push API both require the use of a trigger_id. These are identifiers representing specific user interactions. Slack uses these to prevent applications from haphazardly opening modals in users' faces willy-nilly. Without a trigger_id, your application can't create a modal and open a view. FYI trigger_ids are also known as interactivity_pointers.

As such, there are two ways to open a view from inside a Run-On-Slack application: doing so from a function directly vs. doing so from a Block Action Handler. The sections covering each approach below discuss how to retrieve the trigger_id in each scenario.

We will explore implementing our contrived example above by opening a view from a function. In a section further below, we will also cover opening a view from a Block Action Handler.

Opening a view from a custom function

As mentioned in the previous section, we need to have a trigger_id handy in order to open a view. This is why we defined an interactivity input in our function definition earlier: this input will magically provide us with a trigger_id. The property to use as a trigger_id exists on inputs with the type Schema.slack.types.interactivity under the interactivity_pointer property. Check out the code below for an example:

import { SlackFunction } from "deno-slack-sdk/mod.ts";
// DiaryFunction is the function we defined in the previous section
import { DiaryFunction } from "./definition.ts";

export default SlackFunction(DiaryFunction, async ({ inputs, client }) => {
  console.log('Someone might want to write a diary entry...');

  await client.views.open({
    trigger_id: inputs.interactivity.interactivity_pointer,
    view: {
      "type": "modal",
      "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.",
          },
        },
      ],
      "close": {
        "type": "plain_text",
        "text": "Cancel",
      },
      "submit": {
        "type": "plain_text",
        "text": "Save",
      },
      "callback_id": "view_identifier_12", // <-- 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,
  };
};

Opening a view from a block action handler

If Block Kit Action Handlers is a foreign concept to you, we recommend first checking out its documentation before venturing deeper into this section.

Similarly to opening a view from a function, doing so from a Block Action Handler is straightforward though slightly different. It is important to remember that trigger_ids represent a unique user interaction with a particular interactive component within Slack's UI. As such, when responding to a Block Kit Action interactive component, we don't want to use your function's inputs to retrieve the interactivity_pointer, as we did in the previous section, but rather, we want to retrieve a trigger_id that is unique to the Block Kit interactive component.

Luckily for us, this is provided as a parameter to Block Kit Action Handlers! You can use the value of body.interactivity.interactivity_pointer within an action handler to open a view, like so:

export default SlackFunction(DiaryFunction, async ({ inputs, client }) => {
  // ... the rest of your DiaryFunction logic here ...
}).addBlockActionsHandler(
  "deny_request",
  async ({ action, body, client }) => {
    await client.views.open({
      trigger_id: body.interactivity.interactivity_pointer,
      view: {/* your view object goes here */},
    });
  },
);

Adding view handlers

The Deno Slack SDK - which comes bundled in your generated Run-on-Slack application - provides a means for defining handlers to execute every time a user interacts with a view. In this way you can route view-related events to specific handlers inside your application. The key identifier that we'll need to keep handy is the callback_id we assigned to any views we created. This ID will be the property that determines which view event handler will respond to incoming view events.

Continuing with our above example, we can now define handlers that will listen for view submission and closed events and respond accordingly. The code to add view handlers is "chained" off of your top-level function, and would look like this:

export default SlackFunction(DiaryFunction, async ({ inputs, client }) => {
  // ... the rest of your DiaryFunction logic here ...
}).addViewSubmissionHandler(
  /view/, // The first argument to any of the addView*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);
  },
)
  .addViewClosedHandler(
    /view/,
    async ({ view, body, token }) => {
      console.log("Incoming view closed handler invocation", body);
    },
  );

Importantly, more complex applications will likely be modifying views as users interact with them: updating the view contents (to e.g. add new form fields), perhaps pushing a new view onto the view stack to introduce a new UI to the user, maybe reporting errors to the user for some manner of faulty interaction, or even clearing the entire view stack altogether. All of these modal interaction responses are covered in depth on our API documentation site - make sure to spend the time to understand the concepts presented there.

In particular, modal interactions can be responded to by using the API, or by returning particularly-crafted responses directly from inside the view handlers. On our API site detailing view modification, these returned view handler responses are called response_actions.

As an example, consider the following two code snippets. They yield identical behavior!

export default SlackFunction(DiaryFunction, async ({ inputs, client }) => {
  // ... the rest of your DiaryFunction logic here ...
}).addViewSubmissionHandler(/view/, async ({ client, body }) => {
  // A view submission handler that pushes a new view using the API
  await client.views.push({
    trigger_id: body.trigger_id,
    view: {/* your view object goes here */},
  });
}).addSubmissionHandler(/view/, async () => {
  // A view submission handler that pushes a new view using the `response_action`
  return {
    response_action: "push",
    view: {/* your view object goes here */},
  };
});

API reference

addViewSubmissionHandler(constraint, handler)

SlackFunction({ ... }).addViewSubmissionHandler("my_view_callback_id", async (ctx) => { ... });

addViewSubmissionHandler registers a view handler based on a constraint argument. If any incoming view_submission event matches the constraint, then the specified handler will be invoked with the event payload. This allows for authoring focussed, single-purpose view handlers and provides a concise but flexible API for registering handlers to specific view interactions.

constraint can be either a string, an array of strings, or a regular expression.

  • A simple string constraint must match a view's callback_id exactly.
  • An array of strings constraint must match a view's callback_id to any of the strings in the array.
  • A regular expression constraint must match a view's callback_id.
addViewClosedHandler(constraint, handler)
SlackFunction({ ... }).addViewClosedHandler("my_view_callback_id", async (ctx) => { ... });

⚠️ IMPORTANT: you must set a view's notify_on_close property to true for the view_closed event to trigger; by default this property is false. See the View reference documentation - in particular the Fields section for more information.

addViewClosedHandler registers a view handler based on a constraint argument. If any incoming view_closed event matches the constraint, then the specified handler will be invoked with the event payload. This allows for authoring focussed, single-purpose view handlers and provides a concise but flexible API for registering handlers to specific view interactions.

constraint can be either a string, an array of strings, or a regular expression.

  • A simple string constraint must match a view's callback_id exactly.
  • An array of strings constraint must match a view's callback_id to any of the strings in the array.
  • A regular expression constraint must match a view's callback_id.