EDA.Modal (EDA v0.5.0-beta.2)

Copy Markdown View Source

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:

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 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 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

checkbox(custom_id, opts \\ [])

@spec checkbox(
  String.t(),
  keyword()
) :: map()

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))

checkbox_group(custom_id, options, opts \\ [])

@spec checkbox_group(String.t(), [map()], keyword()) :: map()

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 to true)

:min_values of 0 needs required: false.

Example

label("Free days", checkbox_group("days", [choice("Monday", "mon"), choice("Friday", "fri")]))

choice(label, value, opts \\ [])

@spec choice(String.t(), String.t(), keyword()) :: map()

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")

file_upload(custom_id, opts \\ [])

@spec file_upload(
  String.t(),
  keyword()
) :: map()

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 to true)
  • :file_types — up to 10 filters: :image, :video, :audio, or a dot-prefixed extension such as ".pdf". See EDA.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]))

get_attachments(interaction, custom_id)

@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

get_value(interaction, custom_id, default \\ nil)

@spec get_value(map(), String.t(), term()) :: term()

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")

get_values(arg1)

@spec get_values(map()) :: %{required(String.t()) => term()}

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/2 returns the files
  • radio group — the chosen value, or nil
  • checkbox — true or false

Example

EDA.Modal.get_values(interaction)
# => %{"subject" => "Bug report", "platform" => ["windows"], "agree" => true}

label(text, component, opts \\ [])

@spec label(String.t(), map(), keyword()) :: map()

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")

modal(custom_id, title, input1, input2 \\ nil, input3 \\ nil, input4 \\ nil, input5 \\ nil)

@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))
])

radio_group(custom_id, options, opts \\ [])

@spec radio_group(String.t(), [map()], keyword()) :: map()

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 to true)

Example

label("Class", radio_group("class", [choice("Warrior", "warrior"), choice("Rogue", "rogue")]))

text_field(custom_id, style, opts \\ [])

@spec text_field(String.t(), :short | :paragraph, keyword()) :: map()

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 to true)
  • :value — pre-filled text (max 4000 chars)

Example

label("About you", text_field("bio", :paragraph, max_length: 1000, required: false))

text_input(custom_id, label, style, opts \\ [])

@spec text_input(String.t(), String.t(), :short | :paragraph, keyword()) :: map()

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)