How to use phx.gen.html in an Ash setup?

I have a minimal Ash, PostgreSQL and Phoenix setup in which I do a

mix phx.gen.html Shop Product products name:string price:float --no-context

Obviously the controller doesn’t work out of the box. With some minor changes it works for
index, show and delete.

lib/app_web/controllers/product_controller.ex:

defmodule AppWeb.ProductController do
  use AppWeb, :controller

  # alias App.Shop
  alias App.Shop.Product

  def index(conn, _params) do
    products = Product.read!()
    render(conn, :index, products: products)
  end

  # def new(conn, _params) do
  #   changeset = Shop.change_product(%Product{})
  #   render(conn, :new, changeset: changeset)
  # end

  # def create(conn, %{"product" => product_params}) do
  #   case Shop.create_product(product_params) do
  #     {:ok, product} ->
  #       conn
  #       |> put_flash(:info, "Product created successfully.")
  #       |> redirect(to: ~p"/products/#{product}")

  #     {:error, %Ecto.Changeset{} = changeset} ->
  #       render(conn, :new, changeset: changeset)
  #   end
  # end

  def show(conn, %{"id" => id}) do
    product = Product.by_id!(id)
    render(conn, :show, product: product)
  end

  # def edit(conn, %{"id" => id}) do
  #   product = Shop.get_product!(id)
  #   changeset = Shop.change_product(product)
  #   render(conn, :edit, product: product, changeset: changeset)
  # end

  # def update(conn, %{"id" => id, "product" => product_params}) do
  #  product = Product.by_id!(id)
  #
  # case Product.update(product, product_params) do
  #  {:ok, product} ->
  #     conn
  #     |> put_flash(:info, "Product updated successfully.")
  #     |> redirect(to: ~p"/products/#{product}")

  #   {:error, %Ecto.Changeset{} = changeset} ->
  #     render(conn, :edit, product: product, changeset: changeset)
  # end
  #end

  def delete(conn, %{"id" => id}) do
    product = Product.by_id!(id)
    :ok = Product.destroy(product)

    conn
    |> put_flash(:info, "Product deleted successfully.")
    |> redirect(to: ~p"/products")
  end
end

Is it possible to change the other functions (how?) so that I don’t have to change the forms or do I always have to change the forms too?

For context:
https://elixir-phoenix-ash.com/ash/postgresql/phoenix.html#_phx_gen_html

ash_phoenix has mix ash_phoenix.gen.live. I think this might be what you are looking for.

1 Like

I would like to know if this can be “fixed” in the controller only or if the views have to be changed too.

Oh, I misunderstood. As long as you return the same data as before, it will work. I only use the API part of Phoenix, so I’m unfamiliar with this layer. Still, I’m guessing from the code that I see here that you will probably run into problems with the changeset because the HTML generated from Phoenix probably expects normal Ecto Changesets, and you would have to do a lot yourself to handle all of this.

Yep. You would need to switch to use AshPhoenix.Form.

Something like:

def edit(conn, %{"id" => id}) do
  product = Shop.get_product!(id)
  form = AshPhoenix.Form.for_update(product, :update, actor: ..., api: YourApi)
  render(conn, :edit, product: product, form: form)
end

def update(conn, %{"id" => id, "product" => product_params}) do
  product = Product.by_id!(id)

  product
  |> AshPhoenix.Form.for_update(:update, actor: ..., api: YourApi)
  |> AshPhoenix.Form.submit()
  |> case do
    {:ok, product} ->
      conn
      |> put_flash(:info, "Product updated successfully.")
      |> redirect(to: ~p"/products/#{product}")

    {:error, form} ->
      render(conn, :edit, product: product, form: form)
  end
end

How would the template look like?

Given:

  def edit(conn, %{"id" => id}) do
    product = Product.by_id!(id)
    form = AshPhoenix.Form.for_update(product, :update, actor: nil, api: App.Shop)
    render(conn, :edit, product: product, form: form)
  end

I tried this for edit.html.eex

<.header>
  Edit Product <%= @product.id %>
  <:subtitle>Use this form to manage product records in your database.</:subtitle>
</.header>

<.simple_form for={@form}>
  <.input type="name" label="Name" field={@form[:name]} />
  <.input type="price" label="Price" field={@form[:price]} />
  <:actions>
    <.button>Save</.button>
  </:actions>
</.simple_form>

<.back navigate={~p"/products"}>Back to products</.back>

Which results into this error:

function AshPhoenix.Form.fetch/2 is undefined (AshPhoenix.Form does not implement 
the Access behaviour. If you are using get_in/put_in/update_in, you can specify the 
field to be accessed using Access.key!/1)

Ah right you need to call to_form on the ash form in newer phoenix versions

I can’t find to_form in the documentation and don’t know how to use it.
<.simple_form to_form={@form}> doesn’t work. How do I have to add it?

Like AshPhoenix.Form.for_create(…) |> to_form() this is what you have to do in live view at least if you got the same error there.

Come to think of it, the issue might actually be this:


<.simple_form :let={form} for={@form}>
  <.input type="name" label="Name" field={form[:name]} />
  <.input type="price" label="Price" field={form[:price]} />
  <:actions>
    <.button>Save</.button>
  </:actions>
</.simple_form>

You should be using the phoenix form when accessing the field.

1 Like

That leads to a problem with the update function. Here’s the code:

The controller

  def edit(conn, %{"id" => id}) do
    product = Product.by_id!(id)

    form =
      AshPhoenix.Form.for_update(product, :update, actor: nil, api: App.Shop)

    render(conn, :edit, product: product, form: form)
  end

  def update(conn, %{"id" => id, "product" => product_params}) do
    product = Product.by_id!(id)

    case Product.update(product, product_params) do
      {:ok, product} ->
        conn
        |> put_flash(:info, "Product updated successfully.")
        |> redirect(to: ~p"/products/#{product}")

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, :edit, product: product, changeset: changeset)
    end
  end

The template

<.header>
  Edit Product <%= @product.id %>
  <:subtitle>Use this form to manage product records in your database.</:subtitle>
</.header>

<.simple_form :let={f} for={@form} action={~p"/products/#{@product}"}>
  <.input field={f[:name]} type="text" label="Name" />
  <.input field={f[:price]} type="number" label="Price" step="any" />
  <:actions>
    <.button>Save Product</.button>
  </:actions>
</.simple_form>

<.back navigate={~p"/products"}>Back to products</.back>

The form is displayed. When I click on “Save Product” I get this error:
no function clause matching in AppWeb.ProductController.update/2

1 Like

Ash forms by default put the params inside “form”


  def update(conn, %{“form” => %{"id" => id, "product" => product_params}}) do

Also keep in mind your %Ecto.Changeset{} pattern match will have to change

It works with

def update(conn, %{"form" => product_params, "id" => id}) do

1 Like

ah, right because id comes from the route not the form :slight_smile:

You could say as: :product when constructing the AshPhoenix.Form to have it come in as "product" instead of "form"

I don’t understand how to do this. Can you give me the exact code which needs to be changed?

    form =
      AshPhoenix.Form.for_update(product, :update, actor: nil, api: App.Shop, as: :product)

The other options are documented here: AshPhoenix.Form — ash_phoenix v1.2.19

When I try this I get this error:

Request: GET /products/f4a15f7e-0637-4106-ab00-46a51ca3b68d/edit
** (exit) an exception was raised:
    ** (NimbleOptions.ValidationError) invalid value for :as option: expected string, got: :product

When I use a string it works:

form =
      AshPhoenix.Form.for_update(product, :update, actor: nil, api: App.Shop, as: "product")

But what do I have to change in the actual form to benefit from this?

<.simple_form :let={f} for={@form} action={~p"/products/#{@product}"}>
  <.input field={f[:name]} type="text" label="Name" />
  <.input field={f[:price]} type="number" label="Price" step="any" />
  <:actions>
    <.button>Save Product</.button>
  </:actions>
</.simple_form>

Sorry, as: "product" :slight_smile:

You don’t have to change anything in the form, you’d just be able to use "product" in your param match instead of "form"

i.e

i.e

```elixir
def update(conn, %{"product" => product_params, "id" => id}) do