---
title: "Sorting and Deleting many-to-many assocs with Ecto and LiveView"
description: "Using Ecto's :sort_param and :delete_param options, along with LiveView, to sort and delete elements within many-to-many association"
---

# Sorting and Deleting many-to-many assocs with Ecto and LiveView

[Annie Ruygt](https://annieruygtillustration.com)

In this post, we’ll use Ecto’s :sort_param and :delete_param options, along with LiveView, to sort and delete elements within a many-to-many association. Fly.io is a great place to run your Phoenix LiveView applications! Check out how to [get started](https://docs.fly.io/elixir)!

Ecto enables us to effortlessly work with different types of associations. In a many-to-many relationship, we can easily insert, modify, or delete elements. However, what if we want to sort the elements in a specific order? How can we remove specific records from an association?

Thankfully, Ecto has two new options to make that easier! When dealing with associations or embeds using [cast_assoc/3](https://hexdocs.pm/ecto/Ecto.Changeset.html#cast_assoc/3) and [cast_embed/3](https://hexdocs.pm/ecto/Ecto.Changeset.html#cast_embed/3), respectively, we can use the `:sort_param` and `:drop_param` [options](https://hexdocs.pm/ecto/Ecto.Changeset.html#cast_assoc/3-options). These options enable us to save associations in a specific order or delete associations based on their position.

But wait, there’s more! Passing these new parameters from LiveView is incredibly straightforward. In this post, we will leverage the power of LiveView and Ecto to sort and delete elements in a many-to-many relationship. Here’s what we’ll do:

1. Define the necessary `Ecto.Schemas`.
2. Define the changesets that enable us to use the `:sort_param` and `:drop_param` options.
3. Set up a form with the required inputs to populate a many-to-many association.
4. Incorporate the magic of checkboxes into our form to sort and delete elements.

By the end, we will have achieved something like this:

## Defining the Ecto Schemas

Let’s begin by defining our example. We’ll take inspiration from a bookstore scenario, where a book can have one or many authors, and an author can write one or many books. To represent this, we’ll define three schemas: `books`, `authors`, and `author_books`.

First, we define the `author_books` schema, which serves as the intermediate table in our many-to-many relationship:

```
defmodule ComponentsExamples.Library.AuthorBook do
  use Ecto.Schema
  import Ecto.Changeset

  alias ComponentsExamples.Library.{Author, Book}

  schema "author_books" do
    field :position, :integer
    belongs_to :author, Author, primary_key: true
    belongs_to :book, Book, primary_key: true
    timestamps()
  end
end
```

This schema is straightforward. It has two relations, `:author` and `:book`, to preload information about the author and book, respectively. Additionally, note the `:position` field. This field helps us store the author’s position, enabling us to preload the authors of a book in the order specified by their positions. We’ll explore this in more detail shortly.

Now we define the books schema:

```
defmodule ComponentsExamples.Library.Book do
  use Ecto.Schema
  import Ecto.Changeset

  alias ComponentsExamples.Library.AuthorBook

  schema "books" do
    field :price, :decimal
    field :publication_date, :utc_datetime
    field :title, :string

    has_many :book_authors, AuthorBook, 
      preload_order: [asc: :position], 
      on_replace: :delete

    has_many :authors, through: [:book_authors, :author]
    timestamps()
  end
end
```

The `books` schema consists of three fields: `title`, `price`, and `publication_date`. We have also defined two associations that work neatly together.

The first association, `has_many: book_authors`, enables us to save and query the relationship between books and authors using the `AuthorBook` schema. Two essential options have been included:

1. The `:preload_order` option ensures authors are sorted based on the earlier mentioned `:position` field, in ascending order, when preloaded.
2. The `on_replace: :delete` setting ensures that any association element not included in the parameters sent will be deleted.

It’s important to note that the option `on_replace: :delete` is required for the sort and delete operations, and here’s why it makes sense: Since the client is modifying and deleting children, it becomes necessary for them to provide the full listing. Any elements missing from the listing are considered discarded because there is no way to know if the “old” ones should be preserved or not.

The second association, `has_many: authors`, enables us to conveniently preload the authors of a book. We achieve this by using the associations we mentioned earlier: `[:book_authors, :author]`. The `:book_authors` association guarantees that the authors are already listed according to their positions. It’s a good trick, isn’t it?

Next, we need to define the author schema, which, for this example, does not have any specific constraints:

```
defmodule ComponentsExamples.Library.Author do
  use Ecto.Schema
  import Ecto.Changeset

  alias ComponentsExamples.Library.AuthorBook

  schema "authors" do
    field :bio, :string
    field :birth_date, :string
    field :gender, :string
    field :name, :string
    timestamps()

    many_to_many :books, AuthorBook, join_through: "author_books"
  end
end
```

With this, we have defined the necessary schemas to model the bookstore scenario.

## Using :drop_param and :sort_param

Let’s start by defining the `Book.changeset/3` function, to create or modify a book and its associated authors:

```
  def changeset(book, attrs) do
    book
    |> cast(attrs, [:title, :publication_date, :price])
    |> validate_required([:title, :publication_date, :price])
    |> cast_assoc(:book_authors,
      with: &AuthorBook.changeset/3,
      sort_param: :authors_order,
      drop_param: :authors_delete
    )
  end
```

Within this function, we specify the names of the parameters used for sorting and deleting elements within the `:book_authors` association: `:authors_order` and `:authors_delete`. Additionally, we use the `:with` option, which accepts a function with arity 3 to create the child records. The third argument passed to the function contains the position of each child element.

Warning: Make sure that you are using Ecto `v3.10.2` or a newer version. If you attempt to send a function with arity 3 in the `:with` option on older versions, you may encounter an error.

Now, let’s save the position in our `:author_books` table. To accomplish this, we define the `AuthorBook.changeset/3` function:

```
  def changeset(author_book, attrs, position) do
    author_book
    |> cast(attrs, [:author_id, :book_id])
    |> change(position: position)
    |> unique_constraint([:author, :book], name: "author_books_author_id_book_id_index")
  end
```

In this function, we ensure to handle the position passed as the third argument to our changeset function. We cast the attributes `:author_id` and `:book_id`, then modify the changeset to include the position. Finally, we enforce a unique constraint on the combination of `:author_id` and `:book_id` using the `unique_constraint` function.

With this, our work with `Ecto.Changeset` and `Ecto.Schema` is complete. Now let’s explore how to send `:authors_order` and `:authors_delete` from a form.

## Using `inputs_for` to populate a has_many association

In this example, we assume that there are existing authors in our database, and we need to provide the user with options to choose the author(s) of a book. To achieve this, we first create a book changeset with at least one `book_author` in the `:book_authors` association. We then build a form using this changeset.

Let’s prepare our assigns:

```
  def update(%{book: book} = assigns, socket) do
    book_changeset = Library.change_book(book)

    socket =
      socket
      |> assign(assigns)
      |> assign_form(book_changeset)
      |> assign_authors()

    {:ok, socket}
  end
```

The crucial part is the `assign_form/2` function, where we build a form for the `Book`. If the book does not have any author in the `:book_authors` association, we create an empty `%AuthorBook{}` and include it as a single author in the book’s `:book_authors` association —this allows us to render at least one input to fill in the author information. Finally, we convert this modified changeset into a form:

```
  defp assign_form(socket, %Ecto.Changeset{} = changeset) do
    if Ecto.Changeset.get_field(changeset, :book_authors) == [] do
      book_author = %AuthorBook{}
      changeset = Ecto.Changeset.put_change(changeset, :book_authors, [book_author])
      assign(socket, :form, to_form(changeset))
    else
      assign(socket, :form, to_form(changeset))
    end
  end
```

To provide the user with a selection of authors from the database, we’ll use a select input. The select input expects a list of options in the format `{label, value}`. We assign these options using the `assign_authors/1` function:

```
defp assign_authors(socket) do
  authors =
    Library.list_authors()
    |> Enum.map(&{&1.name, &1.id})

  assign(socket, :authors, authors)
end
```

In this function, we fetch the authors from the database using `Library.list_authors()`. We then transform each author into a `{label, value}` tuple, where the label represents the author’s name and the value corresponds to the author’s ID. Finally, we assign the `authors` list to the socket for use in the template.

With our assigns prepared, we can now define the inputs to send the attributes of the `:book_authors` association.

```
def render(assigns) do
  ~H"""
  <div>
    <.simple_form 
      for={@form} 
      phx-change="validate" 
      phx-submit="save" 
      phx-target={@myself}
    >
      ...
      <div id="authors" phx-hook="SortableInputsFor" class="space-y-2">
        <.inputs_for :let={b_author} field={@form[:book_authors]}>
          <div class="flex space-x-2 drag-item">
            <.icon name="hero-bars-3" data-handle />
            <.input
              type="select"
              field={b_author[:author_id]}
              placeholder="Author"
              options={@authors}
            />
          </div>
        </.inputs_for>
      </div>
      <:actions>
        <.button phx-disable-with="Saving...">
          Save
        </.button>
      </:actions>
    </.simple_form>
  </div>
  """
end
```

We use the function component `<.inputs_for>` to populate the `@form[:book_authors]` association. Within this component, we access the attributes of each individual `:book_authors` using the `b_author` variable.

We create a select input to populate the `b_author[:author_id]` field, passing in the list of authors as available options.

With this, we are now able to send information about books and authors. However, we still need to make one final modification to send our `:sort_param` and `:delete_param` parameters.

## Checkboxes magic

`:drop_param` and `:delete_param` require a list of numerical indexes to identify the elements that need to be reordered or deleted. To send this list of indexes, we need to include the position of each `b_author` element.

To do this, we add a hidden input inside the `<.inputs_for>` component for each `b_author`. This hidden input will send the value of the element’s position using `value={b_author.index}`:

```
  def render(assigns) do
    ~H"""
    <div>
      <.simple_form 
        for={@form} 
        phx-change="validate" 
        phx-submit="save" 
        phx-target={@myself}
      >
        ...
        <div id="authors" phx-hook="SortableInputsFor" class="space-y-2">
          <.inputs_for :let={b_author} field={@form[:book_authors]}>
            <div class="flex space-x-2 drag-item">
              <.icon name="hero-bars-3" data-handle />
+             <input 
+               type="hidden" 
+               name="book[authors_order][]" 
+               value={b_author.index} 
+             />
              <.input
                type="select"
                field={b_author[:author_id]}
                placeholder="Author"
                options={@authors}
              />
            </div>
          </.inputs_for>
        </div>
        <:actions>
          <.button phx-disable-with="Saving...">
            Save
          </.button>
        </:actions>
      </.simple_form>
    </div>
    """
  end
```

The naming convention of this input is crucial. As the form is built from a `Book` changeset, the input names reflect this structure name. For our example, each input name starts with “book” followed by square brackets and the attribute name being filled, such as `name="book[publication_date]"`.

It’s important to note that this convention holds true unless you have specifically set up the `:as` option of the `to_form/2` function and used a different prefix for the form inputs. For example: `to_form(changeset, as: "my_form")`

In the case of `:authors_order`, which involves multiple inputs, each element is uniquely identified by appending an index. For instance, the name attribute would be represented as `name="book[authors_order][]"`.

With this additional input, we can now manage the position of each element. Let’s see how we can add and remove elements from the assoc:

```
  def render(assigns) do
    ~H"""
    <div>
      <.simple_form 
        for={@form} 
        phx-change="validate" 
        phx-submit="save" 
        phx-target={@myself}
      >
        ...
        <div id="authors" phx-hook="SortableInputsFor" class="space-y-2">
          <.inputs_for :let={b_author} field={@form[:book_authors]}>
            <div class="flex space-x-2 drag-item">
              <.icon name="hero-bars-3" data-handle />
              <input type="hidden" name="book[authors_order][]" value={b_author.index} />
              <input 
                type="hidden" 
                name="book[authors_order][]" 
                value={b_author.index} 
              />
+             <label>
+               <input
+                 type="checkbox"
+                 name="book[authors_delete][]"
+                 value={b_author.index}
+                 class="hidden"
+               />
+               <.icon name="hero-x-mark" />
+             </label>
            </div>
          </.inputs_for>
        </div>
        <:actions>
          <.button phx-disable-with="Saving...">
            Save
          </.button>

+          <label class="block cursor-pointer">
+            <input 
+              type="checkbox" 
+              name="book[authors_order][]" 
+              class="hidden" 
+            />
+            <.icon name="hero-plus-circle" /> add more
+          </label>
        </:actions>
      </.simple_form>
    </div>
    """
  end
```

To add elements to the `book[authors_order][]` association, we use another checkbox input hidden inside a label with a plus icon. This input is essential to append new elements to the end of the list. It is placed outside the `<.inputs_for>` function and after it. These checkboxes send the index of each `b_author` element as the value.

For deleting elements, we need to send the index of the element to be removed. Inside the `<.inputs_for>` function, we add another checkbox input hidden inside a label with a `hero-x-mark` icon. These checkboxes also send the index of each `b_author` element as the value.

Let’s see how the information of these checkboxes is sent when the `validate` or `submit` events are triggered:

```
[debug] HANDLE EVENT "validate" in ComponentsExamplesWeb.LibraryLive
  Component: ComponentsExamplesWeb.MulfiFormComponent
  Parameters: %{"_target" => ["book", "authors_order"], "book" => %{"authors_order" => ["0", "on"], "book_authors" => %{"0" => %{"_persistent_id" => "0", "author_id" => "2", "book_id" => "2", "id" => "88"}}, "price" => "2323", "publication_date" => "2023-07-13T19:37", "title" => "Libro prueba"}}
[debug] Replied in 982µs
```

If we focus on the `authors_order` attribute, we can see that a list `["0", "on"]` is sent. This list indicates that there are two authors, with indexes `"0"` and `"on"`… *wait, what?* Well, this weird “on” index is used to identify when we’ve just added a new item using our “add more” checkbox.

What if we have another checkbox, similar to the one we used to add new authors, but placed before the `<.inputs_for>` component? In that case, when adding an element, the `"on"` parameter would be at the beginning of the list: `["on", "0"]`.

Let’s now see what happens when we delete an element:

```
[debug] HANDLE EVENT "validate" in ComponentsExamplesWeb.LibraryLive
  Component: ComponentsExamplesWeb.MulfiFormComponent
  Parameters: %{"_target" => ["book", "authors_delete"], "book" => %{"authors_delete" => ["1"], "authors_order" => ["0", "1"], "book_authors" => %{"0" => %{"_persistent_id" => "1", "author_id" => "1"}, "1" => %{"_persistent_id" => "0", "author_id" => "2", "book_id" => "2", "id" => "88"}}, "price" => "2323", "publication_date" => "2023-07-13T19:37", "title" => "Libro prueba"}}
[debug] Replied in 944µs
```

In this example, the `authors_delete` attribute is sent along with a list of indexes representing the elements to be deleted.

You may have noticed that both example logs include a `_persistent_id`. This identifier serves as internal Phoenix book keeping to track our inputs effectively.

Awesome! We’ve got it all covered now! Adding and deleting elements from the association is a breeze. Just send the required info from our form, and our changesets and schemas will take care of the rest. Way to go!

(For a complete view of the code, please refer to this [PR](https://github.com/bemesa21/components_examples/pull/3).)

## Fly.io ❤️ Elixir

Fly.io is a great way to run your Phoenix LiveView app close to your users. It’s really easy to get started. You can be running in minutes.

[Deploy a Phoenix app today! →](https://docs.fly.io/elixir)

## Some Things to Keep in Mind

When using this approach to sort and delete items in a association, there are a few considerations worth noting:

1. Make sure to preload the association elements before performing any updates. This ensures that the data is correctly ordered.
2. It’s important to be aware that this approach relies on preloaded elements. If you want to order associations in a relationship but haven’t preloaded all the elements, manual handling of reordering will be necessary.
3. While this example demonstrates the manipulation of a many-to-many relationship, you can easily adapt these concepts to work with simpler relationships like has-many.
