Tipos de menu de seleção
O Discord oferece cinco variantes de select menu, e o Spraxium dá suporte a todas elas com decorators dedicados. String selects permitem definir opções personalizadas com rótulo e valor. Já os selects de user, role, mentionable e channel usam os seletores nativos do Discord, que se preenchem automaticamente com dados do servidor. Os cinco tipos compartilham o mesmo padrão de handler e o mesmo sistema de injeção de parâmetros.
Cada tipo é definido como uma classe decorada, vinculado a um handler por meio de um decorator específico e renderizado via SelectService. A principal diferença entre eles está no decorator usado e no tipo de dado que o usuário escolhe. String selects exigem definição explícita de opções. Os outros quatro tipos deixam o Discord preencher a lista automaticamente.
| Tipo | Decorador | Handler | Origem das opções |
|---|---|---|---|
| String | @StringSelect | @StringSelectHandler | Definida pelo desenvolvedor via @SelectOption ou dynamicOptions |
| User | @UserSelect | @UserSelectHandler | Preenchida automaticamente pelo Discord |
| Role | @RoleSelect | @RoleSelectHandler | Preenchida automaticamente pelo Discord |
| Mentionable | @MentionableSelect | @MentionableSelectHandler | Preenchida automaticamente pelo Discord, com usuários e cargos |
| Channel | @ChannelSelect | @ChannelSelectHandler | Preenchida automaticamente pelo Discord, com filtro opcional por tipo |
String selects com opções estáticas
String selects são o tipo mais flexível, porque a lista de opções fica totalmente sob seu controle. Defina essas opções com o decorator empilhável @SelectOption, aplicado na classe do componente. Cada opção exige um label, que é o texto mostrado, um value, que é o valor devolvido ao handler, e pode ter também description e emoji. As opções são empilhadas de baixo para cima na ordem dos decorators.
import { StringSelect, SelectOption } from '@spraxium/components';
@StringSelect({
customId: 'language_picker',
placeholder: 'Choose a language',
minValues: 1,
maxValues: 1,
})
@SelectOption({ label: 'English', value: 'en', description: 'English (US)', emoji: '🇺🇸' })
@SelectOption({ label: 'Português', value: 'pt', description: 'Portuguese (BR)', emoji: '🇧🇷' })
@SelectOption({ label: 'Español', value: 'es', description: 'Spanish', emoji: '🇪🇸' })
export class LanguageSelect {}Referência de configuração do select
O objeto de configuração passado para qualquer decorator de select compartilha um conjunto comum de campos. O customId identifica o select no roteamento da interação, assim como acontece com os botões. Já placeholder, minValues e maxValues controlam o comportamento visual e funcional da interface.
| Campo | Tipo | Obrigatório | Finalidade |
|---|---|---|---|
| customId | string | Sim | Identificador único usado no roteamento da interação. Precisa ser único em todo o bot. |
| placeholder | string | Não | Texto de apoio exibido quando nenhuma opção foi escolhida. |
| minValues | number | Não | Número mínimo de opções que a pessoa precisa selecionar. O padrão é 1. |
| maxValues | number | Não | Número máximo de opções que podem ser selecionadas. O padrão é 1. |
| disabled | boolean | Não | Quando está como true, o select é renderizado sem permitir interação. |
| dynamicOptions | Função que retorna SelectOptionConfig[] | Não | Callback que gera as opções no momento da renderização a partir dos dados de contexto. Só vale para string selects. |
Opções dinâmicas
Quando a lista de opções depende de dados de runtime, use o callback dynamicOptions no lugar de decorators estáticos @SelectOption. Esse callback recebe o objeto data passado para SelectService.build e precisa devolver um array de opções. Esse padrão é comum em selects preenchidos a partir de consultas em banco, respostas de API ou dados específicos do usuário.
import { StringSelect } from '@spraxium/components';
@StringSelect({
customId: 'category_picker',
placeholder: 'Select a category',
dynamicOptions: (data: { categories: Array<{ name: string; id: string }> }) =>
data.categories.map((cat) => ({
label: cat.name,
value: cat.id,
})),
})
export class CategorySelect {}Na renderização, passe o objeto de dados como segundo argumento para SelectService.build. O serviço chama dynamicOptions com esse valor e usa o array retornado para preencher o select menu.
Decorators @SelectOption e dynamicOptions são mutuamente exclusivos. Quando os dois aparecem na mesma classe, o callback dinâmico tem prioridade, e as opções estáticas são ignoradas por completo.
Entity selects
Selects de user, role, mentionable e channel são mais diretos, porque o próprio Discord preenche as opções automaticamente. Você só precisa configurar o customId, eventuais restrições e o handler. No caso de channel selects, existe ainda o filtro channelTypes para limitar quais tipos de canal podem aparecer.
import { UserSelect } from '@spraxium/components';
@UserSelect({
customId: 'target_user',
placeholder: 'Select a user',
maxValues: 3,
})
export class TargetUserSelect {}Injeção de parâmetros no handler
Handlers de select usam o mesmo sistema de decorators de parâmetro dos handlers de botão, com dois decorators extras específicos dessa família. @Selected() injeta a interação completa do menu de seleção, dando acesso a usuários, cargos, canais ou mentionables já resolvidos. @SelectedValues() injeta somente o array string[] vindo de interaction.values, que é o padrão mais comum em string selects.
import { RoleSelectHandler, Ctx, Selected } from '@spraxium/components';
import type { RoleSelectMenuInteraction } from 'discord.js';
import { AssignRoleSelect } from './role-select.component';
@RoleSelectHandler(AssignRoleSelect)
export class AssignRoleSelectHandler {
async handle(
@Ctx() interaction: RoleSelectMenuInteraction,
@Selected() selected: RoleSelectMenuInteraction,
): Promise<void> {
const roles = selected.roles;
const names = roles.map((r) => r.name).join(', ');
await interaction.reply({ content: `Assigning roles: ${names}`, flags: 'Ephemeral' });
}
}Renderização com SelectService
SelectService é o serviço injetável que monta action rows de select menu a partir de classes decoradas. O método build recebe a classe do componente, um argumento opcional de dados para callbacks dynamicOptions e, opcionalmente, um SpraxiumContext para embutir um ID de contexto no customId do menu. O método é assíncrono porque callbacks dinâmicos podem retornar promises.
import { Ctx, SlashCommandHandler } from '@spraxium/common';
import { SelectService, ContextService } from '@spraxium/components';
import type { ChatInputCommandInteraction } from 'discord.js';
import { CategorySelect } from '../components/selects/category-select.component';
import { CategoryCommand } from './category.command';
@SlashCommandHandler(CategoryCommand)
export class CategoryCommandHandler {
constructor(
private readonly selects: SelectService,
private readonly context: ContextService,
) {}
async handle(@Ctx() interaction: ChatInputCommandInteraction): Promise<void> {
const categories = await this.fetchCategories(interaction.guildId!);
const ctx = await this.context.create({ userId: interaction.user.id });
const row = await this.selects.build(CategorySelect, categories, ctx);
await interaction.reply({
content: 'Choose a category:',
components: [row],
});
}
private async fetchCategories(guildId: string) {
return [
{ name: 'General', id: 'general' },
{ name: 'Support', id: 'support' },
{ name: 'Feedback', id: 'feedback' },
];
}
}Alternativa com fluent builder
Em cenários mais programáticos, onde criar uma classe seria exagero, SelectDefBuilder oferece uma API fluente para montar definições de select inline. Esse builder é útil em fluxos dinâmicos, selects temporários ou interfaces geradas por código, onde a abordagem baseada em decorators criaria classes descartáveis demais.
import { Selects } from '@spraxium/components';
const row = Selects.string('quick_pick')
.placeholder('Quick selection')
.minValues(1)
.maxValues(1)
.options([
{ label: 'Option A', value: 'a' },
{ label: 'Option B', value: 'b' },
])
.buildRow();O fluent builder produz a mesma action row do discord.js que a abordagem com decorators. A diferença é que ele não participa do sistema de dispatch de handlers, porque não existe classe nem metadado para registrar. Use esse caminho apenas em selects descartáveis, em que o roteamento de handler não é necessário.
Selects dinâmicos
String selects estáticos definem as opções já no momento da declaração. @DynamicStringSelect quebra essa limitação: você descreve o select uma vez e gera a lista de opções com dados vivos no momento da renderização, como resultados de banco, respostas de API ou qualquer outro valor de runtime. O sistema de payload salva esse conjunto de dados junto do select, então o handler recebe tudo de volta sem precisar consultar a fonte pela segunda vez.
Como definir um string select dinâmico
Decore uma classe com @DynamicStringSelect e implemente um método estático render que receba os dados de payload e devolva um SelectRenderConfig. Esse método pode ser síncrono ou assíncrono.
import { DynamicStringSelect } from '@spraxium/components';
import type { SelectRenderConfig } from '@spraxium/components';
import type { TopicsQuery } from '../topics.data';
@DynamicStringSelect({
baseId: 'topic-pick',
placeholder: 'Pick one or more topics',
minValues: 1,
maxValues: 3,
payloadTtl: 600,
})
export class TopicSelect {
static render(query: TopicsQuery): SelectRenderConfig {
return {
options: query.topics.map((t) => ({
label: t.name,
value: t.slug,
description: t.description,
})),
};
}
}Como renderizar um string select dinâmico
Chame SelectService.buildDynamic com a classe do componente e o payload. O Spraxium salva esse payload e embute uma referência a ele no customId do select.
import { Ctx, SlashCommandHandler } from '@spraxium/common';
import { SelectService } from '@spraxium/components';
import type { ChatInputCommandInteraction } from 'discord.js';
import { TopicsCommand } from '../commands/topics.command';
import { TopicSelect } from '../components/topic-select.component';
import type { TopicsQuery } from '../topics.data';
@SlashCommandHandler(TopicsCommand)
export class TopicsCommandHandler {
constructor(private readonly selects: SelectService) {}
async handle(@Ctx() interaction: ChatInputCommandInteraction): Promise<void> {
const query: TopicsQuery = {
audience: 'developers',
topics: await fetchTopics(),
};
const row = await this.selects.buildDynamic(TopicSelect, query);
await interaction.reply({ content: 'Pick topics:', components: [row] });
}
}Como tratar a interação
Use @DynamicSelectHandler para vincular um handler a uma classe @DynamicStringSelect. O decorator @SelectPayload() injeta o payload armazenado na renderização. Já @SelectedValues() injeta interaction.values como string[]. Os dois podem aparecer junto de @Ctx() e @PayloadRef() em qualquer ordem.
import { Ctx } from '@spraxium/common';
import { DynamicSelectHandler, SelectPayload, SelectedValues } from '@spraxium/components';
import type { StringSelectMenuInteraction } from 'discord.js';
import { TopicSelect } from '../components/topic-select.component';
import type { TopicsQuery } from '../topics.data';
@DynamicSelectHandler(TopicSelect)
export class TopicSelectHandler {
async handle(
@Ctx() interaction: StringSelectMenuInteraction,
@SelectedValues() picks: string[],
@SelectPayload() query: TopicsQuery,
): Promise<void> {
const picked = query.topics.filter((t) => picks.includes(t.slug));
await interaction.reply({
content: `Picked: ${picked.map((t) => t.name).join(', ')}`,
flags: 'Ephemeral',
});
}
}Referência de configuração do select dinâmico
| Opção | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| baseId | string | Sim | Prefixo usado na geração dos custom IDs. Precisa ser único entre todos os string selects dinâmicos da aplicação. |
| placeholder | string | Não | Texto exibido quando nenhuma opção foi selecionada. |
| minValues | number | Não | Número mínimo de opções que a pessoa deve escolher. O padrão é 1. |
| maxValues | number | Não | Número máximo de opções que podem ser escolhidas. O padrão é 1. |
| payloadTtl | number | Não | Tempo em segundos até o payload salvo expirar. O padrão é 600. Só se aplica quando o encoding é store. |
| encoding | store ou inline | Não | Estratégia de armazenamento para os params. store (padrão) persiste o payload completo e embute uma referência opaca no customId. inline codifica o campo params de SelectRenderConfig diretamente no customId. Use inline apenas para dados primitivos pequenos — o limite de 100 caracteres do Discord para custom IDs se aplica. |
Codificação inline de parâmetros
Por padrão, @DynamicStringSelect persiste o payload completo em um store com TTL e embute apenas uma referência opaca no custom ID. Para dados pequenos e primitivos — como uma tag de origem, um flag de modo ou um único ID de registro — você pode usar encoding: 'inline'. O framework codifica o campo params retornado por render() diretamente na string do custom ID, eliminando qualquer dependência do store de payload e do TTL. Os dados vivem na mensagem do Discord enquanto essa mensagem existir.
O limite de 100 caracteres do Discord para custom IDs se aplica a selects
com codificação inline. O framework lança
DynamicSelectInlinePayloadTooLargeError no momento da renderização se o
string codificado ultrapassar esse limite. Mantenha os params inline pequenos:
um ou dois valores curtos. Use o encoding store para qualquer coisa maior.
import { DynamicStringSelect } from '@spraxium/components';
import type { SelectRenderConfig } from '@spraxium/components';
interface Topic {
slug: string;
name: string;
}
@DynamicStringSelect({
baseId: 'topic-filter',
placeholder: 'Filtrar por tópico',
encoding: 'inline',
})
export class FilterTopicSelect {
static render(data: { topics: Topic[]; audience: string }): SelectRenderConfig {
return {
options: data.topics.map((t) => ({ label: t.name, value: t.slug })),
params: { audience: data.audience }, // viaja dentro do customId
};
}
}Note que @SelectParams() substitui @SelectPayload() para selects com codificação inline. Usar @SelectPayload() ou @PayloadRef() em um handler inline causa um erro no bootstrap. O tipo de params é inferido do campo params retornado por render(), então o TypeScript conhece o formato sem nenhum casting manual.
Selects com codificação inline são renderizados com SelectService.build, não com buildDynamic. O serviço detecta o flag encoding: 'inline' automaticamente e roteia para o caminho inline.
Registro no módulo
Só a classe do handler precisa ser registrada no array handlers de um módulo de feature. Isso vale da mesma forma para os cinco tipos de select. As classes de componente decoradas com @StringSelect, @UserSelect e semelhantes existem apenas como metadado e nunca entram em módulo nenhum. O dispatcher lê tudo pela referência de classe armazenada no decorator do handler.
import { Module } from '@spraxium/common';
import { LanguageSelectHandler } from './components/language-select.handler';
import { TargetUserSelectHandler } from './components/user-select.handler';
@Module({
handlers: [LanguageSelectHandler, TargetUserSelectHandler],
})
export class SettingsModule {}