nested form issue

Hi Guys! I am trying to apply a simple nested form, and I can not get through. The use-case is simple: I want to add supplier_prices to my users. I tried to do everything according to the docs, probably missed something. user.ex:
update :update_with_supplier_price do
require_atomic? false
argument :supplier_prices, {:array, :map}, allow_nil?: true

change manage_relationship(:supplier_prices,
type: :append_and_remove)
end
update :update_with_supplier_price do
require_atomic? false
argument :supplier_prices, {:array, :map}, allow_nil?: true

change manage_relationship(:supplier_prices,
type: :append_and_remove)
end
follow up in comment(s)(?).
29 Replies
zskreisz
zskreiszOP•8h ago
user_live/form_component.ex:
@impl true
def render(assigns) do
#...

<.button
type="button"
phx-click="add-supplier-price"
phx-value-path={@form.name <> "[supplier_prices]"}
phx-target={@myself}
class="mt-4"
>
Add Supplier Price
</.button>

<div class="supplier-prices-container mt-4">
<.inputs_for :let={sp} field={@form[:supplier_prices]}>
<div class="supplier-price-item border p-4 mb-4 rounded-md">
<.input
field={sp[:supplier_id]}
type="hidden"
value={@user.id}
/>
<div class="grid grid-cols-2 gap-4">
<.input field={sp[:task_config_id]} type="select" options={@task_configs} label="Task config" />
<.input field={sp[:source_language_id]} type="select" options={@languages} label="Source language" />
<.input field={sp[:target_language_id]} type="select" options={@languages} label="Target language" />
<.input field={sp[:unit_type_id]} type="select" options={@unit_types} label="Unit type" />
<.input field={sp[:currency_id]} type="select" options={@currencies} label="Currency" />
<.input field={sp[:price]} type="number" label="Supplier price" step="any" />
</div>

<%!-- <.button
type="button"
phx-click="remove-supplier-price"
phx-value-path={sp.source.path}
phx-target={@myself}
class="text-red-600 mt-2 text-sm"
>
Remove Price
</.button> --%>
</div>
</.inputs_for>
</div>

#...
@impl true
def render(assigns) do
#...

<.button
type="button"
phx-click="add-supplier-price"
phx-value-path={@form.name <> "[supplier_prices]"}
phx-target={@myself}
class="mt-4"
>
Add Supplier Price
</.button>

<div class="supplier-prices-container mt-4">
<.inputs_for :let={sp} field={@form[:supplier_prices]}>
<div class="supplier-price-item border p-4 mb-4 rounded-md">
<.input
field={sp[:supplier_id]}
type="hidden"
value={@user.id}
/>
<div class="grid grid-cols-2 gap-4">
<.input field={sp[:task_config_id]} type="select" options={@task_configs} label="Task config" />
<.input field={sp[:source_language_id]} type="select" options={@languages} label="Source language" />
<.input field={sp[:target_language_id]} type="select" options={@languages} label="Target language" />
<.input field={sp[:unit_type_id]} type="select" options={@unit_types} label="Unit type" />
<.input field={sp[:currency_id]} type="select" options={@currencies} label="Currency" />
<.input field={sp[:price]} type="number" label="Supplier price" step="any" />
</div>

<%!-- <.button
type="button"
phx-click="remove-supplier-price"
phx-value-path={sp.source.path}
phx-target={@myself}
class="text-red-600 mt-2 text-sm"
>
Remove Price
</.button> --%>
</div>
</.inputs_for>
</div>

#...
def handle_event("add-supplier-price", %{"path" => path}, socket) do
form = AshPhoenix.Form.add_form(socket.assigns.form, path, params: %{
"price" => nil,
"currency_id" => nil,
"supplier_id" => socket.assigns.user.id,
"task_config_id" => nil,
"source_language_id" => nil,
"target_language_id" => nil,
"unit_type_id" => nil,
})
socket =
socket
|> assign(form: form)
|> assign_select_options()

IO.inspect(socket.assigns.form, label: "Form after adding supplier price")
{:noreply, socket}
end


def handle_event("save", %{"user" => user_params}, socket) do
IO.inspect(user_params, label: "User Params for save")
case AshPhoenix.Form.submit(socket.assigns.form, params: user_params) do
{:ok, user} ->
notify_parent({:saved, user})

socket =
socket
|> put_flash(:info, "User #{socket.assigns.form.source.type}d successfully")
|> push_patch(to: socket.assigns.patch)

{:noreply, socket}

{:error, form} ->
IO.inspect(form.source.source.errors, label: "Form Error")
{:noreply, assign(socket, form: form)}
end
end
def handle_event("add-supplier-price", %{"path" => path}, socket) do
form = AshPhoenix.Form.add_form(socket.assigns.form, path, params: %{
"price" => nil,
"currency_id" => nil,
"supplier_id" => socket.assigns.user.id,
"task_config_id" => nil,
"source_language_id" => nil,
"target_language_id" => nil,
"unit_type_id" => nil,
})
socket =
socket
|> assign(form: form)
|> assign_select_options()

IO.inspect(socket.assigns.form, label: "Form after adding supplier price")
{:noreply, socket}
end


def handle_event("save", %{"user" => user_params}, socket) do
IO.inspect(user_params, label: "User Params for save")
case AshPhoenix.Form.submit(socket.assigns.form, params: user_params) do
{:ok, user} ->
notify_parent({:saved, user})

socket =
socket
|> put_flash(:info, "User #{socket.assigns.form.source.type}d successfully")
|> push_patch(to: socket.assigns.patch)

{:noreply, socket}

{:error, form} ->
IO.inspect(form.source.source.errors, label: "Form Error")
{:noreply, assign(socket, form: form)}
end
end
- the error upon submit: - Form Error: [ %Ash.Error.Changes.InvalidArgument{
field: :supplier_prices,
message: "is invalid",
value: %{
"_form_type" => "create",
"_persistent_id" => "0",
"_touched" => "_form_type,_persistent_id,_touched,_unused_currency_id,_unused_source_language_id,_unused_target_language_id,_unused_task_config_id,_unused_unit_type_id,currency_id,price,source_language_id,supplier_id,target_language_id,task_config_id,unit_type_id",
"currency_id" => "1",
"price" => "32434",
"source_language_id" => "1",
"supplier_id" => "2",
"target_language_id" => "1",
"task_config_id" => "1",
"unit_type_id" => "1"
},
splode: nil,
bread_crumbs: [],
vars: [index: 0],
path: [0],
stacktrace: #splode.Stacktrace<>,
class: :invalid
}
]
my guess is, that the user.ex awaits a list of maps instead of a simple map, which is provided (and with correct values) I can not get through this issue.
ZachDaniel
ZachDaniel•8h ago
What exactly should it do when you add a new form to the list? Your current manage relationship is using type: :append_and_remove Which is for relating to existing things on the other end
zskreisz
zskreiszOP•8h ago
love you man, I thought you were out for the weekend 😄
ZachDaniel
ZachDaniel•8h ago
Can I see where you are creating the form?
zskreisz
zskreiszOP•8h ago
so I don't really know, this is kind of a black box for me, I thought append_and_remove can edit or add or remove kind of automatically the prices I also added the render function's <.input_for ... if that is what you mean
ZachDaniel
ZachDaniel•8h ago
No like AshPhoenix.Form.for_create
sevenseacat
sevenseacat•8h ago
that looks like it should be a direct_control
ZachDaniel
ZachDaniel•8h ago
Agreed. The invalid argument is weird though.
zskreisz
zskreiszOP•8h ago
I don't really get what you mean, where do I have to add this? I'll try this
ZachDaniel
ZachDaniel•8h ago
I'm saying that somewhere in your code already Like on mount You are making the form I need to see that code
zskreisz
zskreiszOP•8h ago
defp assign_form(%{assigns: %{user: user}} = socket) do
# Define common form options, including nested form config
form_opts = [
api: OrcaLink.Accounts, # Assuming your User resource is in this API
forms: [
# Configure the supplier_prices relationship
supplier_prices: [
resource: OrcaLink.Accounts.SupplierPrice, # The resource for the nested form
create_action: :create, # Action to use for new supplier prices
update_action: :update, # Action to use for existing supplier prices
# You might also need `destroy_action: :soft_delete` if you allow removal
destroy_action: :soft_delete
]
# Add other nested forms here if needed
],
as: "user"
]

form =
if user do
# Pass the form_opts to for_update
AshPhoenix.Form.for_update(user, :update_with_supplier_price, form_opts) # Use the action that manages the relationship
else
# Pass the form_opts to for_create
AshPhoenix.Form.for_create(OrcaLink.Accounts.User, :create, form_opts) # Ensure :create action also handles supplier_prices if needed
end

assign(socket, form: to_form(form))
end
defp assign_form(%{assigns: %{user: user}} = socket) do
# Define common form options, including nested form config
form_opts = [
api: OrcaLink.Accounts, # Assuming your User resource is in this API
forms: [
# Configure the supplier_prices relationship
supplier_prices: [
resource: OrcaLink.Accounts.SupplierPrice, # The resource for the nested form
create_action: :create, # Action to use for new supplier prices
update_action: :update, # Action to use for existing supplier prices
# You might also need `destroy_action: :soft_delete` if you allow removal
destroy_action: :soft_delete
]
# Add other nested forms here if needed
],
as: "user"
]

form =
if user do
# Pass the form_opts to for_update
AshPhoenix.Form.for_update(user, :update_with_supplier_price, form_opts) # Use the action that manages the relationship
else
# Pass the form_opts to for_create
AshPhoenix.Form.for_create(OrcaLink.Accounts.User, :create, form_opts) # Ensure :create action also handles supplier_prices if needed
end

assign(socket, form: to_form(form))
end
ZachDaniel
ZachDaniel•8h ago
Okay, so that's the issue
zskreisz
zskreiszOP•8h ago
@impl true def update(assigns, socket) do {:ok, socket |> assign(assigns) |> assign_form() |> assign_select_options() } end
ZachDaniel
ZachDaniel•8h ago
1. api hasn't been an option in Ash for like 2 years 😂 2. You don't need to specify the forms option, it is derived from the action automatically
zskreisz
zskreiszOP•8h ago
yeah, kinda relying too much on ai...
ZachDaniel
ZachDaniel•8h ago
If you had wanted to do it with manual config, you'd need type: :list 🙂
zskreisz
zskreiszOP•8h ago
so if i simply do this:
defp assign_form(%{assigns: %{user: user}} = socket) do
form =
if user do
# Pass the form_opts to for_update
AshPhoenix.Form.for_update(user, :update_with_supplier_price) # Use the action that manages the relationship
else
# Pass the form_opts to for_create
AshPhoenix.Form.for_create(OrcaLink.Accounts.User, :create) # Ensure :create action also handles supplier_prices if needed
end

assign(socket, form: to_form(form))
end
defp assign_form(%{assigns: %{user: user}} = socket) do
form =
if user do
# Pass the form_opts to for_update
AshPhoenix.Form.for_update(user, :update_with_supplier_price) # Use the action that manages the relationship
else
# Pass the form_opts to for_create
AshPhoenix.Form.for_create(OrcaLink.Accounts.User, :create) # Ensure :create action also handles supplier_prices if needed
end

assign(socket, form: to_form(form))
end
ZachDaniel
ZachDaniel•8h ago
Please try to let us know to what degree AI has generated code you're asking for help with, as a human likely never would have arrived at that code naturally. I don't mind helping with AI generated code provided you've tried to figure out the answer yourself But it does also make us basically "other people's AI debuggers" 😂
zskreisz
zskreiszOP•8h ago
yeah, no, I first tried to go with the documentations not just plain AI but it didn't work at first, and tried to patch, and patch, and patch I couldn't figure that direct_control, or append_and_remove could have been an issue
ZachDaniel
ZachDaniel•8h ago
All good, manage relationship and nested forms are one of the more complex parts of ash
zskreisz
zskreiszOP•8h ago
now I have a validation issue:
* (FunctionClauseError) no function clause matching in OrcaLinkWeb.UserLive.FormComponent.handle_event/3
(orca_link 0.1.0) lib/orca_link_web/live/forms/user_live/form_component.ex:185: OrcaLinkWeb.UserLive.FormComponent.handle_event("validate", %{"_target" => ["form", "name"], "form" => %{"user_name" => "JD", "tolmacs" => "false", "car" => "false", "_unused_vatstatus" => "", "timezone" => "", "default_sms" => "false", "_unused_post_code" => "", "country_code" => "", "primary_phone_number" => "adwákowd", "_unused_user_name" => "", "_unused_secondary_phone_number" => "", "_unused_default_sms" => "", "_unused_address" => "", "vendor_invoice_folder_link" => "", "city" => "", "_unused_languages" => "", "_unused_tolmacs" => "", "_unused_primary_phone_number" => "", "comment" => "", "billi
* (FunctionClauseError) no function clause matching in OrcaLinkWeb.UserLive.FormComponent.handle_event/3
(orca_link 0.1.0) lib/orca_link_web/live/forms/user_live/form_component.ex:185: OrcaLinkWeb.UserLive.FormComponent.handle_event("validate", %{"_target" => ["form", "name"], "form" => %{"user_name" => "JD", "tolmacs" => "false", "car" => "false", "_unused_vatstatus" => "", "timezone" => "", "default_sms" => "false", "_unused_post_code" => "", "country_code" => "", "primary_phone_number" => "adwákowd", "_unused_user_name" => "", "_unused_secondary_phone_number" => "", "_unused_default_sms" => "", "_unused_address" => "", "vendor_invoice_folder_link" => "", "city" => "", "_unused_languages" => "", "_unused_tolmacs" => "", "_unused_primary_phone_number" => "", "comment" => "", "billi
I believe, the function:
@impl true
def handle_event("validate", %{"user" => user_params}, socket) do
{:noreply, assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, user_params))}
end
@impl true
def handle_event("validate", %{"user" => user_params}, socket) do
{:noreply, assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, user_params))}
end
is not good now as the pattern changed to something like: %{"_target" => ["form", "name"], "form" =>
ZachDaniel
ZachDaniel•7h ago
You removed as: "user" from your opts I assume Add that back in
zskreisz
zskreiszOP•7h ago
yeah, that helped, thanks.
zskreisz
zskreiszOP•7h ago
now i could save one price for a user, and it is preloaded with the form on edit, now without modifying anything, upon trying to save the form, I get:
ZachDaniel
ZachDaniel•7h ago
Something very strange there 😅 Not seen that error before. Hard to say what the issue might be. It's certainly not normal. What are the params you're submitting?
sevenseacat
sevenseacat•7h ago
what's on lib/orca_link/orca_link_ash/business_logic/versioning/historization.ex:47?
zskreisz
zskreiszOP•6h ago
Oh, is it related to that? It's my custom code, but I'll switch it off and try saving without it, and if it is causing the error I'll handle it. Tl;dr, I have a generic versioning implementation.
sevenseacat
sevenseacat•6h ago
its the second line of the error stack trace, so I think so
zskreisz
zskreiszOP•6h ago
yeah, thanks, this is not a framework issue!

Did you find this page helpful?