reform icon indicating copy to clipboard operation
reform copied to clipboard

Nested form, two levels deep

Open domi91c opened this issue 6 years ago • 11 comments

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.

domi91c avatar Apr 30 '18 18:04 domi91c

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?

apotonick avatar Apr 30 '18 19:04 apotonick

@domi91c did you solve this problem?

unrooty avatar Jul 09 '18 14:07 unrooty

@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?

unrooty avatar Jul 09 '18 14:07 unrooty

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.

apotonick avatar Jul 10 '18 04:07 apotonick

@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)

unrooty avatar Jul 10 '18 08:07 unrooty

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.

apotonick avatar Jul 10 '18 08:07 apotonick

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...

unrooty avatar Jul 10 '18 09:07 unrooty

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

apotonick avatar Jul 10 '18 09:07 apotonick

@apotonick thanks for answers :)

unrooty avatar Jul 10 '18 09:07 unrooty

Please let me know what is the problem!

apotonick avatar Jul 10 '18 09:07 apotonick

@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...

unrooty avatar Jul 10 '18 16:07 unrooty