defmodule PlausibleWeb.Live.Components.Form do @moduledoc """ Generic components stolen from mix phx.new templates """ use Phoenix.Component @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!"]} /> """ @default_input_class "text-sm text-gray-900 dark:text-white dark:bg-gray-750 block pl-3.5 py-2.5 border-gray-300 dark:border-gray-800 transition-all duration-150 focus:outline-none focus:ring-3 focus:ring-indigo-500/20 dark:focus:ring-indigo-500/25 focus:border-indigo-500 rounded-md disabled:bg-gray-100 disabled:dark:bg-gray-800 disabled:border-gray-200 disabled:dark:border-gray-800 disabled:text-gray-900/40 disabled:dark:text-white/30 disabled:cursor-not-allowed" attr(:id, :any, default: nil) attr(:name, :any) attr(:label, :string, default: nil) attr(:help_text, :string, default: nil) attr(:value, :any) attr(:width, :string, default: "w-full") attr(:type, :string, default: "text", values: ~w(checkbox color date datetime-local email file hidden month number password range radio search select tel text textarea time url week) ) attr(:field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form, for example: @form[:email]" ) attr(:errors, :list, default: []) 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(: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 x-model) ) attr(:class, :any, default: @default_input_class) attr(:mt?, :boolean, default: true) attr(:max_one_error, :boolean, default: false) slot(:help_content) slot(:inner_block) 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, class: assigns.class, mt?: assigns.mt?, width: assigns.width ) |> 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: "select"} = assigns) do ~H"""
<.label for={@id} class="mb-1.5">{@label}

{@help_text}

<.error :for={msg <- @errors}>{msg}
""" end def input(%{type: "checkbox"} = assigns) do assigns = assign_new(assigns, :checked, fn -> Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value]) end) ~H"""
<.label for={@id} class="font-normal gap-x-2 flex flex-inline items-center sm:justify-start justify-center " > {@label}
""" end def input(%{type: "radio"} = assigns) do ~H"""
<.label :if={@label} class="flex flex-col flex-inline" for={@id}> {@label} {@help_text} {render_slot(@help_content)}
""" end def input(%{type: "textarea"} = assigns) do ~H"""
<.label class="mb-1.5" for={@id}>{@label} <.error :for={msg <- @errors}>{msg}
""" end # All other inputs text, datetime-local, url, password, etc. are handled here... def input(assigns) do errors = if assigns.max_one_error do Enum.take(assigns.errors, 1) else assigns.errors end assigns = assign(assigns, :errors, errors) ~H"""
<.label :if={@label != nil and @label != ""} for={@id} class="mb-1.5"> {@label}

{@help_text}

{render_slot(@inner_block)} <.error :for={msg <- @errors}> {msg}
""" end attr(:rest, :global) attr(:id, :string, required: true) attr(:name, :string, required: true) attr(:label, :string, default: nil) attr(:value, :string, default: "") def input_with_clipboard(assigns) do class = [@default_input_class, "pr-20 w-full"] assigns = assign(assigns, class: class) ~H"""
<.label for={@id} class="mb-2"> {@label}
<.input mt?={false} id={@id} name={@name} value={@value} type="text" readonly="readonly" class={@class} {@rest} /> COPY
""" end attr(:id, :any, default: nil) attr(:label, :string, default: nil) attr(:field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form, for example: @form[:password]", required: true ) attr(:strength, :any) attr(:rest, :global, include: ~w(autocomplete disabled form maxlength minlength readonly required size) ) def password_input_with_strength(%{field: field} = assigns) do {too_weak?, errors} = case pop_strength_errors(field.errors) do {strength_errors, other_errors} when strength_errors != [] -> {true, other_errors} {[], other_errors} -> {false, other_errors} end strength = if too_weak? and assigns.strength.score >= 3 do %{assigns.strength | score: 2} else assigns.strength end assigns = assigns |> assign(:too_weak?, too_weak?) |> assign(:field, %{field | errors: errors}) |> assign(:strength, strength) |> assign( :show_meter?, Phoenix.Component.used_input?(field) && (too_weak? || strength.score > 0) ) ~H""" <.input field={@field} type="password" autocomplete="new-password" label={@label} id={@id} {@rest}> <.strength_meter :if={@show_meter?} {@strength} /> """ end attr(:minimum, :integer, required: true) attr(:class, :any) attr(:ok_class, :any) attr(:error_class, :any) attr(:field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form, for example: @form[:password]", required: true ) def password_length_hint(%{field: field} = assigns) do {strength_errors, _} = pop_strength_errors(field.errors) ok_class = assigns[:ok_class] || "text-gray-500" error_class = assigns[:error_class] || "text-red-500" class = assigns[:class] || ["text-xs", "mt-1"] color = if :length in strength_errors do error_class else ok_class end final_class = [color | class] assigns = assign(assigns, :class, final_class) ~H"""

Min {@minimum} characters

""" end defp pop_strength_errors(errors) do Enum.reduce(errors, {[], []}, fn {_, meta} = error, {detected, other_errors} -> cond do meta[:validation] == :required -> {[:required | detected], other_errors} meta[:validation] == :length and meta[:kind] == :min -> {[:length | detected], other_errors} meta[:validation] == :strength -> {[:strength | detected], other_errors} true -> {detected, [error | other_errors]} end end) end attr(:score, :integer, default: 0) attr(:warning, :string, default: "") attr(:suggestions, :list, default: []) def strength_meter(assigns) do color = cond do assigns.score <= 1 -> ["bg-red-500", "dark:bg-red-500"] assigns.score == 2 -> ["bg-red-300", "dark:bg-red-300"] assigns.score == 3 -> ["bg-indigo-300", "dark:bg-indigo-300"] assigns.score >= 4 -> ["bg-indigo-600", "dark:bg-indigo-500"] end feedback = cond do assigns.warning != "" -> assigns.warning <> "." assigns.suggestions != [] -> List.first(assigns.suggestions) true -> nil end assigns = assigns |> assign(:color, color) |> assign(:feedback, feedback) ~H"""
to_string(@score * 25) <> "%"]} >

Password is too weak

{@feedback}

""" end @doc """ Renders a label. """ attr :for, :string, default: nil slot :inner_block, required: true attr :class, :string, default: "" 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 def translate_error({msg, opts}) do Enum.reduce(opts, msg, fn {key, value}, acc -> String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end) end) end attr :conn, Plug.Conn, required: true attr :name, :string, required: true attr :options, :list, required: true attr :value, :any, default: nil attr :href_base, :string, default: "/" attr :selected_fn, :any, required: true def mobile_nav_dropdown(%{options: options} = assigns) do assigns = assign(assigns, :options, flatten_options(options)) ~H""" <.form for={@conn} class="lg:hidden py-4"> <.input value={ @options |> Enum.find_value(fn {_k, v} -> apply(@selected_fn, [v]) && v end) } name={@name} type="select" options={@options} onchange={"if (event.target.value) { location.href = '#{@href_base}' + event.target.value }"} class="dark:bg-gray-800 mt-1 block w-full pl-3.5 pr-10 py-2.5 text-base border-gray-300 dark:border-gray-500 outline-hidden focus:outline-hidden focus:ring-indigo-500 focus:border-indigo-500 rounded-md dark:text-gray-100" /> """ end defp flatten_options(options, prefix \\ "") do options |> Enum.map(fn {key, suboptions} when is_list(suboptions) -> flatten_options(suboptions, prefix <> key <> ": ") {key, value} when is_binary(value) -> {prefix <> key, value} %{value: value, key: key} when is_binary(value) -> {prefix <> key, value} %{value: submenu_items, key: parent_key} when is_list(submenu_items) -> Enum.map(submenu_items, fn submenu_item -> {"#{prefix}#{parent_key}: #{submenu_item.key}", submenu_item.value} end) end) |> List.flatten() end end