Por que usar modais dinâmicos
Modais estáticos funcionam bem quando os campos já são conhecidos em tempo de compilação. Mas muitos cenários reais pedem modais que se adaptem às condições de runtime. Um modal de criação de ticket pode mostrar campos diferentes de acordo com a categoria do ticket. Um modal de configurações pode incluir campos apenas para recursos aos quais o usuário tem acesso. Um wizard em várias etapas pode repassar dados da etapa anterior para o modal, personalizando o conteúdo.
O Spraxium oferece três mecanismos para modais dinâmicos. O decorator @ModalDynamic marca um modal como orientado a dados, o método @ModalDynamicFields gera campos programaticamente no momento do build, e o decorator @ModalWhen inclui ou exclui campos estáticos individuais de forma condicional com base em um predicado. Esses mecanismos se combinam naturalmente, então você pode misturar campos estáticos, condicionais e gerados dinamicamente no mesmo modal.
O ModalSchemaBuilder resolve as três fontes, decorators estáticos, condições e métodos dinâmicos, em uma única lista plana de campos quando ModalService.build é chamado. Isso significa que a estrutura do modal é determinada no momento da renderização, e não na definição da classe.
Preparando um modal para receber dados
O decorator genérico @ModalDynamic marca uma classe de componente de modal como capaz de receber dados externos de um tipo específico. Quando você chama ModalService.build(MyModal, data), esses dados ficam armazenados e disponíveis para métodos @ModalDynamicFields e predicados @ModalWhen. O parâmetro de tipo oferece segurança em tempo de compilação para o formato dos dados.
import { ModalComponent, ModalDynamic, ModalInput } from '@spraxium/components';
interface ConfigData {
featureFlags: string[];
userRole: 'admin' | 'moderator' | 'member';
}
@ModalComponent({ id: 'config-modal', title: 'Server Configuration' })
@ModalDynamic<ConfigData>()
export class ConfigModal {
@ModalInput({
label: 'Server Name',
style: 'short',
required: true,
})
serverName!: string;
}Campos condicionais com @ModalWhen
O decorator @ModalWhen recebe uma função predicado que recebe os dados do modal e retorna um booleano. Se o predicado retornar true, o campo é incluído no modal renderizado. Se retornar false, o campo é omitido por completo, e não apenas escondido. Isso significa que o campo não entra no limite de 5 campos quando é excluído, liberando action rows para outros campos.
O predicado recebe os dados passados para ModalService.build. Se nenhum dado for fornecido, o predicado recebe undefined, então a sua função deve tratar esse caso com elegância.
import {
ModalComponent,
ModalDynamic,
ModalInput,
ModalWhen,
ModalSelect,
ModalChoice,
} from '@spraxium/components';
interface ConfigData {
featureFlags: string[];
userRole: 'admin' | 'moderator' | 'member';
}
@ModalComponent({ id: 'config-modal', title: 'Server Configuration' })
@ModalDynamic<ConfigData>()
export class ConfigModal {
@ModalInput({
label: 'Server Name',
style: 'short',
required: true,
})
serverName!: string;
@ModalWhen<ConfigData>((data) => data?.userRole === 'admin')
@ModalInput({
label: 'Admin Secret Key',
style: 'short',
required: true,
placeholder: 'Enter the admin secret key',
})
adminKey!: string;
@ModalWhen<ConfigData>((data) => data?.featureFlags.includes('moderation'))
@ModalSelect({ label: 'Auto-mod Level', required: true })
@ModalChoice({ label: 'Off', value: 'off' })
@ModalChoice({ label: 'Low', value: 'low' })
@ModalChoice({ label: 'High', value: 'high' })
autoModLevel!: string;
}Geração dinâmica de campos
O decorator de método @ModalDynamicFields marca um método da classe que retorna um array de objetos ModalFieldDef em runtime. Esse método é chamado durante ModalService.build e recebe os dados passados para a chamada de build. Os campos retornados são acrescentados depois de todos os campos estáticos e condicionais no layout do modal.
Use os helpers do namespace Modal.field para construir definições de campo dentro do método dinâmico. Isso deixa o código legível e com segurança de tipos, em comparação com montar objetos de definição crus.
import {
ModalComponent,
ModalDynamic,
ModalDynamicFields,
ModalInput,
} from '@spraxium/components';
import { Modal } from '@spraxium/components';
import type { ModalFieldDef } from '@spraxium/components';
interface SurveyData {
questions: Array<{ id: string; label: string }>;
}
@ModalComponent({ id: 'survey-modal', title: 'Quick Survey' })
@ModalDynamic<SurveyData>()
export class SurveyModal {
@ModalInput({
label: 'Your Name',
style: 'short',
required: true,
})
name!: string;
@ModalDynamicFields()
generateQuestions(data: SurveyData): ModalFieldDef[] {
return data.questions.slice(0, 4).map((q) =>
Modal.field.text({
id: q.id,
label: q.label,
style: 'paragraph',
required: true,
maxLength: 1000,
}),
);
}
}Montando modais com dados
Quando o seu modal usa @ModalDynamic, passe o objeto de dados como segundo argumento para ModalService.build. O schema builder lê os dados, avalia todos os predicados @ModalWhen, chama métodos @ModalDynamicFields, combina o resultado com os campos estáticos e produz o layout final do modal.
import { Ctx, SlashCommandHandler } from '@spraxium/common';
import { ModalService } from '@spraxium/components';
import type { ChatInputCommandInteraction } from 'discord.js';
import { SurveyModal } from '../components/survey.modal';
import { SurveyCommand } from './survey.command';
@SlashCommandHandler(SurveyCommand)
export class SurveyCommandHandler {
constructor(private readonly modals: ModalService) {}
async handle(@Ctx() interaction: ChatInputCommandInteraction): Promise<void> {
const questions = await this.loadQuestions(interaction.guildId);
const modal = this.modals.build(SurveyModal, {
questions: questions.map((q) => ({ id: q.id, label: q.text })),
});
await interaction.showModal(modal);
}
private async loadQuestions(guildId: string) {
return [
{ id: 'q1', text: 'How would you rate the server?' },
{ id: 'q2', text: 'What could be improved?' },
];
}
}Combinando campos estáticos, condicionais e dinâmicos
As três fontes de campo se combinam em um único modal. O ModalSchemaBuilder as processa nesta ordem: primeiro os campos estáticos, na ordem de declaração, depois os campos condicionais, avaliados com base nos dados, e por fim os campos dinâmicos, acrescentados a partir do retorno do método. O total de campos não pode passar de 5 depois da resolução.
import {
ModalComponent,
ModalDynamic,
ModalDynamicFields,
ModalInput,
ModalWhen,
ModalSelect,
ModalChoice,
} from '@spraxium/components';
import { Modal } from '@spraxium/components';
import type { ModalFieldDef } from '@spraxium/components';
interface ApplicationData {
position: 'developer' | 'designer' | 'moderator';
requiresPortfolio: boolean;
}
@ModalComponent({ id: 'application-modal', title: 'Staff Application' })
@ModalDynamic<ApplicationData>()
export class ApplicationModal {
@ModalInput({
label: 'Your Name',
style: 'short',
required: true,
})
name!: string;
@ModalWhen<ApplicationData>((data) => data?.requiresPortfolio === true)
@ModalInput({
label: 'Portfolio URL',
style: 'short',
required: true,
placeholder: 'https://your-portfolio.com',
})
portfolio!: string;
@ModalDynamicFields()
positionFields(data: ApplicationData): ModalFieldDef[] {
const fields: ModalFieldDef[] = [];
if (data.position === 'developer') {
fields.push(
Modal.field.text({
id: 'languages',
label: 'Programming Languages',
style: 'short',
required: true,
}),
);
}
if (data.position === 'moderator') {
fields.push(
Modal.field.text({
id: 'experience',
label: 'Moderation Experience',
style: 'paragraph',
required: true,
}),
);
}
return fields;
}
}Ordem de resolução do schema
Entender a ordem de resolução ajuda você a prever o layout final do modal. O ModalSchemaBuilder segue um processo determinístico que produz sempre o mesmo resultado para as mesmas entradas.
Coleta dos campos estáticos
O builder lê todos os decorators de propriedade, como @ModalInput, @ModalSelect e outros, da classe do componente de modal. Eles são coletados na ordem de declaração e formam a lista base de campos. Campos sem um decorator @ModalWhen são sempre incluídos.
Avaliação das condições
Para cada campo que possui um decorator @ModalWhen, o builder chama a
função predicado com o objeto de dados. Se o predicado retornar false, o
campo é removido da lista. Esta etapa só pode remover campos; ela nunca
reordena nem adiciona novos itens.
Geração de campos dinâmicos
Se a classe tiver um método @ModalDynamicFields, o builder o chama com o objeto de dados. O array ModalFieldDef retornado é acrescentado ao fim da lista de campos, depois de todos os campos estáticos e condicionais.
Validação e build
O builder valida se o total de campos não ultrapassa 5. Se ultrapassar, um erro é lançado em tempo de build com uma mensagem descritiva indicando qual modal excedeu o limite. Em seguida, os campos validados são convertidos em componentes de action row do Discord.