Current section

Files

Jump to
primer_live lib helpers form_helpers.ex
Raw

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 """
Returns a `PrimerLive.FieldState` struct to facilitate display logic in component rendering functions.
iex> PrimerLive.Helpers.FormHelpers.field_state(:f, :first_name, nil)
%PrimerLive.FieldState{valid?: false, changeset: nil, message: nil, field_errors: []}
# If validation_message_fn returns a string it will be added to FieldState, regardless of the changeset action value:
iex> PrimerLive.Helpers.FormHelpers.field_state(
...> %Phoenix.HTML.Form{
...> source: %Ecto.Changeset{
...> action: :update,
...> changes: %{first_name: "annette"},
...> errors: [],
...> data: nil,
...> valid?: true
...> },
...> },
...> :first_name, fn _field_state -> "always" end)
%PrimerLive.FieldState{valid?: true, changeset: %Ecto.Changeset{action: :update, changes: %{first_name: "annette"}, data: nil, errors: [], valid?: true}, message: "always", field_errors: []}
# If changeset action is :validate and no validation_message_fn is provided, the default field error is added to FieldState:
iex> PrimerLive.Helpers.FormHelpers.field_state(
...> %Phoenix.HTML.Form{
...> source: %Ecto.Changeset{
...> action: :validate,
...> changes: %{},
...> errors: [first_name: {"can't be blank", [validation: :required]}],
...> data: nil,
...> valid?: true
...> },
...> },
...> :first_name, nil)
%PrimerLive.FieldState{valid?: false, changeset: %Ecto.Changeset{action: :validate, changes: %{}, data: nil, errors: [first_name: {"can't be blank", [validation: :required]}], valid?: true}, message: "can't be blank", field_errors: ["can't be blank"]}
# If changeset action is :update and no validation_message_fn is provided, no message is added to FieldState:
iex> PrimerLive.Helpers.FormHelpers.field_state(
...> %Phoenix.HTML.Form{
...> source: %Ecto.Changeset{
...> action: :update,
...> changes: %{},
...> errors: [first_name: {"can't be blank", [validation: :required]}],
...> data: nil,
...> valid?: true
...> },
...> },
...> :first_name, nil)
%PrimerLive.FieldState{valid?: false, changeset: %Ecto.Changeset{action: :update, changes: %{}, data: nil, errors: [first_name: {"can't be blank", [validation: :required]}], valid?: true}, message: "can't be blank", field_errors: ["can't be blank"]}
"""
def field_state(form, field, validation_message_fn) do
changeset = form_changeset(form)
get_field_state_for_changeset(
changeset,
%PrimerLive.FieldState{},
field,
validation_message_fn
)
end
defp get_field_state_for_changeset(changeset, field_state, _field, _validation_message_fn)
when is_nil(changeset),
do: field_state
defp get_field_state_for_changeset(changeset, field_state, field, validation_message_fn) do
with field_errors <- get_field_errors(changeset, field),
valid? <- field_errors == [],
field_state <- %{
field_state
| valid?: valid?,
ignore_errors?: is_nil(changeset.action) && !valid?,
field_errors: field_errors,
changeset: changeset
},
message <-
get_message(changeset.action, field_errors, field_state, validation_message_fn) do
%{field_state | message: message}
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
validation_message_fn.(field_state)
end
@doc """
Returns the form changeset extracted from `form.source`.
Returns nil if no changeset is found.
iex> PrimerLive.Helpers.FormHelpers.form_changeset(nil)
nil
iex> PrimerLive.Helpers.FormHelpers.form_changeset(%{})
nil
iex> PrimerLive.Helpers.FormHelpers.form_changeset(%Phoenix.HTML.Form{})
nil
iex> PrimerLive.Helpers.FormHelpers.form_changeset(%Phoenix.HTML.Form{
...> source: %Ecto.Changeset{}
...> })
%Ecto.Changeset{action: nil, changes: %{}, errors: [], data: nil, valid?: false}
"""
def form_changeset(%Phoenix.HTML.Form{source: %Ecto.Changeset{}} = form) do
form.source
end
def form_changeset(_form), do: nil
@doc """
Returns all errors for a given field from a changeset.
iex> PrimerLive.Helpers.FormHelpers.get_field_errors(
...> %{
...> action: :update,
...> changes: %{},
...> errors: [],
...> data: nil,
...> valid?: true
...> }, :first_name)
[]
iex> PrimerLive.Helpers.FormHelpers.get_field_errors(
...> %{
...> action: :update,
...> changes: %{},
...> errors: [
...> first_name: {"can't be blank", [validation: :required]},
...> work_experience: {"invalid value", [validation: :required]}
...> ],
...> data: nil,
...> valid?: true
...> }, :first_name)
["can't be blank"]
iex> PrimerLive.Helpers.FormHelpers.get_field_errors(
...> %{
...> action: :update,
...> changes: %{},
...> errors: [
...> first_name: {"can't be blank", [validation: :required]},
...> work_experience: {"invalid value", [validation: :required]}
...> ],
...> data: nil,
...> valid?: true
...> }, :work_experience)
["invalid value"]
iex> PrimerLive.Helpers.FormHelpers.get_field_errors(
...> %{
...> action: :update,
...> changes: %{},
...> errors: [
...> first_name: {"should be at most %{count} character(s)",
...> [count: 255, validation: :length, kind: :max, type: :string]}
...> ],
...> data: nil,
...> valid?: true
...> }, :first_name)
["should be at most 255 character(s)"]
"""
def get_field_errors(changeset, field) do
changeset.errors
|> Enum.filter(fn {error_field, _content} -> error_field == field end)
|> Enum.map(fn {_error_field, {content, details}} ->
interpolate_error_detials(content, details)
end)
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_detials(content, details) do
Enum.reduce(details, content, fn {key, value}, content ->
String.replace(content, "%{#{key}}", "#{inspect(value)}")
end)
end
end