Form in Live components disappear after socket update

Hi, I am facing a problem I am unable to resolve.

In my socket assigns I have a list of evenings (structs).
For every evening in the list, I render a live component that contains a form relative to that evening.
There is also a button that adds an evening to the database and assigns the updated list of evenings to the socket.

When the page is first rendered, I can see every form without problem.
When I click the button to add an evening, the last form in the list “disappears”: all the input tags are removed from the html and the labels are emptied.

If I refresh the page, I can see everything.
I wrote a test to resolve this, but it always passes, and if I inspect the page with open_browser everything is ok.

I cannot understand what’s happening here, any help is appreciated.
I am using Phoenix 1.7.11 and LiveView 0.20.2

Try checking the id of each rendered forms is unique. It sounds like the handle_event when an action is performed on one of the forms clears out the others.

If that doesn’t help, show your form rendering code and you might be able to get better help.

1 Like

I managed to create the simplest example I could to show this behavior. I published this example at zagoli/FormDisappearing: Why.

It is a simple LiveView mostly generated with Phoenix generators.
Visiting / we can see a quite simple LiveView: there is one form for every student in the database where we can edit the student’s name.
Each form is inside a live component with a unique id.

But when I click “New student”, a new form for the new student appears, and the old forms get cleared (the input field disappears):


If you reload the page everything is ok.

All the relevant code is inside lib/form_disappearing_web/live/student_live

It’s essentially because the input’s share the same id, across the forms.

The TLDR is that this is a HTML naughty and LV doesn’t know which input has been updated so it wipes the others.

To ‘resolve’ your problem, ensure your input in core_components.ex has a unique id (it uses the name to work out the field on the form, so you don’t want to fiddle with that)

There is probably a better way of generating a unique ID, but below I have attached an example.

  def input(assigns) do
    ~H"""
    <div phx-feedback-for={@name}>
      <.label for={@id}><%= @label %></.label>
      <input
        type={@type}
        name={@name}
        id={"#{@id}-#{Ecto.UUID.generate()}"} # THIS!!!!
        value={Phoenix.HTML.Form.normalize_value(@type, @value)}
        class={[
          "mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
          "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
          @errors == [] && "border-zinc-300 focus:border-zinc-400",
          @errors != [] && "border-rose-400 focus:border-rose-400"
        ]}
        {@rest}
      />
      <.error :for={msg <- @errors}><%= msg %></.error>
    </div>
    """
  end
1 Like

The better way to handle this would be to use to_form(data, id: unique_prefix), which would make the id generation be unique without messing with components.

2 Likes

Can you share a small example please?

Looking at the docs it seems there’s even a separate option for id prefix vs name prefix: Phoenix.Component — Phoenix LiveView v1.0.0-rc.7

You’d need to use a unique prefix per student form you render, where the prefix will be applied to all inputs related to the form.

In your example codebase it could be something like this:

defp assign_form(socket) do
  changeset = Students.change_student(socket.assigns.student)
  socket |> assign(:form, to_form(changeset, id: "student_#{socket.assigns.student.id}"))
end
2 Likes

Great, it worked! Thanks to everyone