defmodule Uikit.FormComponents do @moduledoc """ Provides UIkit form components for Phoenix LiveView applications. These components integrate with `Phoenix.HTML.FormField` for seamless use with `<.form>` and `to_form/2`-based forms, including error display and field name/id generation. ## Setup Run `mix uikit.setup` to automatically configure your project and import this module. See the [installation guide](https://hexdocs.pm/elixir_uikit) for details. ## Basic Example <.uk_form for={@form} id="user-form" phx-change="validate" phx-submit="save"> <.uk_fieldset> <:legend>User Details <.uk_input field={@form[:name]}> <:label>Full Name <.uk_input field={@form[:email]} type="email" margin> <:label>Email * <.uk_input field={@form[:role]} type="select" options={["Admin", "User"]}> <:label>Role <.uk_input field={@form[:bio]} type="textarea"> <:label>Bio """ use Phoenix.Component, global_prefixes: ~w(uk-) @doc """ Renders a UIkit-styled form wrapper. Wraps `<.form>` with optional UIkit layout classes. ## Examples <.uk_form for={@form} id="my-form" phx-submit="save"> ... <.uk_form for={@form} id="my-form" layout="stacked" phx-submit="save"> ... """ attr :for, :any, required: true, doc: "the form data source (result of to_form/2)" attr :id, :string, required: true, doc: "the DOM ID of the form (required for LiveView)" attr :layout, :string, default: nil, values: [nil, "stacked", "horizontal"], doc: "the UIkit form layout — stacked places labels above, horizontal places them beside" attr :class, :any, default: nil, doc: "additional CSS classes" attr :rest, :global, include: ~w(action method phx-change phx-submit phx-trigger-action autocomplete), doc: "arbitrary HTML attributes for the form element" slot :inner_block, required: true, doc: "the form fields" def uk_form(assigns) do ~H""" <.form for={@for} id={@id} class={[ @layout && "uk-form-#{@layout}", @class ]} {@rest} > {render_slot(@inner_block)} """ end @doc """ Renders a UIkit input with label and error messages. Accepts a `Phoenix.HTML.FormField` via the `field` attribute for automatic `id`, `name`, `value`, and error extraction. All attributes can also be passed explicitly when not using a form field. ## Types Supports all standard HTML input types. Use `type="select"` to render a ` <.uk_field_errors errors={@errors} /> """ end # Catch-all for text, email, password, date, number, url, tel, etc. def uk_input(assigns) do assigns = assign(assigns, :effective_state, effective_state(assigns.errors, assigns.state)) ~H"""
<.uk_field_errors errors={@errors} />
""" end @doc """ Renders a UIkit checkbox with label and error messages. Includes the hidden `false` value input required for LiveView form handling. ## Examples <.uk_checkbox field={@form[:agree]}> <:label>I agree to the terms <.uk_checkbox name="notifications" checked> <:label>Enable important notifications """ attr :id, :any, default: nil, doc: "the DOM ID of the checkbox" attr :name, :any, doc: "the name of the checkbox" attr :value, :any, default: "true", doc: "the value submitted when checked" attr :checked, :boolean, doc: "whether the checkbox is checked" attr :field, Phoenix.HTML.FormField, doc: "a form field struct from the form, e.g. @form[:agree]" attr :errors, :list, default: [], doc: "error messages to display below the checkbox" attr :state, :string, default: nil, values: [nil, "danger", "success"], doc: "the UIkit validation state" attr :class, :any, default: nil, doc: "additional CSS classes" attr :rest, :global, include: ~w(disabled form required), doc: "arbitrary HTML attributes passed to the checkbox input" slot :label, doc: "the label content shown beside the checkbox; supports HTML" def uk_checkbox(%{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(:checked, fn -> Phoenix.HTML.Form.normalize_value("checkbox", field.value) end) |> uk_checkbox() end def uk_checkbox(assigns) do assigns = assign_new(assigns, :checked, fn -> Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value]) end) assigns = assign(assigns, :effective_state, effective_state(assigns.errors, assigns.state)) ~H"""
<.uk_field_errors errors={@errors} />
""" end @doc """ Renders a UIkit radio input. Use multiple `<.uk_radio>` components with the same `name` to create a group. ## Examples <.uk_radio field={@form[:role]} value="admin"> <:label>Admin <.uk_radio field={@form[:role]} value="user"> <:label>User <.uk_radio name="color" value="red"> <:label>Red """ attr :id, :any, default: nil, doc: "the DOM ID of the radio input" attr :name, :any, doc: "the name of the radio group" attr :value, :any, required: true, doc: "the value submitted when this radio is selected" attr :checked, :boolean, default: false, doc: "whether this radio is selected" attr :field, Phoenix.HTML.FormField, doc: "a form field struct from the form, e.g. @form[:role]" attr :state, :string, default: nil, values: [nil, "danger", "success"], doc: "the UIkit validation state" attr :class, :any, default: nil, doc: "additional CSS classes" attr :rest, :global, include: ~w(disabled form required), doc: "arbitrary HTML attributes passed to the radio input" slot :label, doc: "the label content shown beside the radio; supports HTML" def uk_radio(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do assigns |> assign(field: nil, id: assigns.id || "#{field.id}_#{assigns.value}") |> assign_new(:name, fn -> field.name end) |> assign_new(:checked, fn -> to_string(field.value) == to_string(assigns.value) end) |> uk_radio() end def uk_radio(assigns) do ~H"""
""" end @doc """ Renders a UIkit range slider. ## Examples <.uk_range field={@form[:volume]} min="0" max="100" step="1"> <:label>Volume <.uk_range name="opacity" min="0" max="1" step="0.1" /> """ attr :id, :any, default: nil, doc: "the DOM ID of the range input" attr :name, :any, doc: "the name of the range input" attr :value, :any, doc: "the current value" attr :field, Phoenix.HTML.FormField, doc: "a form field struct from the form, e.g. @form[:volume]" attr :errors, :list, default: [], doc: "error messages" attr :class, :any, default: nil, doc: "additional CSS classes" attr :rest, :global, include: ~w(min max step disabled form required), doc: "arbitrary HTML attributes passed to the range input" slot :label, doc: "the label content; supports HTML for custom styling" def uk_range(%{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) |> uk_range() end def uk_range(assigns) do ~H"""
<.uk_field_errors errors={@errors} />
""" end @doc """ Renders a UIkit fieldset with optional legend slot. ## Examples <.uk_fieldset> <:legend>Personal Information <.uk_input field={@form[:name]} label="Name" /> """ attr :class, :any, default: nil, doc: "additional CSS classes" attr :rest, :global, doc: "arbitrary HTML attributes for the fieldset" slot :legend, doc: "the fieldset legend text" slot :inner_block, required: true, doc: "the fieldset content" def uk_fieldset(assigns) do ~H"""
{render_slot(@legend)} {render_slot(@inner_block)}
""" end @doc """ Renders a UIkit form label. ## Examples <.uk_form_label for="user-email">Email """ attr :for, :string, default: nil, doc: "the id of the associated form control" attr :class, :any, default: nil, doc: "additional CSS classes" attr :rest, :global, doc: "arbitrary HTML attributes for the label" slot :inner_block, required: true, doc: "the label text" def uk_form_label(assigns) do ~H""" """ end @doc """ Renders a UIkit form controls wrapper. Use inside a horizontal form to wrap inputs alongside labels. Use `text` for checkboxes/radios in horizontal forms to improve alignment. ## Examples <.uk_form_controls> <.uk_input name="email" type="email" /> <.uk_form_controls text> <.uk_checkbox name="agree" label="I agree" /> """ attr :text, :boolean, default: false, doc: "adds uk-form-controls-text for checkbox/radio alignment in horizontal forms" attr :class, :any, default: nil, doc: "additional CSS classes" attr :rest, :global, doc: "arbitrary HTML attributes" slot :inner_block, required: true, doc: "the form controls content" def uk_form_controls(assigns) do ~H"""
{render_slot(@inner_block)}
""" end @doc """ Renders a UIkit form icon — an icon positioned inside an input. Wrap this around your `<.uk_input>` (without the outer div) or a plain ``. The input must have its label managed externally (pass `label={nil}` to `uk_input` and use `<.uk_form_label>` above). ## Examples <.uk_form_label for="user-email">Email <.uk_form_icon icon="mail"> <.uk_form_label for="search">Search <.uk_form_icon icon="search" flip clickable> """ attr :icon, :string, required: true, doc: "the UIkit icon name" attr :flip, :boolean, default: false, doc: "positions the icon on the right side (uk-form-icon-flip)" attr :clickable, :boolean, default: false, doc: "renders the icon as a clickable element" attr :class, :any, default: nil, doc: "additional CSS classes for the icon element" attr :rest, :global, include: ~w(href navigate patch phx-click), doc: "arbitrary HTML attributes for the icon element" slot :inner_block, required: true, doc: "the input element" def uk_form_icon(assigns) do ~H"""
<%= if @clickable do %> <% else %> <% end %> {render_slot(@inner_block)}
""" end defp effective_state(errors, state) do if errors == [], do: state, else: state || "danger" end # Private component to render a list of field errors below an input. defp uk_field_errors(assigns) do ~H"""

{msg}

""" end @doc """ Translates a form field error tuple into a human-readable string. By default, interpolates `%{key}` placeholders with their values from the opts keyword list. To use your own translator (e.g., for Gettext support), configure the `:error_translator` option: # config/config.exs config :elixir_uikit, :error_translator, {MyAppWeb.CoreComponents, :translate_error} The value is a `{module, function}` tuple that calls `apply(module, function, [{msg, opts}])`. When not configured, uses the default placeholder interpolation. """ def translate_error({_msg, _opts} = error) do case Application.get_env(:elixir_uikit, :error_translator) do {module, function} -> apply(module, function, [error]) nil -> default_translate_error(error) end end defp default_translate_error({msg, opts}) do Enum.reduce(opts, msg, fn {key, value}, acc -> String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end) end) end end