defmodule Corex.NativeInput do @moduledoc ~S''' Unified native input component based on [Phoenix Core Components](https://hexdocs.pm/phoenix/components.html#corecomponents) Used for text, textarea, date, datetime-local, time, month, week, email, url, tel, search, color, number, password, checkbox, radio, and select. Optional icon slot and shared styling via data-scope="native-input". Uses same data-part structure as Corex components (root, label, control, input, error) for consistency. The icon slot is ignored for textarea, date/time types, color, number, checkbox, radio, and select. For text, email, url, tel, search, and password the icon slot is shown when provided. ## Anatomy ### Basic ```heex <.native_input type="text" name="user[name]" class="native-input"> <:label>Name ``` ### With icon ```heex <.native_input type="email" name="user[email]" class="native-input"> <:label>Email <:icon><.heroicon name="hero-envelope" class="icon" /> ``` ### Textarea (icon slot ignored) ```heex <.native_input type="textarea" name="user[bio]" class="native-input"> <:label>Bio ``` ### Checkbox, select, radio ```heex <.native_input type="checkbox" name="user[agree]" class="native-input"> <:label>I agree <.native_input type="select" name="user[role]" options={["Admin": "admin", "User": "user"]} prompt="Choose role" class="native-input"> <:label>Role <.native_input type="radio" name="user[size]" options={["Small": "s", "Medium": "m", "Large": "l"]} value="m" class="native-input"> <:label>Size ``` ### With form field Use `field={f[:key]}` or `field={@form[:key]}` with a form built from an Ecto changeset. Set the form `id` in `to_form/2` and use `<.form for={@form}>`. See the Checkbox or NumberInput component docs for the full Controller and Live View pattern. ```heex <.form :let={f} for={@form}> <.native_input type="email" field={f[:email]} class="native-input"> <:label>Email <:error :let={msg}>{msg} ``` ## Read-only Pass `read_only={true}` or `readonly` in global attributes. Both set `data-readonly` on the root for Corex CSS and the HTML `readonly` attribute on the input. ```heex <.native_input type="text" name="user[code]" read_only class="native-input"> <:label>Code ``` ## Form Use `field={f[:email]}` inside `<.form>` with a changeset-backed form. For cross-cutting invalid styling and error presentation, see the [Forms](forms.html) guide. Pass `invalid={Corex.FormField.invalid?(@form[:email])}` when you want alert borders after validation. For `type="select"` with `multiple`, the field name is suffixed with `[]` so Ecto `{:array, :string}` receives a list (same Phoenix convention as [`Corex.Select`](Corex.Select.html) `multiple`). ```heex <.form for={@form} phx-change="validate"> <.native_input type="email" field={@form[:email]} class="native-input"> <:label>Email <:error :let={msg}>{msg} ``` ## Style Target parts with `data-scope` and `data-part`, or use Corex Design: import tokens and `native-input.css`, then set `class="native-input"` on `<.native_input>`. ```css [data-scope="native-input"][data-part="root"] {} [data-scope="native-input"][data-part="label"] {} [data-scope="native-input"][data-part="control"] {} [data-scope="native-input"][data-part="icon"] {} [data-scope="native-input"][data-part="input"] {} [data-scope="native-input"][data-part="error"] {} ``` The root has `data-no-icon` when no icon is shown (icon slot empty or type is textarea/date/time), so the input uses full-width padding. Use the class `native-input` for default Corex styling. If you wish to use the default Corex styling, you can use the class `native-input` on the component. This requires to install `Mix.Tasks.Corex.Design` first and import the component css file. ```css @import "../corex/main.css"; @import "../corex/tokens/themes/neo/light.css"; @import "../corex/components/native-input.css"; ``` ''' @doc type: :component use Phoenix.Component alias Phoenix.HTML.Form @types ~W(text textarea date datetime-local time month week email url tel search color number password checkbox radio select) attr(:type, :string, required: true, values: @types) attr(:id, :string, required: false) attr(:name, :string, required: false) attr(:value, :any) attr(:invalid, :boolean, default: false) attr(:read_only, :boolean, default: false, doc: "Read-only state; also sets HTML readonly on the input" ) attr(:errors, :list, default: [], doc: "List of error messages to display") attr(:class, :any, default: nil) attr(:prompt, :string, default: nil, doc: "Prompt for select inputs") attr(:options, :list, default: nil, doc: "Options for select and radio. Same format as options_for_select." ) attr(:multiple, :boolean, default: false, doc: "Multiple flag for select inputs") attr(:checked, :boolean, doc: "Checked flag for checkbox. Defaults from value.") attr(:field, Phoenix.HTML.FormField, doc: "A form field struct from the form, e.g. @form[:email]" ) attr(:rest, :global, include: ~W(autocomplete disabled maxlength minlength pattern placeholder readonly required cols rows list form min max step accept) ++ ~W(phx-change phx-blur phx-focus phx-target phx-debounce phx-throttle) ) slot :label, required: false do attr(:class, :string, required: false) end slot :icon, required: false, doc: "Optional. Ignored for textarea, date, datetime-local, time, month, week, color, number, checkbox, radio, select." do attr(:class, :string, required: false) end slot(:error, required: false) do attr(:class, :string, required: false) end def native_input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do name = if assigns.multiple, do: field.name <> "[]", else: field.name assigns |> Corex.FormField.assign_form_field(field) |> assign(id: assigns[:id] || field.id) |> assign(:name, name) |> assign_new(:value, fn -> field.value end) |> assign_read_only() |> native_input() end @types_ignoring_icon ~W(textarea date datetime-local time month week color number checkbox radio select) def native_input(%{type: "checkbox"} = assigns) do assigns = assigns |> assign_read_only() |> assign_new(:id, fn -> "native-input-#{System.unique_integer([:positive])}" end) |> assign_new(:checked, fn -> Form.normalize_value("checkbox", assigns[:value]) end) ~H"""
<%= if @error != [] do %> {render_slot(@error, msg)} <% else %> {msg} <% end %>
""" end def native_input(%{type: "select"} = assigns) do assigns = assigns |> assign_read_only() |> assign_new(:id, fn -> "native-input-#{System.unique_integer([:positive])}" end) |> assign_new(:value, fn -> nil end) ~H"""
<%= if @error != [] do %> {render_slot(@error, msg)} <% else %> {msg} <% end %>
""" end def native_input(%{type: "radio"} = assigns) do assigns = assigns |> assign_read_only() |> assign_new(:id, fn -> "native-input-#{System.unique_integer([:positive])}" end) |> assign_new(:value, fn -> nil end) |> assign(:options, assigns[:options] || []) ~H"""
<%= if @error != [] do %> {render_slot(@error, msg)} <% else %> {msg} <% end %>
""" end def native_input(assigns) do assigns = assigns |> assign_read_only() |> assign_new(:id, fn -> "native-input-#{System.unique_integer([:positive])}" end) |> assign_new(:value, fn -> nil end) |> assign(:show_icon, show_icon?(assigns)) ~H"""
{render_slot(icon)}
<%= if @error != [] do %> {render_slot(@error, msg)} <% else %> {msg} <% end %>
""" end defp assign_read_only(assigns) do read_only = assigns[:read_only] == true || assigns.rest[:readonly] == true || assigns.rest[:readonly] == "readonly" rest = if read_only do Map.put(assigns.rest, :readonly, true) else assigns.rest end assign(assigns, read_only: read_only, rest: rest) end defp error_wrapper_class(error_slot) do case List.first(List.wrap(error_slot)) do m when is_map(m) -> m |> Map.new(fn {k, v} -> {to_string(k), v} end) |> Map.get("class") _ -> nil end end defp show_icon?(%{type: type, icon: icon}) do icon != [] and type not in @types_ignoring_icon end end