• Stars
    star
    263
  • Rank 149,425 (Top 4 %)
  • Language
    Elixir
  • License
    Apache License 2.0
  • Created almost 4 years ago
  • Updated 2 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Polymorphic embeds in Ecto

Polymorphic embeds for Ecto

polymorphic_embed brings support for polymorphic/dynamic embedded schemas in Ecto.

Ecto's embeds_one and embeds_many macros require a specific schema module to be specified. This library removes this restriction by dynamically determining which schema to use, based on data to be stored (from a form or API) and retrieved (from the data source).

Usage

Enable polymorphism

Let's say we want a schema Reminder representing a reminder for an event, that can be sent either by email or SMS.

We create the Email and SMS embedded schemas containing the fields that are specific for each of those communication channels.

The Reminder schema can then contain a :channel field that will either hold an Email or SMS struct, by setting its type to the custom type PolymorphicEmbed that this library provides.

Find the schema code and explanations below.

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
defmodule MyApp.Channel.Email do
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key false

  embedded_schema do
    field :address, :string
    field :confirmed, :boolean
  end

  def changeset(email, params) do
    email
    |> cast(params, ~w(address confirmed)a)
    |> validate_required(:address)
    |> validate_length(:address, min: 4)
  end
end
defmodule MyApp.Channel.SMS do
  use Ecto.Schema

  @primary_key false

  embedded_schema do
    field :number, :string
  end
end

In your migration file, you may use the type :map for both polymorphic_embeds_one/2 and polymorphic_embeds_many/2 fields.

add(:channel, :map)

It is not recommended to use {:array, :map} for a list of embeds.

cast_polymorphic_embed/3

cast_polymorphic_embed/3 must be called to cast the polymorphic embed's parameters.

Options

  • :required – if the embed is a required field.

  • :with – allows you to specify a custom changeset. Either pass an MFA or a function:

changeset
|> cast_polymorphic_embed(:channel,
  with: [
    sms: {SMS, :custom_changeset, ["hello"]},
    email: &Email.custom_changeset/2
  ]
)

PolymorphicEmbed Ecto type

The :types option for the PolymorphicEmbed custom type contains a keyword list mapping an atom representing the type (in this example :email and :sms) with the corresponding embedded schema module.

There are two strategies to detect the right embedded schema to use:

[sms: MyApp.Channel.SMS]

When receiving parameters to be casted (e.g. from a form), we expect a "__type__" (or :__type__) parameter containing the type of channel ("email" or "sms").

[email: [
  module: MyApp.Channel.Email,
  identify_by_fields: [:address, :confirmed]]]

Here we specify how the type can be determined based on the presence of given fields. In this example, if the data contains :address and :confirmed parameters (or their string version), the type is :email. A "__type__" parameter is then no longer required.

Note that you may still include a __type__ parameter that will take precedence over this strategy (this could still be useful if you need to store incomplete data, which might not allow identifying the type).

List of polymorphic embeds

Lists of polymorphic embeds are also supported:

polymorphic_embeds_many :contexts,
  types: [
    location: MyApp.Context.Location,
    age: MyApp.Context.Age,
    device: MyApp.Context.Device
  ],
  on_type_not_found: :raise,
  on_replace: :delete

Options

  • :types – discussed above.

  • :type_field – specify a custom type field. Defaults to :__type__.

  • :on_type_not_found – specify what to do if the embed's type cannot be inferred. Possible values are

    • :raise: raise an error
    • :changeset_error: add a changeset error
    • :nilify: replace the data by nil; only for single (non-list) embeds
    • :ignore: ignore the data; only for lists of embeds

    By default, a changeset error "is invalid" is added.

  • :on_replace – mandatory option that can only be set to :update for a single embed and :delete for a list of embeds (we force a value as the default value of this option for embeds_one and embeds_many is :raise).

Displaying form inputs and errors in Phoenix templates

The library comes with a form helper in order to build form inputs for polymorphic embeds and display changeset errors.

In the entrypoint defining your web interface (lib/your_app_web.ex file), add the following import:

def view do
  quote do
    # imports and stuff
    import PolymorphicEmbed.HTML.Form
  end
end

This provides you with the polymorphic_embed_inputs_for/3 and polymorphic_embed_inputs_for/4 functions.

Here is an example form using the imported function:

<%= 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 %>

When using polymorphic_embed_inputs_for/4, you have to manually specify the type. When the embed is nil, empty fields will be displayed.

polymorphic_embed_inputs_for/3 doesn't require the type to be specified. When the embed is nil, no fields are displayed.

They both render a hidden input for the "__type__" field.

Displaying form inputs and errors in LiveView

You may use polymorphic_embed_inputs_for/2 when working with LiveView.

<.form
  let={f}
  for={@changeset}
  id="reminder-form"
  phx-change="validate"
  phx-submit="save"
>
  <%= for channel_form <- polymorphic_embed_inputs_for f, :channel do %>
    <%= hidden_inputs_for(channel_form) %>

    <%= case get_polymorphic_type(channel_form, Reminder, :channel) do %>
      <% :sms -> %>
        <%= label channel_form, :number %>
        <%= text_input channel_form, :number %>

      <% :email -> %>
        <%= label channel_form, :email %>
        <%= text_input channel_form, :email %>
  <% end %>
</.form>

Using this function, you have to render the necessary hidden inputs manually as shown above.

Get the type of a polymorphic embed

Sometimes you need to serialize the polymorphic embed and, once in the front-end, need to distinguish them. get_polymorphic_type/3 returns the type of the polymorphic embed:

PolymorphicEmbed.get_polymorphic_type(Reminder, :channel, SMS) == :sms

traverse_errors/2

The 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.

Features

  • Detect which types to use for the data being cast-ed, based on fields present in the data (no need for a type field in the data)
  • Run changeset validations when a changeset/2 function is present (when absent, the library will introspect the fields to cast)
  • Support for nested polymorphic embeds
  • Support for nested embeds_one/embeds_many embeds
  • Display form inputs for polymorphic embeds in Phoenix templates
  • Tests to ensure code quality

Installation

Add polymorphic_embed for Elixir as a dependency in your mix.exs file:

def deps do
  [
    {:polymorphic_embed, "~> 3.0.5"}
  ]
end

HexDocs

HexDocs documentation can be found at https://hexdocs.pm/polymorphic_embed.

More Repositories

1

tz

Time zone support for Elixir
Elixir
118
star
2

query_builder

Compose Ecto queries without effort
Elixir
73
star
3

redirect

Router macro for redirecting a request at a given path to another
Elixir
20
star
4

i18n_helpers

A set of tools to help you translate your Elixir applications
Elixir
19
star
5

tz_extra

A few utilities to work with time zones in Elixir
Elixir
14
star
6

changeset_helpers

Functions to help working with nested changesets and associations
Elixir
13
star
7

solid-compose

A set of reactive state for commonly used features in web apps
TypeScript
8
star
8

mobile_number_format

Parse and validate mobile phone numbers
Elixir
8
star
9

uploader

File upload library for Elixir
Elixir
6
star
10

user-locale

TypeScript
5
star
11

attrs

Unifying atom and string key handling for user data (attrs maps) given to Ecto's cast function
Elixir
5
star
12

tzdb_test

Testing Elixir time zone database implementations
Elixir
4
star
13

fluent-graphql

JavaScript GraphQL client
JavaScript
4
star
14

binance_interface

Binance API for Elixir
Elixir
3
star
15

eleventy-i18n

i18n for Eleventy with dynamic parameters and pluralization support
JavaScript
3
star
16

graphql-light

A simple GraphQL client
JavaScript
3
star
17

virtual_fields_filler

Fill the virtual fields for your Ecto structs and nested structs recursively
Elixir
3
star
18

datetime-helpers

Utility functions to work with dates and time
TypeScript
3
star
19

regex_to_strings

Get the strings a regex will match
Elixir
2
star
20

retry_on_stale

Elixir utility for retrying stale Ecto entry operations
Elixir
2
star
21

enumx

Additional utility functions to extend the power of Elixir's Enum module
Elixir
2
star
22

auth_n

Authentication library for Elixir applications
Elixir
2
star
23

or-throw

Returns the value if truthy or throws an error
TypeScript
1
star
24

object-array-utils

Utilities for working with arrays and objects
TypeScript
1
star
25

auth_z

Authorization library for Elixir applications
Elixir
1
star
26

retry_on

Elixir utility for retrying Ecto operations
Elixir
1
star