Why does the *name* of a LiveView assigns key cause `form_for/3` to result in a protocol error?

I have a LiveView component which uses form_for/3 with a changeset, in the assigns map.

When I store the changeset in the assigns map using the key name :guide_list, there is an undefined FormData protocol error. However, if I change the key name to :changeset or even :foo, there is no error. The socket seems the same, with the exception of the different assigns key name. The assigns key value is an Ecto.Changeset in all situations.

Clearly, I do not understand something about Phoenix because I am surprised the key name matters. :slight_smile: Can someone please point me to documentation about this?

Thanks for any help!

Here are the component, inspects of the socket, and the error:

This code will result in a Protocol error, because the assigns key name is :guide_list:

defmodule CrisprWeb.GuideSourceComponent do
  use CrisprWeb, :live_component

  alias Crispr.GuideList

  def render(assigns) do
    ~L"""
      <%= f = form_for @guide_list, "#", phx_target: @myself %>
        <%= textarea f, :guides %>
        <%= error_tag f, :guides %>
        <%= submit "Set guides" %>
      </form>
    """
  end

  def mount(socket) do
    {:ok, socket  |> assign_guide_list |> IO.inspect(label: "GuideSourceComponent: mount socket")}
  end

  defp assign_guide_list(socket) do
    socket |> assign(guide_list: %GuideList{} |> GuideList.changeset(%{}))
  end
end

Changing these lines to rename the key will allow the form to render:

  <%= f = form_for @foo, ...

  ...
    socket |> assign(foo: %GuideList{} |> GuideList.changeset(%{}))
 ...

Here are the sockets from both cases:

GuideSourceComponent: mount socket: #Phoenix.LiveView.Socket<
  assigns: %{
    flash: %{},
    guide_list: #Ecto.Changeset<
      action: nil,
      changes: %{},
      errors: [guides: {"can't be blank", ...}],
      data: #Crispr.GuideList<>,
      valid?: false
    >,
    myself: %Phoenix.LiveComponent.CID{cid: 1}
  },
  changed: %{flash: true, guide_list: true},
  endpoint: CrisprWeb.Endpoint,
  id: "guides-selector",
  parent_pid: #PID<0.502.0>,
  root_pid: nil,
  router: CrisprWeb.Router,
  view: CrisprWeb.GuideSelectorLive,
  ...
>

GuideSourceComponent: mount socket: #Phoenix.LiveView.Socket<
  assigns: %{
    flash: %{},
    foo: #Ecto.Changeset<
      action: nil,
      changes: %{},
      errors: [guides: {"can't be blank", ...}],
      data: #Crispr.GuideList<>,
      valid?: false
    >,
    myself: %Phoenix.LiveComponent.CID{cid: 2}
  },
  changed: %{flash: true, foo: true},
  endpoint: CrisprWeb.Endpoint,
  id: "guides-selector",
  parent_pid: #PID<0.1587.0>,
  root_pid: #PID<0.1587.0>,
  router: CrisprWeb.Router,
  view: CrisprWeb.GuideSelectorLive,
  ...
>

Here is the error when the key name is :guide_list:

[error] #PID<0.502.0> running CrisprWeb.Endpoint (connection #PID<0.501.0>, stream id 1) terminated
Server: localhost:4000 (http)
Request: GET /
** (exit) an exception was raised:
    ** (Protocol.UndefinedError) protocol Phoenix.HTML.FormData not implemented for %Crispr.GuideList{guides: nil} of type Crispr.GuideList (a struct). This protocol is implemented for the following type(s): Ecto.Changeset, Plug.Conn, Atom
        (phoenix_html 2.14.3) lib/phoenix_html/form_data.ex:1: Phoenix.HTML.FormData.impl_for!/1
        (phoenix_html 2.14.3) lib/phoenix_html/form_data.ex:15: Phoenix.HTML.FormData.to_form/2
        (phoenix_html 2.14.3) lib/phoenix_html/form.ex:325: Phoenix.HTML.Form.form_for/3
        ...snip

Is the parent LiveView / LiveComponent passing a guide_list parameter when it renders the GuideSourceComponent? That would override the starting value you set in mount.

P.S. try calling <% IO.inspect(@guide_list) %> inside your template, rather than inside the mount callback. This way you’ll see what’s actually set at render time.

The parent LiveView was indeed passing a parameter named :guide_list, which was a %Crispr.GuideList{}! I should have caught this, ugh…

Thanks for the fast reply, great guess, and for the tip about inspecting in the template! :slight_smile:

1 Like