Spraxium logoSpraxium

Handlers e Injeção de Campos

Processe submissões de modal com @ModalHandler e injete valores de campo diretamente nos parâmetros de handle(). Aprenda @ModalField, os dez decorators tipados de campo e @Ctx.

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.

src/modules/feedback/handlers/feedback.handler.ts
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.

src/modules/feedback/handlers/feedback.handler.ts
@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.

src/modules/profile/handlers/profile.handler.ts
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.

DecoratorTipo de campo do componenteTipo TypeScript resolvido
@ModalTextField(fieldId)@ModalInputstring
@ModalStringSelectField(fieldId)@ModalSelectstring | null (único) ou string[] (múltiplo)
@ModalUserSelectField(fieldId)@ModalUserSelectUser | null (único) ou User[] (múltiplo)
@ModalRoleSelectField(fieldId)@ModalRoleSelectRole | null (único) ou Role[] (múltiplo)
@ModalMentionableSelectField(fieldId)@ModalMentionableSelectUser | Role | null
@ModalChannelSelectField(fieldId)@ModalChannelSelectGuildBasedChannel | null (único) ou GuildBasedChannel[] (múltiplo)
@ModalRadioGroupField(fieldId)@ModalRadioGroupstring | null
@ModalCheckboxGroupField(fieldId)@ModalCheckboxGroupstring[]
@ModalCheckboxField(fieldId)@ModalCheckboxboolean
@ModalFileUploadField(fieldId)@ModalFileUploadAttachment[]

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.

src/modules/survey/handlers/survey.handler.ts
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.

src/modules/ticket/handlers/ticket-submit.handler.ts
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.

src/modules/feedback/feedback.module.ts
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.