Feeling limited and a bit frustrated by the inputs_for helper, should I :)?

Hello folks!

Like the title says right now I’m feeling a bit frustrated by the inputs_for helper.

It’s not the first time I’m bumping into this problem, maybe already the second or third time, so probably there’s something I don’t understand quite well. I would love to have your advice on that :slight_smile:

I’m coming from the Rails ecosystem, and with Rails and its fields_for helper, you can basically pass an attribute name OR a random attribute and any collection as arguments to it, and Rails will create a nested form iterating on the given association name OR attribute and collection.

This is very useful, because sometimes you don’t want to just iterate basically on a many association, but on a filtered sub-set for example. And with the Rails helper, it’s quite easy to do this simple filtering directly in the view.

Right now, in Phoenix, it looks like inputs_for can just take an attribute from the underlying form data. So it’s not really easy to simply filter elements from a has_many relationship on the view or template level.
I would need to create a different changeset, and even a different underlying struct for it. OK, I can still do that…

But what’s more annoying is that if this attribute is not a real schema association, it raises an exception Check the field exists and it is one of embeds_one, embeds_many, has_one, has_many, belongs_to or many_to_many.

So how is it possible to just use inputs_for on a virtual field, a simple list of Structs, or something that is not related to the DB?

Would it be difficult to make it accept an optional simple list of Struct as an argument on which it would iterate like the Rails fields_for helper?

2 Likes

I use inputs_for without Changeset, you can do

f = form_for :atom, "#", []
for f0 <- inputs_for f, :nest, default: [%{a: 1}, %{a: 2}]  do
  text_input(f0, :a)
end
<input id="atom_nest_0_a" name="atom[nest][0][a]" type="text" value="1">
<input id="atom_nest_1_a" name="atom[nest][1][a]" type="text" value="2">

I don’t even use the text_input helper, and use input_name to have full control.

If you use Phoenix.LiveView and don’t use phx-change or phx-submit, there are URI.encode_www_form() and Plug.Conn.Query.decode() to control individual input from client.

1 Like

Part of that frustration might come from the fact that you seem to be trying to make your changeset of the db persisted data work for concerns of your form. You can create a changeset/alter the changeset so it works for the constraints you have in your form. It’s great that often you can use one and the same changeset for both the db persistance as well as powering the form in the frontend, but as soon as they’re dealing with differently shaped data you likely want to split up both concerns and only convert between them at the edges (loading form the db and persisting changes to the db).

2 Likes

@LostKobrakai honestly I tried to go the other way around like you’re saying.

I tried to create a changeset that is NOT based on how it is persisted in my DB but based on the UI.

But I figured that this way is also very complex and frustrating (IMHO).

Why? Because just creating a changeset was not working, I had to create the underlying Struct, had to use also embeds_many relationships inside the struct because otherwise it’s triggering an exception.

I managed to create the struct and the changeset based on my UI, create it dynamically from the data in my database and the form was working fine.

But then, persisting the data from the form triggered another issue : my first attempt was to validate the UI changeset with apply_action and then, take the data to fill my DB-based changeset.
apply_action gives me back the final Struct with the relationships filled with list of Structs, and I tried to use this data on my final DB-based changeset using put_embed. But then, updating records was not triggering any changes on the DB changeset. It looks like put_embed needs to have the underlying changesets in the List to trigger individual changes (it looks like it’s not triggering changes with a List of Structs).

So in final, how can I use a UI-based changeset, validate it with apply_action if I can’t use the resulting Struct later to update a relationship with put_embed because it needs individual changesets?

All this still make me think that some stuff could be improved overall. It still looks very complex to me.

Creating embeddded schemas (also using inline embeds) specific to a form imo it actually the simpler solution than trying to go with schemaless changesets. Schemaless changesets are fine for smaller adhoc changesets, but neither support nesting nor would I say they’re saving you complexity over embeds.

You don’t need to use the resulting struct as the baseline for mapping to your db changeset. You can also use the form specific changeset to pull out just the changes to apply. That’s the great part about changeset. It holds all the parts separately: the initial data, the params, the changes, while the resulting structs will flatten all those details giving you a hard time getting to those details in the later steps.

Thanks for your answer again :ok_hand:.

So basically you’re advising me to directly use the data in the changeset using multiple get_field calls for instance to consolidate the data instead of using the final struct given by apply_action.
That’s maybe a solution.
I will try that.

At the end, even if it’s working it’s still a bit convoluted.

There are many things that are more flexible to do in Elixir & Phoenix than Rails & ActiveRecord and when it’s the case I recognise that fully. But for this use case, it looks like the way it’s handled by Rails helpers is a lot more easy and straightforward.

Honestly I’m still trying to figure out if we could improve something and what.

Something not related to inputs_for but I was a bit annoyed when finding out that put_embed for an embeds_many relationship needs a List of changesets to trigger any changes to existing records. Passing it a List of Structs does not trigger changes at all and I have not found anything written about that in the documentation.

Did those structs have the ids included? Without id’s there’s no way to match existing embeds to changed embeds. This is explained in the docs for put_assoc, which the docs for put_embed refer to.

Hello again, and thanks for your help.

Yes, the Structs have the ids included.

You can see that it’s not triggering any changes:

iex(13)> expert.availabilities
[
  %EpsyliaCore.Schema.Availability{
    day: "monday",
    ends_at: "18:30",
    id: "9528c65e-25bb-4940-8f3e-91aa6ad9154e",
    starts_at: "14:30"
  },
  %EpsyliaCore.Schema.Availability{
    day: "wednesday",
    ends_at: "18:30",
    id: "a101da4a-2b0b-4522-8601-56426e0abd56",
    starts_at: "21:30"
  }
]

availabilities = [
  %EpsyliaCore.Schema.Availability{
    day: "FOOOO",
    ends_at: "FOOOO",
    id: "9528c65e-25bb-4940-8f3e-91aa6ad9154e",
    starts_at: "FOOOO"
  },
  %EpsyliaCore.Schema.Availability{
    day: "FOOOO",
    ends_at: "FOOOO",
    id: "a101da4a-2b0b-4522-8601-56426e0abd56",
    starts_at: "FOOOO"
  }
]

iex(17)> expert |> change() |> put_embed(:availabilities, availabilities)
#Ecto.Changeset<action: nil, changes: %{}, errors: [],
 data: #EpsyliaCore.Schema.Customer<>, valid?: true>

I have not seen this behaviour mentioned in the doc, or I’ve missed something?

I missed one important point: Structs are not checked for changes, only maps are.

OK thanks for the point, I will try to remember it, if feels weird when it happens :neutral_face:.

So we’re back to the fact that when I have my UI-based changeset, I cannot use apply_action on it to validate and get back the final struct, because I cannot use it to consolidate the data (put_embed needs maps or changesets), right (?)

I will have to use multiple get_field calls on my changeset to consolidate the data in my DB-based changeset…
It would still work I think. But don’t you think it’s a bit convoluted? Do you think something could be improved in the process, in Ecto or Phoenix HTML helpers?

I have no idea about the differences between your form and your db schema. What we’re talking about is mapping one changeset to another, which is inherently complex. I’m not sure how any tooling should be able to know what to do in that situation. This does not necessarily mean that you cannot have a simpler solution, which doesn’t need a mapping between multiple changesets.

I’m trying to see if this is a XY problem…

Because my initial use case is quite simple. I simply have a embeds_many :availabilities relationship, but in my template I need to group availabilities depending on their week day.

I’m quite sure in Rails I would have simply filtered directly in the view, with something like:

fields_for :availabilities, @expert.availabilities.filter { |a| a.day == "monday" } do |af|
   ...

fields_for :availabilities, @expert.availabilities.filter { |a| a.day == "tuesday" } do |af|
   ...

And probably just loop like that on each day week. And that’s it. No need to change anything to the model.

But with Phoenix now, it looks like I’m doing very complex stuff to have the same result.

      <%= f = form_for @changeset, Routes.user_path(@conn, :create), opts %>
        Name: <%= text_input f, :name %>
        <%= for friend_form <- inputs_for(f, :friends), input_value(f, :day) == "monday" do %>
          # for generating hidden inputs.
          <%= hidden_inputs_for(friend_form) %>
          <%= text_input friend_form, :name %>
        <% end %>
      </form>
3 Likes