Por que usar validação em modais
A validação nativa de modais do Discord se limita às restrições minLength, maxLength e required em inputs de texto. Em modais do mundo real, é comum precisar de validações mais ricas: contagem mínima de palavras para relatórios, verificação de formato de e-mail em formulários de contato, validação de URL para links, correspondência de padrão para códigos ou regras de negócio totalmente personalizadas. A validação de modal do Spraxium roda no servidor depois da submissão, dando controle total sobre o que conta como entrada válida.
O sistema de validação usa uma abordagem declarativa com regras @ModalValidate aplicadas a campos individuais. As regras são executadas em sequência e interrompem na primeira falha. Quando a validação falha, o sistema pode colocar em cache os valores enviados pelo usuário para que sejam preenchidos novamente quando o modal for reaberto, evitando que a pessoa precise digitar tudo de novo. Um embed de erro personalizado é enviado como resposta efêmera, explicando o que deu errado.
Essa combinação de validação, cache e apresentação de erro cria uma experiência fluida em que submissões inválidas são barradas imediatamente e os usuários conseguem corrigir a entrada sem perder o progresso.
Aplicando regras de validação
O decorator @ModalValidate aceita um array de regras de validação e fica em uma propriedade de campo ao lado do decorator de tipo do campo. Cada regra é uma chamada de função que retorna um objeto de regra. As regras são executadas na ordem do array e param na primeira falha.
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;
}Regras de validação nativas
O Spraxium traz cinco regras de validação nativas que cobrem cenários comuns. Cada regra é uma função factory que retorna um objeto ModalValidationRule. O método validate da regra recebe o valor do campo como string e retorna true se ele for válido, ou uma string com a mensagem de erro se for inválido.
| Regra | Uso | Falha quando |
|---|---|---|
| minWords | minWords(count) | O valor do campo contém menos de count palavras separadas por espaço em branco. |
| matchesPattern | matchesPattern(regex, message?) | O valor do campo não corresponde à expressão regular fornecida. |
| emailFormat | emailFormat() | O valor do campo não está em um formato de endereço de e-mail válido. |
| urlFormat | urlFormat() | O valor do campo não é uma URL válida iniciada por http:// ou https://. |
| isNumeric | isNumeric() | O valor do campo não pode ser interpretado como um número finito. |
Regras de validação personalizadas
Crie regras personalizadas fornecendo um objeto com um método validate. Esse método recebe o valor do campo e retorna true para entrada válida ou uma string com a mensagem de erro para entrada inválida. Regras personalizadas se integram sem atrito às regras nativas dentro do array @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;
}Personalizando erros de validação
O decorator de classe @ModalValidationConfig personaliza o embed de erro enviado quando a validação falha. Por padrão, o sistema envia um embed efêmero simples com a mensagem de erro. Você pode sobrescrever a cor do embed, o título, o template de descrição, o rodapé e se a resposta 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 da validação
O fluxo completo de validação envolve várias etapas coordenadas entre o dispatcher, o validator runner, o cache de campos e o componente responsável pela resposta de erro. Entender esse ciclo de vida ajuda você a projetar modais que oferecem uma experiência suave para o usuário.
Submissão recebida
O ModalDispatcher recebe o ModalSubmitInteraction do Discord. Ele resolve o customId do modal para o handler registrado e verifica se a classe do componente de modal tem decorators @ModalValidate em algum campo. Se não houver regras de validação, ele pula direto para a resolução dos campos e a chamada do handler.
Execução das regras
O ModalValidatorRunner percorre cada campo que possui regras @ModalValidate. Para cada campo, ele extrai o valor enviado e executa as regras na ordem do array. Na primeira regra que retorna uma mensagem de erro, a execução é interrompida imediatamente. O runner não verifica os campos nem as regras restantes.
Cache em caso de falha
Se o modal tiver @ModalCache e a validação falhar, o runner armazena todos os valores enviados no ModalFieldCache. A chave de cache é customId:userId, e os valores em cache formam um Record<string, string> que mapeia IDs de campo para os respectivos valores enviados. A entrada de cache recebe o TTL especificado em @ModalCache.
Resposta de erro
O dispatcher constrói um embed usando as configurações de @ModalValidationConfig, ou os padrões, e a mensagem de erro retornada pela regra que falhou. Ele envia isso como uma resposta efêmera para a interação de submissão do modal. O handler não é invocado.
Modal reaberto com cache
Quando o usuário aciona o modal novamente, por exemplo clicando no mesmo botão, a chamada ModalService.buildFor detecta valores em cache para essa combinação de usuário e modal. Ela preenche cada campo com o valor em cache, então o usuário vê a entrada anterior. Depois disso, basta corrigir o campo inválido e reenviar.
Submissão bem-sucedida
Quando todas as regras de validação passam, o runner retorna sucesso. Se o modal tiver @ModalCache, o dispatcher chama ModalService.clearCacheFor para remover entradas antigas desse usuário e modal. Em seguida, o dispatcher resolve os decorators @Field e invoca o handler normalmente.
Combinando validação com modais dinâmicos
A validação funciona sem atrito com campos dinâmicos e condicionais. O ModalValidatorRunner só valida campos que estão presentes no schema resolvido do modal. Se um campo foi excluído por @ModalWhen ou não foi gerado por @ModalDynamicFields, as regras de validação dele não são executadas. Isso significa que você pode aplicar @ModalValidate com segurança a campos condicionais sem se preocupar com validação quando eles estiverem 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;
}