Current section
Files
Jump to
Current section
Files
lib/cerberus/driver/live/form_data.ex
defmodule Cerberus.Driver.Live.FormData do
@moduledoc false
alias Cerberus.Html
alias Cerberus.Session
alias Plug.Conn.Query
@spec put_form_value(map(), String.t() | nil, String.t(), term()) :: map()
def put_form_value(form_data, form, name, value) do
put_form_value(form_data, form, nil, name, value)
end
@spec put_form_value(map(), String.t() | nil, String.t() | nil, String.t(), term()) :: map()
def put_form_value(form_data, form, form_selector, name, value) do
%{active_form: active_form, values: values} = normalize_form_data(form_data)
key = form_key(form, form_selector, active_form)
form_values = Map.get(values, key, %{})
next_values = Map.put(values, key, Map.put(form_values, name, value))
%{
active_form: key,
active_form_selector: submit_form_selector(%{form: form, form_selector: form_selector}),
values: next_values
}
end
@spec params_for_submit(struct(), map(), String.t() | nil) :: map()
def params_for_submit(session, button, form_selector) do
params =
session
|> submit_defaults_for_selector(form_selector)
|> Map.merge(pruned_params_for_form(session, button.form, form_selector))
case button_payload(button) do
nil -> params
{name, value} -> Map.put(params, name, value)
end
end
@spec clear_submitted_form(map(), String.t() | nil) :: map()
def clear_submitted_form(form_data, form), do: clear_submitted_form(form_data, form, nil)
@spec clear_submitted_form(map(), String.t() | nil, String.t() | nil) :: map()
def clear_submitted_form(form_data, form, form_selector) do
%{active_form: active_form, values: values} = normalize_form_data(form_data)
key = form_key(form, form_selector, active_form)
%{active_form: nil, active_form_selector: nil, values: Map.delete(values, key)}
end
@spec active_form_selector(map()) :: String.t() | nil
def active_form_selector(form_data) do
%{active_form: active_form} = normalized = normalize_form_data(form_data)
case Map.get(normalized, :active_form_selector) do
selector when is_binary(selector) and selector != "" -> selector
_ -> selector_from_key(active_form)
end
end
@spec submit_form_payload(struct(), map()) :: map()
def submit_form_payload(session, button) do
defaults = submit_form_defaults(session, button)
form_selector = submit_form_selector(button)
active = pruned_params_for_form(session, button.form, form_selector)
defaults |> Map.merge(active) |> decode_query_params()
end
@spec submit_form_selector(map()) :: String.t() | nil
def submit_form_selector(%{form_selector: selector}) when is_binary(selector) and selector != "", do: selector
def submit_form_selector(%{form: form}) when is_binary(form) and form != "" do
~s(form[id="#{form}"])
end
def submit_form_selector(_), do: nil
@spec button_payload_map(map()) :: map()
def button_payload_map(button) do
case button_payload(button) do
nil -> %{}
{name, value} -> %{name => value}
end
end
@spec form_payload_for_change(struct(), map()) :: map()
def form_payload_for_change(session, field) do
defaults = form_defaults_for_change(session, field)
active = pruned_active_form_values(session, field)
defaults
|> Map.merge(active)
|> decode_query_params()
end
@spec toggled_checkbox_value(struct(), map(), boolean()) :: term()
def toggled_checkbox_value(session, field, checked?) do
name = field.name
defaults = form_defaults_for_change(session, field)
active = pruned_active_form_values(session, field)
current = Map.get(active, name, Map.get(defaults, name))
input_value = field[:input_value] || "on"
hidden_unchecked_value = checkbox_hidden_unchecked_value(session, field)
if String.ends_with?(name, "[]") do
current_list = checkbox_value_list(current)
if checked? do
ensure_checkbox_value(current_list, input_value)
else
Enum.reject(current_list, &(&1 == input_value))
end
else
if checked? do
input_value
else
checkbox_unchecked_value(defaults, name, input_value, hidden_unchecked_value)
end
end
end
@spec select_value_for_update(struct(), map(), term(), [String.t()], boolean(), atom()) :: term()
def select_value_for_update(_session, _field, _option, values, false, _route_kind) do
List.first(values)
end
def select_value_for_update(session, field, option, values, true, _route_kind) do
selected_values =
if is_list(option) do
values
else
current_multi_select_values(session, field) ++ values
end
uniq_values(selected_values)
end
@spec upload_value_for_update(struct(), map(), map(), String.t(), atom()) :: term()
def upload_value_for_update(session, field, file, source_path, _route_kind) do
upload = %Plug.Upload{
path: source_path,
filename: file.file_name,
content_type: file.mime_type
}
if String.ends_with?(field.name, "[]") do
defaults = form_defaults_for_change(session, field)
active = pruned_active_form_values(session, field)
current = Map.get(active, field.name, Map.get(defaults, field.name))
checkbox_value_list(current) ++ [upload]
else
upload
end
end
@spec decode_query_params(map()) :: map()
def decode_query_params(params) when is_map(params) do
params
|> expand_query_entries()
|> Enum.map_join("&", fn {name, value} ->
"#{URI.encode_www_form(name)}=#{URI.encode_www_form(to_string(value))}"
end)
|> Query.decode()
end
@spec target_path(String.t()) :: [String.t()]
def target_path(name) when is_binary(name) do
name
|> then(&"#{URI.encode_www_form(&1)}=cerberus")
|> Query.decode()
|> map_target_path()
end
defp submit_form_defaults(session, button) do
case submit_form_selector(button) do
selector when is_binary(selector) and selector != "" ->
Html.form_defaults(session.document, selector, Session.scope(session))
_ ->
%{}
end
end
defp form_defaults_for_change(%{} = session, field) do
case field[:form_selector] do
selector when is_binary(selector) and selector != "" ->
Html.form_defaults(session.document, selector, Session.scope(session))
_ ->
%{}
end
end
defp active_form_values(form_data, field) do
%{active_form: active_form, values: values} = normalize_form_data(form_data)
key = form_key(field.form, field[:form_selector], active_form)
Map.get(values, key, %{})
end
defp pruned_active_form_values(session, field) do
active = active_form_values(session.form_data, field)
keep = form_field_name_allowlist(session, field[:form_selector])
prune_form_params(active, keep)
end
defp checkbox_value_list(nil), do: []
defp checkbox_value_list(value) when is_list(value), do: value
defp checkbox_value_list(value), do: [value]
defp current_multi_select_values(session, field) do
defaults = form_defaults_for_change(session, field)
active = pruned_active_form_values(session, field)
active
|> Map.get(field.name, Map.get(defaults, field.name))
|> to_value_list()
end
defp to_value_list(nil), do: []
defp to_value_list(value) when is_list(value), do: Enum.map(value, &to_string/1)
defp to_value_list(value), do: [to_string(value)]
defp uniq_values(values) do
Enum.reduce(values, [], fn value, acc ->
if Enum.any?(acc, &(&1 == value)), do: acc, else: acc ++ [value]
end)
end
defp ensure_checkbox_value(values, input_value) do
if Enum.any?(values, &(&1 == input_value)) do
values
else
values ++ [input_value]
end
end
defp checkbox_unchecked_value(defaults, name, input_value, hidden_unchecked_value) do
if is_binary(hidden_unchecked_value) do
hidden_unchecked_value
else
case Map.get(defaults, name) do
^input_value -> ""
nil -> ""
other -> other
end
end
end
defp checkbox_hidden_unchecked_value(session, field) do
form_selector = field[:form_selector]
if is_binary(form_selector) and form_selector != "" do
Html.checkbox_unchecked_value(session.document, form_selector, field.name, Session.scope(session))
end
end
defp expand_query_entries(params) do
Enum.flat_map(params, fn
{name, value} when is_list(value) ->
Enum.map(value, &{name, &1})
{name, value} ->
[{name, value}]
end)
end
defp map_target_path(map) when is_map(map) and map_size(map) == 1 do
[{key, value}] = Map.to_list(map)
[key | value_target_path(value)]
end
defp map_target_path(_), do: []
defp value_target_path(value) when is_map(value) and map_size(value) == 1 do
[{key, nested}] = Map.to_list(value)
[key | value_target_path(nested)]
end
defp value_target_path(value) when is_list(value), do: []
defp value_target_path(_), do: []
defp submit_defaults_for_selector(_session, selector) when selector in [nil, ""], do: %{}
defp submit_defaults_for_selector(session, selector) when is_binary(selector) do
Html.form_defaults(session.document, selector, Session.scope(session))
end
@spec pruned_params_for_form(struct(), String.t() | nil, String.t() | nil) :: map()
def pruned_params_for_form(session, form, form_selector) do
active = params_for_form(session.form_data, form, form_selector)
keep = form_field_name_allowlist(session, form_selector)
prune_form_params(active, keep)
end
defp params_for_form(form_data, form, form_selector) do
%{active_form: active_form, values: values} = normalize_form_data(form_data)
key = form_key(form, form_selector, active_form)
Map.get(values, key, %{})
end
defp form_field_name_allowlist(_session, selector) when selector in [nil, ""], do: nil
defp form_field_name_allowlist(session, selector) do
Html.form_field_names(session.document, selector, Session.scope(session))
end
defp prune_form_params(params, nil) when is_map(params), do: params
defp prune_form_params(params, %MapSet{} = keep) when is_map(params), do: Map.take(params, MapSet.to_list(keep))
defp normalize_form_data(%{active_form: active_form, values: values} = data) when is_map(values) do
selector = Map.get(data, :active_form_selector) || selector_from_key(active_form)
Map.put(data, :active_form_selector, selector)
end
defp normalize_form_data(values) when is_map(values) do
%{active_form: "__default__", active_form_selector: nil, values: %{"__default__" => values}}
end
defp normalize_form_data(_), do: empty_form_data()
defp empty_form_data do
%{active_form: nil, active_form_selector: nil, values: %{}}
end
defp form_key(form, _form_selector, _active_form) when is_binary(form) and form != "", do: "form:" <> form
defp form_key(_form, form_selector, _active_form) when is_binary(form_selector) and form_selector != "",
do: "selector:" <> form_selector
defp form_key(_form, _form_selector, active_form) when is_binary(active_form), do: active_form
defp form_key(_form, _form_selector, _active_form), do: "__default__"
defp selector_from_key("form:" <> form_id), do: ~s(form[id="#{form_id}"])
defp selector_from_key("selector:" <> selector) when selector != "", do: selector
defp selector_from_key(_), do: nil
@spec button_payload(map()) :: {String.t(), String.t()} | nil
def button_payload(button) do
case {button.button_name, button.button_value} do
{name, value} when is_binary(name) and name != "" -> {name, value || ""}
_ -> nil
end
end
end