Skip to content

Autorecovery broken for <input form="..." /> in Firefox #4021

@ulfurinn

Description

@ulfurinn

Environment

  • Elixir version (elixir -v): 1.18.4
  • Phoenix version (mix deps): 1.8.1
  • Phoenix LiveView version (mix deps): 1.1.14
  • Operating system: macos
  • Browsers you attempted to reproduce this bug on (the more the merrier): Firefox, specifically
  • Does the problem persist after removing "assets/node_modules" and trying again? Yes/no:

Actual behavior

Given a view like:

  def render(assigns) do
    ~H"""
    <div>
      <form id="demo-form" phx-change="change">
        <input name="f1" />
        <input form="demo-form" name="f2" />
      </form>
      <input form="demo-form" name="f3" />
      <button phx-click="terminate">terminate</button>
    </div>
    """
  end

  def handle_event("change", _, socket) do
    {:noreply, socket}
  end

  def handle_event("terminate", _, _socket) do
    exit(:normal)
  end

the "change" handler receives %{"f1" => "", "f2" => "", "f3" => ""} after a normal change, but only %{"f1" => ""} in the autorecovery phase.

Expected behavior

The "change" handler should always receive all of %{"f1" => "", "f2" => "", "f3" => ""}.

Investigation notes

There are two ways to assign an input element to a form:

  • placing it inside the form element
  • using the form attribute

When both are used conflictingly (i.e. the input element is inside the form element but its form attribute points to another form that is not its DOM ancestor), browsers seem to resolve this conflict differently:

  • Chrome and Safari prioritize the DOM tree structure
  • Firefox prioritizes the form attribute

Therefore, after clonedForm.appendChild(clonedEl), Firefox does not include clonedEl in clonedForm.elements, because its form is still pointing to the original form element before the cloning. As a result, it is not included in the submit payload created by FormData or other means.

A minimal fix that I found is

--- assets/js/phoenix_live_view/view.js
+++ assets/js/phoenix_live_view/view.js
@@ -2160,6 +2160,7 @@ export default class View {
           const clonedEl = el.cloneNode(true);
           morphdom(clonedEl, el);
           DOM.copyPrivates(clonedEl, el);
+          clonedEl.removeAttribute("form");
           clonedForm.appendChild(clonedEl);
         });
         return clonedForm;

However, this only addresses the f3 field in my example.

The f2 field is covered by the if (form.contains(el)) branch and this simple change does not work for it. (However, this case has an available userspace workaround of not using the form attribute redundantly when the input is already inside the form.)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions