Liveview phx-click clears unrelated input fields

I have a liveview page with a series of checkbox inputs. I noticed that if I dispatch an unrelated event using phx-click to the server, all of the checkboxes lose their checked state. Its as if liveview is re-rendering the entire page.

My understand of Liveview could be wrong, but I thought it diffed changes and only patched the DOM where new changes are coming in. The phx-click I am dispatching is totally isolated from the checkboxes, so I am confused as to why my page seems to be re-rendering the checkboxes causing them to lose their checked state.

I’ve thought of a workaround that would include using phx-change on the form containing the checkboxes and keeping state within the liveview of which checkboxes are selected. When I render the checkboxes I could compare this list of selected checkboxes from the liveview state to which checkbox is currently being rendered and mark it as checked from there, but I’m hoping to understand why this is necessary.

My thinking tells me that since the checkboxes are not included in the updates coming from the original phx-click there should be no reason the checkboxes are re-rendering and losing their state in the browser.

Thanks for the help! I can add some code if it’s helpful.

Hi @mcgingras,

In general, yes, liveview will only patch diffs. However, it is not guaranteed and you shouldn’t rely on it. The diff-patching approach is for performance optimisation only. In certain situations (which become rarer in each release) it can’t be applied.

Are you able to put up a minimal code example that illustrates your issue, and also shows how the checkbox state is used.cause the form to be re-rendered. We may be able to give you some more specific feedback.

@mindok,

Thank you for the help. It’s good to know that patching diffs is not guaranteed. Here is a small code snippet demonstrating the behavior. This code is something I wrote up quickly and is not the original problem, but it’s small and serves to show the issue I am experiencing just the same.

def render(assigns) do
    ~L"""
    <div>
      <h1>The count is: <%= @count %></h1>
      <button phx-click="decrement">-</button>
      <button phx-click="increment">+</button>
    </div>
    <input type="checkbox" />
    """
  end

  def mount(_params, session, socket) do
    {:ok,
     assign(socket, %{
       count: 0
      }
     )}
  end

  def handle_event("increment", _params, socket) do
    {:noreply, update(socket, :count, &(&1 + 1))}
  end

  def handle_event("decrement", _params, socket) do
    {:noreply, update(socket, :count, &(&1 - 1))}
  end

If I click the checkbox to put it into a checked state, then dispatch an event through either of the two buttons, the checkbox loses its state. This behavior is odd to me since the only thing that should get patched is the h1 where the count variable is updated. The checkbox is independent. However, the checkbox loses it’s state, which leads me to believe it too is getting re-rendered.

Thanks for the help!

Try setting the phx-update attribute on the checkbox, e.g.

<input phx-update="ignore" type="checkbox" />

That works for me…

2 Likes

Thank you. This should do the trick.

I’d still like to know why the checkbox re-renders when it’s not included as a change. It makes me feel like I can not be certain what will or will not be re-rendered each time - not just in this example but in future as well. phx-update="ignore" will definitely do the trick and I appreciate the help, but I don’t want to have to fence off pieces of the code each time just to be sure. I feel like expected behavior is that it shouldn’t re-render.

Thank you again!

The programming model is broadly: events update server-side assigns, ui is update to template + server-side assigns. This is by design to keep the conceptual load down to a reasonable level. A button event handler changes server-side assigns, so the output will be re-rendered. If you look at the web-socket traffic you will see that only the count change is re-sent in the payload.

In terms of guidelines for understanding what will be re-rendered - if you change something client side that’s in a LiveView template, and that change isn’t captured server-side, expect it to be overwritten when something server-side is updated unless it’s marked to be ignored. There are very few exception to this (e.g. updating a text box that currently has the focus - for fun you could change your checkbox to a text input, insert a :timer.sleep(1000) into the increment event handler and see that if you jump straight back to the text box after clicking the increment button, the text doesn’t get overwritten.)

This clean re-rendering that may overwrite client side changes is actually what you want by default even if it isn’t intuitive. Multiple sources of update increase the unpredictability of the system and also the cognitive load to figure out what’s happening. Where there is a need to “escape” from this behaviour (e.g. JS libraries updating portions of the DOM client-side) there is the phx-update escape hatch. Again, you can fiddle around inspecting the client-side templates to see how that part is removed from client-side template.

In terms of the mechanics, the client-side DOM patching combines the updates with a copy of template cached on the client-side. If you want to take a look in more detail add a couple more assigns & events to update them, then inspect liveSocket.main.rendered in the browser. Take a look at the websocket payload and see how it is interleaved with the template held in liveSocket.main.rendered. Once it is interleaved with the update the whole node is replaced, so anything in the template that has been changed outside of LiveView will be replaced with it.

3 Likes

Thank you for such a thoughtful reply! This is incredibly helpful for me to gather a more conceptual understanding. I can’t thank you enough. The elixir community is wonderful!

1 Like