Phoenix: casting multiple checkboxes to {:array, Ecto.Enum}

Let’s say an ecto schema has an array of enum field:

field :colors, {:array, Ecto.Enum}, values: [:red, :green]

I’d like the user to edit this using a form with checkboxes:

checkbox(form, :colors,
  name: input_name(form, :colors <> "[]",
  checked: :red in (input_value(form, :colors) || []),
  hidden_input: false
)

and the same for :green

This works almost perfectly.
The issue is that if no checkbox is checked, no data for that field is sent to the server.

I tried to add an empty value:

<input type="hidden" name="<%= input_name(form, :colors) %>[]" value="">

But that systematically fails as Phoenix will combine these parameters and not ignore the "", so params might be %{"colors" => ["", "green"]}. When I try to cast it as it tries to cast the "" to an enum instead of ignoring it.

Is there an easy way to have Phoenix discard that "", or cast ignore empty strings for {:array, Enum}, or any good solution for this?

Note: this would be the equivalent treatment than collection_checkboxes in Rails

3 Likes

Re: collection_check_boxes - note that ActiveRecord will silently drop blanks (the compact_blank part) when assigning a collection association via IDs like category_ids in the example.

1 Like

I don’t think it does this by default, you may just have to write a custom Ecto type that checks it for you, or filter params out at the controller’s side before creating the changeset.

2 Likes

Right, I am currently pre-filtering the parameters in my changeset function, but it would be nice to propose a cleaner solution that would work out of the box for Phoenix…

1 Like

It feels this should be solved in Ecto. We have something called empty_values to filter values but it doesn’t consider array types it seems? And maybe it should.

2 Likes

Glad you think so.
Filtering happens here, before doing the actual casting with Ecto.Type.cast.

Do you think a PR that would instead do a Ecto.Type.reject_empty_values(type, value, empty_values) and use the same kind of dispatching that cast do would be acceptable?

This way, even very complex types like {:array, {:array, Ecto.Enum}} would work (not that I have a use for that :laughing:). I haven’t played with protocols yet, but I imagine that a new callback with a default could be added for custom types?

2 Likes

I think for now we can make it recursive and handle arrays as you propose. I am not worried about custom types because then the custom types themselves can reject empty values. PR welcome!

2 Likes

I created a PR for this, hope it looks ok.

2 Likes