Using Polymorphic Embed library on an embeds_many field

Hello I already posted this question in the Elixir slack but I’m moving it here in the hopes of getting more eyes on it. A direct answer to my question or a link to an example would be much appreciated, thanks!

Im attempting to work with the PolymorphicEmbed library in a Phoenix 1.7.0-rc.0 app with LiveView 0.18.3. I want to make a form backed by changesets so that I benefit from easy validation. The issue that I am having is two-fold:

  1. When I start with a populated changeset like so Poem.changeset(%Poem{}, %{stanzas: [%{content: "", __type__: "text"}]}) nil is the only clause that matches in PolymorphicEmbed.HTML.Form.get_polymorphic_type/3
  2. When I start with an empty changeset like so Poem.changeset(%Poem{}, %{}) no fields render at all

How do I use the library to show form fields in a way that allows me to use Ecto changesets? I wish to show both empty forms when creating new poems AND populated forms when editing existing poems. If I need to manually cast form fields to the correct validators doesn’t that defeat the purpose of this library?

My schemas are:

defmodule Help.Poem do
  use Ecto.Schema
  import Ecto.Changeset
  import PolymorphicEmbed

  schema "poems" do
    field :title, :string
    polymorphic_embeds_many :stanzas,
      types: [
        text: [
          module: __MODULE__.TextStanza,
          identify_by_fields: [:content]],
        image: [
          module: __MODULE__.ImageStanza,
          identify_by_fields: [:url]]],
      on_type_not_found: :raise,
      on_replace: :delete

    timestamps()
  end

  def changeset(poem, attrs) do
    poem
    |> cast(attrs, [])
    |> cast_polymorphic_embed(:stanzas,
      with: [
        text: &__MODULE__.TextStanza.changeset/2,
        file: &__MODULE__.ImageStanza.changeset/2
      ])
    |> validate_required([])
  end

  defmodule TextStanza do
    use Ecto.Schema

    embedded_schema do
      field :content, :string
    end

    def changeset(text, attrs) do
      text
      |> cast(attrs, [:content])
      |> validate_required(:content)
      |> validate_length(:content, min: 5)
    end
  end

  defmodule ImageStanza do
    use Ecto.Schema

    embedded_schema do
      field :url, :string
    end

    def changeset(image, attrs) do
      image
      |> cast(attrs, [:url])
      |> validate_required(:url)
    end
  end
end

My heex:

<.simple_form :let={poem_form} for={@poem_form} phx-change="validate-poem-form">
  <%= for stanza_form <- polymorphic_embed_inputs_for poem_form, :stanzas do %>
    <%= hidden_inputs_for(stanza_form) %>

    <%= case get_polymorphic_type(stanza_form, Help.Poem, :stanzas) do %>
      <% :text -> %>
        <%!-- Never matches even with Poem.changeset(%Poem{}, %{stanzas: [%{content: "", __type__: "text"}]}) --%>

      <% :image -> %>
        <%!-- Never matches even with Poem.changeset(%Poem{}, %{stanzas: [%{url: "", __type__: "image"}]}) --%>

      <% nil -> %>
        <%!-- Always matches but how do I manually construct an input for an embeds_many field --%>
    <% end %>
  <% end %>

  <:actions>
    <.button>Post Poem</.button>
  </:actions>
</.simple_form>

Just scanning this I see your ‘with’ has [:text, :file], but your embed definition has [:text, :image]. I think you don’t have to define the ‘with’, because you’re using the default fn name of ‘changeset’

Thanks for pointing out those inconsistencies. After fixing them my problem persists.

Version 3.0.5 included a bug fix that broke get_polymorphic_type/3 in certain situations. I’m not sure whether this is the cause in your case, but can you try whether it works for you in 3.0.4? The issue is fixed in Fix get type by woylie · Pull Request #66 · mathieuprog/polymorphic_embed · GitHub, but it hasn’t been merged yet.

Downgrading did not solve the problem. I think I have to drop the library. In a last ditch attempt to solve the problem however here is more data, perhaps it will reveal where the error lies.

Assuming I start with the following “poem_form” changeset, Poem.changeset(%Poem{}, %{stanzas: [%{url: ""}]}), my call to polymorphic_embed_inputs_for poem_form, :stanzas returns the following “stanza_form”:

 %Phoenix.HTML.Form{
  source: #Ecto.Changeset<action: nil, changes: %{}, errors: [],
   data: #Help.Poem.ImageStanza<>, valid?: true>,
  impl: Phoenix.HTML.FormData.Ecto.Changeset,
  id: "poem_stanzas",
  name: "poem[stanzas]",
  data: #Ecto.Changeset<
    action: :insert,
    changes: %{},
    errors: [url: {"can't be blank", [validation: :required]}],
    data: #Help.Poem.ImageStanza<>,
    valid?: false
  >,
  hidden: [__type__: ""],
  params: %{url: ""},
  errors: [],
  options: [multipart: false],
  index: nil,
  action: nil
}

hidden_inputs_for(stanza_form) returns:

[
  safe: [
    60,
    "input",
    [" id=\"", "poem_stanzas___type__", 34, 32, "name", 61, 34,
     "poem[stanzas][__type__]", 34, 32, "type", 61, 34, "hidden", 34, 32,
     "value", 61, 34, [], 34],
    62
  ]
]

get_polymorphic_type(stanza_form, Help.Poem, :stanzas) returns nil

The only issue I can see, and its really more of a guess on my end, is that “stanza_form” has no field :stanzas and therefore returns nil. If that is indeed the case, then what field should I be searching on in get_polymorphic_type(stanza_form, Help.Poem, :stanzas)? I’ve already attempted to replace :stanzas with every member returned in “stanza_form”.

Do you have any examples of a successful polymorphic_embeds_many form?

I’m using the library only for one form, and there the embeds_many points to another schema module, which the polymorphic field in it (without embeds_many). So in that case, the nested form still has a field that can be found by PolymorphicEmbed.HTML.Form.get_polymorphic_type.

PolymorphicEmbed.HTML.Form.get_polymorphic_type/3 wouldn’t work in your case, since it needs to reference a field at which to find the value: polymorphic_embed/form.ex at v3.0.5 · mathieuprog/polymorphic_embed · GitHub. As you can see, the function uses input_value/2 to find the current field value.

You might be able to get the type by using PolymorphicEmbed.get_polymorphic_type/3 (not from PolymorphicEmbed.HTML.Form!) directly instead.

<%= case PolymorphicEmbed.get_polymorphic_type(Help.Poem, :stanzas, stanza_form.source.data) do %>

I don’t know from the top of my hat what possible values form.source can have. If the source is always a changeset with a struct in the data field, this should work. The library could use some better documentation for your scenario, and maybe a PolymorphicEmbed.HTML.Form.get_field_type/2 variant.

Just making sure I understand correctly, you have 2 levels of embeds? Something like Poem.content.some_type where content is an embedded field with a traditional ecto embeds_many and some_type is a polymorphic_embeds_one of my stanza types (text or image)?

Correct. We did that because we have some meta fields that are common to all of the polymorphic variants.

But we should be able to figure out how it works with embeds_many.

Its not ideal but I suppose I could use this work around for now. How do you model this on the db side of things? Right now my migration looks like:

alter table("poems") do
    add :stanzas, :map
end

If I’m not mistaken this uses a json column under the hood and json supports arbitrarily deep nesting so I shouldn’t need to change anything correct?

Before you change anything, did you try this?

<%= case PolymorphicEmbed.get_polymorphic_type(Help.Poem, :stanzas, stanza_form.source.data) do %>

Didn’t it work?

The database schema would stay the same for the workaround.

It does work when I start with a populated changeset which is nice. Where it falls apart is when I attempt to dynamically add new stanza fields to the changeset. Ecto.Changeset.put_embed(changeset, :stanzas, existing ++ [%{url: ""}]) blows up with
(ArgumentError) expected 'stanzas' to be an embed in 'put_embed', got: '{:array, {:parameterized, PolymorphicEmbed, %{default: [], on_replace: :delete, on_type_not_found: :raise, type_field: "__type__", types_metadata: [%{identify_by_fields: ["content"], module: Help.Poem.TextStanza, type: :text}, %{identify_by_fields: ["url"], module: Help.Poem.ImageStanza, type: :file}]}}}'

What I like about the double nesting is all of the embeds_many work is done with regular ecto which has a lot of documentation and examples and the PolymorphicEmbed library handles the bit that its documentation has a working example for. Ideally the PolymorphicEmbed lib would handle everything but I don’t know enough about it or Ecto to troubleshoot all of the errors that I’m getting.

You need to use Changeset.put instead of Changeset.put_embed here, since it is not a real Ecto embed. The embeds_many macro of the library creates a regular array field (field :stanzas, {:array, PolymorphicEmbed})

1 Like

That works. PolymorphicEmbed.get_polymorphic_type/3 does what I want but PolymorphicEmbed.HTML.Form.get_polymorphic_type/3 does not. Thank you!

Nice to see @woylie responding and you could resolve the problem.
I greatly appreciate, if you could post the final working solution at the end - and - marking it as a solution. It will be helpful for many like me.

Sure thing. I haven’t gotten a completely working answer just yet but I’m making progress. My new issue is that new fields of the same polymorphic type “overwrite” existing fields of the same type. For example if I generate 2 new image fields only the value of the second field is passed to the LiveView for validation. Attempting to update the first form field results in no values being sent to the server for validation.

Here is what gets sent to the server if two image stanzas are present on the form:
%{"stanzas" => %{"__type__" => "", "url" => "another value"}}.
I would expect something like this:
%{"stanzas" => [%{"__type__" => "", "url" => "some value"}, %{"__type__" => "", "url" => "another value"}]}

1 Like

@woylie I believe the issue here is that the fields generated by PolymorphicEmbed.get_polymorphic_type/3 are all identically named poem[stanzas][stanza-type] and identically id-ed by poem_stanzas_url. There’s no way to tell them apart. the same problem occurs with PolymorphicEmbed.HTML.Form.hidden_inputs_for/1 btw as those hidden inputs are identically named and id-ed as poem[stanzas][__type__] and poem_stanzas___type__ respectively. Im not sure how to solve this problem, as IDs aren’t generated for the embedded schemas until they’re inserted in the db but that happens after they’re rendered in the form. Perhaps the library could give each a temporary id that it keeps track of

I don’t think an ID for the embedded entry would help here. Seems to me like the name attribute for all generated fields is the same.

You can try generating an index and modifying the input name and id attributes. Something like this (not tested):

<%= for {stanza_form, index} <- polymorphic_embed_inputs_for poem_form, :stanzas |> Enum.with_index() do %>
  # ...
  <.input
    field={{stanza_form, :url}}
    type="url"
    name={input_name(stanza_form, :url) <> "[#{index}]"}
    id={input_id(stanza_form) <> "_#{index}"}
  />

Or if you are still using the Phoenix.HTML.Form input functions:

<%= url_input(stanza_form, :url, name: input_name(stanza_form, :url) <> "[#{index}]", id: input_id(stanza_form) <> "_#{index}") %>

phoenix_ecto does something similar in its to_form implementation: phoenix_ecto/html.ex at master · phoenixframework/phoenix_ecto · GitHub. But it looks like polymorphic_embed does modify the name for array fields: polymorphic_embed/form.ex at master · mathieuprog/polymorphic_embed · GitHub.

I left a comment here: During form submission, embedded arrays only contain the last entry · Issue #55 · mathieuprog/polymorphic_embed · GitHub. And here’s an issue for improving docs and tests for array fields: tests and examples for array fields · Issue #70 · mathieuprog/polymorphic_embed · GitHub.

1 Like

Thank you for raising this issue with the library’s core team. I tweaked how I create names compared to your example.

When using your example name={input_name(stanza_form, :url) <> "[#{index}]"} ( name={input_name(stanza_form, :content) <> "[#{index}]"} for text fields) I receive data in the following shape in my validator:

%{
    "stanzas" => %{
        "content" => %{"0" => "val1"}
    }
}

The problem becomes more apparent when the form contains fields of both types. For example if I make a text field then an image field then another text field:

%{
    "stanzas" => %{
        "content" => %{"0" => "val1", "2" => "val3"},
        "url" => %{"1" => "val2"}
    }
}

Before I can generate a changeset with this data I need to:

  1. replace the top-level keys in the "stanzas" map with the indexes of each field
  2. convert each value in the innermost maps to a map where the key is one of either "content" or "url"

That said, the changes I made look like this: name={input_name(poem_form, :stanzas) <> "[#{index}][url]"} (name={input_name(poem_form, :stanzas) <> "[#{index}][content]"} for text fields).

  1. Instead of using the stanza_form in input_name/2 I’m using the poem_form. Using the stanza_form results in a double-nested structure like so:
%{
    "stanzas" => %{
        "stanzas" => %{...}
    }
}
  1. I’m concatenating "[#{index}][url]" or "[#{index}][content]" instead of just "[#{index}]" which results in data of the following shape:
%{
    "stanzas" => %{
        "0" => %{"content" => "val1"},
        "1" => %{"url" => "val2"},
        "2" => %{"content" => "val3"}
    }
}

Now converting this to a list that will work with my changeset function is as simple as for {k, v} <- params["stanzas"], into: [], do: v, giving:

[
    %{"content" => "val1"}, 
    %{"url" => "val2"}, 
    %{"content" => "val3"}
]

which is exactly the data that I want. The form inputs do not display changeset errors when they exist but this is a big step in the right direction. Thank you for your help