EctoNestedChangeset (Ecto Nested Changeset v1.0.0)

View Source

This module defines function for manipulating nested changesets.

All functions take a path as the second argument. The path is a list of atoms (for field names) and integers (for indexes in lists).

Summary

Functions

Appends a value to the field referenced by the path.

Deletes the item at the given path.

Returns a value from a changeset referenced by the path.

Inserts a value into a field at the given position.

Prepends a value to the field referenced by the path.

Updates the value in the changeset at the given position with the given update function.

Functions

append_at(changeset, path, value)

@spec append_at(Ecto.Changeset.t(), [atom() | non_neg_integer()] | atom(), any()) ::
  Ecto.Changeset.t()

Appends a value to the field referenced by the path.

The last path segment must be an atom referencing either a to-many relation field or an array field.

Example

iex> %Owner{pets: [%Pet{}, %Pet{toys: [%Toy{name: "stick"}]}]}
...> |> Ecto.Changeset.change()
...> |> append_at(changeset, [:pets, 1, :toys], %Toy{name: "ball"})
...> |> Ecto.Changeset.apply_changes()
%Owner{
  pets: [
    %Pet{},
    %Pet{toys: [%Toy{name: "stick"}, %Toy{name: "ball"}]}
  ]
}

delete_at(changeset, path, opts \\ [])

@spec delete_at(Ecto.Changeset.t(), [atom() | non_neg_integer()] | atom(), keyword()) ::
  Ecto.Changeset.t()

Deletes the item at the given path.

The last path segment is expected to be an integer index.

Items that are not persisted in the database yet will always be removed from the list. For structs that are already persisted in the database, there are three different modes.

  • [mode: {:action, :replace}] (default) - The item will be wrapped in a changeset with the :replace action. This only works if an appropriate :on_replace option is set for the relation in the schema.
  • [mode: {:action, :delete}] - The item will be wrapped in a changeset with the action set to :delete.
  • [mode: {:flag, field}] - Puts true as a change for the given field.

The flag option useful for explicitly marking items for deletion in form parameters. In this case, you would configure a virtual field on the schema and set the changeset action to :delete in the changeset function in case the value is set to true.

schema "pets" do
  field :name, :string
  field :delete, :boolean, virtual: true, default: false
end

def changeset(pet, attrs) do
  pet
  |> cast(attrs, [:name, :delete])
  |> validate_required([:name])
  |> maybe_mark_for_deletion()
end

def maybe_mark_for_deletion(%Ecto.Changeset{} = changeset) do
  if Ecto.Changeset.get_change(changeset, :delete),
    do: Map.put(changeset, :action, :delete),
    else: changeset
end

Examples

iex> changeset = Ecto.Changeset.change(
       %Owner{pets: [%Pet{name: "George"}, %Pet{name: "Patty"}]}
...> )
iex> delete_at(changeset, [:pets, 1])
%Ecto.Changeset{
  changes: [
    %Changeset{action: :replace, data: %Post{name: "Patty"}},
    %Changeset{action: :update, data: %Post{name: "George"}},
  ]
}
iex> delete_at(changeset, [:pets, 1], mode: {:action, :delete})
%Ecto.Changeset{
  changes: [
    %Changeset{action: :update, data: %Post{name: "George"}},
    %Changeset{action: :delete, data: %Post{name: "Patty"}},
  ]
}
iex> delete_at(changeset, [:pets, 1], mode: {:field, :delete})
%Ecto.Changeset{
  changes: [
    %Changeset{action: :update, data: %Post{name: "George"}},
    %Changeset{
      action: :update,
      changes: %{delete: true},
      data: %Post{name: "Patty"}
    },
  ]
}

get_at(changeset, path)

@spec get_at(Ecto.Changeset.t(), [atom() | non_neg_integer()] | atom()) :: any()

Returns a value from a changeset referenced by the path.

Example

iex> %Owner{pets: [%Pet{}, %Pet{toys: [%Toy{name: "stick"}]}]}
...> |> Ecto.Changeset.change()
...> |> get_at(changeset, [:pets, 1, :toys])
[%Toy{name: "stick"}, %Toy{name: "ball"}]

insert_at(changeset, path, value)

@spec insert_at(Ecto.Changeset.t(), [atom() | non_neg_integer()] | atom(), any()) ::
  Ecto.Changeset.t()

Inserts a value into a field at the given position.

The last path segment must be an integer for the position.

Example

iex> %Owner{
...>   pets: [
...>     %Pet{},
...>     %Pet{toys: [%Toy{name: "stick"}, %Toy{name: "ball"}]}
...>   ]
...> }
...> |> Ecto.Changeset.change()
...> |> insert_at(changeset, [:pets, 1, :toys, 1], %Toy{name: "rope"})
...> |> Ecto.Changeset.apply_changes()
%Owner{
  pets: [
    %Pet{},
    %Pet{
      toys: [
        %Toy{name: "ball"},
        %Toy{name: "rope"},
        %Toy{name: "stick"}
      ]
    }
  ]
}

prepend_at(changeset, path, value)

@spec prepend_at(Ecto.Changeset.t(), [atom() | non_neg_integer()] | atom(), any()) ::
  Ecto.Changeset.t()

Prepends a value to the field referenced by the path.

The last path segment must be an atom referencing either a to-many relation field or an array field.

Example

iex> %Owner{pets: [%Pet{}, %Pet{toys: [%Toy{name: "stick"}]}]}
...> |> Ecto.Changeset.change()
...> |> prepend_at(changeset, [:pets, 1, :toys], %Toy{name: "ball"})
...> |> Ecto.Changeset.apply_changes()
%Owner{
  pets: [
    %Pet{},
    %Pet{toys: [%Toy{name: "ball"}, %Toy{name: "stick"}]}
  ]
}

update_at(changeset, path, func)

@spec update_at(
  Ecto.Changeset.t(),
  [atom() | non_neg_integer()] | atom(),
  (any() -> any())
) :: Ecto.Changeset.t()

Updates the value in the changeset at the given position with the given update function.

The path may lead to any field, including arrays and relation fields. Unlike Ecto.Changeset.update_change/3, the update function is always applied, either to the change or to existing value.

If the path points to a field with a simple type, the update function will receive the raw value of the field. However, if the path points to the field of a *-to-many relation, the list values will not be unwrapped, which means that the update function has to handle a list of changesets.

Examples

iex> %Owner{pets: [%Pet{toys: [%Toy{name: "stick"}, %Toy{name: "ball"}]}]}
...> |> Ecto.Changeset.change()
...> |> update_at(
...>      changeset,
...>      [:pets, 1, :toys, 1, :name],
...>      &String.upcase/1
...>    )
...> |> Ecto.Changeset.apply_changes()
%Owner{
  pets: [
    %Pet{},
    %Pet{
      toys: [
        %Toy{name: "stick"},
        %Toy{name: "BALL"}
      ]
    }
  ]
}