phoenixframework/phoenix_ecto

inputs_for loosing options when using action: :ignore on changeset

LostKobrakai opened this issue · 8 comments

I've an entity, which can be linked to children. Now I've a screen where I present current children and possible other children using inputs_for f, :children, [append: @others], fn finner ->. I've also the constraint that it should not be possible to remove all children.

For asserting what's to be added or removed I simply use a checkbox of a virtual field like it's described in the Ecto.Changeset.cast_assoc/4 docs.

The problem though is that ignored changesets of assocs are no longer part of the parent changeset and the append setting is also ignored if a form was submitted, but has errors, essentially leaving me with a screen that shows the error, but is missing options I previously had.

I'm not sure this is the place where this can / should be fixed, but I currently don't see a good solution besides a lot of manual checks of what's still present and what isn't.

Can you provide a minimal app for us to look at? So we can play with things and try to grasp the issue? Thank you.

It was late yesterday, so here a bit of explanation:

  • Install like any other phoenix app
  • Open localhost
  • Either uncheck all children to cause an error though required: true on cast_assoc
  • Or uncheck the last checkbox to trigger an error unrelated to the children on the form
  • When submitting while causing an error it'll loose all the unchecked options, which were shown prev. via the [append: …] option, as their changesets were set to action: :ignore

Thanks! I thought that whatever we give on append is always ignored when we submit the form? So the issue here is the action being set to :ignore. Why is it being to set :ignore? :)

Oh, I see why it is ignored. It is because it is marked as inactive and it doesn't have an ID. What happens if you set it to :delete without an ID? Does it fail later on when it succeeds?

It does fail always with:

cannot delete related %InputsForExample.Example.Intermediate{__meta__: #Ecto.Schema.Metadata<:built, "intermediate">, active: nil, child: #Ecto.Association.NotLoaded<association :child is not loaded>, child_id: nil, id: nil, inserted_at: nil, parent: #Ecto.Association.NotLoaded<association :parent is not loaded>, parent_id: nil, updated_at: nil} because it already exists and it is not currently associated with the given struct. Ecto forbids casting existing records through the association field for security reasons. Instead, set the foreign key value accordingly

One last question: who is discarding the :ignore? The view or ecto? You can check this by looking at the changeset after you call cast. If it is Ecto, then there is nothing we can do here and I am afraid you will have to do a separate pass where you remove everything that is set to deleted but it doesn't have an ID, but only when cast+validations does not return any error.

Another option is to change the UI (if at all possible) to discard entries, removing them from the page, instead of doing active/inactive. But of course, it really depends on the domain and the UX. Sometimes this is good, sometimes it isn't.

I think multiple passes was I needed to hear. I've it now working for my initial usecase. It's a bit more complex than I hoped for, but not to much. I might update the example repo later, but I think this issue can be closed for now.

For anyone wondering: I'm doing the cast_assoc, but if there are errors I use the prev. changeset state and apply the errors of the casted one to it and cast the children again but using action :insert instead of :ignore. This way I get errors and changesets for inputs_for