Unexpectedly large diff in LiveView update when changing form fields

I use LiveView to display a dynamic form to the user in which selecting a value in a <select> causes other form fields (e.g. other <select> elements) to be updated. I noticed that it doesn’t quite work as expected: changing the value in the <select> causes (via phx_change) my handle_event definition to be invoked but the resulting diff sent back to the browser covers the complete form. This causes all form fields to be reset, i.e. even the parts which don’t depend on the new socket assigns at all.

I created a tiny example project using LiveView to reproduce this: GitHub - frerich/liveview_test: A playground for experimenting with Phoenix LiveView

This is basically the result of ‘mix phx.new --live’ plus a patch which adjusts the example view such that it shows two select fields: changing the value of the ‘Continent’ select should populate the ‘Country’ select. This works, except that it also re-sets the ‘Continent’ select. Does anybody have some idea what might be going on here, or how to debug surprisingly diffs further?

1 Like

Yes. Because the form is parameterized over the @form assign and the form assign changes whenever any field changes, we re-render the whole form. We are aware of this problem and this is something we want to tackle at some point.

Hmm, I’m not sure I understand: as far as I can tell, there is no @form assign. Looking at liveview_test/page_live.html.leex at main · frerich/liveview_test · GitHub it seems to me that the form in the template is not parametrised at all - I just specify an atom as the first argument to the form function). Also, looking at the assigns shows no sign of a @form assign.

For what it’s worth, I noticed a peculiar effect: not using the select function but rather writing the <select> tag manually and then using options_for_select/2 seems to help with things: the diff

diff --git a/lib/liveview_test_web/live/page_live.html.leex b/lib/liveview_test_web/live/page_live.html.leex
index 3e0001e..9535747 100644
--- a/lib/liveview_test_web/live/page_live.html.leex
+++ b/lib/liveview_test_web/live/page_live.html.leex
@@ -1,7 +1,9 @@
 <section class="phx-hero">
   <%= f = form_for :form, "#", phx_change: :form_changed %>
   <div>
-    Continent: <%= select f, :continent, @continent_options, prompt: "Choose a continent" %>
+    Continent: <select id="form_continent" name="form[continent]">
+      <%= options_for_select @continent_options, "" %>
+    </select>
   </div>
   <div>
     Countries: <%= multiple_select f, :country, @country_options %>

causes the diff send with the response to look like this:

{"diff": {
  "2": {
    "0":"<form action=\"#\" method=\"post\" phx-change=\"form_changed\"><input name=\"_csrf_token\" type=\"hidden\" value=\"S3QgWyAaDAwrPzcCFikwKnRSLys_XS83zYxmuLCmnmOnpZSxBjhorkVU\">",
    "2":"<select id=\"form_country\" multiple=\"\" name=\"form[country][]\"><option value=\"Germany\">Germany</option><option value=\"France\">France</option><option value=\"Spain\">Spain</option></select>"
  }
}}

I.e. the opening tag of the <form> element itself (why?) and the <select> element for the countries is part of the diff (the latter is expected), but at least the <select> for the continent isn’t!

Something else is fishy with this though: with this patch applied, switching from the first to the second option in the ‘Continent’ select (i.e. changing ‘Europe’ to ‘Asia’) does update the ‘Countries’ select, but the (display-)value of the ‘Continent’ select itself remains unchanged. I need to select the second option twice to update the display value. Very strange…

You could implement an approach based on this: PhoenixUndeadView - let's discuss optimization possibilities for something like Phoenix LiveView - #68 by tmbb

The problem is that you’d have to turn form_for into a macro.

Apologies, I read the part about generators and assumed it was the generated data forms by Phoenix.

In any case, it happens because of the variable. All variables are dirty. Because you are not really generating anything based on a changeset or similar, I would get rid of form_for. Use HTML directly. You can keep the select though and pass an atom as first argument instead of f.

3 Likes

We are aware of this problem and this is something we want to tackle at some point.