Packages
polymorphic_embed
5.0.6
5.0.6
5.0.5
5.0.3
5.0.2
5.0.1
5.0.0
4.1.1
4.1.0
4.0.0
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
2.0.1
2.0.0
1.10.0
1.9.0
1.8.1
1.8.0
1.7.2
1.7.1
1.7.0
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.0
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.2.0
1.1.0
1.0.0
0.16.0
0.15.0
0.14.0
0.13.0
0.12.0
0.11.0
0.10.0
0.9.0
0.8.0
0.7.0
0.6.0
0.5.0
0.4.0
0.3.0
0.2.0
0.1.0
Polymorphic embeds in Ecto
Current section
Files
Jump to
Current section
Files
polymorphic_embed
README.md
README.md
# 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.
```elixir
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
```
```elixir
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, [:address, :confirmed])
|> validate_required(:address)
|> validate_length(:address, min: 4)
end
end
```
```elixir
defmodule MyApp.Channel.SMS do
use Ecto.Schema
import Ecto.Changeset
@primary_key false
embedded_schema do
field :number, :string
end
def changeset(sms, params) do
sms
|> cast(params, [:number])
|> validate_required(:number)
end
end
```
In your migration file, you may use the type `:map` for both `polymorphic_embeds_one/2` and `polymorphic_embeds_many/2` fields.
```elixir
add(:channel, :map)
```
[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.
### `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.
```elixir
changeset
|> cast_polymorphic_embed(:channel,
with: [
sms: &SMS.custom_changeset/2,
email: &Email.custom_changeset/2
]
)
```
* `: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).
* `: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).
* `: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.
### 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:
1.
```elixir
[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"`).
2.
```elixir
[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:
```elixir
polymorphic_embeds_many :contexts,
types: [
location: MyApp.Context.Location,
age: MyApp.Context.Age,
device: MyApp.Context.Device
],
on_replace: :delete,
on_type_not_found: :raise,
nilify_unlisted_types_on_load: [:deprecated_type]
```
#### Options
* `:types` – discussed above.
* `:type_field_name` – specify a custom field name for the field holding the type. Defaults to `:__type__`.
* `:use_parent_field_for_type` – fetch the type from a specified field in the parent schema (the schema holding the embed).
* `: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`).
* `: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.
* `:nilify_unlisted_types_on_load`: same as `:retain_unlisted_types_on_load`, but nilify the struct on load.
### 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, change `view` to the one your component use:
```elixir
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:
```elixir
<%= 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 `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).
```elixir
<.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>
```
### Get the type and module of a polymorphic embed
Sometimes you need to serialize the polymorphic embed and, once in the front-end, need to distinguish them.
`PolymorphicEmbed.get_polymorphic_type/3` returns the type of the polymorphic embed:
```elixir
PolymorphicEmbed.get_polymorphic_type(Reminder, :channel, SMS) == :sms
```
To get the module for a specific type, use:
```elixir
PolymorphicEmbed.get_polymorphic_module(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.
## Installation
Add `polymorphic_embed` for Elixir as a dependency in your `mix.exs` file:
```elixir
def deps do
[
{:polymorphic_embed, "~> 5.0"}
]
end
```
## HexDocs
HexDocs documentation can be found at [https://hexdocs.pm/polymorphic_embed](https://hexdocs.pm/polymorphic_embed).