Por qué validar modales
La validación integrada de Discord para modales está limitada a restricciones minLength, maxLength y required en entradas de texto. Los modales de producción suelen necesitar validaciones más ricas: mínimo de palabras para reportes, comprobación de formato de correo para formularios de contacto, validación de URL para enlaces, coincidencia de patrones para códigos o reglas de negocio totalmente personalizadas. La validación de modales en Spraxium se ejecuta del lado servidor tras el envío, dándote control total sobre qué se considera entrada válida.
El sistema de validación usa un enfoque declarativo con reglas @ModalValidate aplicadas a campos individuales. Las reglas se ejecutan secuencialmente y hacen short-circuit en el primer fallo. Cuando la validación falla, el sistema puede almacenar en caché los valores enviados por la persona usuaria para precargarlos cuando el modal se abra de nuevo, evitando que tenga que volver a escribir todo. Se envía un embed de error personalizado como respuesta efímera explicando qué salió mal.
Esta combinación de validación, caché y presentación de errores crea una experiencia fluida donde los envíos inválidos se detectan al instante y las personas pueden corregir su entrada sin perder progreso.
Aplicar reglas de validación
El decorator @ModalValidate acepta un arreglo de reglas de validación y se coloca en una propiedad de campo junto con el decorator del tipo de campo. Cada regla es una llamada de función que devuelve un objeto de regla. Las reglas se ejecutan en el orden del arreglo y se detienen en el primer fallo.
import {
ModalComponent,
ModalInput,
ModalValidate,
minWords,
emailFormat,
urlFormat,
} from '@spraxium/components';
@ModalComponent({ id: 'contact-modal', title: 'Contact Us' })
export class ContactModal {
@ModalInput({
label: 'Email',
style: 'short',
required: true,
placeholder: '[email protected]',
})
@ModalValidate([emailFormat()])
email!: string;
@ModalInput({
label: 'Website',
style: 'short',
required: false,
placeholder: 'https://your-website.com',
})
@ModalValidate([urlFormat()])
website!: string;
@ModalInput({
label: 'Message',
style: 'paragraph',
required: true,
minLength: 20,
})
@ModalValidate([minWords(5)])
message!: string;
}Reglas de validación integradas
Spraxium incluye cinco reglas de validación integradas que cubren escenarios comunes. Cada regla es una función factory que devuelve un objeto ModalValidationRule. El método validate de la regla recibe el valor del campo como string y devuelve true si es válido, o un string con mensaje de error si es inválido.
| Regla | Uso | Falla cuando |
|---|---|---|
| minWords | minWords(count) | El valor del campo contiene menos de count palabras separadas por espacios. |
| matchesPattern | matchesPattern(regex, message?) | El valor del campo no coincide con la expresión regular proporcionada. |
| emailFormat | emailFormat() | El valor del campo no tiene un formato de correo válido. |
| urlFormat | urlFormat() | El valor del campo no es una URL válida que comience con http:// o https://. |
| isNumeric | isNumeric() | El valor del campo no puede parsearse como un número finito. |
Reglas de validación personalizadas
Crea reglas personalizadas proporcionando un objeto con método validate. El método recibe el valor del campo y devuelve true para entrada válida o un string de mensaje de error para entrada inválida. Las reglas personalizadas se integran de forma natural con las reglas integradas dentro del arreglo @ModalValidate.
import {
ModalComponent,
ModalInput,
ModalValidate,
matchesPattern,
} from '@spraxium/components';
import type { ModalValidationRule } from '@spraxium/components';
const noSpaces: ModalValidationRule = {
validate(value: string) {
return value.includes(' ') ? 'Invite codes cannot contain spaces.' : true;
},
};
const isUppercase: ModalValidationRule = {
validate(value: string) {
return value === value.toUpperCase()
? true
: 'Invite code must be all uppercase.';
},
};
@ModalComponent({ id: 'invite-modal', title: 'Enter Invite Code' })
export class InviteCodeModal {
@ModalInput({
label: 'Invite Code',
style: 'short',
required: true,
placeholder: 'XXXX-XXXX-XXXX',
})
@ModalValidate([
noSpaces,
isUppercase,
matchesPattern(
/^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/,
'Invalid invite code format. Expected: XXXX-XXXX-XXXX',
),
])
code!: string;
}Personalizar errores de validación
El decorator a nivel de clase @ModalValidationConfig personaliza el embed de error enviado cuando la validación falla. Por defecto, el sistema envía un embed efímero simple con el mensaje de error. Puedes sobrescribir color, título, plantilla de descripción, footer y si la respuesta debe ser efímera.
import {
ModalComponent,
ModalInput,
ModalValidate,
ModalValidationConfig,
ModalCache,
minWords,
} from '@spraxium/components';
@ModalComponent({ id: 'strict-modal', title: 'Strict Input' })
@ModalCache({ ttl: 180 })
@ModalValidationConfig({
ephemeral: true,
embed: (errors) => ({
color: 0xff4444,
title: 'Validation Failed',
footer: { text: 'Please fix the highlighted field and try again.' },
}),
})
export class StrictModal {
@ModalInput({
label: 'Essay',
style: 'paragraph',
required: true,
})
@ModalValidate([minWords(50)])
essay!: string;
}Ciclo de vida de validación
El flujo completo de validación incluye varios pasos coordinados entre dispatcher, validator runner, caché de campos y respuesta de error. Entender este ciclo de vida te ayuda a diseñar modales con una experiencia de usuario fluida.
Envío recibido
El ModalDispatcher recibe el ModalSubmitInteraction desde Discord. Resuelve el customId del modal hacia el handler registrado y comprueba si la clase del componente modal tiene decorators @ModalValidate en algún campo. Si no existen reglas de validación, salta directamente a resolución de campos e invocación del handler.
Ejecución de reglas
El ModalValidatorRunner itera por cada campo que tiene reglas @ModalValidate. Para cada campo, extrae el valor enviado y ejecuta las reglas en el orden del arreglo. En la primera regla que devuelve un mensaje de error, la ejecución se detiene inmediatamente. El runner no revisa los campos restantes ni las reglas restantes.
Guardar en caché al fallar
Si el modal tiene @ModalCache y la validación falla, el runner almacena todos los valores de campo enviados en ModalFieldCache. La clave de caché es customId:userId, y los valores en caché son un mapeo Record<string, string> de IDs de campo a valores enviados. La entrada de caché usa el TTL especificado en @ModalCache.
Respuesta de error
El dispatcher construye un embed usando la configuración de @ModalValidationConfig (o valores por defecto) y el mensaje de error de la regla fallida. Lo envía como respuesta efímera a la interacción de envío del modal. El handler no se invoca.
Modal reabierto con caché
Cuando la persona usuaria dispara de nuevo el modal (por ejemplo, haciendo clic en el mismo botón), la llamada ModalService.buildFor detecta valores en caché para esta combinación de usuario y modal. Precarga cada campo con su valor en caché, así que la persona ve su entrada anterior. Luego puede corregir el campo inválido y reenviar.
Envío exitoso
Cuando todas las reglas de validación pasan, el runner devuelve éxito. Si el modal tiene @ModalCache, el dispatcher llama a ModalService.clearCacheFor para eliminar cualquier entrada de caché obsoleta de esta persona y modal. Después el dispatcher resuelve decorators @Field e invoca el handler normalmente.
Combinar validación con modales dinámicos
La validación funciona de forma natural con campos dinámicos y condicionales. El ModalValidatorRunner solo valida campos que estén presentes en el esquema de modal resuelto. Si un campo fue excluido por @ModalWhen o no fue generado por @ModalDynamicFields, sus reglas de validación no se ejecutan. Esto significa que puedes aplicar @ModalValidate a campos condicionales sin preocuparte por validarlos cuando estén ausentes.
import {
ModalComponent,
ModalDynamic,
ModalInput,
ModalWhen,
ModalValidate,
ModalCache,
emailFormat,
urlFormat,
minWords,
} from '@spraxium/components';
interface FormData {
includeContact: boolean;
}
@ModalComponent({ id: 'dynamic-form', title: 'Dynamic Form' })
@ModalDynamic<FormData>()
@ModalCache({ ttl: 300 })
export class DynamicValidatedModal {
@ModalInput({
label: 'Full Name',
style: 'short',
required: true,
})
name!: string;
@ModalWhen<FormData>((data) => data?.includeContact === true)
@ModalInput({
label: 'Contact Email',
style: 'short',
required: true,
})
@ModalValidate([emailFormat()])
email!: string;
@ModalWhen<FormData>((data) => data?.includeContact === true)
@ModalInput({
label: 'Website',
style: 'short',
required: false,
})
@ModalValidate([urlFormat()])
website!: string;
@ModalInput({
label: 'Message',
style: 'paragraph',
required: true,
})
@ModalValidate([minWords(10)])
message!: string;
}