Spraxium logoSpraxium

Menús de selección

Construye los cinco tipos de select menu de Discord con opciones estáticas o dinámicas. Aprende a trabajar con selects de string, user, role, mentionable y channel usando handlers, generación dinámica de opciones y renderizado con SelectService.

Tipos de menú de selección

Discord ofrece cinco variantes de select menu, y Spraxium da soporte a todas con decorators dedicados. String selects permiten definir opciones personalizadas con etiqueta y valor. Los selects de user, role, mentionable y channel usan los selectores nativos de Discord, que se rellenan automáticamente con datos del servidor. Los cinco tipos comparten el mismo patrón de handler y el mismo sistema de inyección de parámetros.

Cada tipo se define como una clase decorada, se vincula a un handler por medio de un decorator específico y se renderiza vía SelectService. La diferencia principal entre ellos está en el decorator usado y en el tipo de dato que el usuario selecciona. String selects exigen definición explícita de opciones. Los otros cuatro tipos dejan que Discord rellene la lista automáticamente.

TipoDecoradorHandlerOrigen de opciones
String@StringSelect@StringSelectHandler

Definida por el desarrollador vía @SelectOption o dynamicOptions

User@UserSelect@UserSelectHandlerRellenada automáticamente por Discord
Role@RoleSelect@RoleSelectHandlerRellenada automáticamente por Discord
Mentionable@MentionableSelect@MentionableSelectHandlerRellenada automáticamente por Discord, con usuarios y roles
Channel@ChannelSelect@ChannelSelectHandler

Rellenada automáticamente por Discord, con filtro opcional por tipo

String selects con opciones estáticas

String selects son el tipo más flexible, porque la lista de opciones queda totalmente bajo tu control. Define esas opciones con el decorator apilable @SelectOption, aplicado en la clase del componente. Cada opción exige un label, que es el texto mostrado, un value, que es el valor devuelto al handler, y también puede incluir description y emoji. Las opciones se apilan de abajo hacia arriba en el orden de los 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 {}

Referencia de configuración del select

El objeto de configuración pasado a cualquier decorator de select comparte un conjunto común de campos. customId identifica el select en el enrutamiento de interacción, igual que en botones. placeholder, minValues y maxValues controlan comportamiento visual y funcional de la interfaz.

CampoTipoObligatorioFinalidad
customIdstringSíIdentificador único usado en el enrutamiento de interacción. Debe ser único en todo el bot.
placeholderstringNoTexto de apoyo mostrado cuando ninguna opción fue elegida.
minValuesnumberNoNúmero mínimo de opciones que la persona debe seleccionar. El predeterminado es 1.
maxValuesnumberNoNúmero máximo de opciones que pueden seleccionarse. El predeterminado es 1.
disabledbooleanNoCuando está en true, el select se renderiza sin permitir interacción.
dynamicOptionsFunción que retorna SelectOptionConfig[]NoCallback que genera opciones en el momento del renderizado a partir de datos de contexto. Solo vale para string selects.

Opciones dinámicas

Cuando la lista de opciones depende de datos de runtime, usa el callback dynamicOptions en lugar de decorators estáticos @SelectOption. Ese callback recibe el objeto data pasado a SelectService.build y debe devolver un array de opciones. Este patrón es común en selects rellenados a partir de consultas en base de datos, respuestas de API o datos específicos del usuario.

src/components/selects/category-select.component.ts
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 {}

En el renderizado, pasa el objeto de datos como segundo argumento a SelectService.build. El servicio llama dynamicOptions con ese valor y usa el array retornado para rellenar el select menu.

Decorators @SelectOption y dynamicOptions son mutuamente excluyentes. Cuando ambos aparecen en la misma clase, el callback dinámico tiene prioridad, y las opciones estáticas se ignoran por completo.

Entity selects

Selects de user, role, mentionable y channel son más directos, porque el propio Discord rellena las opciones automáticamente. Solo necesitas configurar customId, eventuales restricciones y el handler. En channel selects, existe además el filtro channelTypes para limitar qué tipos de canal pueden aparecer.

import { UserSelect } from '@spraxium/components';

@UserSelect({
  customId: 'target_user',
  placeholder: 'Select a user',
  maxValues: 3,
})
export class TargetUserSelect {}

Inyección de parámetros en el handler

Handlers de select usan el mismo sistema de decorators de parámetro que handlers de botón, con dos decorators extras específicos de esta familia. @Selected() inyecta la interacción completa del menú de selección, dando acceso a usuarios, roles, canales o mentionables ya resueltos. @SelectedValues() inyecta solo el array string[] de interaction.values, que es el patrón más común en string selects.

src/components/selects/role-select.handler.ts
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}`, ephemeral: true });
  }
}

Renderizado con SelectService

SelectService es el servicio inyectable que monta action rows de select menu a partir de clases decoradas. El método build recibe la clase del componente, un argumento opcional de datos para callbacks dynamicOptions y, opcionalmente, un SpraxiumContext para embutir un ID de contexto en el customId del menú. El método es asíncrono porque callbacks dinámicos pueden retornar promises.

src/commands/category.command-handler.ts
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 con fluent builder

En escenarios más programáticos, donde crear una clase sería excesivo, SelectDefBuilder ofrece una API fluente para montar definiciones de select inline. Este builder es útil en flujos dinámicos, selects temporales o interfaces generadas por código, donde el enfoque basado en decorators crearía demasiadas clases descartables.

src/commands/dynamic-select.command.ts
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();

El fluent builder produce la misma action row de discord.js que el enfoque con decorators. La diferencia es que no participa del sistema de dispatch de handlers, porque no existe clase ni metadato para registrar. Usa este camino solo en selects descartables, donde el enrutamiento de handler no sea necesario.

Selects dinámicos

String selects estáticos definen opciones ya en el momento de la declaración. @DynamicStringSelect rompe esa limitación: describes el select una vez y generas la lista de opciones con datos vivos en el momento del renderizado, como resultados de base de datos, respuestas de API o cualquier otro valor de runtime. El sistema de payload guarda ese conjunto de datos junto al select, así el handler recibe todo de vuelta sin consultar la fuente por segunda vez.

Cómo definir un string select dinámico

Decora una clase con @DynamicStringSelect e implementa un método estático render que reciba los datos de payload y devuelva un SelectRenderConfig. Este método puede ser síncrono o asíncrono.

src/modules/topics/components/topic-select.component.ts
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,
      })),
    };
  }
}

Cómo renderizar un string select dinámico

Llama SelectService.buildDynamic con la clase del componente y el payload. Spraxium guarda ese payload y embebe una referencia a él en el customId del select.

src/modules/topics/handlers/topics-command.handler.ts
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] });
  }
}

Cómo tratar la interacción

Usa @DynamicSelectHandler para vincular un handler a una clase @DynamicStringSelect. El decorator @SelectPayload() inyecta el payload almacenado en el renderizado. @SelectedValues() inyecta interaction.values como string[]. Ambos pueden aparecer junto a @Ctx() y @PayloadRef() en cualquier orden.

src/modules/topics/handlers/topic-select.handler.ts
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(', ')}`,
      ephemeral: true,
    });
  }
}

Referencia de configuración del select dinámico

OpciónTipoObligatorioDescripción
baseIdstringSíPrefijo usado en la generación de custom IDs. Debe ser único entre todos los string selects dinámicos de la aplicación.
placeholderstringNoTexto mostrado cuando ninguna opción fue seleccionada.
minValuesnumberNoNúmero mínimo de opciones que la persona debe escoger. El predeterminado es 1.
maxValuesnumberNoNúmero máximo de opciones que pueden escogerse. El predeterminado es 1.
payloadTtlnumberNoTiempo en segundos hasta que expire el payload guardado. El predeterminado es 600. Solo aplica cuando el encoding es store.
encodingstore o inlineNoEstrategia de almacenamiento para los params. store (predeterminado) persiste el payload completo y embebe una referencia opaca en el customId. inline codifica el campo params de SelectRenderConfig directamente en el customId. Usar inline solo para datos primitivos pequeños — aplica el límite de 100 caracteres de Discord para custom IDs.

Codificación inline de parámetros

Por defecto, @DynamicStringSelect persiste el payload completo en un store con TTL y embebe solo una referencia opaca en el custom ID. Para datos pequeños y primitivos — como una etiqueta de origen, un flag de modo, o un solo ID de registro — puedes usar encoding: 'inline'. El framework codifica el campo params devuelto por render() directamente en el custom ID, eliminando cualquier dependencia del store de payload y el TTL. Los datos viven en el mensaje de Discord mientras ese mensaje exista.

El límite de 100 caracteres de Discord para custom IDs aplica a los selects con codificación inline. El framework lanza DynamicSelectInlinePayloadTooLargeError en tiempo de renderizado si el string codificado supera ese límite. Mantén los params inline pequeños: uno o dos valores cortos. Usa el encoding store para cualquier cosa más grande.

import { DynamicStringSelect } from '@spraxium/components';
import type { SelectRenderConfig } from '@spraxium/components';

interface Topic {
  slug: string;
  name: string;
}

@DynamicStringSelect({
  baseId: 'topic-filter',
  placeholder: 'Filtrar por tema',
  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 del customId
    };
  }
}

Nota que @SelectParams() reemplaza @SelectPayload() para selects con codificación inline. Usar @SelectPayload() o @PayloadRef() en un handler inline provoca un error en el bootstrap. El tipo de params se infiere del campo params devuelto por render(), por lo que TypeScript conoce la forma sin ningún casting manual.

Los selects con codificación inline se renderizan con SelectService.build, no con buildDynamic. El servicio detecta el flag encoding: 'inline' automáticamente y enruta hacia el path inline.

Registro en el módulo

Solo la clase del handler necesita registrarse en el array handlers de un módulo de feature. Esto aplica igual para los cinco tipos de select. Las clases de componente decoradas con @StringSelect, @UserSelect y similares existen solo como metadato y nunca entran en ningún módulo. El dispatcher lee todo por la referencia de clase almacenada en el decorator del handler.

src/modules/settings/settings.module.ts
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 {}