A classe de handler
Uma classe de handler é uma classe independente decorada com @ModalHandler que contém a lógica de submissão de um modal. O decorator recebe a classe do componente de modal como argumento, criando um vínculo de metadados entre o schema do componente e a lógica de submissão. A classe do handler é instanciada pelo container de DI durante o carregamento do módulo, então os parâmetros do construtor são resolvidos automaticamente a partir dos providers do módulo.
O handler precisa expor um método handle. Quando chega uma submissão de modal cujo customId corresponde ao id do componente, o ModalDispatcher chama esse método depois que o pipeline de guards é aprovado. Os parâmetros do método são preenchidos a partir de metadados: @Ctx() injeta o ModalSubmitInteraction bruto, e os decorators de campo injetam o valor resolvido para cada campo nomeado.
import { Ctx } from '@spraxium/common';
import { ModalHandler, ModalTextField, type ModalContext } from '@spraxium/components';
import { FeedbackModal } from '../components/feedback.modal';
@ModalHandler(FeedbackModal)
export class FeedbackHandler {
async handle(
@ModalTextField('subject') subject: string,
@ModalTextField('message') message: string,
@Ctx() ctx: ModalContext,
): Promise<void> {
await ctx.reply({
content: `Thanks for your feedback on *${subject}*!`,
flags: 'Ephemeral',
});
}
}ModalContext é exportado por @spraxium/components e é um alias direto de ModalSubmitInteraction. Use o que fizer mais sentido para o estilo de importação do seu time; ambos são intercambiáveis.
O decorator @ModalHandler
@ModalHandler aceita a classe do componente de modal como único argumento. Não existe configuração de roteamento; cada handler pertence exatamente a um componente, identificado pela string id do componente.
@ModalHandler(FeedbackModal)
export class FeedbackHandler {
async handle(...): Promise<void> { ... }
}O Spraxium lê o id do decorator @ModalComponent em FeedbackModal e registra esse handler sob esse ID. Quando o Discord envia uma interação MODAL_SUBMIT, o dispatcher compara o customId com o ID registrado e chama handle.
Injeção de parâmetros com @Ctx e decorators de campo
Os parâmetros do método são resolvidos pelo dispatcher no momento da chamada. Duas categorias de decorators controlam o que cada posição recebe.
@Ctx() injeta o ModalSubmitInteraction bruto, com alias ModalContext. Ele pode aparecer em qualquer posição e é o ponto de entrada para toda a superfície da API do Discord disponível na interação: reply, deferReply, followUp e o manager bruto de fields.
Decorators de campo injetam o valor enviado para um campo específico, identificado pelo nome da propriedade definida na classe do componente de modal. O @ModalField(fieldId) genérico funciona para qualquer tipo de campo. As variantes tipadas, documentadas na próxima seção, deixam explícita a intenção do tipo de campo e devem ser preferidas quando esse tipo já é conhecido em tempo de compilação.
Na prática, isso significa que a maioria dos handlers só precisa de duas coisas: os valores resolvidos dos campos e o objeto de interação para responder. Depois que esses valores são injetados, o corpo do método pode continuar focado em persistência, chamadas de service e fluxo de resposta.
import { Ctx } from '@spraxium/common';
import {
ModalCheckboxField,
ModalCheckboxGroupField,
ModalHandler,
ModalRadioGroupField,
ModalStringSelectField,
type ModalContext,
} from '@spraxium/components';
import { ProfileModal } from '../components/profile.modal';
@ModalHandler(ProfileModal)
export class ProfileHandler {
async handle(
@Ctx() ctx: ModalContext,
@ModalStringSelectField('role') role: string,
@ModalRadioGroupField('timezone') timezone: string | null,
@ModalCheckboxGroupField('notifications') notifications: string[],
@ModalCheckboxField('acceptedRules') acceptedRules: boolean,
): Promise<void> {
if (!acceptedRules) {
await ctx.reply({ content: '❌ You must accept the rules.', flags: 'Ephemeral' });
return;
}
await ctx.reply({
content: [
'✅ Profile saved!',
`**Role:** ${role}`,
`**Timezone:** ${timezone ?? 'Not set'}`,
`**Notifications:** ${notifications.join(', ') || 'None'}`,
].join('\n'),
flags: 'Ephemeral',
});
}
}Decorators tipados de campo
São fornecidos dez decorators tipados de parâmetro, cada um correspondente a um tipo de campo específico na classe do componente de modal. Usá-los deixa explícito no código do handler qual é o tipo pretendido do campo, sem custo adicional em runtime; todos os decorators tipados armazenam o mesmo metadado { index, fieldId } que o @ModalField genérico.
| Decorator | Tipo de campo do componente | Tipo TypeScript resolvido |
|---|---|---|
| @ModalTextField(fieldId) | @ModalInput | string |
| @ModalStringSelectField(fieldId) | @ModalSelect | string | null (único) ou string[] (múltiplo) |
| @ModalUserSelectField(fieldId) | @ModalUserSelect | User | null (único) ou User[] (múltiplo) |
| @ModalRoleSelectField(fieldId) | @ModalRoleSelect | Role | null (único) ou Role[] (múltiplo) |
| @ModalMentionableSelectField(fieldId) | @ModalMentionableSelect | User | Role | null |
| @ModalChannelSelectField(fieldId) | @ModalChannelSelect | GuildBasedChannel | null (único) ou GuildBasedChannel[] (múltiplo) |
| @ModalRadioGroupField(fieldId) | @ModalRadioGroup | string | null |
| @ModalCheckboxGroupField(fieldId) | @ModalCheckboxGroup | string[] |
| @ModalCheckboxField(fieldId) | @ModalCheckbox | boolean |
| @ModalFileUploadField(fieldId) | @ModalFileUpload | Attachment[] |
Todos os decorators tipados são importados de @spraxium/components junto com ModalHandler.
Escolha o decorator tipado sempre que o tipo do campo já estiver claro a partir do schema do componente. Isso comunica a intenção imediatamente durante a revisão de código e poupa a próxima pessoa de mapear mentalmente uma chamada genérica de @ModalField de volta para a definição do componente.
Selects de valor único vs. múltiplo
Para campos de select de string, usuário, cargo e canal, o valor injetado ser um item único ou um array depende da configuração maxValues do campo na classe do componente. Quando maxValues é maior que 1, o dispatcher retorna um array; caso contrário, retorna um único valor ou null.
Acessando campos brutos via @Ctx
Para campos que não têm um decorator de campo correspondente, como campos radio_group acessados fora da injeção ou campos lidos condicionalmente, use @Ctx() e chame diretamente o método apropriado em ctx.fields.
import { Ctx } from '@spraxium/common';
import { ModalHandler, ModalTextField, type ModalContext } from '@spraxium/components';
import { SurveyModal } from '../components/survey.modal';
@ModalHandler(SurveyModal)
export class SurveyHandler {
async handle(
@ModalTextField('feedback') feedback: string,
@Ctx() ctx: ModalContext,
): Promise<void> {
// Read radio group values directly from the interaction
const rating = ctx.fields.getRadioGroup('rating') ?? 'N/A';
const tags = ctx.fields.getCheckboxGroup('tags');
await ctx.reply({
content: `Rating: ${rating}\nTags: ${tags.join(', ')}\nFeedback: ${feedback}`,
flags: 'Ephemeral',
});
}
}Combinando services de DI com injeção de campos
Como classes de handler são instanciadas pelo container de DI, qualquer provider do módulo pode ser injetado pelo construtor. Esse é o padrão normal para acessar bancos de dados, APIs externas ou services da aplicação de dentro de um handler.
import { Ctx } from '@spraxium/common';
import { ModalHandler, ModalTextField, type ModalContext } from '@spraxium/components';
import { Injectable } from '@spraxium/core';
import { TicketModal } from '../components/ticket.modal';
import { TicketService } from '../ticket.service';
@Injectable()
@ModalHandler(TicketModal)
export class TicketSubmitHandler {
constructor(private readonly tickets: TicketService) {}
async handle(
@ModalTextField('subject') subject: string,
@ModalTextField('description') description: string,
@Ctx() ctx: ModalContext,
): Promise<void> {
const ticket = await this.tickets.create({
userId: ctx.user.id,
subject,
description,
});
await ctx.reply({
content: `✅ Ticket **#${ticket.id}** created!`,
flags: 'Ephemeral',
});
}
}Registro no módulo
Registre as classes de handler no array handlers de um módulo de feature. Classes de componente de modal decoradas com @ModalComponent existem apenas como metadados e nunca são registradas; o Spraxium as resolve de forma transitiva a partir do vínculo @ModalHandler do handler.
import { Module } from '@spraxium/core';
import { FeedbackHandler } from './handlers/feedback.handler';
@Module({
handlers: [FeedbackHandler],
})
export class FeedbackModule {}Registre o handler, não o componente
Apenas classes de handler decoradas com @ModalHandler entram no array handlers. Adicionar a classe do componente de modal ao módulo não terá efeito e pode gerar uma saída de log confusa durante o boot.