Skip to content
Merged
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## Unreleased

## [0.1.2] - 2021-08-29

### Fixed

- Handle `Ecto.Association.NotLoaded` structs when appending, prepending or
inserting data into relations that are child relations of newly added, not
persisted data.

## [0.1.1] - 2021-08-28

### Changed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Add `ecto_nested_changeset` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:ecto_nested_changeset, "~> 0.1.1"}
{:ecto_nested_changeset, "~> 0.1.2"}
]
end
```
Expand Down
14 changes: 0 additions & 14 deletions example/lib/nested_web/live/owner_live/form_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ defmodule NestedWeb.OwnerLive.FormComponent do

@impl true
def handle_event("validate", %{"owner" => owner_params}, socket) do
owner_params = prepare_params(owner_params)

changeset =
socket.assigns.owner
|> Members.change_owner(owner_params)
Expand Down Expand Up @@ -110,18 +108,6 @@ defmodule NestedWeb.OwnerLive.FormComponent do
end
end

# puts empty lists into association fields if no associations were added
defp prepare_params(owner_params) do
owner_params
|> Map.put_new("pets", [])
|> Map.update!(
"pets",
&Enum.into(&1, %{}, fn {key, pet} ->
{key, Map.put_new(pet, "toys", [])}
end)
)
end

defp deleted?(form) do
input_value(form, :delete) in ["true", true]
end
Expand Down
2 changes: 1 addition & 1 deletion example/lib/nested_web/live/owner_live/show.html.leex
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<ul>
<%= for pet <- @owner.pets do %>
<li>
<strong>Name:</strong> <%= @owner.name %>,
<strong>Name:</strong> <%= pet.name %>,
<strong>Toys:</strong> <%= pet.toys |> Enum.map(& &1.name) |> Enum.join(", ") %>
</li>
<% end %>
Expand Down
36 changes: 26 additions & 10 deletions lib/ecto_nested_changeset.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule EctoNestedChangeset do

import Ecto.Changeset

alias Ecto.Association.NotLoaded
alias Ecto.Changeset

@doc """
Expand Down Expand Up @@ -215,11 +216,13 @@ defmodule EctoNestedChangeset do

defp nested_update(:append, %Changeset{} = changeset, [field], value)
when is_atom(field) do
Changeset.put_change(
changeset,
field,
get_change_or_field(changeset, field) ++ [value]
)
new_value =
case {get_change_or_field(changeset, field), changeset.action} do
{%NotLoaded{}, :insert} -> [value]
{previous_value, _} -> previous_value ++ [value]
end

Changeset.put_change(changeset, field, new_value)
end

defp nested_update(:append, %{} = data, [field], value) when is_atom(field) do
Expand All @@ -230,11 +233,13 @@ defmodule EctoNestedChangeset do

defp nested_update(:prepend, %Changeset{} = changeset, [field], value)
when is_atom(field) do
Changeset.put_change(
changeset,
field,
[value | get_change_or_field(changeset, field)]
)
new_value =
case {get_change_or_field(changeset, field), changeset.action} do
{%NotLoaded{}, :insert} -> [value]
{previous_value, _} -> [value | previous_value]
end

Changeset.put_change(changeset, field, new_value)
end

defp nested_update(:prepend, %{} = data, [field], value)
Expand All @@ -249,6 +254,17 @@ defmodule EctoNestedChangeset do
List.insert_at(items, index, value)
end

defp nested_update(:insert, %Changeset{} = changeset, [field, index], value)
when is_atom(field) and is_integer(index) do
new_value =
case {get_change_or_field(changeset, field), changeset.action} do
{%NotLoaded{}, :insert} -> [value]
{previous_value, _} -> List.insert_at(previous_value, index, value)
end

Changeset.put_change(changeset, field, new_value)
end

defp nested_update(:update, %Changeset{} = changeset, [field], func)
when is_atom(field) do
value = get_change_or_field(changeset, field)
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule EctoNestedChangeset.MixProject do
use Mix.Project

@version "0.1.1"
@version "0.1.2"
@source_url "https://github.com/woylie/ecto_nested_changeset"

def project do
Expand Down
75 changes: 75 additions & 0 deletions test/ecto_nested_changeset_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,31 @@ defmodule EctoNestedChangesetTest do
} = changeset.changes
end

test "appends item at a sub field of a new list item" do
changeset =
%Category{id: 1, posts: []}
|> change()
|> append_at(:posts, %Post{title: "first"})
|> append_at([:posts, 0, :comments], %Comment{})

assert %{
posts: [
%Ecto.Changeset{
action: :insert,
changes: %{
comments: [
%Ecto.Changeset{
action: :insert,
data: %Comment{}
}
]
},
data: %Post{title: "first"}
}
]
} = changeset.changes
end

test "appends item at a root level field with existing data" do
changeset =
%Category{id: 1, posts: [%Post{id: 1, title: "existing"}]}
Expand Down Expand Up @@ -164,6 +189,31 @@ defmodule EctoNestedChangesetTest do
} = changeset.changes
end

test "prepends item at a sub field of a new list item" do
changeset =
%Category{id: 1, posts: []}
|> change()
|> prepend_at(:posts, %Post{title: "first"})
|> prepend_at([:posts, 0, :comments], %Comment{})

assert %{
posts: [
%Ecto.Changeset{
action: :insert,
changes: %{
comments: [
%Ecto.Changeset{
action: :insert,
data: %Comment{}
}
]
},
data: %Post{title: "first"}
}
]
} = changeset.changes
end

test "prepend item at a root level field with existing data" do
changeset =
%Category{id: 1, posts: [%Post{id: 1, title: "existing"}]}
Expand Down Expand Up @@ -283,6 +333,31 @@ defmodule EctoNestedChangesetTest do
} = changeset.changes
end

test "inserts item at a sub field of a new list item" do
changeset =
%Category{id: 1, posts: []}
|> change()
|> insert_at([:posts, 0], %Post{title: "first"})
|> insert_at([:posts, 0, :comments, 0], %Comment{})

assert %{
posts: [
%Ecto.Changeset{
action: :insert,
changes: %{
comments: [
%Ecto.Changeset{
action: :insert,
data: %Comment{}
}
]
},
data: %Post{title: "first"}
}
]
} = changeset.changes
end

test "inserts item at a root level field with existing data" do
changeset =
%Category{
Expand Down