Skip to content

Commit 3c03e24

Browse files
committed
Include values from form in submit_form and follow_trigger_action
Closes #3885.
1 parent 5ed6479 commit 3c03e24

5 files changed

Lines changed: 155 additions & 119 deletions

File tree

lib/phoenix_live_view/test/client_proxy.ex

Lines changed: 24 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,12 @@ defmodule Phoenix.LiveViewTest.ClientProxy do
653653
end)
654654
end
655655

656+
def handle_call({:get_lazy, %Element{} = element}, _from, state) do
657+
view = fetch_view_by_topic!(state, proxy_topic(element))
658+
{state, root} = root(state, view.id)
659+
{:reply, {:ok, root}, state}
660+
end
661+
656662
def handle_call({:get_lazy, id}, _from, state) do
657663
{state, root} = root(state, id)
658664
{:reply, {:ok, root}, state}
@@ -1242,41 +1248,29 @@ defmodule Phoenix.LiveViewTest.ClientProxy do
12421248

12431249
defp maybe_values(:hook, _root, _node, _element), do: {:ok, %{}}
12441250

1245-
defp maybe_values(type, root, {tag, attrs, _} = node, element)
1251+
defp maybe_values(type, root, {tag, _, _} = node, element)
12461252
when type in [:change, :submit] do
12471253
cond do
12481254
tag == "form" ->
1249-
form_inputs = filtered_inputs(node)
1250-
1251-
{value_inputs, lazy_submitter} =
1252-
case Enum.into(attrs, %{}) do
1253-
%{"id" => id} ->
1254-
by_form_id = DOM.all(root, ~s<[form="#{id}"]>) |> DOM.to_tree()
1255-
named_inputs = filtered_inputs(by_form_id)
1256-
1257-
# All inputs including buttons
1258-
# Remove the named inputs first to remove any possible
1259-
# duplicates if the child inputs also had a form attribite.
1260-
value_inputs = (form_inputs -- named_inputs) ++ named_inputs
1261-
1262-
{value_inputs,
1263-
fn ->
1264-
# a lazy function that returns a lazy node with all form inputs
1265-
# that could be the submitter to collect the submitter by selector
1266-
DOM.all(root, ~s<
1267-
##{id} :is(input, button):not([form]:not([form="#{id}"])),
1268-
:is(input, button)[form="#{id}"]
1269-
>)
1270-
end}
1271-
1272-
_ ->
1255+
value_inputs = DOM.all_value_inputs(node, root)
1256+
defaults = DOM.collect_form_values(node, root, fn defaults -> defaults end)
1257+
1258+
lazy_submitter =
1259+
case TreeDOM.attribute(node, "id") do
1260+
nil ->
12731261
# to collect the submitter by selector,
12741262
# need to convert the tree to a lazy here :(
1275-
{form_inputs, fn -> DOM.to_lazy([node]) end}
1263+
fn -> DOM.to_lazy([node]) end
1264+
1265+
id ->
1266+
# a lazy function that returns a lazy node with all form inputs
1267+
# that could be the submitter to collect the submitter by selector
1268+
fn -> DOM.all(root, ~s<
1269+
##{id} :is(input, button):not([form]:not([form="#{id}"])),
1270+
:is(input, button)[form="#{id}"]
1271+
>) end
12761272
end
12771273

1278-
defaults = Enum.reduce(value_inputs, Query.decode_init(), &form_defaults/2)
1279-
12801274
with {:ok, defaults} <-
12811275
maybe_submitter(defaults, type, lazy_submitter, element),
12821276
{:ok, value} <-
@@ -1291,7 +1285,7 @@ defmodule Phoenix.LiveViewTest.ClientProxy do
12911285
end
12921286

12931287
type == :change and tag in ~w(input select textarea) ->
1294-
{:ok, form_defaults(node, Query.decode_init()) |> Query.decode_done()}
1288+
{:ok, DOM.collect_input_values(node)}
12951289

12961290
true ->
12971291
{:error, :invalid, "phx-#{type} is only allowed in forms, got #{inspect(tag)}"}
@@ -1308,13 +1302,6 @@ defmodule Phoenix.LiveViewTest.ClientProxy do
13081302
defp deep_merge(_target, source),
13091303
do: source
13101304

1311-
defp filtered_inputs(nodes) do
1312-
TreeDOM.filter(nodes, fn node ->
1313-
TreeDOM.tag(node) in ~w(input textarea select) and
1314-
is_nil(TreeDOM.attribute(node, "disabled"))
1315-
end)
1316-
end
1317-
13181305
defp maybe_submitter(defaults, :submit, lazy, %Element{meta: %{submitter: element}}) do
13191306
base = lazy.()
13201307

@@ -1394,81 +1381,6 @@ defmodule Phoenix.LiveViewTest.ClientProxy do
13941381
end
13951382
end
13961383

1397-
defp form_defaults(node, acc) do
1398-
tag = TreeDOM.tag(node)
1399-
1400-
if name = TreeDOM.attribute(node, "name") do
1401-
form_defaults(tag, node, name, acc)
1402-
else
1403-
acc
1404-
end
1405-
end
1406-
1407-
# Selectedness algorithm as outlined in
1408-
# https://html.spec.whatwg.org/multipage/form-elements.html#the-select-element
1409-
defp form_defaults("select", node, name, acc) do
1410-
options = TreeDOM.filter(node, &(TreeDOM.tag(&1) == "option"))
1411-
1412-
multiple_display_size =
1413-
case valid_display_size(node) do
1414-
int when is_integer(int) and int > 1 -> true
1415-
_ -> false
1416-
end
1417-
1418-
all_selected =
1419-
if TreeDOM.attribute(node, "multiple") || multiple_display_size do
1420-
Enum.filter(options, &TreeDOM.attribute(&1, "selected"))
1421-
else
1422-
List.wrap(
1423-
Enum.find(Enum.reverse(options), &TreeDOM.attribute(&1, "selected")) ||
1424-
Enum.find(options, &(!TreeDOM.attribute(&1, "disabled")))
1425-
)
1426-
end
1427-
1428-
Enum.reduce(all_selected, acc, fn selected, acc ->
1429-
Plug.Conn.Query.decode_each({name, TreeDOM.attribute(selected, "value")}, acc)
1430-
end)
1431-
end
1432-
1433-
defp form_defaults("textarea", node, name, acc) do
1434-
value = TreeDOM.to_text(node, false)
1435-
1436-
if value == "" do
1437-
Plug.Conn.Query.decode_each({name, ""}, acc)
1438-
else
1439-
Plug.Conn.Query.decode_each({name, String.replace_prefix(value, "\n", "")}, acc)
1440-
end
1441-
end
1442-
1443-
defp form_defaults("input", node, name, acc) do
1444-
type = TreeDOM.attribute(node, "type") || "text"
1445-
value = TreeDOM.attribute(node, "value") || default_value(type)
1446-
1447-
cond do
1448-
type in ["radio", "checkbox"] ->
1449-
if TreeDOM.attribute(node, "checked") do
1450-
Plug.Conn.Query.decode_each({name, value}, acc)
1451-
else
1452-
acc
1453-
end
1454-
1455-
type in ["image", "submit"] ->
1456-
acc
1457-
1458-
true ->
1459-
Plug.Conn.Query.decode_each({name, value}, acc)
1460-
end
1461-
end
1462-
1463-
defp valid_display_size(node) do
1464-
with size when not is_nil(size) <- TreeDOM.attribute(node, "size"),
1465-
{int, ""} when int > 0 <- Integer.parse(size) do
1466-
int
1467-
else
1468-
_ -> nil
1469-
end
1470-
end
1471-
14721384
defp fill_in_map([{key, value} | rest], prefix, node, acc) do
14731385
key = to_string(key)
14741386

@@ -1568,7 +1480,7 @@ defmodule Phoenix.LiveViewTest.ClientProxy do
15681480
type = TreeDOM.attribute(node, "type") || "text"
15691481

15701482
if type in ["radio", "checkbox", "hidden"] do
1571-
value = TreeDOM.attribute(node, "value") || default_value(type)
1483+
value = TreeDOM.attribute(node, "value") || DOM.default_value(type)
15721484
{[type | types], [value | values]}
15731485
else
15741486
{[type | types], values}
@@ -1592,9 +1504,6 @@ defmodule Phoenix.LiveViewTest.ClientProxy do
15921504
{types, values}
15931505
end
15941506

1595-
defp default_value("checkbox"), do: "on"
1596-
defp default_value(_type), do: ""
1597-
15981507
defp fill_in_name("", name), do: name
15991508
defp fill_in_name(prefix, name), do: prefix <> "[" <> name <> "]"
16001509

lib/phoenix_live_view/test/dom.ex

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ defmodule Phoenix.LiveViewTest.DOM do
44
@phx_component "data-phx-component"
55

66
alias Phoenix.LiveViewTest.TreeDOM, as: Tree
7+
alias Plug.Conn.Query
78

89
defguardp is_lazy(html) when is_struct(html, LazyHTML)
910

@@ -230,4 +231,120 @@ defmodule Phoenix.LiveViewTest.DOM do
230231
{attribute(node, "id"), attribute(node, "data-phx-static")}
231232
end)
232233
end
234+
235+
## Forms
236+
237+
def all_value_inputs({"form", attrs, _} = form, root) do
238+
form_inputs = filtered_inputs(form)
239+
240+
case Enum.into(attrs, %{}) do
241+
%{"id" => id} ->
242+
by_form_id = all(root, ~s<[form="#{id}"]>) |> to_tree()
243+
named_inputs = filtered_inputs(by_form_id)
244+
245+
# All inputs including buttons
246+
# Remove the named inputs first to remove any possible
247+
# duplicates if the child inputs also had a form attribite.
248+
(form_inputs -- named_inputs) ++ named_inputs
249+
250+
_ ->
251+
form_inputs
252+
end
253+
end
254+
255+
def collect_form_values(form, root, done \\ &Query.decode_done/1) do
256+
form
257+
|> all_value_inputs(root)
258+
|> Enum.reduce(Query.decode_init(), &form_defaults/2)
259+
|> then(done)
260+
end
261+
262+
def collect_input_values(node) do
263+
form_defaults(node, Query.decode_init()) |> Query.decode_done()
264+
end
265+
266+
defp form_defaults(node, acc) do
267+
tag = Tree.tag(node)
268+
269+
if name = Tree.attribute(node, "name") do
270+
form_defaults(tag, node, name, acc)
271+
else
272+
acc
273+
end
274+
end
275+
276+
# Selectedness algorithm as outlined in
277+
# https://html.spec.whatwg.org/multipage/form-elements.html#the-select-element
278+
defp form_defaults("select", node, name, acc) do
279+
options = Tree.filter(node, &(Tree.tag(&1) == "option"))
280+
281+
multiple_display_size =
282+
case valid_display_size(node) do
283+
int when is_integer(int) and int > 1 -> true
284+
_ -> false
285+
end
286+
287+
all_selected =
288+
if Tree.attribute(node, "multiple") || multiple_display_size do
289+
Enum.filter(options, &Tree.attribute(&1, "selected"))
290+
else
291+
List.wrap(
292+
Enum.find(Enum.reverse(options), &Tree.attribute(&1, "selected")) ||
293+
Enum.find(options, &(!Tree.attribute(&1, "disabled")))
294+
)
295+
end
296+
297+
Enum.reduce(all_selected, acc, fn selected, acc ->
298+
Plug.Conn.Query.decode_each({name, Tree.attribute(selected, "value")}, acc)
299+
end)
300+
end
301+
302+
defp form_defaults("textarea", node, name, acc) do
303+
value = Tree.to_text(node, false)
304+
305+
if value == "" do
306+
Plug.Conn.Query.decode_each({name, ""}, acc)
307+
else
308+
Plug.Conn.Query.decode_each({name, String.replace_prefix(value, "\n", "")}, acc)
309+
end
310+
end
311+
312+
defp form_defaults("input", node, name, acc) do
313+
type = Tree.attribute(node, "type") || "text"
314+
value = Tree.attribute(node, "value") || default_value(type)
315+
316+
cond do
317+
type in ["radio", "checkbox"] ->
318+
if Tree.attribute(node, "checked") do
319+
Plug.Conn.Query.decode_each({name, value}, acc)
320+
else
321+
acc
322+
end
323+
324+
type in ["image", "submit"] ->
325+
acc
326+
327+
true ->
328+
Plug.Conn.Query.decode_each({name, value}, acc)
329+
end
330+
end
331+
332+
def default_value("checkbox"), do: "on"
333+
def default_value(_type), do: ""
334+
335+
defp valid_display_size(node) do
336+
with size when not is_nil(size) <- Tree.attribute(node, "size"),
337+
{int, ""} when int > 0 <- Integer.parse(size) do
338+
int
339+
else
340+
_ -> nil
341+
end
342+
end
343+
344+
defp filtered_inputs(nodes) do
345+
Tree.filter(nodes, fn node ->
346+
Tree.tag(node) in ~w(input textarea select) and
347+
is_nil(Tree.attribute(node, "disabled"))
348+
end)
349+
end
233350
end

lib/phoenix_live_view/test/live_view_test.ex

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1914,15 +1914,23 @@ defmodule Phoenix.LiveViewTest do
19141914
end
19151915

19161916
def __render_trigger_submit__(%Element{} = form, name, required_attr, error_msg) do
1917+
root = call(form, {:get_lazy, form})
1918+
19171919
case render_tree(form) do
1918-
{"form", attrs, _child_nodes} ->
1920+
{"form", attrs, _child_nodes} = node ->
19191921
if not List.keymember?(attrs, required_attr, 0) do
19201922
raise ArgumentError, error_msg <> ", got: #{inspect(attrs)}"
19211923
end
19221924

19231925
{"action", path} = List.keyfind(attrs, "action", 0) || {"action", call(form, :url)}
19241926
{"method", method} = List.keyfind(attrs, "method", 0) || {"method", "get"}
1925-
{method, path, form.form_data || %{}}
1927+
1928+
values =
1929+
node
1930+
|> DOM.collect_form_values(root)
1931+
|> Map.merge(form.form_data || %{})
1932+
1933+
{method, path, values}
19261934

19271935
{tag, _, _} ->
19281936
raise ArgumentError,

test/phoenix_live_view/integrations/elements_test.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -608,12 +608,12 @@ defmodule Phoenix.LiveView.ElementsTest do
608608

609609
assert conn.method == "GET"
610610
assert conn.request_path == "/elements"
611-
assert conn.query_string == "foo=bar"
611+
assert %{"foo" => "bar", "from-form" => "included"} = URI.decode_query(conn.query_string)
612612

613613
conn = view |> form("#trigger-form-value", %{"baz" => "bat"}) |> follow_trigger_action(conn)
614614
assert conn.method == "POST"
615615
assert conn.request_path == "/not_found"
616-
assert conn.params == %{"baz" => "bat"}
616+
assert %{"baz" => "bat", "from-form" => "included"} = conn.params
617617
end
618618
end
619619

test/support/live_views/elements.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ defmodule Phoenix.LiveViewTest.Support.ElementsLive do
244244
phx-submit="form-submit-trigger"
245245
phx-trigger-action={@trigger_action}
246246
>
247+
<input type="hidden" name="from-form" value="included" />
247248
</form>
248249
249250
<form id="submit-form-default" action="/not_found"></form>
@@ -255,6 +256,7 @@ defmodule Phoenix.LiveViewTest.Support.ElementsLive do
255256
phx-submit="form-submit-trigger"
256257
phx-trigger-action={@trigger_action}
257258
>
259+
<input type="hidden" name="from-form" value="included" />
258260
</form>
259261
260262
<form id="named" phx-submit="form-submit-named">

0 commit comments

Comments
 (0)