Component System
Component System
The component system provides a type-safe way to handle Discord UI components like buttons, select menus, and modals.
Table of Contents
- Overview
- Button Components
- Select Menu Components
- Modal Components
- Component Options
- Component Patterns
- Creating Interactive UIs
- Best Practices
Overview
The component system features:
- Type-safe handling of buttons, select menus, and modals
- Support for exact ID matching or pattern-based matching
- Component cooldowns
- Organization by categories
Components are managed through the ComponentManager
class, which:
- Loads component handlers from files
- Registers handlers by component ID or pattern
- Routes interactions to the appropriate handlers
- Provides cooldown management
Button Components
Buttons are the simplest UI component. You can handle button interactions in two ways:
Simple Button
For buttons with a fixed custom ID:
// src/components/Utility/closeButton.ts
import { ButtonInteraction, EmbedBuilder } from "discord.js";
import { buttonComponent } from "../../utils/types/componentManager.js";
export const data = buttonComponent({
id: "close_menu", // Exact match for buttons with this ID
options: {
cooldown: 2, // Optional cooldown in seconds
category: "Utility"
},
execute: async (client, interaction: ButtonInteraction) => {
// Check if the user who clicked is the same who created the menu
const creatorId = interaction.message.embeds[0]?.footer?.text?.match(/ID: (\d+)/)?.[1];
if (creatorId && creatorId !== interaction.user.id) {
return interaction.reply({
content: "Only the person who opened this menu can close it.",
ephemeral: true
});
}
// Delete the message with the menu
await interaction.message.delete();
}
});
Button with Pattern Matching
For buttons with dynamic IDs (like pagination):
// src/components/Utility/paginationButtons.ts
import { ButtonInteraction, EmbedBuilder } from "discord.js";
import { buttonPattern } from "../../utils/types/componentManager.js";
export const data = buttonPattern({
idPattern: /^page_(\d+)_of_(\d+)_user_(\d+)$/,
options: {
cooldown: 1,
category: "Utility"
},
// With pattern matching, the execute function receives matches array as third parameter
execute: async (client, interaction: ButtonInteraction, matches: string[]) => {
// matches contains: [full_match, group1, group2, group3, ...]
// For "page_2_of_5_user_123456789", matches would be:
// ["page_2_of_5_user_123456789", "2", "5", "123456789"]
const [, currentPage, totalPages, userId] = matches;
// Check permissions
if (interaction.user.id !== userId) {
return interaction.reply({
content: "Only the command author can change pages.",
ephemeral: true
});
}
// Update the page content
const pageNum = parseInt(currentPage);
const maxPages = parseInt(totalPages);
// Get content for the current page
const pageContent = await getPageContent(pageNum, maxPages);
// Update the message
await interaction.update({
embeds: [
new EmbedBuilder()
.setTitle(`Page ${pageNum} of ${maxPages}`)
.setDescription(pageContent)
.setFooter({ text: `Requested by user ID: ${userId}` })
],
components: [createPaginationRow(pageNum, maxPages, userId)]
});
}
});
// Helper function to create pagination controls
function createPaginationRow(current, total, userId) {
const { ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
const row = new ActionRowBuilder();
// Previous button
const prevBtn = new ButtonBuilder()
.setCustomId(`page_${Math.max(1, current - 1)}_of_${total}_user_${userId}`)
.setLabel('Previous')
.setStyle(ButtonStyle.Primary)
.setDisabled(current <= 1);
// Next button
const nextBtn = new ButtonBuilder()
.setCustomId(`page_${Math.min(total, current + 1)}_of_${total}_user_${userId}`)
.setLabel('Next')
.setStyle(ButtonStyle.Primary)
.setDisabled(current >= total);
row.addComponents(prevBtn, nextBtn);
return row;
}
// Get content for a specific page
async function getPageContent(page, total) {
// Fetch content for the requested page
return `This is page ${page} of ${total}.\n\nContent for this page goes here.`;
}
Select Menu Components
Select menus allow users to choose from a list of options. There are several types:
String Select Menu
// src/components/Settings/roleSelector.ts
import { StringSelectMenuInteraction } from "discord.js";
import { stringSelectComponent } from "../../utils/types/componentManager.js";
export const data = stringSelectComponent({
id: "role_selector",
options: {
cooldown: 5,
category: "Settings"
},
execute: async (client, interaction: StringSelectMenuInteraction) => {
const selectedRoles = interaction.values;
// Process the selected roles
await interaction.reply({
content: `You selected the following roles: ${selectedRoles.join(", ")}`,
ephemeral: true
});
// Add/remove roles logic here...
}
});
User Select Menu
// src/components/Moderation/userSelector.ts
import { UserSelectMenuInteraction } from "discord.js";
import { userSelectComponent } from "../../utils/types/componentManager.js";
export const data = userSelectComponent({
id: "warn_user_selector",
options: {
category: "Moderation"
},
execute: async (client, interaction: UserSelectMenuInteraction) => {
const selectedUsers = interaction.values;
// Show warning form for selected users
await interaction.reply({
content: `You're about to warn ${selectedUsers.length} users. Please provide a reason:`,
components: [
{
type: 1,
components: [
{
type: 4, // Text input
custom_id: "warn_reason",
style: 2, // Paragraph
label: "Warning Reason",
placeholder: "Enter the reason for the warning",
required: true
}
]
}
],
ephemeral: true
});
}
});
Modal Components
Modals allow collecting form data from users:
// src/components/Forms/feedbackForm.ts
import { ModalSubmitInteraction } from "discord.js";
import { modalComponent } from "../../utils/types/componentManager.js";
export const data = modalComponent({
id: "feedback_form",
options: {
category: "Forms"
},
execute: async (client, interaction: ModalSubmitInteraction) => {
// Get values from the form fields
const feedbackType = interaction.fields.getTextInputValue('feedback_type');
const feedbackContent = interaction.fields.getTextInputValue('feedback_content');
// Process the feedback
await interaction.reply({
content: `Thank you for your ${feedbackType} feedback!`,
ephemeral: true
});
// Log or store the feedback
client.logger.info(`Feedback from ${interaction.user.tag}: ${feedbackContent}`);
// You could also send it to a feedback channel
const feedbackChannel = client.channels.cache.get('FEEDBACK_CHANNEL_ID');
if (feedbackChannel?.isTextBased()) {
await feedbackChannel.send({
embeds: [{
title: `New ${feedbackType} Feedback`,
description: feedbackContent,
fields: [{ name: 'From', value: interaction.user.tag }],
color: 0x00FF00,
timestamp: new Date()
}]
});
}
}
});
Component Options
Components can have various options:
options: {
// Component category for organization
category: "Category",
// Cooldown in seconds (0 = no cooldown)
cooldown: 5
}
Component Patterns
For components with dynamic IDs, use pattern-based components:
// Button with a pattern
buttonPattern({
idPattern: /^delete_message_(\d+)_by_(\d+)$/,
execute: async (client, interaction, matches) => {
// matches array contains [full_match, messageId, authorId]
const [, messageId, authorId] = matches;
// Check permission
if (interaction.user.id !== authorId) {
return interaction.reply({
content: "You can only delete your own messages.",
ephemeral: true
});
}
// Delete the message
await interaction.message.delete();
}
});
// Select menu with a pattern
stringSelectPattern({
idPattern: /^quiz_(\d+)_question_(\d+)$/,
execute: async (client, interaction, matches) => {
// matches array contains [full_match, quizId, questionId]
const [, quizId, questionId] = matches;
// Process the answer
const selectedAnswer = interaction.values[0];
// Quiz logic...
}
});
Creating Interactive UIs
You can combine components to create interactive UIs:
// Example command that shows an interactive menu
export const data = commandFile({
data: new SlashCommandBuilder()
.setName("menu")
.setDescription("Shows an interactive menu"),
execute: async (cmdExecutor) => {
// Create buttons
const row1 = new ActionRowBuilder<ButtonBuilder>()
.addComponents(
new ButtonBuilder()
.setCustomId('menu_option_1')
.setLabel('Option 1')
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId('menu_option_2')
.setLabel('Option 2')
.setStyle(ButtonStyle.Secondary)
);
// Create a select menu
const row2 = new ActionRowBuilder<StringSelectMenuBuilder>()
.addComponents(
new StringSelectMenuBuilder()
.setCustomId('menu_select')
.setPlaceholder('Choose an option')
.addOptions([
{ label: 'Option A', value: 'a' },
{ label: 'Option B', value: 'b' },
{ label: 'Option C', value: 'c' }
])
);
// Send the menu
await cmdExecutor.reply({
content: 'Here\'s your interactive menu:',
components: [row1, row2]
});
}
});
// Then handle interactions in separate component handlers
Best Practices
-
Pattern Matching Usage - When using patterns, remember you get an array of matches:
// In pattern components: execute: async (client, interaction, matches) => { const [fullMatch, ...captureGroups] = matches; // Use captureGroups as needed }
-
Security checking - Always verify the user has permission to use the component
-
Error handling - Include try/catch blocks to gracefully handle errors
-
Time limits - Remember that components expire after 24 hours in messages
-
Component organization - Group related components in the same file or directory
-
State management - For complex interactions, consider storing state in a database
-
Ephemeral responses - Use ephemeral responses for administrative actions