defmodule SigmaKit.Components.Forms do use Phoenix.LiveComponent import SigmaKit.Components.Icons, only: [icon: 1] import SigmaKit.Components.Forms.Autocomplete, only: [autocomplete: 1] @doc """ Renders a simple form. ## Examples <.simple_form for={@form} phx-change="validate" phx-submit="save"> <.input field={@form[:email]} label="Email"/> <.input field={@form[:username]} label="Username" /> <:actions> <.button>Save """ attr :for, :any, required: true, doc: "the data structure for the form" attr :as, :any, default: nil, doc: "the server side parameter to collect all input under" attr :rest, :global, include: ~w(autocomplete name rel action enctype method novalidate target multipart), doc: "the arbitrary HTML attributes to apply to the form tag" slot :inner_block, required: true slot :actions, doc: "the slot for form actions, such as a submit button" def simple_form(assigns) do ~H""" <.form :let={f} for={@for} as={@as} {@rest}>
{render_slot(@inner_block, f)}
{render_slot(action, f)}
""" end @doc """ Renders an input with label and error messages. A `Phoenix.HTML.FormField` may be passed as argument, which is used to retrieve the input name, id, and values. Otherwise all attributes may be passed explicitly. ## Examples <.input field={@form[:email]} type="email" /> <.input name="my-input" errors={["oh no!"]} /> """ attr :id, :any, default: nil, doc: "the id of the input" attr :name, :any, doc: "the name of the input" attr :label, :string, default: nil, doc: "the label for the input" attr :value, :any, doc: "the value of the input" attr :type, :string, default: "text", values: ~w(switch checkbox color date datetime-local email file month number password range search select tel text textarea time url week hidden radio-group autocomplete), doc: "the type of the input" attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form, for example: @form[:email]" attr :errors, :list, default: [], doc: "A list of strings to display as field errors" attr :checked, :boolean, doc: "the checked flag for checkbox inputs" attr :prompt, :string, default: nil, doc: "the prompt for select inputs" attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2" attr(:target, :string, default: nil, doc: ~s|the target of the call for remote options. Will default to the current live view. For a live component, pass `remove_options_target={@myself}` if the event is handled on the live component.| ) attr(:event, :string, default: nil, doc: "The event name to trigger when searching for remote options. That event must return a li" ) attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs" attr :rest, :global, include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength multiple pattern placeholder readonly required rows size step) slot :help, doc: "A slot used to display help text under the input" def input(%{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 -> if assigns.multiple, do: field.name <> "[]", else: field.name end) |> assign_new(:value, fn -> field.value end) |> input() end def input(%{type: "checkbox"} = assigns) do assigns = assign_new(assigns, :checked, fn -> Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value]) end) ~H"""
<.field_error :for={msg <- @errors}>{msg}
""" end def input(%{type: "switch"} = assigns) do assigns = assign_new(assigns, :checked, fn -> Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value]) end) ~H"""
<.field_error :for={msg <- @errors}>{msg}
""" end def input(%{type: "select"} = assigns) do ~H"""
<.label for={@id}>{@label} <.field_error :for={msg <- @errors}>{msg}
{render_slot(@help)}
""" end def input(%{type: "autocomplete"} = assigns) do ~H"""
<.label for={@id}>{@label} <.autocomplete id={@id} name={@name} value={@value} options={@options} target={@target} event={@event} /> <.field_error :for={msg <- @errors}>{msg}
{render_slot(@help)}
""" end def input(%{type: "textarea"} = assigns) do ~H"""
<.label for={@id}>{@label} <.field_error :for={msg <- @errors}>{msg}
{render_slot(@help)}
""" end def input(%{type: "radio-group"} = assigns) do ~H"""
<.field_error :for={msg <- @errors}>{msg}
""" end # All other inputs text, datetime-local, url, password, etc. are handled here... def input(assigns) do ~H"""
<.label for={@id}> {@label} <.field_error :for={msg <- @errors}>{msg}
{render_slot(@help)}
""" end @doc """ Renders a label. """ attr :for, :string, default: nil slot :inner_block, required: true def label(assigns) do ~H""" """ end slot :inner_block, required: true def field_error(assigns) do ~H"""

<.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" /> {render_slot(@inner_block)}

""" end @doc """ Translates an error message using gettext. """ def translate_error({msg, opts}) do # When using gettext, we typically pass the strings we want # to translate as a static argument: # # # Translate the number of files with plural rules # dngettext("errors", "1 file", "%{count} files", count) # # However the error messages in our forms and APIs are generated # dynamically, so we need to translate them by calling Gettext # with our gettext backend as first argument. Translations are # available in the errors.po file (as we use the "errors" domain). gt_module = Application.get_env(:sigma_kit, :gettext) if count = opts[:count] do Gettext.dngettext(gt_module, "errors", msg, msg, count, opts) else Gettext.dgettext(gt_module, "errors", msg, opts) end end @doc """ Translates the errors for a field from a keyword list of errors. """ def translate_errors(errors, field) when is_list(errors) do for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts}) end end