Blocks
Create messages with block kit more functionally and with less recall and repetition. Never need to remember json formats or accidentally type markdown
instead of mrkdwn
again!
This package helps abstract away some of the specifics, and deduplicate some of the repetitive code needed to create blocks being sent to Slack.
It also automatically handles various slack API limitations on content to ensure blocks built with dynamic content at least don't break when sent to Slack. Learn more about this in the Limits section.
Install
# Yarn
yarn add @slack-wrench/blocks
# npm
npm install @slack-wrench/blocks
Usage
Example block kit builder search channel message
import { App } from '@slack/bolt';
import { Actions, Blocks, Button, Context, DateString, Divider, Markdown, MdSection, User } from '@slack-wrench/blocks';
const app = new App({ /* token, secret */ });
app.message(':wave:', async ({ message, say }) => {
say({
blocks: Blocks([
MdSection(`Hello, ${User(message.user)}! Let me help you find some channels.`, {
accessory: Button('Search', 'changeSearch'),
}),
Divider(),
MdSection('*Channels*'),
MdSection('*house-ravenclaw*\nDiscuss Ravenclaw business'),
Context([
Markdown(
`120 members\nLast post: ${DateString(1575643433, 'date_pretty', '1575643433')}`,
),
]),
Actions([
Button(':thumbsup:', 'thumbsUp', {
value: 'house-ravenclaw',
}),
Button(':thumbsdown:', 'thumbsDown', {
value: 'house-ravenclaw',
}),
]),
});
});
Before:
const { App } = require('@slack/bolt');
const app = new App({
/* token, secret */
});
app.message(':wave:', async ({ message, say }) => {
say({
blocks: [
{
accessory: {
type: 'button',
text: { type: 'plain_text', text: 'Search', emoji: true },
action_id: 'changeSearch',
},
text: {
type: 'mrkdwn',
text: `Hello, <@${message.user}>Let me help you find some channels.`,
},
type: 'section',
},
{ type: 'divider' },
{ text: { type: 'mrkdwn', text: '*Channels*' }, type: 'section' },
{
text: {
type: 'mrkdwn',
text: '*house-ravenclaw*\nDiscuss Ravenclaw business',
},
type: 'section',
},
{
type: 'context',
elements: [
{
type: 'mrkdwn',
text:
'120 members\nLast post: <!date^1575643433^{date_pretty}|1575643433>',
},
],
},
{
type: 'actions',
elements: [
{
type: 'button',
text: { type: 'plain_text', text: ':thumbsup:', emoji: true },
action_id: 'thumbsUp',
value: 'house-ravenclaw',
},
{
type: 'button',
text: { type: 'plain_text', text: ':thumbsdown:', emoji: true },
action_id: 'thumbsDown',
value: 'house-ravenclaw',
},
],
},
],
});
});
Limits
The Slack API fails completely if all of the various fields don't meet the length requirements outlined in their documentation. This is a lot to keep track of, so this package makes an attempt at sane defaults for cutting off content so that your requests to the Slack APIs won't error, even if you have very long dynamic content.
Depending on the type of the field being limited, different default limit behavior is applied.
Defaults
There are three primary functions applied, depending on the field type: truncate
, ellipsis
, or disallow
.
-
truncate
cuts the field at the maximum. Examples:- List of checkboxes - maximum 10 via Slack API; if you try to builds blocks with 15 we'll give you back just the first 10)
- URLs - in many blocks maximum 3000 characters, but if above that we just cut off the end
-
ellipsis
happens for most text fields - titles, descriptions, placeholders, etc.- title, descriptions, placeholders, etc.
- automatically processes text elements (
{ text: '<string>'}
) as well asstring
fields
-
disallow
happens for any field that is typically programmatic and the value is required to stay the same for app functionality- block IDs, action IDs, etc.
For example, on an Option composition object, here are the functions applied by default and the limits for the fields.
const optionLimits: LimitOpts = {
text: [75, ellipsis],
value: [75, disallow],
description: [75, ellipsis],
url: [3000, truncate],
};
So, the text
field has a maximum length of 75
. If the provided text in building the block is greater than 75, then the text field is 'limited' via the ellipsis
function.
// { text: 'text', value: 'value' }
Option('text', 'value');
// { text: '<first 73 characters> …', value: 'value' }
Option('<80 character text>', 'value');
// Throws
Option('text', '<80 character id value>');
Overriding
You can override the applied functions in most blocks by using the LimiterFuncs
argument. This involves passing an object mapping of fields (strings) to functions. Provided functions include truncate
, ellipsis
, disallow
, and identity
.
For example, if you didn't want it to throw on a value being too long, you could truncate that field instead of disallowing:
// map the `value` field to the truncate function instead of the default disallow
Option('title', dynamicText, undefined, { value: truncate });
Custom Limiter Functions
In the same way that you can override, you can also provide your own custom functions (e.g. parsing URLs and removing query parameters, showing the last 10 instead of the first 10 in an array...).
The limiter function is passed two values - the limit (number) for the field in context and the string or array on which the check is done.
For example, to provide a function that just rendered an error string for text that is too long, you could do this:
Option(dynamicText, 'value', undefined, {
text: (limit, dynamicText) =>
`ERR: TOO LONG. LONGER than (${limit}): ${dynamicText.substring(0, 10)}...`,
});
Limiter function signature: <T>(limit: number, value: T) => T
. Be sure to return a value that is the same type as the value
passed - text element, string, option object array, etc. depending on the field.
Note that this function only gets called when the passed dynamicText is greater than the limit. Also, if you end up returning a value under the limit
, the block could break when the API call is made to Slack (since Slack will refuse the request).
Nested Limits
For some blocks, rules are applied to nested blocks in specific fields. For example, the Section block fields
field requires that each text object in fields
has a maximum text length of 2000.
In cases such as this, you can override the default limiter function by providing a tuple instead of just a value limiter: [valLimiter, eachLimiters]
, where eachLimiters
is another mapping of Limiters
. For example, to truncate at 2000 characters instead of ellipsis for fields on a section, you could do:
Section(
{
text: PlainText(text),
fields: [{ text: dynamicText }],
},
{
fields: [truncate, { text: truncate }],
},
);