Get_field nested Changeset

This is a simplified example of my data structure. I am trying to use get_field/3 to check the values of

  • some_key_one
  • some_key_two
  • some_key_three
  • some_key_four

If the values of the fields above are “type_a” and the value of the key type_a is nil, then I want to return an error on the form with a message, such as “cannot choose type_a when type_a is nil”

Same logic applies for “type_b”

If the values of the fields above are “type_b” and the value of the key type_b is nil, then I want to return an error on the form with a message, such as “cannot choose type_b when type_b is nil”

The problem is get_field/3 doesn’t accept 2 keys (at least I don’t think it does).

Using 2 arguments, I can do get_field(changeset, :list_of_things) and that will return list_of_things [...]

Do I now need to use Enum.map and Map.key to traverse through every key in the list of maps checking for each key-value pair?

%System.Articles.Things{
type_a: nil,
type_b: nil,
list_of_things: [
  %System.Articles.ListOfThings{
    __meta__: #Ecto.Schema.Metadata<:loaded, "list_of_things">,
    delete: false,
    some_key_one: "type_a",
    some_key_two: "empty",
    some_key_three: "type_a",
    some_key_four: "type_b",
    ...
  },
  %System.Articles.ListOfThings{
    __meta__: #Ecto.Schema.Metadata<:loaded, "list_of_things">,
    delete: false,
    some_key_one: "empty",
    some_key_two: "type_b",
    some_key_three: "type_b",
    some_key_four: "type_b",
    ...
  }
],
updated_at: ~N[...]

Break your solution down into small functions:

# lib/system/articles/thing.ex
def changeset(thing, attrs) do
  thing
  |> cast(attrs, [:type_a, :type_b, :list_of_things])
  |> validate_required([:type_a, :type_b, :list_of_things])
  |> validate_type_a()
  |> validate_type_b()
end

As you can see in the changeset/2 function validate_type_a/1 and validate_type_b/1 will be called. You can even narrow it down to just one validation function validate_type/2 by passing the changeset and perhaps the atoms :type_a or :type_b and use the atom to figure out which field you’re working on:

# lib/system/articles/thing.ex
def changeset(thing, attrs) do
  thing
  |> cast(attrs, [:type_a, :type_b, :list_of_things])
  |> validate_required([:type_a, :type_b, :list_of_things])
  |> validate_type(:type_a)
  |> validate_type(:type_b)
end

If you’re happy with the smaller functions, you can even create another function called validate_types/2:

def validate_types(changeset, types) do
  for type <- types, do: validate_type(changeset, type)
end

Now, in your changeset/2 function you can just call:

validate_types([:type_a, :type_b])

The point is not to solve your problem in one big function, but in smaller functions.

Hope this helps. :grinning:

3 Likes

Thank you, that helped me come up with a solution.

I came to the point where I’m now adding an error to the changeset, but it is displaying the generic “oops, something went wrong. please check errors below” message, but it is not displaying the error in the alert section or under the field.

I am using the same key name in the error_tag/2

    case result do
      true -> add_error(changeset, type, "cannot be nil")
      false -> changeset
    end

type is either :type_a or :type_b

Update: found the problem. had a typo in the error tag key name. :blush:

1 Like

Awesome! I’m glad I was able to help you out.

Happy new year! :tada:

1 Like