etiennelacoursiere

etiennelacoursiere

How to properly handle nested associations in forms with manual data manipulation

I’m struggling with the proper way to handle nested associations in a Phoenix form when I need to manually manipulate form data.

Let me explain,
The structure of my data look like this

%Order{
  configuration: %Configuration{
    panel_groups: []
  }
}

I’m building a UI where you can select the number of panels you want, then you can group and ungroup panels. For example, when a user select panels and clicks a “group panels” button, I want to take the selected panels and group them.

First i select that i want 4 panels:

%Order{
  configuration: %Configuration{
    panel_groups: [
      %PanelGroup{image_id: some_id, panels: [%Panel{}]},
      %PanelGroup{image_id: some_id, panels: [%Panel{}]},
      %PanelGroup{image_id: some_id, panels: [%Panel{}]},
      %PanelGroup{image_id: some_id, panels: [%Panel{}]}
    ]
  }
}

Then i group some panels together:

%Order{
  configuration: %Configuration{
    panel_groups: [
      %PanelGroup{image_id: some_id, panels: [%Panel{}, %Panel{}]},
      %PanelGroup{image_id: some_id, panels: [%Panel{}, %Panel{}]}
    ]
  }
}

I used get_assoc and put_assoc everywhere to get and update the data in my form. Here is a pseudo example of grouping panels together.

%{source: changeset} = form 

configuration = Changeset.get_assoc(form.source, :configuration)
current_panel_groups = Changeset.get_assoc(configuration, :panel_groups)
new_panel_groups = function_that_rebuild_the_panel_groups()
custom_configuration = Changeset.put_assoc(configuration, new_panel_groups)
changeset = Changeset.put_assoc(changeset, configuration)
to_form(changeset, action: :update)

This works fine when creating a new order, but I started facing issues when adding the possibility to update an existing order:

  1. Somewhere in my page i need to display the panels. I made a function that takes the form and return the panel_groups using get_assoc, In the new form i didn’t had problem, but when editing an existing order, when I use get_assoc after a put_assoc, I get both the old panel groups (with action: :replace) and the new ones (with action: :insert). As a quick workaround i decided to filter out the :replace ones, but this feels wrong and naive.
  2. This doesn’t happen in a new form but when editing, If I try to perform a second put_assoc operation (e.g., ungroup panels, then group different ones), I get this error
cannot replace related %PanelGroup{} This typically happens when you are calling put_assoc/put_embed with the results of a previous put_assoc/put_embed/cast_assoc/cast_embed operation, which is not supported. You must call such operations only once per embed/assoc, in order for Ecto to track changes efficiently

So i’m starting to understand that put_assoc is maybe not the right choice here. What would be the best way to manipulate form data manually?

PS: The reason i’m using a form and not just a state using a map where i can manipulate freely my data is that the panels stuff is just a small part of my form and i didn’t want to have multiple state/source-of-truth and also i need validation

Most Liked

sodapopcan

sodapopcan

Ah crap, I totally forgot you can’t manipulate through associations. Sorry about that!

I’m confused by this part:

At this point, there is no concept of group.

But then you say:

The image is attached to the group not the panel.

Is this a completely different step or do you mean that there is no concept as far as the user is concerned at this point? Assuming it is just from the user’s perspective, I would still build a group any time a panel is added using inputs_for as normal, even if it’s not visible. But yes, sorry if that isn’t what you’re talking about.

As for not being able to call put_assoc multiple times, it looks like you are trying to manipulating the same changeset after it’s been validated. The handle_event that is doing the updates should be constructing a brand new changeset from params. Can you share what you have there?

As for updating nested changeset, it’s just generally a pain. This library may be of interest:
https://github.com/woylie/ecto_nested_changeset

Lastly, for a bit of nitpicky advice, your order changeset function would be a bit more robust if you didn’t use Map.get on params and instead do this:

  def changeset(order, attrs) do
    changeset = cast(order, attrs, @required)
    status = get_change(changeset, :status)

    changeset
    |> maybe_validate_required(status)
    |> ...

Sorry if this still isn’t super helpful, it is quite complicated and hard to tell without seeing everything!

Last Post!

al2o3cr

al2o3cr

Haven’t read through the whole thread yet, but FWIW this is precisely what phoenix_ecto does in to_form:

https://github.com/phoenixframework/phoenix_ecto/blob/76e5b7f8286c6212db5cabb68934c0ee3758d667/lib/phoenix_ecto/html.ex#L171-L176

Where Next?

Popular in Questions Top

minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" => #BSON.ObjectId<58eb1a7a9ad169198c3dXXXX>, "email" => ...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
Lily
In templates/appointment/index.html.eex: <%= for appointment <- @appointments do %> <tr> <td><%= appoi...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New

Other popular topics Top

minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 31494 112
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 40042 209
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New

We're in Beta

About us Mission Statement