Current section
Files
Jump to
Current section
Files
lib/helpers/form_helpers.ex
defmodule PrimerLive.Helpers.FormHelpers do
@moduledoc false
# Helper functions for components that interact with forms and changesets.
use Phoenix.HTML
# Map of input name to atom value that can be used by Phoenix.HTML.Form to render the appropriate input element.
@text_input_types %{
"color" => :color_input,
"date" => :date_input,
"datetime-local" => :datetime_local_input,
"email" => :email_input,
"file" => :file_input,
"hidden" => :hidden_input,
"number" => :number_input,
"password" => :password_input,
"range" => :range_input,
"search" => :search_input,
"telephone" => :telephone_input,
"text" => :text_input,
"textarea" => :textarea,
"time" => :time_input,
"url" => :url_input
}
def text_input_types(), do: @text_input_types
@doc """
Validates if the input is a form.
"""
def is_form(%Phoenix.HTML.Form{}), do: true
# Handle other types of forms
def is_form(%_{} = maybe_form)
when not is_nil(maybe_form.__struct__) do
form_type = "#{maybe_form.__struct__}"
cond do
form_type |> String.contains?("AshPhoenix.Form") ->
true
true ->
false
end
end
def is_form(_), do: false
@doc """
Returns a `PrimerLive.FieldState` struct to facilitate display logic in component rendering functions.
iex> PrimerLive.Helpers.FormHelpers.field_state(:f, :first_name, nil, nil)
%PrimerLive.FieldState{valid?: false, changeset: nil, message: nil, field_errors: [], caption: nil}
# If validation_message_fn returns a string it will be added to FieldState, regardless of the changeset action value:
iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
...> update_changeset = %Ecto.Changeset{changeset |
...> action: :update,
...> changes: %{first_name: "annette"}
...> }
...> PrimerLive.Helpers.FormHelpers.field_state(
...> %Phoenix.HTML.Form{
...> impl: Phoenix.HTML.FormData.Ecto.Changeset,
...> source: update_changeset
...> },
...> :first_name,
...> fn _field_state -> "always" end,
...> nil
...> )
%PrimerLive.FieldState{valid?: false, required?: true, changeset: update_changeset, message: "always", field_errors: ["can't be blank"]}
# If changeset action is :validate and no validation_message_fn is provided, the default field error is added to FieldState:
iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
...> validate_changeset = %Ecto.Changeset{changeset |
...> action: :validate,
...> changes: %{first_name: "annette"}
...> }
...> PrimerLive.Helpers.FormHelpers.field_state(
...> %Phoenix.HTML.Form{
...> impl: Phoenix.HTML.FormData.Ecto.Changeset,
...> source: validate_changeset
...> },
...> :first_name,
...> nil,
...> nil
...> )
%PrimerLive.FieldState{valid?: false, required?: true, changeset: validate_changeset, message: "can't be blank", field_errors: ["can't be blank"]}
# Custom success message: if changeset action is :update and a validation_message_fn is provided, the resulting message is added to FieldState:
iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
...> update_changeset = %Ecto.Changeset{changeset |
...> action: :update,
...> changes: %{first_name: "annette"},
...> errors: [],
...> valid?: true,
...> }
...> PrimerLive.Helpers.FormHelpers.field_state(
...> %Phoenix.HTML.Form{
...> impl: Phoenix.HTML.FormData.Ecto.Changeset,
...> source: update_changeset,
...> },
...> :first_name,
...> fn field_state -> if field_state.valid?, do: "Great!" end,
...> nil
...> )
%PrimerLive.FieldState{valid?: true, required?: true, changeset: update_changeset, message: "Great!", field_errors: []}
# Caption: return a caption if changeset is nil
iex> PrimerLive.Helpers.FormHelpers.field_state(nil, nil, nil, fn _field_state -> "Caption" end)
%PrimerLive.FieldState{required?: false, valid?: false, changeset: nil, message: nil, field_errors: [], caption: "Caption"}
# Caption: return a static caption
iex> PrimerLive.Helpers.FormHelpers.field_state(:f, :first_name, nil, "Caption")
%PrimerLive.FieldState{required?: false, valid?: false, changeset: nil, message: nil, field_errors: [], caption: "Caption"}
# Caption: return a caption regardless the state
iex> PrimerLive.Helpers.FormHelpers.field_state(:f, :first_name, nil, fn _field_state -> "Caption" end)
%PrimerLive.FieldState{required?: false, valid?: false, changeset: nil, message: nil, field_errors: [], caption: "Caption"}
# Caption: return a caption dependent on the state
iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
...> validate_changeset = %Ecto.Changeset{changeset |
...> action: :validate,
...> changes: %{first_name: "annette"}
...> }
...> PrimerLive.Helpers.FormHelpers.field_state(
...> %Phoenix.HTML.Form{
...> impl: Phoenix.HTML.FormData.Ecto.Changeset,
...> source: validate_changeset
...> },
...> :first_name,
...> nil,
...> fn field_state -> if !field_state.valid?, do: "Please select your availability" end
...> )
%PrimerLive.FieldState{caption: "Please select your availability", changeset: validate_changeset, field_errors: ["can't be blank"], ignore_errors?: false, message: "can't be blank", message_id: nil, required?: true, valid?: false}
"""
def field_state(maybe_form, field, validation_message_fn, caption_fn) do
form = get_form(maybe_form)
changeset = get_form_changeset(form)
get_field_state_for_changeset(
form,
changeset,
%PrimerLive.FieldState{},
field,
validation_message_fn,
caption_fn
)
end
defp get_field_state_for_changeset(
_form,
changeset,
field_state,
_field,
_validation_message_fn,
caption_fn
)
when is_nil(changeset) do
caption = get_caption(field_state, caption_fn)
%{field_state | caption: caption}
end
defp get_field_state_for_changeset(
form,
changeset,
field_state,
field,
validation_message_fn,
caption_fn
) do
with field_errors <- get_field_errors(changeset, field),
required? <- get_field_required(form, changeset, field),
valid? <- Enum.count(field_errors) == 0,
field_state <- %{
field_state
| valid?: valid?,
ignore_errors?: is_nil(changeset.action) && !valid?,
field_errors: field_errors,
changeset: changeset,
required?: required?
},
message <-
get_message(changeset.action, field_errors, field_state, validation_message_fn),
caption <-
get_caption(field_state, caption_fn) do
%{field_state | message: message, caption: caption}
end
end
# changeset.action is nil
defp get_message(nil, _field_errors, _field_state, _), do: nil
# validation_message_fn is nil, field_errors is empty
defp get_message(_changeset_action, field_errors, _field_state, nil) when field_errors == [],
do: nil
# validation_message_fn is nil, field_errors exists
defp get_message(_changeset_action, field_errors, _field_state, nil) do
[field_error | _rest] = field_errors
field_error
end
# validation_message_fn exists
defp get_message(_changeset_action, _field_errors, field_state, validation_message_fn) do
case :erlang.fun_info(validation_message_fn)[:arity] do
0 -> validation_message_fn.()
1 -> validation_message_fn.(field_state)
_ -> nil
end
end
defp get_caption(_field_state, nil), do: nil
defp get_caption(_field_state, []), do: nil
defp get_caption(_field_state, caption_str) when is_binary(caption_str), do: caption_str
defp get_caption(_field_state, caption_slot) when is_list(caption_slot), do: nil
defp get_caption(field_state, caption_fn) do
case :erlang.fun_info(caption_fn)[:arity] do
0 -> caption_fn.()
1 -> caption_fn.(field_state)
_ -> nil
end
end
@doc """
Extracts the form struct from either a form struct or a nested struct that contains a form.
iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
...> PrimerLive.Helpers.FormHelpers.get_form(%Phoenix.HTML.Form{
...> impl: Phoenix.HTML.FormData.Ecto.Changeset,
...> source: changeset
...> })
%Phoenix.HTML.Form{source: changeset, impl: Phoenix.HTML.FormData.Ecto.Changeset}
iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
...> PrimerLive.Helpers.FormHelpers.get_form(%Phoenix.HTML.Form{
...> impl: Phoenix.HTML.FormData.Ecto.Changeset,
...> source: %Phoenix.HTML.Form{
...> impl: Phoenix.HTML.FormData.Ecto.Changeset,
...> source: %Ecto.Changeset{changeset |
...> action: :update,
...> changes: %{first_name: "annette"}
...> }
...> }
...> })
%Phoenix.HTML.Form{source: %Ecto.Changeset{changeset | action: :update, changes: %{first_name: "annette"}}, impl: Phoenix.HTML.FormData.Ecto.Changeset}
iex> PrimerLive.Helpers.FormHelpers.get_form(%PrimerLive.Helpers.TestForm{
...> impl: Phoenix.HTML.FormData.Ecto.Changeset,
...> source: %PrimerLive.Helpers.TestForm{
...> impl: Phoenix.HTML.FormData.Ecto.Changeset,
...> source: %Phoenix.HTML.Form{
...> impl: Phoenix.HTML.FormData.Ecto.Changeset
...> }
...> }
...> })
%Phoenix.HTML.Form{source: nil, impl: Phoenix.HTML.FormData.Ecto.Changeset, id: nil, name: nil, data: nil, hidden: [], params: %{}, errors: [], options: [], index: nil, action: nil}
iex> PrimerLive.Helpers.FormHelpers.get_form(:form)
nil
"""
# Handle different kinds of forms
def get_form(%_{source: _} = maybe_form) when not is_nil(maybe_form.source.__struct__) do
form_type = "#{maybe_form.source.__struct__}"
if form_type |> String.contains?("Form") do
# Find a possible nested form
get_form(maybe_form.source)
else
maybe_form
end
end
def get_form(%Phoenix.HTML.Form{} = form) do
form
end
def get_form(_), do: nil
@doc """
Gets the form name - either an atom or a string.
iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
...> PrimerLive.Helpers.FormHelpers.get_form_name(%Phoenix.HTML.Form{
...> impl: Phoenix.HTML.FormData.Ecto.Changeset,
...> name: :profile,
...> source: changeset
...> })
:profile
iex> PrimerLive.Helpers.FormHelpers.get_form_name(%PrimerLive.Helpers.TestForm{
...> impl: Phoenix.HTML.FormData.Ecto.Changeset,
...> source: %PrimerLive.Helpers.TestForm{
...> name: "test",
...> source: %Phoenix.HTML.Form{
...> name: "profile",
...> }
...> }
...> })
"profile"
iex> PrimerLive.Helpers.FormHelpers.get_form_name(:profile)
:profile
iex> PrimerLive.Helpers.FormHelpers.get_form_name("profile")
"profile"
iex> PrimerLive.Helpers.FormHelpers.get_form_name(1)
nil
iex> PrimerLive.Helpers.FormHelpers.get_form_name(nil)
nil
"""
def get_form_name(%_{source: _} = maybe_form) when not is_nil(maybe_form.source.__struct__) do
if "#{maybe_form.source.__struct__}" |> String.contains?("Form") do
get_form_name(maybe_form.source)
else
get_form_name(maybe_form.name)
end
end
def get_form_name(name) when is_atom(name), do: name
def get_form_name(name) when is_binary(name), do: name
def get_form_name(%Phoenix.HTML.Form{} = form) do
form.name
end
def get_form_name(_), do: nil
@doc """
Returns the form changeset extracted from `form.source`.
Returns nil if no changeset is found.
iex> PrimerLive.Helpers.FormHelpers.get_form_changeset(nil)
nil
iex> PrimerLive.Helpers.FormHelpers.get_form_changeset(%{})
nil
iex> PrimerLive.Helpers.FormHelpers.get_form_changeset(%Phoenix.HTML.Form{})
nil
iex> PrimerLive.Helpers.FormHelpers.get_form_changeset(%Phoenix.HTML.Form{
...> source: %Ecto.Changeset{}
...> })
%Ecto.Changeset{action: nil, changes: %{}, errors: [], data: nil, valid?: false}
"""
# Handle other types of changesets
def get_form_changeset(%_{source: _} = form_or_changeset)
when not is_nil(form_or_changeset.source.__struct__) do
form_or_changeset_type = "#{form_or_changeset.source.__struct__}"
cond do
form_or_changeset_type |> String.contains?("Ash.Changeset") ->
ash_changeset = form_or_changeset
_get_ash_framework_changeset(ash_changeset)
true ->
form_or_changeset.source
end
end
def get_form_changeset(%Phoenix.HTML.Form{source: _} = form) do
form.source
end
def get_form_changeset(_form), do: nil
# Handle other types of changesets: Ash Framework
def _get_ash_framework_changeset(ash_changeset), do: ash_changeset.source
@doc """
Returns all errors for a given field from a changeset.
iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
...> PrimerLive.Helpers.FormHelpers.get_field_errors(
...> %Ecto.Changeset{
...> changeset | action: :update, errors: []
...> },
...> :first_name
...> )
[]
iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
...> PrimerLive.Helpers.FormHelpers.get_field_errors(
...> %Ecto.Changeset{
...> changeset | action: :update,
...> errors: [
...> first_name: {"can't be blank", [validation: :required]},
...> work_experience: {"invalid value", [validation: :required]}
...> ],
...> },
...> :first_name
...> )
["can't be blank"]
iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
...> PrimerLive.Helpers.FormHelpers.get_field_errors(
...> %Ecto.Changeset{
...> changeset | action: :update,
...> errors: [
...> first_name: {"should be at most %{count} character(s)",
...> [count: 255, validation: :length, kind: :max, type: :string]}
...> ],
...> },
...> :first_name
...> )
["should be at most 255 character(s)"]
iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
...> PrimerLive.Helpers.FormHelpers.get_field_errors(
...> %Ecto.Changeset{
...> changeset | action: :update,
...> errors: [
...> first_name: {"can't be blank", [validation: :required]},
...> work_experience: {"invalid value", [validation: :required]}
...> ],
...> },
...> :work_experience
...> )
["invalid value"]
"""
def get_field_errors(%Ecto.Changeset{} = changeset, field) do
changeset.errors
|> Enum.filter(fn {error_field, _content} -> error_field == field end)
|> Enum.map(fn {_error_field, {content, details}} ->
interpolate_error_details(content, details)
end)
end
# Handle other types of changesets
def get_field_errors(%_{} = maybe_changeset, field)
when not is_nil(maybe_changeset.__struct__) do
changeset_type = "#{maybe_changeset.__struct__}"
cond do
changeset_type |> String.contains?("Ash.Changeset") ->
ash_changeset = maybe_changeset
_get_ash_framework_field_errors(ash_changeset, field)
true ->
[]
end
end
def get_field_errors(_, _), do: []
# Handle other types of changesets: Ash Framework
def _get_ash_framework_field_errors(ash_changeset, field) do
cond do
Map.has_key?(ash_changeset.attributes, field) &&
Map.get(ash_changeset.attributes, field) === nil ->
ash_changeset.errors
|> Enum.filter(fn ash_error -> ash_error.field == field end)
|> Enum.map(fn ash_error ->
{message, _binding} =
Code.eval_string("Ash.ErrorKind.message(e)", [e: ash_error],
file: __ENV__.file,
line: __ENV__.line
)
message
end)
true ->
[]
end
end
@doc """
Returns the required state for a given field from a changeset.
iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
...> PrimerLive.Helpers.FormHelpers.get_field_required(
...> %Phoenix.HTML.Form{
...> impl: Phoenix.HTML.FormData.Ecto.Changeset,
...> data: nil,
...> source: changeset
...> },
...> changeset,
...> :first_name
...> )
true
iex> changeset = PrimerLive.TestHelpers.Repo.Users.init()
...> PrimerLive.Helpers.FormHelpers.get_field_required(
...> %Phoenix.HTML.Form{
...> impl: Phoenix.HTML.FormData.Ecto.Changeset,
...> data: nil,
...> source: changeset
...> },
...> changeset,
...> :work_experience
...> )
false
"""
def get_field_required(%Phoenix.HTML.Form{} = form, %Ecto.Changeset{} = _changeset, field) do
input_validations(form, field)[:required] == true
end
# Handle other types of changesets
def get_field_required(form, %_{} = maybe_changeset, field)
when not is_nil(maybe_changeset.__struct__) do
changeset_type = "#{maybe_changeset.__struct__}"
cond do
changeset_type |> String.contains?("Ash.Changeset") ->
ash_changeset = maybe_changeset
get_ash_framework_field_required(form, ash_changeset, field)
true ->
false
end
end
def get_field_required(_, _), do: false
# Handle other types of changesets: Ash Framework
defp get_ash_framework_field_required(form, ash_changeset, field) do
Phoenix.HTML.FormData.input_validations(form, ash_changeset, field)[:required] == true
end
@doc """
Get the input type atom from a name, e.g. "search" returns :search_input
## Tests
iex> PrimerLive.Helpers.FormHelpers.text_input_type_as_atom("x")
:text_input
iex> PrimerLive.Helpers.FormHelpers.text_input_type_as_atom("text")
:text_input
iex> PrimerLive.Helpers.FormHelpers.text_input_type_as_atom("color")
:color_input
"""
def text_input_type_as_atom(type_name) do
input_type = Map.get(text_input_types(), type_name)
if is_nil(input_type), do: :text_input, else: input_type
end
defp interpolate_error_details(content, details) do
Enum.reduce(details, content, fn {key, value}, content ->
String.replace(content, "%{#{key}}", "#{inspect(value)}")
end)
end
end