I was just trying to re-use the changeset between liveview handle_event/3
calls and stumbled on a from my pov a bit unexpected behavior regarding chaining changeset function calls.
It seems that validation errors are not set/reset consistently. There are at least the following two cases I think are not what I would expect:
A) When a changeset function is run with an invalid value, an error is added. When the changeset function is run again, another error is added. That error is equal to the previous one, a duplicate. When the changeset function is run yet again, there is no other error being added, the amount of duplicate errors is capped to two somehow.
B) When the changeset function is run with an invalid value, then with a valid one, the error is not cleared. And when it is run yet again with an invalid value, a duplicate is added.
Take this example schema with a changeset function
defmodule Foo do
use Ecto.Schema
import Ecto.Changeset
@primary_key false
embedded_schema do
field :foo, :string
end
def changeset(data, attrs) do
data
|> cast(attrs, [:foo])
|> validate_inclusion(:foo, ["foo"])
end
end
When (for case A) I run the following:
%Foo{}
|> Foo.changeset(%{"foo" => "not foo"})
|> tap(&IO.inspect(%{changes: &1.changes, errors: &1.errors}, label: "Should now have one error on foo"))
|> Foo.changeset(%{"foo" => "not foo"})
|> tap(&IO.inspect(%{changes: &1.changes, errors: &1.errors}, label: "Should still have one error on foo?"))
|> Foo.changeset(%{"foo" => "not foo"})
|> tap(&IO.inspect(%{changes: &1.changes, errors: &1.errors}, label: "Should still have one error on foo?"))
I get this behavior
Should now have one error on foo: %{
errors: [foo: {"is invalid", [validation: :inclusion, enum: ["foo"]]}],
changes: %{foo: "not foo"}
}
Should still have one error on foo?: %{
errors: [
foo: {"is invalid", [validation: :inclusion, enum: ["foo"]]},
foo: {"is invalid", [validation: :inclusion, enum: ["foo"]]}
],
changes: %{foo: "not foo"}
}
Should still have one error on foo?: %{
errors: [
foo: {"is invalid", [validation: :inclusion, enum: ["foo"]]},
foo: {"is invalid", [validation: :inclusion, enum: ["foo"]]}
],
changes: %{foo: "not foo"}
}
When for case B) I run
%Foo{}
|> Foo.changeset(%{"foo" => "not foo"})
|> tap(&IO.inspect(%{changes: &1.changes, errors: &1.errors}, label: "Should now have one error on foo"))
|> Foo.changeset(%{"foo" => "foo"})
|> tap(&IO.inspect(%{changes: &1.changes, errors: &1.errors}, label: "Should now have no error on foo?"))
|> Foo.changeset(%{"foo" => "not foo"})
|> tap(&IO.inspect(%{changes: &1.changes, errors: &1.errors}, label: "Should now have one error on foo?"))
I get
Should now have one error on foo: %{
errors: [foo: {"is invalid", [validation: :inclusion, enum: ["foo"]]}],
changes: %{foo: "not foo"}
}
Should now have no error on foo?: %{
errors: [foo: {"is invalid", [validation: :inclusion, enum: ["foo"]]}],
changes: %{foo: "foo"}
}
Should now have one error on foo?: %{
errors: [
foo: {"is invalid", [validation: :inclusion, enum: ["foo"]]},
foo: {"is invalid", [validation: :inclusion, enum: ["foo"]]}
],
changes: %{foo: "not foo"}
}
After looking around here on the forum and on github I came across those related pages
- Should changeset remove errors after recasting with valid data? - #2 by cmo
- Validations don't clear existing errors · Issue #2680 · elixir-ecto/ecto · GitHub
The github issue even has commenter suggest a solution in the form of a helper function that is to be called in your changeset and removes any errors for fields that changed prior to running other validations.
Jose mentions in the ticket that it would be a breaking change to remove errors when a validation is run again.
It would seem to me though, that in the context of chaining/piping the changeset function (e.g. in a liveview where the changeset is assigned to the socket), it can be intuitive that the errors are somehow cleaned up along the way (much like the helper function in the ticket attempts to do).
Maybe it could be useful to adjust cast/3
somehow so that, given a changeset, the errors pertaining to the changed fields are cleared?
Is this a strange train of thought I am having? Am I (over) thinking this in a (maybe completely wrong) way?