Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ add(:channel, :map)

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

After casting a polymorphic embed, its entry in `changeset.params` contains the child changeset's parameters. For `polymorphic_embeds_many`, this is a list in the final cast order, with dropped and ignored rows removed. Form helpers use these parameters to preserve submitted values and row identifiers.

#### Options

* `:required` – if the embed is a required field.
Expand Down
15 changes: 13 additions & 2 deletions lib/polymorphic_embed.ex
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ defmodule PolymorphicEmbed do
{action, struct} ->
embed_changeset = changeset_fun.(struct, params)
embed_changeset = %{embed_changeset | action: action}
changeset = put_embed_params(changeset, field, embed_changeset.params || params)

case embed_changeset do
%{valid?: true} = embed_changeset ->
Expand Down Expand Up @@ -431,21 +432,27 @@ defmodule PolymorphicEmbed do
%{changeset_fun.(struct(module), params) | action: :insert}
end

maybe_apply_changes(embed_changeset)
{maybe_apply_changes(embed_changeset), embed_changeset.params || params}
end
end)

if Enum.any?(embeds, &(&1 == :error)) do
Ecto.Changeset.add_error(changeset, field, "is invalid")
else
embeds = Enum.filter(embeds, &(&1 != :ignore))
{embeds, params} =
embeds
|> Enum.reject(&(&1 == :ignore))
|> Enum.unzip()

any_invalid? =
Enum.any?(embeds, fn
%{valid?: false} -> true
_ -> false
end)

# Keep each row's parameters in cast order, including children converted to structs.
changeset = put_embed_params(changeset, field, params)

changeset = Ecto.Changeset.put_change(changeset, field, embeds)

if any_invalid? do
Expand All @@ -464,6 +471,10 @@ defmodule PolymorphicEmbed do

defp maybe_apply_changes(%Changeset{valid?: false} = changeset), do: changeset

defp put_embed_params(changeset, field, params) do
%{changeset | params: Map.put(changeset.params || %{}, to_string(field), params)}
end

@impl true
def cast(_data, _params),
do:
Expand Down
110 changes: 75 additions & 35 deletions lib/polymorphic_embed/html/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -49,45 +49,24 @@ if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) d
form.source.data.__struct__
end

def to_form(%{action: parent_action} = source_changeset, form, field, options) do
def to_form(source_changeset, %{action: parent_action} = form, field, options) do
id = to_string(form.id <> "_#{field}")
name = to_string(form.name <> "[#{field}]")

params = Map.get(source_changeset.params || %{}, to_string(field), %{}) |> List.wrap()

struct = Ecto.Changeset.apply_changes(source_changeset)

list_data =
case Map.get(struct, field) do
nil ->
type = Keyword.get(options, :polymorphic_type, get_polymorphic_type(form, field))
module = PolymorphicEmbed.get_polymorphic_module(struct.__struct__, field, type)
if module, do: [struct(module)], else: []

data ->
List.wrap(data)
end

list_data
resolve_field_data(source_changeset, struct, form, field, options)
|> Enum.with_index()
|> Enum.map(fn {data, i} ->
params = Enum.at(params, i) || %{}

%Ecto.Changeset{} =
changeset =
data
|> Ecto.Changeset.change()
|> apply_action(parent_action)

errors = get_errors(changeset)

changeset = %Ecto.Changeset{
changeset
| action: parent_action,
params: params,
errors: errors,
valid?: errors == []
}
|> Enum.map(&prepare_changeset(&1, params, parent_action))
|> Enum.map(fn prepared_data ->
%{
changeset: changeset,
params: params,
errors: errors,
index: i
} = prepared_data

%schema{} = source_changeset.data

Expand All @@ -107,7 +86,7 @@ if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) d
name: if(array?, do: name <> "[" <> index_string <> "]", else: name),
index: if(array?, do: i),
errors: errors,
data: data,
data: changeset.data,
action: parent_action,
params: params,
hidden: [{type_field_name, to_string(type)}],
Expand All @@ -116,9 +95,70 @@ if Code.ensure_loaded?(Phoenix.HTML) && Code.ensure_loaded?(Phoenix.HTML.Form) d
end)
end

# If the parent changeset had no action, we need to remove the action
# from children changeset so we ignore all errors accordingly.
defp apply_action(changeset, nil), do: %{changeset | action: nil}
defp resolve_field_data(source_changeset, struct, form, field, options) do
case Map.get(source_changeset.changes, field) do
nil ->
case Map.get(struct, field) do
nil ->
type = Keyword.get(options, :polymorphic_type, get_polymorphic_type(form, field))
module = PolymorphicEmbed.get_polymorphic_module(struct.__struct__, field, type)
if module, do: [struct(module)], else: []

data ->
List.wrap(data)
end

data ->
List.wrap(data)
end
end

defp prepare_changeset({%Ecto.Changeset{} = changeset, i}, _params, parent_action) do
params = changeset.params || %{}
changeset = apply_action(changeset, parent_action)
errors = get_errors(changeset)

%{
changeset: changeset,
params: params,
errors: errors,
index: i
}
end

defp prepare_changeset({data, i}, params, parent_action) do
params = Enum.at(params, i) || %{}

changeset =
data
|> Ecto.Changeset.change()
|> apply_action(parent_action)

errors = get_errors(changeset)

changeset = %{changeset | params: params}

# The changeset has no changes, so phoenix_ecto would read nested embed
# values from the raw form parameters. Drop them to read the cast data.
# The changeset keeps them for nested polymorphic forms.
params = Map.drop(params, embed_param_keys(changeset.types))

%{changeset: changeset, params: params, errors: errors, index: i}
end

defp embed_param_keys(types) do
for {field, type} <- types, embed_type?(type), do: to_string(field)
end

defp embed_type?({:embed, _}), do: true
defp embed_type?({:parameterized, {PolymorphicEmbed, _}}), do: true
defp embed_type?({:array, {:parameterized, {PolymorphicEmbed, _}}}), do: true
defp embed_type?(_), do: false

defp apply_action(changeset, action) when action in [nil, :ignore],
do: %{changeset | action: action}

defp apply_action(%{action: nil} = changeset, action), do: %{changeset | action: action}
defp apply_action(changeset, _action), do: changeset

defp get_errors(%{action: nil}), do: []
Expand Down
Loading
Loading