# Nested Forms Make sure you're familiar with the basics of `AshPhoenix.Form` before reading this guide. When we talk about "nested" or "related" forms, we mean sets of form inputs that are for resource actions for related or embedded resources. For example, you might have a form for creating a "business" that can also include multiple "locations". In some cases, you may have buttons to add or remove from a list of nested forms, you may be able to drag and drop to reorder forms, etc. In other cases, the form may just be for one related thing, think a form for updating a "user" that also contains inputs for its associated "profile". ## Defining the structure ### Inferring from the action `AshPhoenix.Form` automatically infers what "nested forms" are available, based on introspecting actions which use `change manage_relationship`. For example, in the following action: ```elixir # on a `MyApp.Operations.Business` resource create :create do accept [:name] argument :locations, {:array, :map} change manage_relationship(:locations, type: :create) end ``` With this action, you could submit an input like so: ```elixir %{name: "Wally World", locations: [%{name: "HQ", address: "1 hq street"}]} ``` `AshPhoenix.Form` will look at the action, allowing you to use `Phoenix`'s `<.inputs_for` component for `locations`. Here is what it might look like in practice: ```heex <.simple_form for={@form} phx-change="validate" phx-submit="submit"> <.input field={@form[:email]} /> <.inputs_for :let={location} field={@form[:locations]}> <.input field={location[:name]} /> ``` To turn this automatic behavior off, you can specify `forms: [auto?: false]` when creating the form. ### Manually defining nested forms You can manually specify nested form configurations using the `forms` option. For example: ```elixir AshPhoenix.Form.for_create( MyApp.Operations.Business, :create, forms: [ locations: [ type: :list, resource: MyApp.Operations.Location, create_action: :create ] ] ) ``` You should prefer to use the automatic form definition wherever possible, but this exists as an escape hatch to customize configuration. ## Updating existing data You should be sure to load any relationships that are necessary for your `manage_relationship`s when you want to update the nested items. For example, if the form above was for an update action, you may want to allow updating the existing locations all in a single form. `AshPhoenix.Form` will show a form for each existing location, but only if the locations are loaded on the business already. For example: ```elixir business = Ash.load!(business, :locations) form = AshPhoenix.Form.for_update(business, :update) ``` > ### Not using tailwind? {: .warning} > > If you're not using tailwind, you'll need to replace `class="hidden"` > in the examples below with something else. In standard HTML, you'd do > ``. As long as the checkbox is hidden, you're good! ## Adding nested forms There are two ways to add nested forms. ### The `_add_*` checkbox ```heex <.simple_form for={@form} phx-change="validate" phx-submit="submit"> <.input field={@form[:email]} /> <.inputs_for :let={location} field={@form[:locations]}> <.input field={location[:name]} /> ``` This checkbox, when checked, will add a parameter like `form[_add_locations]=end`. When `AshPhoenix.Form` is handling nested forms, it will see that and append an empty form at the end. Valid values are `"start"`, `"end"` and an index, i.e `"3"`, in which case the new form will be inserted at that index. > ### But the checkbox is hidden, what gives? {: .info} > > If you're anything like me, the label + checkbox combo above may confuse you > at first sight. When you have a checkbox inside of a label, clicking on the label > [counts as clicking the checkbox itself](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#providing_a_bigger_hit_area_for_your_checkboxes)! ### `AshPhoenix.Form.add_form` In some cases, you may want to add a form either in a way that can't be triggered by a checkbox or that requires some additional data (like non-empty starting params). In those cases, you can use a button and a `handle_event` For example: ```heex <.simple_form for={@form} phx-change="validate" phx-submit="submit"> <.input field={@form[:email]} /> <.inputs_for :let={location} field={@form[:locations]}> <.input field={location[:name]} /> <.button type="button" phx-click="add-form" phx-value-path={@form.name <> "[locations]"}> <.icon name="hero-plus" /> ``` > ### whats with `@form.name <> "[locations]"` {: .info} > > By always using a path "relative" to the root form, we can handle cases where we are > adding a form to a multiply-nested form. So the path could be somethign like > locations[0][addresses][1]. The event handler has to know exactly where we are adding > a form. In the example above, we *could* just say `add_form(form, :locations)`. It would > be simpler, but we want to highlight how to work with potentially deeply nested data. ```elixir def handle_event("add-form", %{"path" => path}, socket) do form = AshPhoenix.Form.add_form(socket.assigns.form, path, params: %{ address: "Put your address here!" }) {:noreply, assign(socket, :form, form)} end ``` ## Removing nested forms Just like adding nested forms, there are two ways to *remove* nested forms. ### Using the `_drop_*` checkbox The `_drop_*` checkbox uses checkboxes which add form indices to a list that should be *removed* from the list. For example, given the following: ```heex <.simple_form for={@form} phx-change="validate" phx-submit="submit"> <.input field={@form[:email]} /> <.inputs_for :let={location} field={@form[:locations]}> <.input field={location[:name]} /> ``` When the checkbox is checked, the server sees: ```elixir %{"form" => %{"_drop_locations" => ["0"]}} ``` We use this information to automatically remove the item at that index on validate. ### Using `AshPhoenix.Form.remove_form` Just like adding forms, there is a manual way to remove forms. In this case we pass the full path to the form being removed. ```heex <.simple_form for={@form} phx-change="validate" phx-submit="submit"> <.input field={@form[:email]} /> <.inputs_for :let={location} field={@form[:locations]}> <.input field={location[:name]} /> <.button type="button" phx-click="remove-form" phx-value-path={location.name}> <.icon name="hero-x-mark" /> ``` ```elixir def handle_event("remove-form", %{"path" => path}, socket) do form = AshPhoenix.Form.remove_form(socket.assigns.form, path) {:noreply, assign(socket, :form, form)} end ``` ## Sorting nested forms Just like adding and removing forms, there are two ways to *sort* nested forms too! ### Using `_sort_*` checkboxes This method is useful when combined with something like [`sortable.js`](https://sortablejs.github.io/Sortable/) to allow for dragging and dropping on the front end. > ### the `order_is_key` option {: .info} > > If you are working with a sorted relationship, you will likely want to couple it > with the `order_is_key` option of `managed_relationships`. This writes the order > of items in the list of inputs into each input, as if it was provided as an input > > `change manage_relationship(:locations, type: :direct_control, order_is_key: :position)` > In the above example, if you provided a list of inputs like > `[%{address: "foo"}, %{address: "bar"}]`, it would first be converted into > `[%{address: "foo, order: 0}, %{address: "bar", order: 1}]` before being > processed. Lets say you had the following `Sortable` hook in your `app.js` ```js import Sortable from "sortablejs" export const Sortable = { mounted() { new Sortable(this.el, { animation: 150, draggable: '[data-sortable="true"]', ghostClass: "bg-yellow-100", dragClass: "shadow-2xl", onEnd: (evt) => { this.el.closest("form").querySelector("input").dispatchEvent(new Event("input", {bubbles: true})) } }) } } ... let Hooks = {} Hooks.Sortable = Sortable ``` You could use the `_sort_*` checkbox in each nested form like so: ```heex <.simple_form for={@form} phx-change="validate" phx-submit="submit"> <.input field={@form[:email]} />