{"id":13491622,"url":"https://github.com/mathieuprog/polymorphic_embed","last_synced_at":"2025-10-07T15:44:49.012Z","repository":{"id":43380890,"uuid":"267630990","full_name":"mathieuprog/polymorphic_embed","owner":"mathieuprog","description":"Polymorphic embeds in Ecto","archived":false,"fork":false,"pushed_at":"2024-04-25T19:59:08.000Z","size":235,"stargazers_count":276,"open_issues_count":21,"forks_count":59,"subscribers_count":6,"default_branch":"master","last_synced_at":"2024-05-01T16:00:18.163Z","etag":null,"topics":["ecto","elixir","elixir-lang","phoenix","polymorphic-embed"],"latest_commit_sha":null,"homepage":null,"language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mathieuprog.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"mathieuprog"}},"created_at":"2020-05-28T15:47:50.000Z","updated_at":"2024-05-04T08:23:37.757Z","dependencies_parsed_at":"2024-04-25T20:51:37.232Z","dependency_job_id":null,"html_url":"https://github.com/mathieuprog/polymorphic_embed","commit_stats":{"total_commits":184,"total_committers":13,"mean_commits":"14.153846153846153","dds":"0.19021739130434778","last_synced_commit":"78e51d90a55d73a0f81a99de1c28316f057d37b8"},"previous_names":[],"tags_count":47,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mathieuprog%2Fpolymorphic_embed","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mathieuprog%2Fpolymorphic_embed/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mathieuprog%2Fpolymorphic_embed/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mathieuprog%2Fpolymorphic_embed/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mathieuprog","download_url":"https://codeload.github.com/mathieuprog/polymorphic_embed/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247767232,"owners_count":20992538,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ecto","elixir","elixir-lang","phoenix","polymorphic-embed"],"created_at":"2024-07-31T19:00:58.737Z","updated_at":"2025-10-07T15:44:43.961Z","avatar_url":"https://github.com/mathieuprog.png","language":"Elixir","readme":"# Polymorphic embeds for Ecto\n\n`polymorphic_embed` brings support for polymorphic/dynamic embedded schemas in Ecto.\n\nEcto's `embeds_one` and `embeds_many` macros require a specific schema module to be specified. This library removes this restriction by\n**dynamically** determining which schema to use, based on data to be stored (from a form or API) and retrieved (from the\ndata source).\n\n## Usage\n\n### Enable polymorphism\n\nLet's say we want a schema `Reminder` representing a reminder for an event, that can be sent either by email or SMS.\n\nWe create the `Email` and `SMS` embedded schemas containing the fields that are specific for each of those communication\nchannels.\n\nThe `Reminder` schema can then contain a `:channel` field that will either hold an `Email` or `SMS` struct, by setting\nits type to the custom type `PolymorphicEmbed` that this library provides.\n\nFind the schema code and explanations below.\n\n```elixir\ndefmodule MyApp.Reminder do\n  use Ecto.Schema\n  import Ecto.Changeset\n  import PolymorphicEmbed\n\n  schema \"reminders\" do\n    field :date, :utc_datetime\n    field :text, :string\n\n    polymorphic_embeds_one :channel,\n      types: [\n        sms: MyApp.Channel.SMS,\n        email: MyApp.Channel.Email\n      ],\n      on_type_not_found: :raise,\n      on_replace: :update\n  end\n\n  def changeset(struct, values) do\n    struct\n    |\u003e cast(values, [:date, :text])\n    |\u003e cast_polymorphic_embed(:channel, required: true)\n    |\u003e validate_required(:date)\n  end\nend\n```\n\n```elixir\ndefmodule MyApp.Channel.Email do\n  use Ecto.Schema\n  import Ecto.Changeset\n\n  @primary_key false\n\n  embedded_schema do\n    field :address, :string\n    field :confirmed, :boolean\n  end\n\n  def changeset(email, params) do\n    email\n    |\u003e cast(params, [:address, :confirmed])\n    |\u003e validate_required(:address)\n    |\u003e validate_length(:address, min: 4)\n  end\nend\n```\n\n```elixir\ndefmodule MyApp.Channel.SMS do\n  use Ecto.Schema\n  import Ecto.Changeset\n\n  @primary_key false\n\n  embedded_schema do\n    field :number, :string\n  end\n\n  def changeset(sms, params) do\n    sms\n    |\u003e cast(params, [:number])\n    |\u003e validate_required(:number)\n  end\nend\n```\n\nIn your migration file, you may use the type `:map` for both `polymorphic_embeds_one/2` and `polymorphic_embeds_many/2` fields.\n\n```elixir\nadd(:channel, :map)\n```\n\n[It is not recommended](https://hexdocs.pm/ecto/3.11.2/Ecto.Schema.html#embeds_many/3) to use `{:array, :map}` for a list of embeds.\n\n### `cast_polymorphic_embed/3`\n\n`cast_polymorphic_embed/3` must be called to cast the polymorphic embed's parameters.\n\n#### Options\n\n* `:required` – if the embed is a required field.\n\n* `:with` – allows you to specify a custom changeset.\n```elixir\nchangeset\n|\u003e cast_polymorphic_embed(:channel,\n  with: [\n    sms: \u0026SMS.custom_changeset/2,\n    email: \u0026Email.custom_changeset/2\n  ]\n)\n```\n\n* `:drop_param` – see [sorting-and-deleting-from-many-collections](https://hexdocs.pm/ecto/3.11.2/Ecto.Changeset.html#cast_assoc/3-sorting-and-deleting-from-many-collections).\n\n* `:sort_param` – see [sorting-and-deleting-from-many-collections](https://hexdocs.pm/ecto/3.11.2/Ecto.Changeset.html#cast_assoc/3-sorting-and-deleting-from-many-collections).\n\n* `:default_type_on_sort_create` – in some cases, [`sort` creates a new entry](https://github.com/elixir-ecto/ecto/blob/v3.11/test/ecto/changeset/embedded_test.exs#L464); this option specifies which type to use by default for the entry.\n\n### PolymorphicEmbed Ecto type\n\nThe `:types` option for the `PolymorphicEmbed` custom type contains a keyword list mapping an atom representing the type\n(in this example `:email` and `:sms`) with the corresponding embedded schema module.\n\nThere are two strategies to detect the right embedded schema to use:\n\n1.\n```elixir\n[sms: MyApp.Channel.SMS]\n```\n\nWhen receiving parameters to be casted (e.g. from a form), we expect a `\"__type__\"` (or `:__type__`) parameter\ncontaining the type of channel (`\"email\"` or `\"sms\"`).\n\n2.\n```elixir\n[email: [\n  module: MyApp.Channel.Email,\n  identify_by_fields: [:address, :confirmed]]]\n```\n\nHere we specify how the type can be determined based on the presence of given fields. In this example, if the data\ncontains `:address` and `:confirmed` parameters (or their string version), the type is `:email`. A `\"__type__\"`\nparameter is then no longer required.\n\nNote that you may still include a `__type__` parameter that will take precedence over this strategy (this could still be\nuseful if you need to store incomplete data, which might not allow identifying the type).\n\n#### List of polymorphic embeds\n\nLists of polymorphic embeds are also supported:\n\n```elixir\npolymorphic_embeds_many :contexts,\n  types: [\n    location: MyApp.Context.Location,\n    age: MyApp.Context.Age,\n    device: MyApp.Context.Device\n  ],\n  on_replace: :delete,\n  on_type_not_found: :raise,\n  nilify_unlisted_types_on_load: [:deprecated_type]\n```\n\n#### Options\n\n* `:types` – discussed above.\n* `:type_field_name` – specify a custom field name for the field holding the type. Defaults to `:__type__`.\n* `:use_parent_field_for_type` – fetch the type from a specified field in the parent schema (the schema holding the embed).\n* `:on_type_not_found` – specify what to do if the embed's type cannot be inferred.\n  Possible values are\n  - `:raise`: raise an error\n  - `:changeset_error`: add a changeset error\n  - `:nilify`: replace the data by `nil`; only for single (non-list) embeds\n  - `:ignore`: ignore the data; only for lists of embeds\n\n  By default, a changeset error \"is invalid\" is added.\n* `:on_replace` – mandatory option that can only be set to `:update` for a single embed and `:delete` for a list of\n  embeds (we force a value as the default value of this option for `embeds_one` and `embeds_many` is `:raise`).\n* `:retain_unlisted_types_on_load`: allow unconfigured types to be loaded without raising an error. Useful for handling deprecated structs still present in the database.\n* `:nilify_unlisted_types_on_load`: same as `:retain_unlisted_types_on_load`, but nilify the struct on load.\n\n### Displaying form inputs and errors in Phoenix templates\n\nThe library comes with a form helper in order to build form inputs for polymorphic embeds and display changeset errors.\n\nIn the entrypoint defining your web interface (`lib/your_app_web.ex` file), add the following import, change `view` to the one your component use:\n\n```elixir\ndef view do\n  quote do\n    # imports and stuff\n    import PolymorphicEmbed.HTML.Form\n  end\nend\n```\n\nThis provides you with the `polymorphic_embed_inputs_for/3` and `polymorphic_embed_inputs_for/4` functions.\n\nHere is an example form using the imported function:\n\n```elixir\n\u003c%= inputs_for f, :reminders, fn reminder_form -\u003e %\u003e\n  \u003c%= polymorphic_embed_inputs_for reminder_form, :channel, :sms, fn sms_form -\u003e %\u003e\n    \u003cdiv class=\"sms-inputs\"\u003e\n      \u003clabel\u003eNumber\u003c/label\u003e\n      \u003c%= text_input sms_form, :number %\u003e\n      \u003cdiv class=\"error\"\u003e\n        \u003c%= error_tag sms_form, :number %\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c% end %\u003e\n\u003c% end %\u003e\n```\n\nWhen using `polymorphic_embed_inputs_for/4`, you have to manually specify the type. When the embed is `nil`, empty fields will be displayed.\n\n`polymorphic_embed_inputs_for/3` doesn't require the type to be specified. When the embed is `nil`, no fields are displayed.\n\nThey both render a hidden input for the `\"__type__\"` field.\n\n### Displaying form inputs and errors in LiveView\n\nYou may use `PolymorphicEmbed.HTML.Component.polymorphic_embed_inputs_for/1` when working with LiveView, which functions similarly to [`Phoenix.Component.inputs_for/1`](Phoenix.Component.inputs_for).\n\n```elixir\n\u003c.form\n  :let={f}\n  for={@changeset}\n  id=\"reminder-form\"\n  phx-change=\"validate\"\n  phx-submit=\"save\"\n\u003e\n  \u003c.polymorphic_embed_inputs_for field={f[:channel]} :let={channel_form}\u003e\n    \u003c%= case source_module(channel_form) do %\u003e\n      \u003c% SMS -\u003e %\u003e\n        \u003c.input field={channel_form[:number]} label=\"Number\" /\u003e\n\n      \u003c% Email -\u003e %\u003e\n        \u003c.input field={channel_form[:address]} label=\"Email Address\" /\u003e\n    \u003c% end %\u003e\n  \u003c/.polymorphic_embed_inputs_for\u003e\n\u003c/.form\u003e\n```\n\n### Get the type and module of a polymorphic embed\n\nSometimes you need to serialize the polymorphic embed and, once in the front-end, need to distinguish them.\n`PolymorphicEmbed.get_polymorphic_type/3` returns the type of the polymorphic embed:\n\n```elixir\nPolymorphicEmbed.get_polymorphic_type(Reminder, :channel, SMS) == :sms\n```\n\nTo get the module for a specific type, use:\n\n```elixir\nPolymorphicEmbed.get_polymorphic_module(Reminder, :channel, :sms) == SMS\n```\n\n### `traverse_errors/2`\n\nThe function `Ecto.changeset.traverse_errors/2` won't include the errors of polymorphic embeds. You may instead use `PolymorphicEmbed.traverse_errors/2` when working with polymorphic embeds.\n\n## Installation\n\nAdd `polymorphic_embed` for Elixir as a dependency in your `mix.exs` file:\n\n```elixir\ndef deps do\n  [\n    {:polymorphic_embed, \"~\u003e 5.0\"}\n  ]\nend\n```\n\n## HexDocs\n\nHexDocs documentation can be found at [https://hexdocs.pm/polymorphic_embed](https://hexdocs.pm/polymorphic_embed).\n","funding_links":["https://github.com/sponsors/mathieuprog"],"categories":["Elixir"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmathieuprog%2Fpolymorphic_embed","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmathieuprog%2Fpolymorphic_embed","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmathieuprog%2Fpolymorphic_embed/lists"}