Liveview re-rendering entire form instead of one input

hey @muelthe – maybe it helps if I give a more realistic use-case rather than a trimmed down minimal example as above. Imagine you have a form to upload books. The user starts by typing the title of the book, then maybe the year it came out, the topic, etc. and after that the author(s) of the book. You already have 90% of the autors in your database, so you want to help the user by providing an autocomplete on the author text input. At every keystroke, ecto will search through the authors table, and offer matches to the user so they don’t have to type in the whole author.

The form might look something like this, assuming each field is its own function component for simplicity of the code below (but it works exactly the same for our purposes if we don’t use function components):

  def render(assigns) do
    ~H"""
      <.form phx-submit="save" etc ... >
        <.title title={@title} etc ></.title>
        <.year ... ></.year>
        ... more fields here ...
        <.author_autocomplete suggested_authors={@suggested_authors} etc></.author_autocomplete>
        ... yet more fields ...
        <%= submit "Continue" %>
      </.form>
    """
  end

  def author_autocomplete(assigns) do
    ...
    <input type="text" phx-change="search-author" list="author-datalist" name="author" ... >
    <datalist id="author-datalist">
        <%= for author <- @suggested_authors do %>
          <option value={author} />
        <% end %>
    </datalist>
    ...
  end

  def handle_event("search-author", %{"author" => query}, socket) do
    ...
    {:noreply, assign(socket, :suggested_authors, result_of_ecto_search)}
  end

As usual, the form sends data for all the field upon submit, triggering handle_event("save", params, socket). So far, it sounds innocuous (I hope), but consider what will actually happen:

The user types in the title, the year, and whatever other fields, then starts typing into the authors field. As soon as they type the first character (or two), the search-author handler is invoked, which populates the suggested_authors assign, which triggers a re-render. Server-side, liveview is smart so it only renders the author_autocomplete() component inside the form, ignoring the others, and only transmit the suggested_authors down the socket, leaving all other fields out.

But when the data reaches the browser, things suddenly go very wrong for the end user: the liveview javascript uses the suggested_authors data to recalculate the html for the entire component, based on a cached version of the data used to render the earlier version of the component + the diff received from the server. The recalculated html now replaces the user-visible version of the component, thus … wiping out all the data that the poor user typed in (but offering the autocomplete suggestion).

There are a few things one can do to avoid this, but I was asking above if liveview could natively avoid it without us working around it, by not replacing the entire component (in the DOM) and rather replacing only the minimal dom nodes that were actually changed (leafs of the dom). The answer is “no” :slight_smile:

Possible options to work around this include:

  • encapsulate the autocomplete field into its own live_component (I haven’t tried this yet, so for now I’m only assuming that updating an embedded live component does not trigger a re-render of the parent component.)
  • add a phx-update="ignore" to each of the other fields, as @egze suggested
  • revert to ‘dead’ routes (ajax-style) to update the list of suggestions (the datalist in the example above), thus bypassing liveview re-rendering
  • add a phx-change to each field (actually, on the whole form, thanks @cmo for the correction), thus keeping all state on the server in the assigns as soon as the user fills in that field

And probably more :slight_smile:

At least that’s my understanding for now, I’m happy to be corrected. I hope that helped!

2 Likes