etiennelacoursiere
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:
- 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 useget_assocafter aput_assoc, I get both the old panel groups (withaction: :replace) and the new ones (withaction: :insert). As a quick workaround i decided to filter out the:replaceones, but this feels wrong and naive. - This doesn’t happen in a new form but when editing, If I try to perform a second
put_assocoperation (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
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #blog-post
- #phoenix_html
- #iex
- #graphql
- #ai
- #genstage
- #elixirconf-us
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #security
- #hex










Showing Posts 1 to 8- Show Best Posts
- Show All Posts (oldest first)
- Show All Posts (newest first)
sodapopcan
Hello and welcome!
Without seeing your actual form it sounds like that from the user’s perspective they are adding “panels” to the configuration, not “panel groups.” You would have a much easier time if also associated
Panelstraight toConfigurationand used that association for your forms. You can still have thepanel_groupsassociation which, with the proper preloads, will be auto-magically grouped for you.Something like this:
(note the lack of
required: trueon thecast_assocs)etiennelacoursiere
I’m not sure i understand how this would help? This would make my panels one level less deep but it looks like it won’t solve my problems with
put_assocsince i would still have to use it to update the configuration.I think i may have to separate my
panel_groupsinto a state of its own. Manipulate the data there and then when i submit the form,put_assoconce the state of thepanel_groupsinto theconfiguration. But then i would loose the validation i need to do on my panels.Still would love to know the best practice for this kind of stuff.
etiennelacoursiere
Thinking about it, having a separate state would not work i think, because i need Ecto validation on the panels for the width and other fields. So i need to update the form in the liveview.
If this help, here what my schemas looks like. I trimmed them down to keep only the relevant stuff.
sodapopcan
I can take a closer look at this later and would help too if you can share the form.
If you make it one less level deep you wouldn’t need
put_assoc, you can just|> cast_assoc(:panels)using the panel’s changeset. This would just be for the form itself. This assumes a panel would have apanel_group_idwhen adding it to the order/configuration. Again, this would only be for the form, you can still have thepanel_groupassociation which you can preload along with their panels and then all the panels would be in their proper groups.I’m saying this based off the information that you are trying to manually sort them into groups.
etiennelacoursiere
I tinkered a bit around with the idea you had of having the panels one levels less deep via
has_many :panels, through: panel_groupson myConfiguration. This seems promising! But then i realize it would not work since:throughassociations are read-onlyFrom the ecto docs:
My form is quite big so i can’t really show it here but i will show a trimmed down version that hopefully get the big picture.
First let me explain what i’m trying to do.
I have a sidebar with a long vertical form in it with multiple sections for the order. There is a “Panel” section where the user choose the number of panel and then choose the dimensions and finish and other properties for each panel. At this point, there is no concept of group. The user is just configuring panel individually.
Let say he choose 4 panel. Then, on the right part of the screen he will see 4 empty panels with the right dimensions.
In reality here we have 4 panel group with each 1 panel inside.
Then the user can select 1 or more panels and assign an image to it. This is where the group come in play. The image is attached to the group not the panel. Let say the user select the two first panel and assign an image to it. Now the interface look something like this,
Again in reality we have now 1 group with two panels and 2 groups with each 1 panel.
I will admit, this form is more complex than anything i had to do form wise before and got me quite struggling. I did some weird workaround to make it work regarding CSS.
To me, my data structure make sense. I tried other approach of having the panels directly on the configuration. Then they could maybe have a group, maybe not. But having them always in a group and having the image on the group is what made more sense in the end.
My struggle is with manipulating the groups in the context of the form really. How can i move the panel around, changing in which group they belong without using
put_assocmultiple times? What would be the right tool here? I want to keep the validation on the panels so that when the user enters an invalid number for the width in the sidebar for example, the UI reflect the error and set the input red.sodapopcan
Ah crap, I totally forgot you can’t manipulate
throughassociations. Sorry about that!I’m confused by this part:
But then you say:
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_foras 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_assocmultiple 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.geton params and instead do this:Sorry if this still isn’t super helpful, it is quite complicated and hard to tell without seeing everything!
etiennelacoursiere
Sorry that was unclear. Yes, i meant that at this point from the user perspective in the UI there is no concept of group but the there is in the data structure.
This opened my eyes. I wasn’t working with the form params for the grouping operation. I was only working with the changeset ( source ) of the form. Updating the data with
put_assoc. I changed my approach and now i’m only working with the form params. Accessing them viasocket.assigns.form.params, updating them manually when i group/ungroup panels and not touching the changeset at all. This seems to work like a charm. The only problem i have is that, on the first mount,form.paramsis empty until aphx-changeis triggered at least once. I worked around that by having a hidden input with a js hook that trigger a change on mount. A bit sketchy but seems to work just fine.al2o3cr
Haven’t read through the whole thread yet, but FWIW this is precisely what
phoenix_ectodoes into_form:https://github.com/phoenixframework/phoenix_ecto/blob/76e5b7f8286c6212db5cabb68934c0ee3758d667/lib/phoenix_ecto/html.ex#L171-L176