AshPhoenix.Form.value of nested form returns different format after adding a new item

I am using SortableJS to allow rearranging of embedded items of an Ash resource. When I access the items in a handle_event call as they are from the database, the return of AshPhoenix.Form.value(form, :steps) are the database embedded structs. However, if I add a new item to the array using AshPhoenix.Form.add_form then when I go to access AshPhoenix.Form.value(form, :steps) in a subsequent handle_event call, it is now a simple Map in the form:

 %{
    "0" => %{
      "_form_type" => "update",
      "id" => "fb70646d-4c46-4a07-bd32-5804da5a3663"
    },
    "1" => %{
      "_form_type" => "update",
      "id" => "7afebcfd-f390-4a42-867b-18f4335c54ee"
    }
...
  "8" => %{
      "_form_type" => "create",
      "_touched" => "id,step",
      "id" => "3be695ba-7104-4f0f-b531-58310cd1ba3f",
      "step" => 9
    }

Is there a way to get a value of that nested array that is a merged view of the original items from the database and the new items? I can manually hack one together myself, but it feels like I am doing something wrong.

Additionally, how would you go about rearranging the embedded resources?

  def handle_event("reposition", %{"new" => new_position, "old" => old_position}, socket) do
    ash_form = socket.assigns.ash_form

    # Convert from index to offset, since we start steps at 1 (I don't remember why)
    old_position = old_position + 1
    new_position = new_position + 1

    mapped_steps =
      ash_form
      |> AshPhoenix.Form.value(:steps)
      |> Enum.map(fn step ->
        cond do
          step.step == old_position ->
            %{step | step: new_position}

          new_position > old_position and step.step > old_position and step.step <= new_position ->
            %{step | step: step.step - 1}

          new_position < old_position and step.step >= new_position and step.step < old_position ->
            %{step | step: step.step + 1}

          true ->
            step
        end
      end)
      |> Enum.sort_by(& &1.step)
      |> Enum.reduce(%{}, fn step, acc ->
        Map.put(acc, Integer.to_string(step.step - 1), %{
          "id" => step.id,
          "step" => step.step,
          "title" => step.title,
          "component" => step.component,
          "required" => step.required,
          "form_id" => step.form_id
        })
      end)

    params = MyApp.Utils.MapUtil.deep_merge(ash_form.params, %{"steps" => mapped_steps})

    ash_form = AshPhoenix.Form.validate(ash_form, params)

    {:noreply, assign(socket, ash_form: ash_form)}
  end

This works, but the reduce step that has to completely copy the items also makes it seem like I am doing something wrong, but without that, the nested form logic breaks.

:thinking: A couple thoughts. First, for accessing the child forms, I’d suggest using form.forms[:steps], which will get you a consistent output (the nested forms in order). As for reordering them, I’m thinking we ought to add a built in utility, like AshPhoenix.Form.reorder_form(:steps, from: 1, to: 3). What that would do is use update_form to update the container form (in this case the parent) and reorder the values in .forms.

Someone in the elixir discord ended up doing something like this:

form =
  AshPhoenix.Form.update_form(socket.assigns.form, [], fn parent ->
    Map.update(parent, :forms, %{}, fn parent_forms ->
      Map.update(parent_forms, :steps, [], fn
        child_forms ->
          ids = Enum.map(child_forms, fn form -> {form.id, form.name} end)

          {[child], rest} =
            child_forms
            |> Enum.split_with(fn child -> child.name == name end)

          rest
          |> List.insert_at(to, child)
          |> Enum.zip_with(ids, fn form, {id, name} -> %{form | id: id, name: name} end)
      end)
    end)
  end)

Which was for adding forms at a specific index. We could also potentially support specifying an index to add forms in AshPhoenix.Form.add_form, just work that hasn’t been done yet. I think that if you reorder the forms in .forms[:steps] that it should work?

This code is actually for repositioning, the Enum.split_with pops a child form with a specific name from the list and inserts it at a to position.

@lardcanoe, if you want to use old_position and new_position, you can do:

  def handle_event("reposition", %{"new" => new_position, "old" => old_position}, socket) do
form =
  AshPhoenix.Form.update_form(socket.assigns.form, [], fn parent ->
    Map.update(parent, :forms, %{}, fn parent_forms ->
      Map.update(parent_forms, :steps, [], fn
        child_forms ->
          ids = Enum.map(child_forms, fn form -> {form.id, form.name} end)

          {[child], rest} =
            child_forms
            |> Enum.split_with(fn {child, index} -> index == old_position end)

          rest
          |> List.insert_at(new_position, child)
          |> Enum.zip_with(ids, fn form, {id, name} -> %{form | id: id, name: name} end)
      end)
    end)
  end)

  # Do your validations

  {:noreply, assign(socket, form: form)}
end

Thank you both, but I can’t for the life of me get AshPhoenix.Form.update_form to work, like ever, in any situation :frowning:

So I am taking the approach of reconstructing the params of the nested form.

  def handle_event("reposition", %{"new" => new_position, "old" => old_position}, socket) do
    ash_form = socket.assigns.ash_form

    # Convert from index to offset, since we start steps at 1 (I don't remember why)
    old_position = old_position + 1
    new_position = new_position + 1

    updated_step_params =
      ash_form.source.forms[:steps]
      |> Enum.map(fn step_form ->
        remap_step_form(step_form, new_position, old_position)
      end)
      |> Enum.sort_by(& &1["step"])
      |> Enum.reduce(%{}, fn step, acc ->
        Map.put(acc, Integer.to_string(step["step"] - 1), step)
      end)

    params =
      ash_form
      |> AshPhoenix.Form.params()
      |> Map.put("steps", updated_step_params)

    ash_form = AshPhoenix.Form.validate(ash_form, params)

    {:noreply, assign(socket, ash_form: ash_form)}
  end

Ugly, but it works for all scenarios in my form.

When you say you can’t get it to work, do you mean you can’t get it to do anything? Like it never updates a form? Or just that every variation of using it hasn’t had the desired effect.