Nested form example

dongrami
2023-03-05

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?

``` form = AshPhoenix.Form.for_update(tweet, :update, <<<< line 21 api: Tweets, forms: [ items: [ resource: Item, data: tweet.items, create_action: :create, update_action: :update ] ``` ``` 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 ```` ``` 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 ```

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]

``` <%= f = form_for @form, "#", [phx_change: :validate, phx_submit: :save] %> <%= label f, :tweet_name %> <%= text_input f, :tweet_name %> <%= f.name %>
<%= for item_form <- inputs_for(f, :items) do %>
<%= 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]`
<% end %>
<%= submit "Save" %>
```

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
end

or 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:

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!!