defmodule LivebookWeb.FormComponents do use Phoenix.Component import LivebookWeb.CoreComponents alias Phoenix.LiveView.JS @doc """ Renders a text input with label and error messages, as well as a "http(s)://" prefix. """ attr :id, :any, default: nil attr :name, :any attr :label, :string, default: nil attr :value, :any attr :errors, :list, default: [] attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form" attr :help, :string, default: nil attr :class, :string, default: nil attr :rest, :global, include: ~w(autocomplete readonly disabled step min max) def schemaless_url_field(assigns) do assigns = assigns_from_field(assigns) ~H""" <.field_wrapper id={@id} name={@name} label={@label} errors={@errors} help={@help}>
http(s)://
""" end @doc """ Renders a text input with label and error messages. """ attr :id, :any, default: nil attr :name, :any attr :label, :string, default: nil attr :value, :any attr :errors, :list, default: [] attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form" attr :help, :string, default: nil attr :type, :string, default: "text" attr :class, :string, default: nil attr :rest, :global, include: ~w(autocomplete readonly disabled step min max) def text_field(assigns) do assigns = assigns_from_field(assigns) ~H""" <.field_wrapper id={@id} name={@name} label={@label} errors={@errors} help={@help}> """ end defp input_classes(errors) do [ "w-full px-3 py-2 text-sm font-normal border rounded-lg placeholder-gray-400 disabled:opacity-70 disabled:cursor-not-allowed", if errors == [] do "bg-gray-50 border-gray-200 text-gray-600" else "bg-red-50 border-red-600 text-red-600" end, "invalid:bg-red-50 invalid:border-red-600 invalid:text-red-600" ] end @doc """ Renders a textarea input with label and error messages. """ attr :id, :any, default: nil attr :name, :any attr :label, :string, default: nil attr :value, :any attr :errors, :list, default: [] attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form" attr :help, :string, default: nil attr :class, :string, default: nil attr :monospace, :boolean, default: false attr :rest, :global, include: ~w(autocomplete readonly disabled rows cols) def textarea_field(assigns) do assigns = assigns_from_field(assigns) ~H""" <.field_wrapper id={@id} name={@name} label={@label} errors={@errors} help={@help}> """ end @doc """ Renders a hidden input. """ attr :id, :any, default: nil attr :name, :any attr :value, :any attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form" attr :rest, :global, include: ~w(autocomplete readonly disabled) def hidden_field(assigns) do assigns = assigns_from_field(assigns) ~H""" """ end @doc """ Renders a password input with label and error messages. """ attr :id, :any, default: nil attr :name, :any attr :label, :string, default: nil attr :value, :any attr :errors, :list, default: [] attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form" attr :help, :string, default: nil attr :class, :string, default: nil attr :rest, :global, include: ~w(autocomplete readonly disabled) def password_field(assigns) do assigns = assigns_from_field(assigns) ~H""" <.field_wrapper id={@id} name={@name} label={@label} errors={@errors} help={@help}>
<.icon_button data-show type="button" aria-label="show password" tabindex="-1" phx-click={ JS.set_attribute({"type", "text"}, to: "##{@id}-toggle input") |> JS.add_class("hidden", to: "##{@id}-toggle [data-show]") |> JS.remove_class("hidden", to: "##{@id}-toggle [data-hide]") } > <.remix_icon icon="eye-line" /> <.icon_button class="hidden" data-hide type="button" aria-label="hide password" tabindex="-1" phx-click={ JS.set_attribute({"type", "password"}, to: "##{@id}-toggle input") |> JS.remove_class("hidden", to: "##{@id}-toggle [data-show]") |> JS.add_class("hidden", to: "##{@id}-toggle [data-hide]") } > <.remix_icon icon="eye-off-line" />
""" end @doc """ Renders a hex color input with label and error messages. """ attr :id, :any, default: nil attr :name, :any attr :label, :string, default: nil attr :value, :any attr :errors, :list, default: [] attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form" attr :help, :string, default: nil attr :randomize, JS, default: %JS{} attr :rest, :global def hex_color_field(assigns) do assigns = assigns_from_field(assigns) ~H""" <.field_wrapper id={@id} name={@name} label={@label} errors={@errors} help={@help}>
<.icon_button type="button" phx-click={@randomize}> <.remix_icon icon="refresh-line" />
""" end @doc """ Renders a switch input with label and error messages. """ attr :id, :any, default: nil attr :name, :any attr :label, :string, default: nil attr :value, :any attr :errors, :list, default: [] attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form" attr :help, :string, default: nil attr :disabled, :boolean, default: false attr :checked_value, :string, default: "true" attr :unchecked_value, :string, default: "false" attr :rest, :global def switch_field(assigns) do assigns = assigns_from_field(assigns) ~H"""
<%= @label %> <.help :if={@help} text={@help} />
<.error :for={msg <- @errors}><%= msg %>
""" end @doc """ Renders checkbox input with label and error messages. """ attr :id, :any, default: nil attr :name, :any attr :label, :string, default: nil attr :value, :any attr :errors, :list, default: [] attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form" attr :help, :string, default: nil attr :checked_value, :string, default: "true" attr :unchecked_value, :any, default: "false", doc: "when set to `nil`, unchecked value is not sent" attr :small, :boolean, default: false attr :rest, :global def checkbox_field(assigns) do assigns = assigns_from_field(assigns) ~H"""
<.error :for={msg <- @errors}><%= msg %>
""" end @doc """ Renders radio inputs with label and error messages. """ attr :id, :any, default: nil attr :name, :any attr :label, :string, default: nil attr :value, :any attr :errors, :list, default: [] attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form" attr :help, :string, default: nil attr :options, :list, default: [], doc: "a list of `{value, description}` tuples" attr :rest, :global def radio_field(assigns) do assigns = assigns_from_field(assigns) ~H"""
<.label :if={@label} for={@id} help={@help}><%= @label %>
<.error :for={msg <- @errors}><%= msg %>
""" end @doc """ Renders radio inputs presented with label and error messages presented as button group. """ attr :id, :any, default: nil attr :name, :any attr :label, :string, default: nil attr :value, :any attr :errors, :list, default: [] attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form" attr :help, :string, default: nil attr :options, :list, default: [], doc: "a list of `{value, description}` tuples" attr :full_width, :boolean, default: false attr :rest, :global def radio_button_group_field(assigns) do assigns = assigns_from_field(assigns) ~H"""
<.label :if={@label} for={@id} help={@help}><%= @label %>
<.error :for={msg <- @errors}><%= msg %>
""" end @doc """ Renders emoji input with label and error messages. """ attr :id, :any, default: nil attr :name, :any attr :label, :string, default: nil attr :value, :any attr :errors, :list, default: [] attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form" attr :help, :string, default: nil attr :rest, :global def emoji_field(assigns) do assigns = assigns_from_field(assigns) ~H""" <.field_wrapper id={@id} name={@name} label={@label} errors={@errors} help={@help}>
<%= @value %>
""" end @doc """ Renders select input with label and error messages. """ attr :id, :any, default: nil attr :name, :any attr :label, :string, default: nil attr :value, :any attr :errors, :list, default: [] attr :class, :string, default: "" attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form" attr :help, :string, default: nil attr :options, :list, default: [] attr :prompt, :string, default: nil attr :rest, :global, include: ~w(disabled) def select_field(assigns) do assigns = assigns_from_field(assigns) ~H""" <.field_wrapper id={@id} name={@name} label={@label} errors={@errors} help={@help}>
<.remix_icon icon="arrow-down-s-line" />
""" end defp assigns_from_field(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do errors = if Phoenix.Component.used_input?(field), do: field.errors, else: [] assigns |> assign(field: nil, id: assigns.id || field.id) |> assign(:errors, Enum.map(errors, &translate_error(&1))) |> assign_new(:name, fn -> field.name end) |> assign_new(:value, fn -> field.value end) end defp assigns_from_field(assigns), do: assigns @doc """ Translates an error message using gettext. """ def translate_error({msg, opts}) do # Because the error messages we show in our forms and APIs # are defined inside Ecto, we need to translate them dynamically. Enum.reduce(opts, msg, fn {key, value}, acc -> String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end) end) end attr :id, :any, required: true attr :name, :any, required: true attr :label, :string, required: true attr :errors, :list, required: true attr :help, :string, required: true slot :inner_block, required: true defp field_wrapper(assigns) do ~H"""
<.label :if={@label} for={@id} help={@help}><%= @label %> <%= render_slot(@inner_block) %> <.error :for={msg <- @errors}><%= msg %>
""" end @doc """ Renders a label. """ attr :for, :string, default: nil attr :help, :string, default: nil slot :inner_block, required: true def label(assigns) do ~H""" """ end @doc """ Generates a generic error message. """ slot :inner_block, required: true def error(assigns) do ~H"""

<%= render_slot(@inner_block) %>

""" end defp help(assigns) do ~H""" <.remix_icon icon="question-line" class="text-sm leading-none" /> """ end @doc """ Renders a drag-and-drop area for the given upload. Once a file is selected, renders the entry. ## Examples <.file_drop_input upload={@uploads.file} label="File" on_clear={JS.push("clear_file", target: @myself)} /> """ attr :upload, Phoenix.LiveView.UploadConfig, required: true attr :label, :string, required: true attr :on_clear, Phoenix.LiveView.JS, required: true def file_drop_input(%{upload: %{entries: []}} = assigns) do ~H"""
<.live_file_input upload={@upload} class="hidden" />
""" end def file_drop_input(assigns) do ~H"""
<.live_file_input upload={@upload} class="hidden" /> <.label><%= @label %>
<.file_entry entry={entry} on_clear={@on_clear} />
""" end @doc """ Renders a file entry with progress. ## Examples <.file_entry entry={entry} on_clear={JS.push("clear_file", target: @myself)} /> """ attr :entry, Phoenix.LiveView.UploadEntry, required: true attr :on_clear, Phoenix.LiveView.JS, required: true attr :name, :string, default: nil def file_entry(assigns) do ~H"""
<%= @name || @entry.client_name %> <%= @entry.progress %>%
""" end @doc """ Checks if the given upload makes the form disabled. """ @spec upload_disabled?(Phoenix.LiveView.UploadConfig.t()) :: boolean() def upload_disabled?(upload) do upload.entries == [] or upload.errors != [] or Enum.any?(upload.entries, & &1.preflighted?) end end