Spraxium logoSpraxium

Modales dinámicos

Genera campos de modal en tiempo de ejecución con @ModalDynamic, @ModalDynamicFields y @ModalWhen. Construye modales conscientes de contexto que adapten su estructura según datos de usuario o estado de la aplicación.

Por qué modales dinámicos

Los modales estáticos funcionan bien cuando los campos se conocen en tiempo de compilación. Sin embargo, muchos escenarios reales requieren modales que se adapten a condiciones de runtime. Un modal de creación de tickets puede mostrar campos distintos según la categoría del ticket. Un modal de configuración puede incluir campos solo para funcionalidades a las que la persona usuaria tiene acceso. Un asistente de varios pasos puede pasar datos de un paso previo al modal para personalizar su contenido.

Spraxium ofrece tres mecanismos para modales dinámicos. El decorator @ModalDynamic marca un modal como consciente de datos, el método @ModalDynamicFields genera campos de forma programática al construir, y el decorator @ModalWhen incluye o excluye condicionalmente campos estáticos individuales según un predicado. Estos mecanismos se componen de forma natural, por lo que puedes mezclar campos estáticos, condicionales y generados dinámicamente en el mismo modal.

El ModalSchemaBuilder resuelve las tres fuentes (decorators estáticos, condicionales y métodos dinámicos) en una única lista plana de campos cuando se llama a ModalService.build. Esto significa que la estructura del modal se determina en el momento de renderizar, no en el momento de definir la clase.

Hacer que un modal sea consciente de datos

El decorator genérico @ModalDynamic marca una clase de componente modal como receptora de datos externos de un tipo específico. Cuando llamas a ModalService.build(MyModal, data), los datos se almacenan y quedan disponibles para métodos @ModalDynamicFields y predicados @ModalWhen. El parámetro de tipo aporta seguridad en tiempo de compilación sobre la forma de los datos.

src/components/config-modal.modal.ts
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 condicionales con @ModalWhen

El decorator @ModalWhen recibe una función predicado que toma los datos del modal y devuelve un booleano. Si el predicado devuelve true, el campo se incluye en el modal renderizado. Si devuelve false, el campo se omite por completo, no solo se oculta. Esto significa que el campo no cuenta para el límite de 5 campos cuando está excluido, liberando filas de acción para otros campos.

El predicado recibe los datos pasados a ModalService.build. Si no se proporcionan datos, el predicado recibe undefined, así que tu predicado debe manejar ese caso con seguridad.

src/components/config-modal.modal.ts
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;
}

Generación dinámica de campos

El decorator de método @ModalDynamicFields marca un método de clase que devuelve en runtime un arreglo de objetos ModalFieldDef. Este método se llama durante ModalService.build y recibe los datos pasados a la llamada de build. Los campos devueltos se agregan después de todos los campos estáticos y condicionales en el layout del modal.

Usa los helpers del namespace Modal.field para construir definiciones de campo dentro del método dinámico. Esto mantiene el código legible y tipado frente a construir objetos de definición crudos.

src/components/survey.modal.ts
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,
      }),
    );
  }
}

Construir modales conscientes de datos

Cuando tu modal usa @ModalDynamic, pasa el objeto de datos como segundo argumento de ModalService.build. El schema builder lee los datos, evalúa todos los predicados @ModalWhen, llama a métodos @ModalDynamicFields, mezcla resultados con campos estáticos y produce el layout final del 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?' },
    ];
  }
}

Combinar campos estáticos, condicionales y dinámicos

Las tres fuentes de campo se componen en un único modal. El ModalSchemaBuilder las procesa en orden: primero campos estáticos (en orden de declaración), luego campos condicionales (evaluados contra datos), y después campos dinámicos (agregados desde el retorno del método). El total de campos no debe superar 5 tras la resolución.

src/components/application.modal.ts
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;
  }
}

Orden de resolución del esquema

Entender el orden de resolución te ayuda a predecir el layout final del modal. El ModalSchemaBuilder sigue un proceso determinista que produce el mismo resultado con las mismas entradas.

Recopilar campos estáticos

El builder lee todos los decorators de propiedad (@ModalInput, @ModalSelect, etc.) de la clase componente modal. Se recopilan en orden de declaración y forman la lista base de campos. Los campos sin decorator @ModalWhen siempre se incluyen.

Evaluar condicionales

Para cada campo que tenga un decorator @ModalWhen, el builder llama a la función predicado con el objeto de datos. Si el predicado devuelve false, el campo se elimina de la lista. Este paso solo puede eliminar campos, nunca reordenarlos ni agregarlos.

Generar campos dinámicos

Si la clase tiene un método @ModalDynamicFields, el builder lo llama con el objeto de datos. El arreglo ModalFieldDef devuelto se agrega al final de la lista de campos, después de todos los campos estáticos y condicionales.

Validar y construir

El builder valida que el total de campos no supere 5. Si lo supera, se lanza un error en tiempo de build con un mensaje descriptivo que indica qué modal excedió el límite. Luego los campos validados se convierten en componentes de action row de Discord.