Nested form example

While applying the example on this page to my project, https://ash-hq.org/docs/module/ash_phoenix/latest/ashphoenix-form, I ran into this error.
no case clause matching: [#Tweets.Item<...
no case clause matching: [#Tweets.Item<...
Can you please give me some pointers? <my code in TweetEdit>
form =
AshPhoenix.Form.for_update(tweet, :update, <<<< line 21
api: Tweets,
forms: [
items: [
resource: Item,
data: tweet.items,
create_action: :create,
update_action: :update
]
form =
AshPhoenix.Form.for_update(tweet, :update, <<<< line 21
api: Tweets,
forms: [
items: [
resource: Item,
data: tweet.items,
create_action: :create,
update_action: :update
]
<lib/ashphoenix/form/form.ex> `` import AshPhoenix.FormData.Helpers @doc "Calls the corresponding for*` function depending on the action type" def for_action(resource_or_data, action, opts) do {resource, data} = case resource_or_data do module when is_atom(resource_or_data) -> {module, module.struct()} %resource{} = data -> {resource, data} end
<stacktrace>
<stacktrace>
ash_phoenix lib/ash_phoenix/form/form.ex:379 AshPhoenix.Form.for_action/3 ash_phoenix lib/ash_phoenix/form/form.ex:3480 AshPhoenix.Form.handle_form_without_params/13 elixir lib/enum.ex:2468 Enum."-reduce/3-lists^foldl/2-0-"/3 ash_phoenix lib/ash_phoenix/form/form.ex:538 AshPhoenix.Form.for_update/3 lib/tweet_web/live/tweet_live/tweet_edit.ex:21 TweetLive.TweetEdit.mount/3 ```
Ash HQ
Guide: Getting Started With Ash And Phoenix
Read the "Getting Started With Ash And Phoenix" guide on Ash HQ
22 Replies
ZachDaniel
ZachDaniel3y ago
Add type: :list to the configuration of items Looks like that isn’t shown in the examples.
Jason
JasonOP3y ago
Thank you! Now I'm getting Invalid or non-existent path errors when I click on the add buttons. It's possible I'm not understanding the example code in the said documentation. In the leex template below, if I click the Add Tweet button, I get Invalid or non-existent path: [] for the Add Item button, I get Invalid or non-existent path: [:items, 0] <leex>
<%= f = form_for @form, "#", [phx_change: :validate, phx_submit: :save] %>
<%= label f, :tweet_name %>
<%= text_input f, :tweet_name %>
<%= f.name %>

<div class="my-10">
<%= for item_form <- inputs_for(f, :items) do %>
<div class="mt-3">
<%= hidden_inputs_for(item_form) %>
<%= text_input item_form, :item_name %>
<%= text_input item_form, :item_count %>
<%= item_form.name %> ### This prints `form[items][0]`
</div>
<button phx-click="remove_form" phx-value-path="<%= item_form.name %>">Remove item</button>
<button phx-click="add_form" phx-value-path="<%= item_form.name %>">Add item</button>
<% end %>
</div>

<div>
<button phx-click="add_form" phx-value-path="<%= f.name %>">Add Tweet</button>

<span>
<%= submit "Save" %>
</span>
</div>
</form>
<%= f = form_for @form, "#", [phx_change: :validate, phx_submit: :save] %>
<%= label f, :tweet_name %>
<%= text_input f, :tweet_name %>
<%= f.name %>

<div class="my-10">
<%= for item_form <- inputs_for(f, :items) do %>
<div class="mt-3">
<%= hidden_inputs_for(item_form) %>
<%= text_input item_form, :item_name %>
<%= text_input item_form, :item_count %>
<%= item_form.name %> ### This prints `form[items][0]`
</div>
<button phx-click="remove_form" phx-value-path="<%= item_form.name %>">Remove item</button>
<button phx-click="add_form" phx-value-path="<%= item_form.name %>">Add item</button>
<% end %>
</div>

<div>
<button phx-click="add_form" phx-value-path="<%= f.name %>">Add Tweet</button>

<span>
<%= submit "Save" %>
</span>
</div>
</form>
ZachDaniel
ZachDaniel3y ago
Ah, yeah so that’s not how you’d add a form. That form’s name includes the index. You’d want something like phx-click=add-item And then in the add item handler you’d use add_form(form, :items) For top level forms it’s pretty simple. But for multiply nested forms you’d do parent_form.name <> “[items]”
Jason
JasonOP3y ago
Thank you! I was able to get the form validation to work such that a new item entered by add_item was included in the form's params. Form submission, though it returned an ok tuple, didn't persist the new item in the database, and I realized I need to add manage_relationship to the parent (Tweet) resource's update action, which I did like this. <tweet.ex> ``` update :update do ... argument :items, {:array, :map} change manage_relationship(:items, type: :update) relationships do has_many :items, MyApp.Tweets.Item end ``` Then it gave me this error as soon as the app started. What am I missing? ``` ** (EXIT from #PID<0.104.0>) an exception was raised: ** (Spark.Error.DslError) [MyApp.Tweets.tweet] actions -> update -> update -> change -> manage_relationship -> items: The following error was raised when validating options provided to manage_relationship. ** (FunctionClauseError) no function clause matching in Ash.Changeset.manage_relationship_opts/1 (ash 2.6.10) lib/ash/changeset/changeset.ex:2010: Ash.Changeset.manage_relationship_opts(:update) (ash 2.6.10) lib/ash/resource/transformers/validate_manage_relationship_opts.ex:68: anonymous fn/3 in Ash.Resource.Transformers.ValidateManagedRelationshipOpts.transform/1 (elixir 1.14.1) lib/enum.ex:975: Enum."-each/2-lists^foreach/1-0-"/2 (ash 2.6.10) lib/ash/resource/transformers/validate_manage_relationship_opts.ex:19: Ash.Resource.Transformers.ValidateManagedRelationshipOpts.transform/1 (spark 0.4.5) lib/spark/dsl/extension.ex:563: anonymous fn/4 in Spark.Dsl.Extension.run_transformers/4 (elixir 1.14.1) lib/enum.ex:4751: Enumerable.List.reduce/3 (elixir 1.14.1) lib/enum.ex:2514: Enum.reduce_while/3 (elixir 1.14.1) lib/enum.ex:975: Enum."-each/2-lists^foreach/1-0-"/2 ``` The form definition in the mount` function looks like this.
form =
AshPhoenix.Form.for_update(tweet, :update,
api: MyApp.Tweets,
forms: [
items: [
resource: Item,
data: tweet.items,
create_action: :create,
update_action: :update,
type: :list
]
form =
AshPhoenix.Form.for_update(tweet, :update,
api: MyApp.Tweets,
forms: [
items: [
resource: Item,
data: tweet.items,
create_action: :create,
update_action: :update,
type: :list
]
ZachDaniel
ZachDaniel3y ago
:update is not a valid type What do you want to happen with the given items? Should it essentially replace the relationship in its entirety? deleting any thing that is missing, adding new things, and updating any currently related things? If so, then you want type: :direct_control
Jason
JasonOP3y ago
Thank you! That did the trick. 🙂 I have a follow up question. When a nested form is validated, Ash.Changeset.before_action idoesn't seem to behave the way I expected. In the above example, Tweet's update action triggers Item's update action through change manage_relationship(:items, type: direct_control). Item's update action happens to have a custom change module like this
change {RequireValidItemCount, []}
change {RequireValidItemCount, []}
which is defined as
defmodule RequireValidItemCount do
use Ash.Resource.Change

def change(changeset, opts, %{actor: actor}) do
IO.puts("print 1")
Ash.Changeset.before_action(changeset, fn changeset ->
IO.puts("print 2")
item_count = Ash.Changeset.get_argument(changeset, :item_count)

if item_count <= 4 do
changeset
else
Ash.Changeset.add_error(changeset,
field: :item_count,
message: "This is invalid count."
)
end
end)
end
end
defmodule RequireValidItemCount do
use Ash.Resource.Change

def change(changeset, opts, %{actor: actor}) do
IO.puts("print 1")
Ash.Changeset.before_action(changeset, fn changeset ->
IO.puts("print 2")
item_count = Ash.Changeset.get_argument(changeset, :item_count)

if item_count <= 4 do
changeset
else
Ash.Changeset.add_error(changeset,
field: :item_count,
message: "This is invalid count."
)
end
end)
end
end
When item_count is changed in a nested form, this RequireValidItemCount is not validated. Interestingly, print 1 is printed, but not print 2 which means the validation flow reachesRequireValidItemCount but the Ash.Changeset.before_action block doesn't run. If I update Item directly like item |> Ash.Changeset.for_update ... RequireValidItemCount does its job just fine. How do I make Ash.Changeset.before_action work through manage_relationship?
ZachDaniel
ZachDaniel3y ago
Before action hooks are not called until the actual action is invoked (i.e the form is submitted) as that is their purpose(to delay things until the action lifecycle).
Jason
JasonOP3y ago
Got it. Thanks. Is there an example of custom validation?
ZachDaniel
ZachDaniel3y ago
In that case, you should be able to do validate compare(:item_count, greater_than_or_equal_to: 4) You can put that in an individual action:
actions do
create :create do
...
validate ...
end
end
actions do
create :create do
...
validate ...
end
end
or for all actions
validations do
validate ....
end
validations do
validate ....
end
Jason
JasonOP3y ago
Yeah, I tried it and it worked perfectly. I just wanted to know how to write the same using customer module in case it comes in handy.
ZachDaniel
ZachDaniel3y ago
Ah, gotcha
ZachDaniel
ZachDaniel3y ago
GitHub
ash/builtins.ex at v2.6.20 · ash-project/ash
A declarative and extensible framework for building Elixir applications. - ash/builtins.ex at v2.6.20 · ash-project/ash
ZachDaniel
ZachDaniel3y ago
So all of the built in validations are technically custom validations They are just provided with convenient function names like compare/1
ZachDaniel
ZachDaniel3y ago
GitHub
ash/compare.ex at v2.6.20 · ash-project/ash
A declarative and extensible framework for building Elixir applications. - ash/compare.ex at v2.6.20 · ash-project/ash
ZachDaniel
ZachDaniel3y ago
Naturally, though, the built in ones are a bit more involved because they cover all kinds of cases.
Jason
JasonOP3y ago
I see. And to use it in a resource is something like this? Apparently this doesn't seem to work.
validations do
validate {RequireValidItemCount, []} do
message: "Item count must be greater than 0"
end
end
validations do
validate {RequireValidItemCount, []} do
message: "Item count must be greater than 0"
end
end
ZachDaniel
ZachDaniel3y ago
validations do
validate {RequireValidItemCount, []} do
message "Item count must be greater than 0"
end
end
validations do
validate {RequireValidItemCount, []} do
message "Item count must be greater than 0"
end
end
RequireValidItemCount needs to be an Ash.Resource.Validation as well and then you can also return the message from the validation directly {:error, "Item count must be greater than 0"}
Jason
JasonOP3y ago
Okay. Still digesting the compare example for that. one sec. I thought this would work as a minimalistic example, but it doesn't. What am I missing?
defmodule RequireValidItemCount do
use Ash.Resource.Validation

@impl true
def validate(changeset, opts) do
item_count = Ash.Changeset.get_attribute(changeset, :item_count)

if item_count <= 4 do
:ok
else
{:error, "This is more than needed."}
end
end
end
defmodule RequireValidItemCount do
use Ash.Resource.Validation

@impl true
def validate(changeset, opts) do
item_count = Ash.Changeset.get_attribute(changeset, :item_count)

if item_count <= 4 do
:ok
else
{:error, "This is more than needed."}
end
end
end
I'm guessing invalid attribute error is needed in the error tuple?
ZachDaniel
ZachDaniel3y ago
That looks like it should work just fine really When you say it doesn't work, why not? Do you mean its not showing up in your form?
Jason
JasonOP3y ago
No error message in the form. Right.
ZachDaniel
ZachDaniel3y ago
{:error, message: "This is more than needed.", field: :item_count}
{:error, message: "This is more than needed.", field: :item_count}
SO yes an invalid attribute error would also have done it
{:error, Ash.Error.Invalid.InvalidAttribute.exception(field: :item_count, message: "This is more than needed.")}
{:error, Ash.Error.Invalid.InvalidAttribute.exception(field: :item_count, message: "This is more than needed.")}
Jason
JasonOP3y ago
Yay! It's working. Thank you!!

Did you find this page helpful?