Unable to test dynamic form using cast_assoc :drop_param
I found an issue related to the use of dynamic forms and associations with :drop_param, or at least, with the suggested implementation from Phoenix Live View docs.
I have a dynamic form where the user can add, sort, and delete content rows. When I submit a form with one or more content rows while testing in the browser or using PhoenixTestPlaywright, the params look like this:
%{
"content" => %{
"0" => %{"_persistent_id" => "0", ...},
"1" => %{"_persistent_id" => "1", ...}
"2" => %{"_persistent_id" => "2", ...}
},
"content_drop" => [""], <- important bit
"content_sort" => ["0", "1", "2"],
"title" => "homepage"
}
content_drop being empty results in no changes from the cast_assoc while building the changeset.
The same action, using PhoenixTest, results in params like this:
%{
"content" => %{
"0" => %{"_persistent_id" => "0", ...},
"1" => %{"_persistent_id" => "1", ...}
"2" => %{"_persistent_id" => "2", ...}
},
"content_drop" => ["0"], <-- Should be [""] not ["0"]
"content_sort" => ["0", "1", "2"],
"title" => "homepage"
}
content_drop being ["0"] results in the cast_assoc deleting the first item in the association while building the changeset.
This is likely caused by the use of this "button" disguised as an input, from the docs:
<button
type="button"
name="mailing_list[emails_drop][]"
value={ef.index}
phx-click={JS.dispatch("change")}
>
I'm not sure, but I think they suggest using a button, so this "field" is not sent to the form when saving the form, but it is sent when the user clicks on it and the change event is triggered. The phx-change event will then rebuild the changeset and remove the item. For some reason, when testing with PhoenixTest, that field is sent with the form submission. Interestingly, it is sent with the value of one of the items only (the first one), not all items (which would be the normal behavior if it were just a simple input).
More details about my implementation:
The page changeset:
def changeset(page, attrs) do
page
|> cast(attrs, [:title])
|> cast_assoc(:content,
sort_param: :content_sort,
drop_param: :content_drop,
with: &Content.changeset/3
)
end
the form:
<.form
:let={f}
id="custom-page-form"
for={@changeset}
phx-submit="save"
phx-change="validate"
>
<.input field={f[:title]} type="text" label="Title" placeholder="Enter page title" />
<ul class="d-list" id="content-list" phx-hook="SortableList">
<.inputs_for :let={content_form} field={f[:content]}>
<li>
<input type="hidden" name="page[content_sort][]" value={content_form.index} />
<.button
type="button"
name="page[content_drop][]"
value={content_form.index}
phx-click={JS.dispatch("change")}
>
<.icon name="hero-trash" class="w-6 h-6" />Delete
</.button>
</div>
</li>
</.inputs_for>
</ul>
<input type="hidden" name="page[content_drop][]" value />
</.form>
@RobertoSchneiders thanks so much for opening this issue!
Could you confirm what version of PhoenixTest you're using? I'm wondering if this is related to #203.
If you point your app to main does your test pass? If it doesn't, could you create a sample repo that reproduces the error?
@germsvel, sorry for the late response, I missed the notification.
I was using 0.6.0 when I found this issue. I'm currently on the latest version. Can you confirm if this may be fixed on v0.7.0? Then I can convert the specs back to PhoenixTest to check if they work.
This is still an issue on v0.7.0. Let me know if I should test with main.
@RobertoSchneiders sorry for the confusion. We had briefly fixed the issue, but it introduced a regression in forms that didn't have a phx-change at all (so I didn't ship it with v0.7.0). So we had to remove the "fix" for now. It's in my list of to-dos to get to this, but it's somewhat tricky to handle this.
No problem. Let me know if you find a solution.
thank you.
Isn't the root issue here that the button uses phx-change={JS.dispatch("change")} - a javascript command which phoenix_test can't handle (yet)? I imagine for this very special case phoenix_test could try to emulate JS commands.
For now I'm using this workaround
# Simulate JS.dispatch
defp click_add_row_button(conn, "Add email") do
unwrap(conn, &(&1 |> form("#user-form") |> render_change(user: [emails_sort: "new"])))
end