Skip to content

Phoenix LiveView form does not show errors on polymorphic_embeds_many fields #113

Open

Description

@anjou-low

Hi and thanks for the great work !

I'm trying to create a form for a schema which has polymorphic_embeds_many field using polymorphic_embed_inputs_for but the validation errors are not shown on the inner forms when I use the base input core component.

Below is a minimum example using the schemas defined in the documentation (MyApp.Channel.Email and MyApp.Channel.SMS are omitted for brevity) and the code for my LiveView.

Inputing invalid values in the inner fields does not show any error while it does for the outer field. Am I doing something wrong with the way I build my form ?

After looking around a bit, I see that the errors are properly set on the inner fields but they are not displayed and this is related to the use of Phoenix.Component.used_input?/1 in the input core component. I was able to make it work by modifying a few lines of to_form in PolymorphicEmbed.HTML.Helpers but I'd like to know if I am properly constructing my form before going there.

defmodule MyApp.Reminder do
  use Ecto.Schema
  import Ecto.Changeset
  import PolymorphicEmbed

  schema "reminders" do
    field :date, :utc_datetime
    field :text, :string

    polymorphic_embeds_many :channels,
      types: [
        sms: MyApp.Channel.SMS,
        email: MyApp.Channel.Email
      ],
      on_type_not_found: :raise,
      on_replace: :delete
  end

  def changeset(struct, values) do
    struct
    |> cast(values, [:date, :text])
    |> cast_polymorphic_embed(:channels, required: true)
    |> validate_required(:date)
  end
end
defmodule PolembedWeb.ReminderLive.Index do
  use PolembedWeb, :live_view

  import PolymorphicEmbed.HTML.Helpers
  alias MyApp.Reminder

  @impl true
  def mount(_params, _session, socket) do
    form =
      Reminder.changeset(%Reminder{}, %{
        "channels" => [
          %{"__type__" => "sms", "number" => "32"},
          %{"__type__" => "email"}
        ]
      })
      |> to_form()

    {:ok, assign(socket, form: form)}
  end

  @impl true
  def handle_event("validate", %{"reminder" => reminder_params}, socket) do
    form = Reminder.changeset(%Reminder{}, reminder_params) |> to_form(action: :validate)
    {:noreply, assign(socket, form: form)}
  end

  @impl true
  def handle_event("save", _params, socket) do
    {:noreply, socket}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <.form for={@form} id="reminders-form" phx-change="validate" phx-submit="save">
      <.input field={@form[:date]} type="date" label="Name" />
      <.polymorphic_embed_inputs_for :let={channel} field={@form[:channels]}>
        <%= case source_module(channel) do %>
          <% MyApp.Channel.SMS -> %>
            <.input field={channel[:number]} type="number" label="Number" />
          <% MyApp.Channel.Email -> %>
            <.input field={channel[:address]} type="text" label="Address" />
        <% end %>
      </.polymorphic_embed_inputs_for>
    </.form>
    """
  end
end

Activity

  1. nduitz commented on Dec 5, 2024

    @nduitz
    Contributor

    @anjou-low Hey, i can confirm the issue with Phoenix.Component.used_input?/1.
    Could you elaborate what changes you did to to_form to make it work?

  2. JacobAlexander commented on Jun 23, 2025

    @JacobAlexander

    bump

  3. anjou-low commented on Jul 28, 2025

    @anjou-low
    Author

    The issue seems to come from the fact that Phoenix.Component.used_input?/1 looks at the field's params to infer if it has been used but they are not always set correctly here:

    params = Enum.at(params, i) || %{}

    If I construct the form like this

    form = 
      Reminder.changeset(%Reminder{}, %{
        "channels" => [
          %{"__type__" => "sms", "number" => "32"},
          %{"__type__" => "email"}
        ]
      })
      |> to_form(action: :validate)

    then

    [
      %{"__type__" => "sms", "number" => "32"}, 
      %{"__type__" => "email"}
    ] = Map.get(form.source.params, "channels")

    but as soon as the params are sent by the client we get something like

    %{"inputs" => %{
      "0" => %{"__type__" => "sms", "_persistent_id" => "0", "number" => "3"},
      "1" => %{"__type__" => "email", "_persistent_id" => "1", "_unused_address" => "", ...}
    }, ...
    }

    and this format is not handled by PolymorphicEmbed.HTML.Helpers.to_form/4.

    One quick fix could be to not List.wrap the changeset's params and do something like that

    params =
    cond do
      is_list(params) -> Enum.at(params, i) || %{}
      is_map(params) -> Map.get(params, index_string) || %{}
    end

    but it feels hacky.

    I think this is related to #74 because the params are properly handled by cast_polymorphic_embed/2

    params = %{"channels" => %{"0" => %{"__type__" => "sms"}}}
    
    changeset = 
      %Reminder{}
      |> Ecto.Changeset.cast(params, [])
      |> PolymorphicEmbed.cast_polymorphic_embed(:channels)
    
    %{"__type__" => "sms"} = Map.get(changeset.changes, :inputs) |> List.first() |> Map.get(:params)

    but we can't always rely on this since the changeset is transformed to a struct if it valid.

  4. nduitz commented on Aug 20, 2025

    @nduitz
    Contributor

    Hey,
    I implemented something like you suggested in #132
    Sadly this will introduce other problems:
    when adding inputs using the sort_param it will populate prior params if you prepend new inputs.

    If I find the time I will try to dig a bit deeper

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions