Qué son los modales en Spraxium
Los modales son formularios superpuestos que Discord muestra a los usuarios cuando se activan desde un clic en botón, una interacción de menú select o un comando de aplicación. Recopilan entrada estructurada mediante campos como entradas de texto, menús select, grupos de radio y casillas de verificación. En Spraxium, los modales siguen el mismo patrón declarativo de decorator-handler que botones y selects, pero con un sistema de campos más rico, validación integrada, campos dinámicos y caché por usuario que justifican una categoría de documentación dedicada.
El sistema de modales consta de dos piezas principales: una clase @ModalComponent que declara la estructura del modal (título, campos y opciones), y una clase @ModalHandler que procesa los valores enviados. El ModalService renderiza el componente a un objeto modal listo para Discord, y el ModalDispatcher enruta los envíos al handler correcto después de ejecutar las reglas de validación.
A diferencia de botones y selects, donde el componente es un decorator ligero sobre una clase, los modales usan decorators a nivel de propiedad para definir cada campo. Esto te da control granular sobre tipos de campo, texto placeholder, restricciones de validación, inclusión condicional y generación dinámica, todo expresado mediante decorators de TypeScript.
Cuándo un modal es la opción correcta
Usa un modal cuando la persona necesite escribir o revisar varias piezas de información antes de enviar. Formularios de feedback, creación de tickets, configuración de perfil, flujos de aplicación y pantallas de entrada para administración son los casos típicos.
Si la persona solo necesita elegir una opción o confirmar con un clic, normalmente un botón o un menú select es más simple. Un modal resulta más útil cuando la interacción requiere entrada de texto estructurada y un límite claro de envío.
Definir un componente modal
Usa @ModalComponent para marcar una clase como definición de modal. El decorator acepta un objeto con id (el customId que Discord devuelve al enviar) y title (mostrado en la parte superior del modal). Los decorators de campo en las propiedades de la clase definen los campos de entrada del modal.
import { ModalComponent, ModalInput } from '@spraxium/components';
@ModalComponent({ id: 'feedback-modal', title: 'Send Feedback' })
export class FeedbackModal {
@ModalInput({
label: 'Subject',
placeholder: 'Brief summary of your feedback',
required: true,
minLength: 3,
maxLength: 100,
style: 'short',
})
subject!: string;
@ModalInput({
label: 'Details',
placeholder: 'Describe your feedback in detail...',
required: true,
minLength: 10,
maxLength: 4000,
style: 'paragraph',
})
details!: string;
}Crear un handler de modal
El decorator @ModalHandler vincula una clase handler a un componente modal específico. El método handle del handler recibe el envío del modal y cualquier parámetro inyectado. Usa decorators de campo etiquetados para extraer valores individuales por nombre de propiedad. El genérico @ModalField(fieldId) funciona para cualquier tipo de campo; las nueve variantes tipadas (@ModalTextField, @ModalStringSelectField, @ModalRadioGroupField, etc.) hacen explícita la intención y se recomiendan cuando el tipo de campo se conoce en tiempo de compilación.
import { Ctx } from '@spraxium/common';
import { ModalHandler, ModalTextField, type ModalContext } from '@spraxium/components';
import { FeedbackModal } from './feedback.modal';
@ModalHandler(FeedbackModal)
export class FeedbackModalHandler {
async handle(
@ModalTextField('subject') subject: string,
@ModalTextField('details') details: string,
@Ctx() ctx: ModalContext,
): Promise<void> {
await ctx.reply({
content: `Thank you for your feedback on '${subject}'.`,
flags: 'Ephemeral',
});
}
}Para la documentación completa de todos los decorators de campo, integración con guards y la API completa de inyección, consulta Handlers and Field Injection.
Mostrar un modal
Usa ModalService para construir un modal listo para Discord y mostrárselo a la persona usuaria. El método build recibe la clase del componente modal y devuelve una respuesta de interacción modal. Normalmente lo llamas desde un handler de botón, un handler de select o un comando slash que haya recibido una interacción con método showModal.
import { ButtonHandler, Ctx } from '@spraxium/components';
import { ModalService } from '@spraxium/components';
import type { ButtonInteraction } from 'discord.js';
import { OpenFeedbackButton } from './open-feedback.button';
import { FeedbackModal } from './feedback.modal';
@ButtonHandler(OpenFeedbackButton)
export class OpenFeedbackHandler {
constructor(private readonly modals: ModalService) {}
async handle(@Ctx() interaction: ButtonInteraction): Promise<void> {
const modal = this.modals.build(FeedbackModal);
await interaction.showModal(modal);
}
}Inyección de campos en handlers
La clase handler con @ModalHandler recibe los valores enviados mediante decorators de parámetro. Usa decorators tipados como @ModalTextField, @ModalStringSelectField y @ModalCheckboxField para inyectar cada valor directamente en el método handle(), sin necesidad de parsear manualmente el objeto de interacción.
@ModalHandler(FeedbackModal)
export class FeedbackHandler {
async handle(
@ModalTextField('subject') subject: string,
@ModalTextField('message') message: string,
@Ctx() ctx: ModalContext,
): Promise<void> {
await ctx.reply({ content: `Received: ${subject}`, flags: 'Ephemeral' });
}
}El decorator antiguo @Field está deprecado en 0.2.0 a favor de las variantes tipadas. Para la lista completa de decorators de campo tipados y sus tipos de TypeScript resueltos, consulta Handlers and Field Injection.
Referencia de API de ModalService
| Método | Firma | Propósito |
|---|---|---|
| build | build(modal: Type, data?: unknown) | Construye el modal a partir de la clase componente. Pasa data para suministrar contexto de campos dinámicos. |
| buildFor | buildFor(modal: Type, interaction: Interaction, data?: unknown) | Construye con contexto de usuario. Precarga valores de campo en caché si un envío previo falló validación. |
| storeCache | storeCache(modal: Type, interaction: ModalSubmitInteraction, values: Record<string, string>) | Guarda manualmente valores de campo en caché. Se usa internamente en el flujo de validación. |
| clearCacheFor | clearCacheFor(modal: Type, interaction: ModalSubmitInteraction) | Elimina valores en caché para una combinación específica de usuario y modal. |
| getCacheFor | getCacheFor(modal: Type, interaction: Interaction) | Recupera valores de campo en caché. Devuelve null si no existe caché. |
La pareja más común es build para el primer render y buildFor para flujos donde quieres precarga por usuario tras una falla de validación. En otras palabras, build es el camino simple; buildFor es el camino ergonómico para formularios con muchos reintentos.
Registro en módulo
Registra tus handlers de modal en el arreglo handlers de un módulo de funcionalidad. Las clases de componente modal decoradas con @ModalComponent y las clases de botón decoradas con @Button solo contienen metadatos y nunca se registran en módulos. El sistema escanea todas las instancias handler registradas en el ciclo de arranque para encontrar metadatos de @ModalHandler y lee los metadatos del componente modal referenciado por clase.
import { Module } from '@spraxium/core';
import { FeedbackModalHandler } from './feedback.handler';
import { OpenFeedbackHandler } from './open-feedback.handler';
@Module({
handlers: [FeedbackModalHandler, OpenFeedbackHandler],
})
export class FeedbackModule {}Flujo de despacho
Cuando una persona envía un modal, la interacción pasa por varias etapas antes de llegar a tu handler. Entender este flujo te ayuda a razonar dónde ocurren validación, caché y resolución de campos.
Interacción recibida
El cliente de Discord.js emite un evento interactionCreate con un ModalSubmitInteraction. El ComponentDispatcher comprueba si el customId coincide con un handler de modal registrado, separando el custom ID en el separador de tilde para extraer el ID base y un ID de contexto opcional.
Resolución de contexto
Si el customId contiene un ID de contexto (después de la tilde), el dispatcher recupera el SpraxiumContext desde el adapter de almacenamiento. Si el contexto está expirado o la persona usuaria está restringida, el dispatcher envía una respuesta efímera y detiene el procesamiento. Si no se necesita contexto, este paso se omite.
Ejecución de validación
Si el componente modal tiene reglas @ModalValidate, el ModalValidatorRunner las ejecuta secuencialmente contra los valores de campo enviados. En el primer fallo, el runner guarda en caché todos los valores enviados (si existe @ModalCache), envía el embed de error de validación y detiene el proceso. Consulta la página de validación para más detalles.
Resolución de campos e invocación del handler
El dispatcher resuelve decorators de parámetro de campo (@ModalTextField, @ModalStringSelectField, etc.) extrayendo valores del envío. El decorator @Ctx recibe el ModalSubmitInteraction crudo. Si existe un decorator @FlowContext, se inyecta el contexto resuelto. El método handle del handler se llama con todos los parámetros resueltos.
Este flujo por capas es lo que mantiene pequeños los handlers de modal. La validación, la carga de caché, la carga de contexto y la extracción de campos ocurren antes de que se ejecute tu lógica de negocio, así que el handler puede centrarse en el envío aceptado en lugar de reconstruir el estado del formulario manualmente.