Collecting Enums in PhoenixAsh forms

Hi,

I’m trying out the Ash Framework (loving it so far!) and am facing some issues with defining forms that have a select option for Enums. The setup is quite simple: it’s a trivial resource definition, similar to the one on AshHq, and a barebone LiveView defining a AshPhoenix.Form. Here’s the code:

Resource definition:

defmodule MyApp.Products.Product do
  use Ash.Resource, data_layer: AshPostgres.DataLayer

  postgres do
    table "products"

    repo MyApp.Repo
  end

  code_interface do
    define_for MyApp.Products

    define :create, action: :create
    define :read_all, action: :read
    define :update, action: :update
    define :destroy, action: :destroy
    define :get_by_id, args: [:id], action: :by_id
  end

  actions do
    defaults [:create, :read, :update, :destroy]

    read :by_id do
      argument :id, :uuid, allow_nil?: false

      # Tells us we expect this action to return a single result
      get? true

      filter expr(id == ^arg(:id))
    end
  end

  attributes do
    uuid_primary_key :id

    attribute :sku, :string

    attribute :title, :string do
      allow_nil? false
    end

    attribute :description, :string

    attribute :price, :integer

    attribute :currency, :atom do
      constraints one_of: MyApp.Currencies
    end

    create_timestamp :created_at
    update_timestamp :updated_at
  end
end

Enum definition:

defmodule MyApp.Currencies do
  use Ash.Type.Enum, values: [:usd, :eur, :gbp]
end

And here’s the LiveComponent code that defines the form:

defmodule MyAppWeb.Live.Products.FormComponent do
  use MyAppWeb, :live_component

  alias MyApp.Products.Product

  require Logger

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <.header>
        <%= @title %>
        <:subtitle>Use this form to manage product records in your database.</:subtitle>
      </.header>

      <.simple_form
        for={@form}
        id="product-form"
        phx-target={@myself}
        phx-change="validate"
        phx-submit="submit"
      >
        <.input type="text" field={@form[:title]} label="Enter a title" />
        <.input type="text" field={@form[:sku]} label="Enter a SKU" />
        <.input type="number" min="0" field={@form[:price]} label="Enter a price" />
        <.input
          type="select"
          field={@form[:currency]}
          label="Select the currency"
          options={[nil] ++ MyApp.Currencies.values()}
        />
        <:actions>
          <.button phx-disable-with="Saving...">Save Product</.button>
        </:actions>
      </.simple_form>
    </div>
    """
  end

  @impl true
  def update(%{product: product} = assigns, socket) do
    form = product |> change_product(assigns.action) |> to_form()

    {:ok,
     socket
     |> assign(assigns)
     |> assign(:form, form)}
  end

  @impl true
  def handle_event("validate", %{"form" => params}, socket) do
    form = AshPhoenix.Form.validate(socket.assigns.form, params)

    {:noreply, assign(socket, form: form)}
  end

  def handle_event("submit", %{"form" => params}, socket) do
    case AshPhoenix.Form.submit(socket.assigns.form, params: params) do
      {:ok, product} ->
        notify_parent({:saved, product})

        {:noreply,
         socket
         |> put_flash(:info, "Successfully saved the order!")
         |> push_patch(to: socket.assigns.patch)}

      {:error, form} ->
        Logger.error("Form errors: #{inspect(form.errors)}")
        {:noreply, assign(socket, form: form)}
    end
  end

  defp change_product(_product, :new) do
    AshPhoenix.Form.for_create(Product, :create, api: MyApp.Products)
  end

  defp change_product(product, :edit) do
    AshPhoenix.Form.for_update(product, :update, api: MyApp.Products)
  end

  defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
end

All form fields on the form work, except for :currencies. More specifically, as soon as I select an option from the dropdown, the GenServer crashes with the following error:

** (Protocol.UndefinedError) protocol Enumerable not implemented for MyApp.Currencies of type Atom
    (elixir 1.15.2) lib/enum.ex:1: Enumerable.impl_for!/1
    (elixir 1.15.2) lib/enum.ex:194: Enumerable.member?/2
    (elixir 1.15.2) lib/enum.ex:1999: Enum.member?/2
    (ash 2.17.4) lib/ash/type/atom.ex:38: anonymous fn/3 in Ash.Type.Atom.apply_constraints/2
    (elixir 1.15.2) lib/enum.ex:2510: Enum."-reduce/3-lists^foldl/2-0-"/3
    (ash 2.17.4) lib/ash/type/atom.ex:36: Ash.Type.Atom.apply_constraints/2
    (ash 2.17.4) lib/ash/type/type.ex:641: Ash.Type.apply_constraints/3
    (ash 2.17.4) lib/ash/changeset/changeset.ex:3487: Ash.Changeset.change_attribute/3
    (stdlib 5.0.2) maps.erl:416: :maps.fold_1/4
    (ash 2.17.4) lib/ash/changeset/changeset.ex:934: Ash.Changeset.handle_params/4
    (ash 2.17.4) lib/ash/changeset/changeset.ex:886: Ash.Changeset.do_for_action/4
    (ash_phoenix 1.2.23) lib/ash_phoenix/form/form.ex:1057: AshPhoenix.Form.validate/3
    (ash_phoenix 1.2.23) lib/ash_phoenix/form/form.ex:948: AshPhoenix.Form.validate/3
    (my_app 0.1.0) lib/my_app_web/live/product_live/form_component.ex:56: MyAppWeb.Live.Products.FormComponent.handle_event/3

I suppose the solution is trivial, but I just can’t quite understand what’s the issue. Would appreciate any pointers!

1 Like

I haven’t tested it, but I think the attribute should be something like:

attribute :currency, MyApp.Currencies

1 Like

That did the trick! Thanks for catching it. Will submit a PR to clarify the documentation slightly for people like myself.

Awesome, yeah it looks like the documentation needs clarification

1 Like