polymorphic_embed

repository·master·Indexed 19 days ago

https://github.com/mathieuprog/polymorphic_embed

Provides support for dynamic embedded schemas in Ecto, allowing a single field to use different embedded modules based on the provided data. It includes macros for defining single and multiple polymorphic embeds, a specialized casting function `cast_polymorphic_embed/3`, and integration helpers for Phoenix templates and LiveView to render nested form inputs.

Tokens
5.1K
Snippets
17
Records
23
Agent score
64%

What's inside polymorphic_embed

  1. Configure type detection strategies for PolymorphicEmbed

    master

    The :types option in polymorphic_embeds_one/2 or polymorphic_embeds_many/2 defines how the library identifies which schema to use. There are two main strategies:

    1. Explicit Type Parameter: Map an atom to a module. The library expects a "__type__" (or :__type__) parameter in the input data containing the type atom (e.g., "email"). Example: [sms: MyApp.Channel.SMS]

    2. Field-based Identification: Map an atom to a keyword list containing the module and identify_by_fields. The library determines the type based on the presence of these fields in the data, removing the need for a "__type__" parameter. Example: [email: [module: MyApp.Channel.Email, identify_by_fields: [:address, :confirmed]]]

    Note: A "__type__" parameter will take precedence over field-based identification.

  2. Display polymorphic embed inputs in Phoenix templates

    master

    To use form helpers in Phoenix templates, import PolymorphicEmbed.HTML.Form in your web module (e.g., lib/your_app_web.ex).

    • polymorphic_embed_inputs_for/3: Does not require a type to be specified. If the embed is nil, no fields are displayed.
    • polymorphic_embed_inputs_for/4: Requires manual type specification. If the embed is nil, empty fields are displayed.

    Both functions render a hidden input for the "__type__" field.

    # In lib/your_app_web.ex
    def view do
      quote do
        import PolymorphicEmbed.HTML.Form
      end
    end
    
    # In your template
    <%= inputs_for f, :reminders, fn reminder_form -> %>
      <%= polymorphic_embed_inputs_for reminder_form, :channel, :sms, fn sms_form -> %>
        <div class="sms-inputs">
          <label>Number</label>
          <%= text_input sms_form, :number %>
          <div class="error">
            <%= error_tag sms_form, :number %>
          </div>
        </div>
      <% end %>
    <% end %>
  3. Display polymorphic embed inputs in LiveView

    master

    Use PolymorphicEmbed.HTML.Component.polymorphic_embed_inputs_for/1 in LiveView. It works similarly to Phoenix.Component.inputs_for/1. You can use source_module/1 to determine which fields to render based on the current module.

    <.form
      :let={f}
      for={@changeset}
      id="reminder-form"
      phx-change="validate"
      phx-submit="save"
    >
      <.polymorphic_embed_inputs_for field={f[:channel]} :let={channel_form}>
        <%= case source_module(channel_form) do %>
          <% SMS -> %>
            <.input field={channel_form[:number]} label="Number" />
    
          <% Email -> %>
            <.input field={channel_form[:address]} label="Email Address" />
        <% end %>
      </.polymorphic_embed_inputs_for
    </.form>
  4. Enable polymorphic embeds in Ecto schemas

    master

    To use polymorphic embeds, use the polymorphic_embeds_one/2 or polymorphic_embeds_many/2 macros within your Ecto schema. You must provide a :types keyword list mapping an atom (the type identifier) to the corresponding embedded schema module.

    In your database migration, use the :map type for these fields. For lists of embeds, it is not recommended to use {:array, :map}.

    defmodule MyApp.Reminder do
      use Ecto.Schema
      import Ecto.Changeset
      import PolymorphicEmbed
    
      schema "reminders" do
        field :date, :utc_datetime
        field :text, :string
    
        polymorphic_embeds_one :channel,
          types: [
            sms: MyApp.Channel.SMS,
            email: MyApp.Channel.Email
          ],
          on_type_not_found: :raise,
          on_replace: :update
      end
    
      def changeset(struct, values) do
        struct
        |> cast(values, [:date, :text])
        |> cast_polymorphic_embed(:channel, required: true)
        |> validate_required(:date)
      end
    end
  5. How polymorphic embeds work

    master

    Polymorphic embeds allow an Ecto schema field to hold different types of data depending on a 'type' field.

    The Lifecycle:

    1. Definition: You define the field using polymorphic_embeds_one or polymorphic_embeds_many and specify the allowed types.
    2. Casting: Because the type is dynamic, you cannot use standard Ecto.Changeset.cast/4. You must use PolymorphicEmbed.cast_polymorphic_embed/3. This function looks at the type_field_name (defaulting to :__type__) in the incoming parameters to decide which module's changeset function to call.
    3. Loading: When loading from the database, the library reads the type field and uses Ecto.embedded_load/3 to instantiate the correct module.
    4. Dumping: When saving, the library converts the struct back to a map and injects the correct type atom into the type_field_name so it can be reloaded later.
  6. Use cast_polymorphic_embed/3 in changesets

    master

    You must call cast_polymorphic_embed/3 in your changeset function to cast the parameters for the polymorphic embed field.

    Available options:

    • :required – If the embed is a required field.
    • :with – Specify custom changeset functions for each type.
    • :drop_param – Follows standard Ecto cast_assoc behavior for dropping parameters.
    • :sort_param – Follows standard Ecto cast_assoc behavior for sorting parameters.
    • :default_type_on_sort_create – Specifies the default type to use when sort creates a new entry.
    changeset
    |> cast_polymorphic_embed(:channel,
      with: [
        sms: &SMS.custom_changeset/2,
        email: &Email.custom_changeset/2
      ]
    )
  7. Get the type and module of a polymorphic embed

    master

    Use these functions to inspect the type or module of a polymorphic embed, which is useful for serialization or frontend logic.

    • PolymorphicEmbed.get_polymorphic_type(schema, field, type_atom): Returns the type atom.
    • PolymorphicEmbed.get_polymorphic_module(schema, field, type_atom): Returns the module.
    PolymorphicEmbed.get_polymorphic_type(Reminder, :channel, SMS) == :sms
    PolymorphicEmbed.get_polymorphic_module(Reminder, :channel, :sms) == SMS
  8. Configure polymorphic_embeds_one and polymorphic_embeds_many options

    master

    When defining polymorphic embeds, you can use the following options:

    • :types – Mapping of type atoms to modules or identification configurations.
    • :type_field_name – Custom field name for the type (defaults to :__type__).
    • :use_parent_field_for_type – Fetch the type from a specified field in the parent schema.
    • :on_type_not_found – Action when type cannot be inferred:
      • :raise: Raise an error.
      • :changeset_error: Add a changeset error.
      • :nilify: Replace data with nil (single embeds only).
      • :ignore: Ignore data (lists only).
    • :on_replace – Mandatory. Set to :update for single embeds or :delete for lists.
    • :retain_unlisted_types_on_load – Allow unconfigured types to be loaded without error.
    • :nilify_unlisted_types_on_load – Nilify unconfigured types on load.
  9. Extract source data and module from a Phoenix form

    master

    The PolymorphicEmbed.HTML.Helpers module provides two utility functions to access the underlying data from a %Phoenix.HTML.Form{}:

    • source_data(form): Returns the raw data structure (form.source.data) from the form.
    • source_module(form): Returns the module (__struct__) of the data structure in the form.
    data = PolymorphicEmbed.HTML.Helpers.source_data(form)
    module = PolymorphicEmbed.HTML.Helpers.source_module(form)