trailblazer / reform

Form objects decoupled from models.
https://trailblazer.to/2.1/docs/reform.html
MIT License
2.5k stars 184 forks source link

Nested form, two levels deep #454

Open domi91c opened 6 years ago

domi91c commented 6 years ago

I had my Reform form working with one nested model, but I need that model to contain another nested model. A tutorial has_many steps, a step has_many images. Here's my attempt:

class TutorialForm < Reform::Form

  collection :steps, populator: :step_populator! do
    property :title
    property :body
    validates :title, presence: true
    validates :body, presence: true
    collection :images, populator: :image_populator! do
      property :image
      validates :image, presence: true
    end
  end

  def step_populator!(collection:, index:, **)
    if (item = collection[index])
      item
    else
      collection.insert(index, Step.new)
    end
  end

  def image_populator!(collection:, index:, **)
    if (item = collection[index])
      item
    else
      collection.insert(index, Image.new)
    end
  end
end

Is this totally wrong? It's not working at the moment.

apotonick commented 6 years ago

Keep in mind that populators are only called when the incoming document contains a corresponding fragment.

"Not working" - what is "not working"? :joy: Does your computer start up? Have you plugged in the power cable? Does the form validate? What's missing? What's your input hash? What's the resulting form?

unrooty commented 6 years ago

@domi91c did you solve this problem?

unrooty commented 6 years ago

@apotonick the problem is that I can't use collection in collection on form. For example I have such code in Reform::Form:

collection :contacts, populate_if_empty: Contact do
      properties :first_name, :last_name, :agency_id, :nickname,
                 :middle_name, :suffix, :prefix, :title

      collection :phones, populate_if_empty: ContactPhone, populator: ->(fragment:, **) {
        return skip! if fragment['delete'] == 'true' && !fragment['id']

        if fragment['id']
          item = phones.find_by(id: fragment['id'])
          phones.delete(item) if fragment['delete'] == 'true' && !item.primary
          item ? item : phones.append(model.phones.build)
        else
          phones.append(model.phones.build)
        end
      } do
        properties :number, :type, :primary
        validates :number, presence: true
      end
    end

and on view I have (just for tests):

<%= f.fields_for :contacts, form.model.contacts.presence || form.model.contacts.build do |ff| %>
      <%= ff.text_field :first_name %>
      <%= ff.hidden_field :agency_id, value: 1 %>
      <%= ff.text_field :last_name %>
      <%= ff.fields_for :phones, form.model.contacts.last.phones || form.model.contacts.last.phones.build do |fff| %>
        <%= fff.text_field :number %>
        <% end %>
    <% end %>

but rails throws error:

undefined method 'number' for #<ContactPhone::ActiveRecord_Associations_CollectionProxy:0x00007f011fb512e8>

When I write accept_nested_attributes :phones in Contact model it works (ContactPhone belongs_to Contact). Can you tell me how use collection in collection on view, please?

apotonick commented 6 years ago

Hi @unrooty, please try this puts form.contacts[0].phones on the console. Reform doesn't do anything other than providing you a decorated, well-defined object graph. I am guessing the second argument with the || is the problem, you're creating objects there, I've never seen this before.

unrooty commented 6 years ago

@apotonick when I wrote p form.contacts[0].phones, I got

[#<#<Class:0x00007f00f265f000>:0x00007f0117da22c0 @fields={"number"=>"648-822-8073", "type"=>"Work", "primary"=>true}, @model=#<ContactPhone id: 12, number: "648-822-8073", type: "Work", contact_id: 12, created_at: "2018-07-07 12:10:37", updated_at: "2018-07-07 12:10:37", primary: true>, @mapper=#<#<Class:0x00007f0127d8a688>:0x00007f0117da2180 @model=#<ContactPhone id: 12, number: "648-822-8073", type: "Work", contact_id: 12, created_at: "2018-07-07 12:10:37", updated_at: "2018-07-07 12:10:37", primary: true>>, @_changes={}, @errors=#<Reform::Form::ActiveModel::Errors:0x00007f0117da1de8 @base=#<#<Class:0x00007f00f265f000>:0x00007f0117da22c0 ...>, @messages={}, @details={}>>]

But when I tried to pass it to ff.fields_for, I've got error:

undefined method `number' for #<Disposable::Twin::Collection:0x00007f0117da1ca8>

Another thing that I want to say is that after || I build model if it doesn't exists (because fields_for requires it to build right form)

apotonick commented 6 years ago

The behavior of Reform is correct, it returns an "enumeratable" object for phones, I have no idea how fields_for works, it probably does some bullshit to figure out if it's an array and doesn't see it because of Rails magic and respond_to?? Wouldn't be surprised, has happened before. You need to look into fields_for, Reform is doing the correct thing.

unrooty commented 6 years ago

The most strange thing for me that I use two same objects in fields_for, but on top level it work and in nested fields_for it stops working. I start to hate Rails...

apotonick commented 6 years ago

Well, we wrote the Formular gem to avoid Rails form helpers, but it's still lacking docs.

unrooty commented 6 years ago

@apotonick thanks for answers :)

apotonick commented 6 years ago

Please let me know what is the problem!

unrooty commented 6 years ago

@apotonick I didn't find solution and desided to use text_field_tag "account[contacts_attributes][#{ff.index}][phones_attributes][0][number]. It's not good solution but I can't come up anything else...