Builder for Discord Modal dialogs.
Modals are popup forms shown as an interaction response (type 9). A modal holds 1–5 top-level components, a title, and a custom_id.
Building a modal
Each field sits in a label/3, which carries the field's label and an optional description.
Inside a label goes one interactive component:
text_field/3— free-form text, single line or paragraphEDA.Component.string_select/3,user_select/2,role_select/2,mentionable_select/2,channel_select/2— the select menus messages use, withrequired: falseto make them optionalfile_upload/2— 0 to 10 filesradio_group/3— exactly one choice among 2–10checkbox_group/3— any number of choices among 1–10checkbox/2— a single yes/no
EDA.Component.text_display/1 can also sit at the top level, for instructions between fields.
import EDA.Modal
import EDA.Component, only: [text_display: 1, user_select: 2]
modal("report", "Report a member", [
text_display("Reports go to the moderators only."),
label("Who?", user_select("target", max_values: 1)),
label("What happened?", text_field("details", :paragraph, max_length: 1000),
description: "Links to messages help."
),
label("Evidence", file_upload("evidence", max_values: 5, file_types: [:image])),
label("Severity", radio_group("severity", [choice("Low", "low"), choice("High", "high")]))
])
|> then(&EDA.Interaction.respond_modal(interaction, &1))Discord's limits are checked when the modal is built — lengths, option counts, where a
component may be placed — so a mistake raises ArgumentError naming it, rather than coming
back as an opaque 50035 when the modal is shown.
The earlier form
text_input/4 builds a text input carrying its own label, which modal/3..7 wraps in an
action row. Discord still accepts that form but no longer recommends it, and it cannot hold any
of the other components. It keeps working unchanged.
Handling submissions
get_values/1 returns every submitted value by custom_id, in the shape of its component: a
string for a text field, a list of strings for a select, a checkbox group or a file upload,
a string or nil for a radio group, a boolean for a checkbox. get_attachments/2 returns the
files a file upload received.
def handle_event({:INTERACTION_CREATE, interaction}) do
if EDA.Interaction.interaction_type(interaction) == :modal_submit and
EDA.Interaction.custom_id(interaction) == "report" do
%{"target" => [user_id], "details" => details, "severity" => severity} =
EDA.Modal.get_values(interaction)
files = EDA.Modal.get_attachments(interaction, "evidence")
# ...
end
end
Summary
Functions
Creates a single checkbox for use inside label/3 (type 23).
Creates a checkbox group for use inside label/3 (type 22): any number of choices among
1–10.
Creates an option for radio_group/3 or checkbox_group/3.
Creates a file upload for use inside label/3 (type 19).
Returns the files a file_upload/2 received, as EDA.Attachment structs, in upload order.
Extracts a single value from a MODAL_SUBMIT interaction by custom_id.
Extracts all submitted values from a MODAL_SUBMIT interaction, by custom_id.
Wraps a modal component with a label and an optional description (type 18).
Creates a modal from its top-level components.
Creates a modal from a list of components. Same as modal/3 with a list.
Creates a radio group for use inside label/3 (type 21): exactly one choice among 2–10.
Creates a text input for use inside label/3 (type 4).
Creates a text input carrying its own label, for the action-row form of modal/3..7.
Functions
Creates a single checkbox for use inside label/3 (type 23).
The submitted value is true or false. A checkbox cannot be required; for a box that must
be ticked, use a checkbox_group/3 with one option.
Options
:default— whether it starts ticked
Example
label("Subscribe to updates", checkbox("subscribe", default: true))
Creates a checkbox group for use inside label/3 (type 22): any number of choices among
1–10.
The submitted value is the list of chosen values, empty when none is ticked.
Options
:min_values— minimum choices (0–10, Discord defaults to 1):max_values— maximum choices (1–10, Discord defaults to the number of options):required— whether a choice is needed to submit (Discord defaults totrue)
:min_values of 0 needs required: false.
Example
label("Free days", checkbox_group("days", [choice("Monday", "mon"), choice("Friday", "fri")]))
Creates an option for radio_group/3 or checkbox_group/3.
Options
:description— shown under the label (max 100 chars):default— whether the option starts selected
Example
choice("Warrior", "warrior", description: "Strong and brave")
Creates a file upload for use inside label/3 (type 19).
The user can attach 0 to 10 files; each is limited by the user's own upload limit in the
channel. The submitted files arrive as attachments — see get_attachments/2.
Options
:min_values— minimum number of files (0–10, Discord defaults to 1):max_values— maximum number of files (1–10, Discord defaults to 1):required— whether a file must be attached to submit (Discord defaults totrue):file_types— up to 10 filters::image,:video,:audio, or a dot-prefixed extension such as".pdf". SeeEDA.FileType: the filter matches the extension only and never inspects the file, so it does not replace checking what was actually uploaded.
:min_values of 0 needs required: false; Discord refuses the combination otherwise.
Example
label("Screenshots", file_upload("shots", max_values: 5, file_types: [:image]))
@spec get_attachments(map(), String.t()) :: [EDA.Attachment.t()]
Returns the files a file_upload/2 received, as EDA.Attachment structs, in upload order.
Returns [] when the component is absent or nothing was uploaded.
Example
for file <- EDA.Modal.get_attachments(interaction, "evidence") do
{file.filename, file.url}
end
Extracts a single value from a MODAL_SUBMIT interaction by custom_id.
Returns default (nil unless given) when the modal has no such component. See
get_values/1 for the shape of each value.
Example
subject = EDA.Modal.get_value(interaction, "subject")
Extracts all submitted values from a MODAL_SUBMIT interaction, by custom_id.
Each value has the shape of its component:
- text field — the text, a string
- select, checkbox group — the chosen values, a list of strings (ids for user, role, mentionable and channel selects)
- file upload — the attachment ids, a list of strings;
get_attachments/2returns the files - radio group — the chosen value, or
nil - checkbox —
trueorfalse
Example
EDA.Modal.get_values(interaction)
# => %{"subject" => "Bug report", "platform" => ["windows"], "agree" => true}
Wraps a modal component with a label and an optional description (type 18).
Every interactive component in a modal sits in a label. The label text is at most 45 characters, the description at most 100; Discord shows the description above or below the component depending on the platform.
A text input built with text_input/4 carries a label of its own, which Discord deprecates
inside a label; use text_field/3 here instead.
Options
:description— hint text for the field (max 100 chars):id— optional numeric identifier for the component
Example
label("Your name", text_field("name", :short))
label("Favourite colour", string_select("colour", options), description: "Pick one")
@spec modal( String.t(), String.t(), [map()] | map(), map() | nil, map() | nil, map() | nil, map() | nil ) :: map()
Creates a modal from its top-level components.
components is a list of 1–5 label/3 and EDA.Component.text_display/1 components. Text
inputs from text_input/4 are also accepted, each wrapped in an action row as before.
The older positional form, modal(custom_id, title, input1, ..., input5), still works.
Example
modal("survey", "Quick Survey", [
label("Favourite colour?", text_field("q1", :short)),
label("Why?", text_field("q2", :paragraph, required: false))
])
Creates a modal from a list of components. Same as modal/3 with a list.
modal_from_list("profile", "Edit Profile", [
label("Name", text_field("name", :short)),
label("Bio", text_field("bio", :paragraph))
])
Creates a radio group for use inside label/3 (type 21): exactly one choice among 2–10.
At most one option may start selected. The submitted value is the chosen option's value, or
nil when the group is optional and left empty.
Options
:required— whether a choice is needed to submit (Discord defaults totrue)
Example
label("Class", radio_group("class", [choice("Warrior", "warrior"), choice("Rogue", "rogue")]))
Creates a text input for use inside label/3 (type 4).
The label and description belong to the label/3 around it.
Options
:placeholder— hint text when empty (max 100 chars):min_length— minimum input length (0–4000):max_length— maximum input length (1–4000):required— whether the field must be filled (Discord defaults totrue):value— pre-filled text (max 4000 chars)
Example
label("About you", text_field("bio", :paragraph, max_length: 1000, required: false))
Creates a text input carrying its own label, for the action-row form of modal/3..7.
Discord no longer recommends this form; label/3 around text_field/3 replaces it and can
also hold the other modal components.
Parameters
custom_id— unique identifier for this field (1–100 chars)label— label displayed above the input (max 45 chars)style—:short(single line) or:paragraph(multi-line)
Options
Those of text_field/3.
Example
text_input("name", "Your Name", :short, placeholder: "John Doe")
text_input("bio", "About You", :paragraph, required: false, max_length: 1000)