Nested form example
dongrami:
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<...
Can you please give me some pointers?
ZachDaniel:
Add
type: :list to the configuration of
items
ZachDaniel:
Looks like that isn’t shown in the examples.
dongrami:
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]
ZachDaniel:
Ah, yeah so that’s not how you’d add a form. That form’s name includes the index.
ZachDaniel:
You’d want something like
phx-click=add-item
ZachDaniel:
And then in the add item handler you’d use
add_form(form, :items)
ZachDaniel:
For top level forms it’s pretty simple. But for multiply nested forms you’d do
parent_form.name <> “[items]”
dongrami:
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
dongrami:
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
]
ZachDaniel:
:update is not a valid type
ZachDaniel:
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?
ZachDaniel:
If so, then you want
type: :direct_control
dongrami:
Thank you! That did the trick. 🙂
dongrami:
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, []}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
dongrami:
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 reaches
RequireValidItemCount but the
Ash.Changeset.before_action block doesn’t run.
dongrami:
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:
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).
dongrami:
Got it. Thanks. Is there an example of custom validation?
ZachDaniel:
In that case, you should be able to do
validate compare(:item_count, greater_than_or_equal_to: 4)
ZachDaniel:
You can put that in an individual action:
actions do
create :create do
...
validate ...
end
endor for all actions
validations do
validate ....
end
dongrami:
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:
Ah, gotcha
ZachDaniel:
https://github.com/ash-project/ash/blob/v2.6.20/lib/ash/resource/validation/builtins.ex#L239
ZachDaniel:
So all of the built in validations are technically custom validations
ZachDaniel:
They are just provided with convenient function names like
compare/1
ZachDaniel:
So the
compare/2 validation’s implementation is here:
https://github.com/ash-project/ash/blob/v2.6.20/lib/ash/resource/validation/compare.ex
ZachDaniel:
Naturally, though, the built in ones are a bit more involved because they cover all kinds of cases.
dongrami:
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
ZachDaniel:
validations do
validate {RequireValidItemCount, []} do
message "Item count must be greater than 0"
end
end
ZachDaniel:
RequireValidItemCount needs to be an
Ash.Resource.Validation as well
ZachDaniel:
and then you can also return the message from the validation directly
{:error, "Item count must be greater than 0"}
dongrami:
Okay. Still digesting the
compare example for that. one sec.
dongrami:
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
dongrami:
I’m guessing
invalid attribute error is needed in the error tuple?
ZachDaniel:
That looks like it should work just fine really
ZachDaniel:
When you say it doesn’t work, why not?
ZachDaniel:
Do you mean its not showing up in your form?
dongrami:
No error message in the form.
dongrami:
Right.
ZachDaniel:
{:error, message: "This is more than needed.", field: :item_count}
ZachDaniel:
SO yes an invalid attribute error would also have done it
ZachDaniel:
{:error, Ash.Error.Invalid.InvalidAttribute.exception(field: :item_count, message: "This is more than needed.")}
dongrami:
Yay! It’s working. Thank you!!