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}>
"""
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}>
"""
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}>
"""
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"""
<.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"""
"""
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 %>
<%= description %>
<.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 %>
<.remix_icon icon="emotion-line" class="text-xl" />
"""
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}>
<%= @prompt %>
<%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
<.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"""
<%= render_slot(@inner_block) %>
<.help :if={@help} text={@help} />
"""
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" />
Click to select a file or drag a local file here
"""
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 %>
<.remix_icon icon="close-line" />
<%= @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