LiveView form auto-recovery not working when form is rendered through a Live Component

TLDR; A recoverable form rendered through a Live Component doesn’t recover after crashes or disconnects.


DETAILS

Specifications

  • Phoenix 1.7.14
  • LiveView 0.20.17 && 1.0.0-rc.7

Context
A form is being rendered through a Live Component. It fulfills the requirements to be recoverable after crashes and disconnects, as per Form bindings — Phoenix LiveView v0.20.17

  • Has an ID.
  • Is marked with phx-change.

all forms marked with phx-change and having id attribute will recover input values automatically after the user has reconnected or the LiveView has remounted after a crash.

Expected Behaviour

  • After the live view crashes or disconnects, the form rendered through the Live Component is recovered.

Current Behavious

  • After the live view crashes or disconnects, the from rendered through the Live Component is not recovered.
on Crash on Disconnect
ezgif-5-f640ca9302 ezgif-5-77f2b21c91

Steps to reproduce

  1. Clone GitHub - ammancilla/phoenix_live_view_unrecoverable_form: A phoenix live view with a form that does not recover after crashes or disconnections (but it should)
  2. mix phx.server
  3. http://localhost:4000/form

or

  • The Live View and respective Live Component
defmodule FormRecoverableWeb.FormLive do
  @moduledoc false

  use FormRecoverableWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    {:ok, assign(socket, :form, to_form(%{"name" => nil}))}
  end

  @impl true
  def handle_event("validate", params, socket) do
    {:noreply, assign(socket, :form, to_form(params))}
  end

  def handle_event("submit", _params, socket), do: raise("break live view")

  @impl true
  def render(assigns) do
    ~H"""
    <div class="mx-auto flex justify-between prose">
      <div>
        <h2>The Form</h2>
        <.simple_form id="person-form" for={@form} phx-change="validate" phx-submit="submit">
          <.input field={@form[:name]} label="name" />
          <:actions>
            <.button phx-disable-with="Saving...">Crash LiveView</.button>
          </:actions>
        </.simple_form>
      </div>

      <div>
        <h2>The Form from a Live Component</h2>
        <.live_component
          id="form-live-component"
          form={@form}
          module={AnnaBailaWeb.FormLiveComponent}
        />
      </div>
    </div>
    """
  end
end

defmodule FormRecoverableWeb.FormLiveComponent do
  @moduledoc false

  use FormRecoverableWeb, :live_component

  @impl true
  def update(_assigns, socket) do
    {:ok, assign(socket, :form, to_form(%{"username" => nil}))}
  end

  @impl true
  def handle_event("validate", params, socket) do
    {:noreply, assign(socket, :form, to_form(params))}
  end

  def handle_event("submit", _params, socket), do: raise("break live component")

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <.simple_form
        id={"#{System.unique_integer()}"}
        for={@form}
        phx-submit="submit"
        phx-target={@myself}
        phx-change="validate"
      >
        <.input id="name-live-component" field={@form[:name]} label="name" />
        <:actions>
          <.button phx-disable-with="Saving...">Crash LiveComponent</.button>
        </:actions>
      </.simple_form>
    </div>
    """
  end
end

  • What am I doing wrong?
  • Is this expected behaviour or a bug?
1 Like

Edit: My rank was upgraded and I could add a demo of the issue to the post :smiley:

Upload it to YouTube and post link?

Whoops, I commented literally seconds after you edited your second comment.

2 Likes

Nonetheless, great idea :+1:

Hello :wave:, can’t try it right now, but don’t you need to use the same id for the form in the live component? You will end up with a different id when you render the live component after the crash, no?

id={"#{System.unique_integer()}"}
3 Likes

Yes this is correct. Using an impure value in the template itself is also quite strange.

2 Likes

Hey Joel :wave: ,

I tried using the ID given to the component as the one of the form but it produced the same outcome.

In the Live View:

<.live_component
    id="form-#{System.unique_integer()"
    form={@form}
    module={AnnaBailaWeb.FormLiveComponent} />

Then, in the Live Component

<.simple_form id={@id} for={@form} phx-submit="submit" phx-target={@myself} phx-change="validate">

The second thing that I noticed is that using a deterministic ID (i.e “unique-form-id”) for the form, would result in the Live View not recovering at all from the crash or the disconnect but failing with:

no component found matching phx-target of 1 undefined

deterministic

I had to use a completely random value as the ID of the form, otherwise the live view wouldn’t recover at all from the crash/disconnect but rather fail with:

no component found matching phx-target of 1 undefined

As explained in my previous comment → LiveView form auto-recovery not working when form is rendered through a Live Component - #7 by ammancilla

You should still be generating distinct ids for each logical form in the assigns and then using that assign in the template. You’re going to get super weird behavior doing an impure call like that in the render itself, which is supposed to be deterministic from the assigns.

I can’t comment on the error you’re getting with a hard coded value, but I would definitely chase down that error and not go with an impure render, which is explicitly called out as a bad idea in the docs.

3 Likes

Please try with a constant ID and see if that works. Because right now on a fresh render the Live Component passes a different ID to the form which does (of course) not match the ID stored in the browser and the whole recovery process goes haywire.

Hey,

Please see the second part of my comment (LiveView form auto-recovery not working when form is rendered through a Live Component - #7 by ammancilla). Using a constant ID under this scenario, results on the live view not recovering at all from the crash/disconnect.

As suggested by @benwilson512, I am shifting the focus of the issue to this particular point.

Hey,

I can’t comment on the error you’re getting with a hard coded value, but I would definitely chase down that error and not go with an impure render, which is explicitly called out as a bad idea in the docs.

Yes, that makes sense. I’ll revise the original post to emphasize on the Live View crash happening when using static IDs under this scenario.

I just tried your code, with LiveView 1.0.0-rc.7 and Phoenix 1.7.14 and I can’t see any problems. No matter if it’s a crash or disconnect/reconnect, the forms are recovered correctly. This is by taking your code almost as is but changing the id of the form in the live component to be static (id="sub_form").

1 Like

This is happening because an integer is not a valid ID. The browser just fails to look it up. I think more recent LiveViews warn on this.

2 Likes

I was recently looking to confirm that fact and MDN seems to suggest that this is not actually the case that plain numbers would be invalid IDs. They might just need excaping to be used in css selectors.

2 Likes

Hey, thanks for trying the code.

After reading your comment, I tried the code again on LiveView 1.0.0-rc.7 and the form is recovered as expected. On the other hand, the error keeps happening on LiveView 0.20.17 when using static IDs.

no component found matching phx-target of 1 undefined

I wonder, did you manage to test the code on version ~> 0.20.17 too?

Hey José,

In the scenario I described using LiveView 0.20.17, I consistently encounter the error, even when using a static ID that is neither an integer nor contains integers. The live view fails and does not recover, displaying the mentioned error.

I verified and can confirm that the error does not occur with LiveView 1.0.0-rc.7.

I think the fix for your problem on 0.20.17 would be in commit 628f885, but this was done on the main branch, just after the latest release of 0.20.

2 Likes

Thank you for double checking! I stand corrected.

Indeed, I cherry-picked that commit into my project and form-recovery worked as expected!

Thank you very much for the help.

1 Like