How do I remove all the child records using the form in a has_many relationship changeset (but keep the parent)?

I have a little sample project I’m using to try to figure things out. I have an album schema and a track schema. The album has a has_many association to the track.

My UI is built so that the user can add and remove track rows and then click save when they are done. I discovered tonight that when I remove ALL the tracks and pass the album struct into the schema’s changeset function, the changeset loses its changes to the track list. This appears to be because there are no longer any tracks in the tracks field on the form data params and the resulting changeset thinks that there are no changes.

This is my album schema. It is very simple:

  schema "albums" do
    field :name, :string
    field :artist, :string
    field :genre, :string
    field :rating, :integer

    has_many(:tracks, Track,
      foreign_key: :album_id,
      on_replace: :delete
    )

    timestamps()
  end

  @doc false
  def changeset(album, attrs) do
    album
    |> cast(attrs, [:name, :artist, :genre, :rating])
    |> cast_assoc(:tracks, with: &Track.changeset/2)
    |> validate_required([:name, :rating, :genre])
  end
end

This is my track schema. The temp_id is to allow me to add and delete records in memory.

  schema "tracks" do
    field :name, :string
    field :position, :integer
    field :temp_id, :string, virtual: true

    belongs_to :album, RecordStore.Albums.Album

    timestamps()
  end

  @doc false
  def changeset(track, attrs) do
    track
    |> cast(attrs, [:name, :position, :temp_id])
    |> validate_required([:name, :position])
  end
end

Again, it seems to work fine until I remove ALL the tracks. If I remove all the tracks, and then make another change on the form to fire the phx-change event and rebuild the changeset, all the tracks jump back in. If I remove all except one it works as expected.

Looking at the form params, there is no key for tracks once they are all removed.

One solution that works, but feels yucky, is to check if there are no tracks in the form params in the phx-change and the phx-submit events and then manually put an empty map into the tracks field.

Contemplating this, I guess the changeset functions assume that an absence of tracks in the form params means there were no changes. Manually sticking an empty map into the tracks field on the params forces it to know that we now have an empty list of tracks. But that seems janky to me.

2 Likes

Yes, this and other similar frustrations are why I’m working to move away from Ecto Changesets for live view form management. The logic that changesets do to compute differences between the “base” data and the “changed” data ends up being super hard to work with, and there are too many cases like this where you can’t easily distinguish between the absence of a change and a positive change that is nil in certain situations (particularly with embeds).

I think your proposed solution is a good one.

4 Likes

That’s what I do as a workaround :slight_smile:

1 Like