Bolt Polling Application
Learn about Slack’s Bolt framework and make a small React application using it.
In this lesson, we build a polling application that allows channel members to participate in a poll.
Create a global shortcut
Our polling application will use a global shortcut. Let’s set up this shortcut by following the steps below:
- On the Slack API homepage, ensure your Slack application is selected, and then go to the “Interactivity & Shortcuts” tab on the left-hand sidebar.
- Click the “Create New Shortcut” button.
- Select the “Global” option and click “Next.”
- Name the shortcut “Create a Poll,” add a relevant description, and enter
new_poll
as the callback ID. - Click “Create” to create the shortcut, then click “Save Changes.”
- Refresh the page, and you will be prompted to reinstall the application. Click “reinstall your app” on the yellow message banner.
- Click “Allow” to reinstall the application to the workspace.
Application workflow
Any member in the conversation can create a poll using the global shortcut we just created. Interacting with the shortcut opens up a modal form. Upon submitting the form, a message with the poll is sent to the conversation, where users can vote on the poll and add new options.
The slides below show us how the Bolt application will function.
Polling application
The widget below includes the Bolt polling application alongside a React application communicating with the Bolt application and displaying the details of the polls created through it.
Click the “Run” button to start serving the application, then click the output URL at the end of the widget to open up the React application in a new tab. Then, go to Slack and use the “Create a Poll” shortcut in the conversation where the application is added.
Note: The application can only send polls where it is added, so make sure to select only a channel where it is already added!
const { App } = require("@slack/bolt"); const express = require("express"); // Declaring a global variable to store poll data let allPolls = {}; // An express server to serve requests from the React app const expressServer = express(); expressServer.use(function (req, res, next) { res.header("Access-Control-Allow-Origin", "*"); res.header( "Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept" ); next(); }); expressServer.get("/", function(req, res) { res.send("Hello World"); }) expressServer.get("/polls", function (req, res) { res.send(allPolls); }) const app = new App({ token: "{{TOKEN}}", signingSecret: "{{SIGNING_SECRET}}", appToken: "{{APP_TOKEN}}", socketMode: true, }); // Function to add new poll options to a created poll function addPollOption(newOption, blocks) { // Each option takes up three blocks (section, context, divider) const newPollOption = { type: "section", text: { type: "mrkdwn", text: `*${newOption}*`, }, accessory: { type: "button", text: { type: "plain_text", text: "Vote", }, action_id: "add_vote", }, }; blocks.splice(blocks.length - 2, 0, newPollOption); blocks.splice(blocks.length - 2, 0, { type: "context", elements: [ { type: "mrkdwn", text: "*Vote count: 0*\n", }, ], }); blocks.splice(blocks.length - 2, 0, { type: "divider", }); return blocks; } // Function to count votes and list voters function addVote(votedBlockId, blocks, user) { let contextBlock; for (var i = 0; i < blocks.length; i++) { if (blocks[i]["block_id"] === votedBlockId) { contextBlock = i + 1; } } let blockText = blocks[contextBlock]["elements"][0]["text"].split(" "); if (!blockText.includes(`<@${user}>`)) { let newString; let originalText = blocks[contextBlock]["elements"][0]["text"]; originalText = originalText.substring(originalText.indexOf("*", 2) + 1); // Extracting the count of votes let originalCount = parseInt(blocks[contextBlock]["elements"][0]["text"].substring(13,blocks[contextBlock]["elements"][0]["text"].indexOf("*\n"))); // Adding one vote to the count and appending the username to the block text if (originalCount === 0) { newString = `*Vote count: 1*\n <@${user}> `; } else { newString = `*Vote count: ${ originalCount + 1 }*${originalText} <@${user}> `; } blocks[contextBlock]["elements"][0]["text"] = newString; } return blocks; } // Listener for the new_poll shortcut app.shortcut("new_poll", async ({ ack, payload, client }) => { // Acknowledge shortcut request await ack(); try { // Call the views.open method using the WebClient passed to listeners const result = await client.views.open({ trigger_id: payload.trigger_id, view: { title: { type: "plain_text", text: "Create a Poll!", }, submit: { type: "plain_text", text: "Create", }, blocks: [ { type: "input", block_id: "poll_title", element: { type: "plain_text_input", action_id: "title", placeholder: { type: "plain_text", text: "Add a title for your poll", }, }, label: { type: "plain_text", text: "Title", }, hint: { type: "plain_text", text: "Example: Should we go for pizza today?", }, }, { type: "input", block_id: "first_option", element: { type: "plain_text_input", action_id: "option_1", placeholder: { type: "plain_text", text: "First option", }, }, label: { type: "plain_text", text: "Option 1", }, }, { type: "input", block_id: "second_option", element: { type: "plain_text_input", action_id: "option_2", placeholder: { type: "plain_text", text: "Second option", }, }, label: { type: "plain_text", text: "Option 2", }, }, { block_id: "channel_id", type: "input", optional: true, label: { type: "plain_text", text: "Select a channel to post the result on", }, element: { action_id: "select_conversation", type: "conversations_select", response_url_enabled: true, default_to_current_conversation: true, filter: { include: ["public"], exclude_bot_users: true, exclude_external_shared_channels: true }, }, }, ], type: "modal", callback_id: "new_poll", }, }); console.log(result); } catch (error) { console.error(error); } }); // Handle a view_submission request app.view("new_poll", async ({ ack, body, view, client, logger }) => { // Acknowledge the view_submission request await ack(); const title = view["state"]["values"]["poll_title"]["title"]["value"]; const option_1 = view["state"]["values"]["first_option"]["option_1"]["value"]; const option_2 = view["state"]["values"]["second_option"]["option_2"]["value"]; const conversation = view["state"]["values"]["channel_id"]["select_conversation"]["selected_conversation"]; const user = body.user.id; const username = body.user.username; console.log(body); // Message to send user let msg = [ { type: "section", text: { type: "mrkdwn", text: `<@${user}> started a poll: *${title}*`, }, }, { type: "divider", }, { type: "input", block_id: "suggested_option", element: { type: "plain_text_input", action_id: "input", placeholder: { type: "plain_text", text: "Add another option to the poll", }, }, label: { type: "plain_text", text: "Suggest an option", }, }, { type: "actions", elements: [ { type: "button", text: { type: "plain_text", emoji: true, text: "Add Option", }, style: "primary", action_id: "suggested", }, ], }, ]; msg = addPollOption(option_1, msg); msg = addPollOption(option_2, msg); try { const response = await client.chat.postMessage({ channel: conversation, blocks: msg, text: `<@${user}> started a poll.`, }); const response2 = await client.conversations.info({ channel: conversation }) allPolls[response['ts']] = { "blocks": msg, "user" : username, "channel": response2['channel']['name'] }; } catch (error) { logger.error(error); } }); // Handling the suggested action app.action("suggested", async ({ body, ack, client }) => { await ack(); const ts = body["container"]["message_ts"]; const channel = body["container"]["channel_id"]; const user = body["user"]["id"]; const suggestion = body["state"]["values"]["suggested_option"]["input"]["value"]; let blocks = body["message"]["blocks"]; blocks = addPollOption(suggestion, blocks); await client.chat.update({ channel: channel, ts: ts, blocks: blocks, text: `<@${user}> added an option to the poll.`, }); allPolls[ts]['blocks'] = blocks; }); // Handling the add_vote action app.action("add_vote", async ({ body, ack, client }) => { await ack(); const ts = body["container"]["message_ts"]; const channel = body["container"]["channel_id"]; const blockId = body["actions"][0]["block_id"]; const user = body["user"]["id"]; let blocks = body["message"]["blocks"]; blocks = addVote(blockId, blocks, user); await client.chat.update({ channel: channel, ts: ts, blocks: blocks, text: `<@${user}> voted on the poll.`, }); allPolls[ts]['blocks'] = blocks; }); // Starting the Bolt app (async () => { await app.start(); console.log("⚡️ Bolt app is running!"); })(); // Starting the express server expressServer.listen(3000); console.log("Server started at http://localhost:3000");
Code explanation
The widget above includes both the files for the Bolt app and the React app. Let’s dive a little deeper into the code contained in these files.
The Bolt app
The functions in the bolt-app/app.js
file handle these aspects:
- Lines 8–24: We define an Express server and its routes to serve the requests made by the React application. The poll data stored within the
allPolls
variable is sent as the response. - Line 34: The
addPollOption
function constructs three blocks (asection
block for the value and “Vote” button, acontext
block for the voting results, and adivider
block) for each new option and inserts them in the message blocks. - Line 72: The
addVote
function adds a new vote to an option by checking whether the user has already voted on the option. - Line 104: The
app.shortcut
handler handles the “Create a poll” shortcut by opening up a new modal view. - Line 207: The
app.view
handler handles the submission of the modal view, extracting the relevant information from the view and constructing a message to send to the conversation. - Line 290: The
app.action
handler handles thesuggested
action when a user adds a new suggestion. It calls theaddPollOption
function defined on line 34 to add a new option to the message, then updates the original message using thechat.update
endpoint. - Line 311: The
app.action
handler handles theadd_vote
action when a user votes on an option. It makes a call to theaddVote
function defined on line 72 to add a new vote to the poll.
The React app
The React application contains a single page that displays a list of all polls created through the Bolt application.
The Home
component within the react-app/components/Home.js
file communicates with the Bolt application by sending requests to the Express server.
- Line 11: We make a request to the Express server to fetch a list of polls that have been created.
- Line 30: Once the data has been fetched, we format it accordingly, extracting the required information from the server response.