Packages
pages
3.4.1
4.1.0
4.0.0
3.5.0
3.4.2
3.4.1
3.4.0
3.3.0
3.2.0
3.1.0
3.0.1
3.0.0
2.3.0
2.2.1
2.2.0
2.1.1
2.1.0
2.0.0
1.3.1
1.3.0
1.2.0
1.1.0
1.0.3
1.0.2
1.0.1
1.0.0
0.14.0
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.1
0.11.0
0.10.1
0.10.0
0.9.0
0.8.0
0.7.1
0.7.0
0.6.2
0.6.1
0.6.0
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.3
0.4.2
0.4.1
0.4.0
0.3.1
0.3.0
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Page pattern for interacting with web pages
Current section
Files
Jump to
Current section
Files
lib/pages/form.ex
defmodule Pages.Form do
@moduledoc false
# Form parsing an manipulation for drivers that do not implement change tracking natively,
# for instance when using Phoenix.ConnTest. This module takes HTML and applies form field
# updates by assigning to the `value` attributes of input fields.
alias HtmlQuery, as: Hq
@type t() :: %__MODULE__{}
defstruct ~w[
errors
data
form_html
id
selector
]a
@spec build(Hq.html(), Hq.Css.selector()) :: {:ok, t()} | {:error, String.t()}
def build(html, selector) do
form = html |> new(selector)
if Enum.any?(form.errors),
do: {:error, Enum.join(form.errors, "\n\n")},
else: {:ok, form}
end
@spec set(t(), keyword() | map()) :: {:ok, t()}
def set(form, data) do
{:ok, %{form | data: data}}
end
@spec to_post(t()) :: {String.t(), map()}
def to_post(form) do
action = Hq.attr(form.form_html, "action")
{action, form.data}
end
@spec update_html(t(), Hq.html()) :: {:ok, binary()}
def update_html(form, html) do
form_inputs = flatten_form_data(form.data)
form_html = update_form_html(form.form_html, form_inputs)
html =
html
|> Hq.parse()
|> Floki.traverse_and_update(fn
{"form", attrs, _children} = f ->
if current_id?(attrs, form.id) do
form_html
else
f
end
other ->
other
end)
{:ok, Floki.raw_html(html)}
end
# # #
defp current_id?(attrs, id) do
Enum.find_value(attrs, fn
{"id", ^id} -> true
_ -> false
end)
end
defp flatten_form_data(data) do
data
|> Enum.reduce(%{}, fn
{key, values}, acc when is_map(values) or is_list(values) ->
Enum.reduce(values, acc, fn {inner_key, value}, acc ->
key = Phoenix.HTML.Form.input_name(key, inner_key)
Map.put(acc, key, value)
end)
{key, value}, acc ->
Map.put(acc, to_string(key), value)
end)
end
defp new(html, selector) do
form = Hq.find(html, selector)
if form do
id = form |> Hq.attr("id")
fields = Hq.form_fields(form)
__struct__(selector: selector, id: id, errors: [], data: fields, form_html: form)
else
errors =
[
"""
No form found for selector: #{Hq.Css.selector(selector)}
"""
]
__struct__(selector: selector, errors: errors)
end
end
defp update_form_html(html, inputs) do
html
|> Hq.parse()
|> Floki.traverse_and_update(fn
{"input", attrs, children} = input ->
{"type", type} = List.keyfind(attrs, "type", 0)
if type == "hidden",
do: input,
else: {"input", update_value(attrs, type, inputs), children}
{"select", attrs, children} ->
{"name", name} = List.keyfind(attrs, "name", 0) || {"name", nil}
value = Map.get(inputs, name)
{"select", attrs, find_and_update_options(children, value)}
other ->
other
end)
end
defp find_and_update_options(options, value) do
Floki.traverse_and_update(options, fn
{"option", attrs, children} ->
attrs =
attrs
|> List.keydelete("selected", 0)
|> then(&maybe_select(&1, value, children))
{"option", attrs, children}
other ->
other
end)
end
defp maybe_select(attrs, value, children) do
cond do
List.keyfind(attrs, "value", 0) == {"value", value} ->
[{"selected", "selected"} | attrs]
Enum.member?(children, value) ->
[{"selected", "selected"} | attrs]
true ->
attrs
end
end
@text_input_types ~w[email number password search tel text textarea url]
defp update_value(attrs, type, inputs) when type in @text_input_types do
{"name", name} = List.keyfind(attrs, "name", 0) || {"name", nil}
value = Map.get(inputs, name)
if value,
do: List.keyreplace(attrs, "value", 0, {"value", value}),
else: List.keydelete(attrs, "value", 0)
end
defp update_value(attrs, "checkbox", inputs) do
{"name", name} = List.keyfind(attrs, "name", 0) || {"name", nil}
{"value", field_value} = List.keyfind(attrs, "value", 0)
checked? = List.keymember?(attrs, "checked", 0)
value = Map.get(inputs, name) |> to_string()
cond do
checked? && field_value == value -> attrs
field_value == value -> [{"checked", "checked"} | attrs]
true -> List.keydelete(attrs, "checked", 0)
end
end
defp update_value(attrs, "radio", inputs) do
{"name", name} = List.keyfind(attrs, "name", 0) || {"name", nil}
{"value", field_value} = List.keyfind(attrs, "value", 0)
checked? = List.keymember?(attrs, "checked", 0)
value = Map.get(inputs, name) |> to_string()
cond do
checked? && field_value == value -> attrs
field_value == value -> [{"checked", "checked"} | attrs]
true -> List.keydelete(attrs, "checked", 0)
end
end
end