Skip to content

Using modals

Modals provided by the framework are your usual Discord modals, they are created from the Modals factory, which you can inject like any other service.

Tip

You can use these modals and non-BC modals at the same time.

Modals can:

Disabling classes depending on modals

You can use @RequiresModals if you want your class to be disabled when modals are not available.

Method modal handlers

You can bind a modal to a handler similar to those used by persistent components, however, these aren't usable after a restart.

Creating the handler

Create a method annotated with @ModalHandler, with the first parameter being a ModalEvent.

Passing data

If you want to pass some values from your interaction which replied with the modal, you will need to declare parameters to retrieve them with @ModalData.

Getting modal input values

To get the inputs, add parameters annotated with @ModalInput, you can see all the supported types in ModalParameterResolver.

Binding to the handler method

Finally, you can bind your modal to the method, here's a full example:

@Command
class SlashReport(private val modals: Modals) {
    @JDASlashCommand(name = "report", description = "Reports a member")
    fun onSlashReport(
        event: GuildSlashEvent,
        @SlashOption(description = "The member to report") member: Member,
    ) {
        val modal = modals.create("Report a member") {
            label("Report message") {
                child = TextInput(INPUT_MESSAGE, TextInputStyle.PARAGRAPH) {
                    minLength = 10
                }
            }

            bindTo(MODAL_NAME, member)
        }

        event.replyModal(modal).queue()
    }

    @ModalHandler(MODAL_NAME)
    fun onReportModal(
        event: ModalEvent,
        @ModalData member: Member,
        @ModalInput(INPUT_MESSAGE) message: String,
    ) {
        event.reply("""
            You reported ${member.asMention} with the following message:
            $message
        """.trimIndent())
            .setEphemeral(true)
            .queue()
    }

    companion object {
        private const val MODAL_NAME = "SlashReport: modal"
        private const val INPUT_MESSAGE = "SlashReport: modal message"
    }
}
@Command
public class SlashReport {
    private static final String MODAL_NAME = "SlashReport: modal";
    private static final String INPUT_MESSAGE = "SlashReport: modal message";

    private final Modals modals;

    public SlashReport(Modals modals) {
        this.modals = modals;
    }

    @JDASlashCommand(name = "report", description = "Reports a member")
    public void onSlashReport(
            GuildSlashEvent event,
            @SlashOption(description = "The member to report") Member member
    ) {
        var modal = modals.create("Report a member")
                .addComponents(
                        Label.of("Report message",
                                TextInput.create(INPUT_MESSAGE, TextInputStyle.PARAGRAPH)
                                        .setMinLength(10)
                                        .build()
                        )
                )
                .bindTo(MODAL_NAME, member)
                .build();

        event.replyModal(modal).queue();
    }

    @ModalHandler(MODAL_NAME)
    public void onReportModal(
            ModalEvent event,
            @ModalData Member member,
            @ModalInput(INPUT_MESSAGE) String message
    ) {
        event.replyFormat("""
                                You reported %s with the following message:
                                %s
                                """,
                        member.getAsMention(),
                        message)
                .setEphemeral(true)
                .queue();
    }
}

Lambda modal handlers

You can also use a lambda, similar to ephemeral components, to handle modal events in the same context as it was declared in.

The input values can be retrieved using ModalEvent.get (getValue in Java), passing the original input (TextInput, StringSelectMenu...).

You can also use the custom IDs set in the inputs, with ModalEvent#getValue.

Full example

@Command
class SlashRequestRole(private val modals: Modals) {

    @JDASlashCommand(name = "request_role", description = "Request a role")
    fun onSlashRequestRole(event: GuildSlashEvent) {
        val modal = modals.create("Role Request Form") {
            text(
                """
                    ### Welcome!
                    Please read the following before continuing:
                    1. Select the role you wish to get
                    2. Select the reason why you want this role
                    3. (Optional) Add any detail about your request

                    -# Abuse of this system may result in penalties
                """.trimIndent()
            )

            label("Role") {
                child = EntitySelectMenu(INPUT_ROLE, SelectTarget.ROLE)
            }

            label("Reason") {
                child = StringSelectMenu(INPUT_REASON) {
                    option("It looks cool!", "cool")
                    option("I like the color", "color")
                    option("I am interested in the relevant discussions", "discussions")
                }
            }

            label("Details") {
                child = TextInput(INPUT_DETAILS, TextInputStyle.PARAGRAPH, isRequired = false)
            }

            // Set the modal to trigger the given method, with the following data
            bindTo(MODAL_NAME, Clock.System.now().toEpochMilliseconds())
        }

        event.replyModal(modal).queue()
    }

    @ModalHandler(MODAL_NAME)
    fun onRequestRoleModal(
        event: ModalEvent,
        // The data passed to "bindTo"
        @ModalData startTime: Long, // Epoch millis
        // The values of the modal inputs
        @ModalInput(INPUT_REASON) reason: List<String>,
        @ModalInput(INPUT_ROLE) roles: List<Role>,
        @ModalInput(INPUT_DETAILS) details: String,
    ) {
        val timeToRequest = Clock.System.now() - Instant.fromEpochMilliseconds(startTime)

        event.reply("""
            Your request has been submitted! It took you $timeToRequest to complete it.
            Reason: ${reason.single()}
            Roles: ${roles.joinToString { it.asMention }}
            Details: ${details.ifEmpty { "<empty>" }}
        """.trimIndent())
            .setEphemeral(true)
            .queue()
    }

    companion object {
        private const val MODAL_NAME = "SlashRequestRole: request role"
        private const val INPUT_REASON = "SlashRequestRole: reason"
        private const val INPUT_ROLE = "SlashRequestRole: role"
        private const val INPUT_DETAILS = "SlashRequestRole: details"
    }
}
@Command
public class SlashRequestRole {
    private static final String MODAL_NAME = "SlashRequestRole: request role";
    private static final String INPUT_REASON = "SlashRequestRole: reason";
    private static final String INPUT_ROLE = "SlashRequestRole: role";
    private static final String INPUT_DETAILS = "SlashRequestRole: details";

    private final Modals modals;

    public SlashRequestRole(Modals modals) {
        this.modals = modals;
    }

    @JDASlashCommand(name = "request_role", description = "Request a role")
    public void onSlashRequestRole(GuildSlashEvent event) {
        var modal = modals.create("Report a member")
                .addComponents(
                        TextDisplay.of("""
                                ### Welcome!
                                Please read the following before continuing:
                                1. Select the role you wish to get
                                2. Select the reason why you want this role
                                3. (Optional) Add any detail about your request

                                -# Abuse of this system may result in penalties
                                """),

                        Label.of("Role",
                                EntitySelectMenu.create(INPUT_ROLE, SelectTarget.ROLE).build()
                        ),

                        Label.of("Reason",
                                StringSelectMenu.create(INPUT_REASON)
                                        .addOption("It looks cool!", "cool")
                                        .addOption("I like the color", "color")
                                        .addOption("I am interested in the relevant discussions", "discussions")
                                        .build()
                        ),

                        Label.of("Details",
                                TextInput.create(INPUT_DETAILS, TextInputStyle.PARAGRAPH)
                                        .setRequired(false)
                                        .build()
                        )
                )
                .bindTo(MODAL_NAME, Instant.now().toEpochMilli())
                .build();

        event.replyModal(modal).queue();
    }

    @ModalHandler(MODAL_NAME)
    public void onRequestRoleModal(
            ModalEvent event,
            // The data passed to "bindTo"
            @ModalData long startTime, // Epoch millis
            // The values of the modal inputs
            @ModalInput(INPUT_REASON) List<String> reason,
            @ModalInput(INPUT_ROLE) List<Role> roles,
            @ModalInput(INPUT_DETAILS) String details
    ) {
        var timeToRequest = Duration.between(Instant.ofEpochMilli(startTime), Instant.now());

        event.replyFormat("""
                                Your request has been submitted! It took you %ss to complete it.
                                Reason: %s
                                Roles: %s
                                Details: %s
                                """,
                        timeToRequest.toMillis() / 1000.0,
                        reason.getFirst(),
                        roles.stream().map(Role::getAsMention).collect(Collectors.joining(", ")),
                        details.isEmpty() ? "<empty>" : details
                )
                .setEphemeral(true)
                .queue();
    }
}