I\'m relatively new to rails and finally found the right way to use accepts_nested_attributes_for
.
However, there are some serious resources on the web who
So, the accepted answer just says why accepts_nested_attributes_for
is often a good solution, but never actually offers a solution on how to do it. And the example in the linked article will run into problems if you want to make your form accept a dynamic number of nested objects. This is the only solution I've found
https://coderwall.com/p/kvsbfa/nested-forms-with-activemodel-model-objects
For posterity this is the basics, but there's a bit more on the site:
class ContactListForm
include ActiveModel::Model
attr_accessor :contacts
def contacts_attributes=(attributes)
@contacts ||= []
attributes.each do |i, contact_params|
@contacts.push(Contact.new(contact_params))
end
end
end
class ContactsController < ApplicationController
def new
@contact_list = ContactListForm.new(contacts: [Contact.new])
end
end
and f.fields_for :contacts
should behave like an has_many
relationship, and easily handled by your form object.
If Contact
isn't an AR
model you'll need to spoof persisted?
as well.
It's much easier to use virtus instead of accepts_nested_attributes_for
than I thought. The most important requirement was to dare to make things that were not covered in any of the tutorials I read yet.
Step by step:
gem 'virtus'
to the Gemfile and ran bundle install
.I wrote a file models/contact.rb and wrote the following code:
class Contact
include Virtus
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attr_reader :name
attr_reader :number
attribute :name, String
attribute :number, Integer
def persisted?
false
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
@person = Person.create!(name: name)
@phones = @person.phones.create!(number: number)
end
end
Then I ran rails generate controller contacts
and filled *models/contacts_controller.rb* with
class ContactsController < ApplicationController
def new
@contact = Contact.new
end
def create
@contact = Contact.new(contact_params)
@contact.save
redirect_to people_path
end
def contact_params
params.require(:contact).permit(:name, :number)
end
end
The next step was the view. I created views/contacts/new.html.erb and wrote this basic form
<%= form_for @contact do |f| %>
<p>
<%= f.label :name %><br />
<%= f.text_field :name %>
</p>
<p>
<%= f.label :number %><br />
<%= f.text_field :number %>
</p>
<%= f.submit %>
<% end %>
Of course I also needed to add the route resources :contacts
That's it. Maybe it could be done more elegant. Maybe it would also pay to use only the Contacts-class, also for the other CRUD-actions. I didn't try that yet...
You can find all changes here: https://github.com/speendo/PhoneBook/tree/virtus/app/models
Your question implies that you believe accepts_nested_attributes functionality to be a bad thing which is totally not the case and works perfectly well.
I'll start by saying that you don't need an alternative to accepts_nested_attributes_for but I'll cover that at the end of this post.
With reference to the link you provide, it cites nothing about why the poster believes accepts_nested_attributes_for should be deprecated at all and just merely states
in my humble opinion, should be deprecated
Nested attributes are an extremely important concept when considering how to capture multiple records related to a parent in a single form which is not just a Ruby on Rails thing but used in most complex web applications for sending data back to the server from the browser regardless of of the languages used to develop the site.
I'm not criticising the article you point to at all. To me it's just pointing out obvious alternatives to filling up a database backed model with lots of code that is not necessarily related to business logic. The specific example used is merely a coding style preference alternative.
When time is money and the pressure is on and one line of code will do the job versus the 22 lines of code shown in the example my preference in most cases (not all cases) is to use one line of code in a model (accepts_nested_attributes_for) to accept nested attributes posted back from a form.
To answer your question properly is impossible as you have not actually stated why YOU think accepts_nested_attributes_for is not good practice however the simplest alternative is to just extract the params hash attributes in your controller action and handle each record individually inside a transaction.
Update - follow up on comment
I think the author of the linked article argues that, following oop-paradigms, every object should only read and write its own data. With accepts_nested_attributes_for, one object however changes some other objects data.
O.K. Lets clear that up. Firstly OO paradigms suggest no such thing. Classes should be discreet but they are allowed to interact with other classes. In fact there would be no point to an OO approach in Ruby if this was the case as EVERYTHING in ruby is a class therefore nothing would be able to talk to anything else. Just imagine what would happen if an object that just happens to be an instance of your controller were not able to interact with models or other controllers?
With accepts_nested_attributes_for, one object however changes some other objects data.
Couple of points on that statement as it's a complex one I'll try to be as brief as possible.
1) Model instances guard the data. In very complex scenarios involving hundreds of tables in any/most other languages (C, Delphi, VB to name a few) a middle tier in a 3 tier solution does just that. In Rails terms a model is a place for business logic and does the job of the middle tier in a 3 tier solution that is normally backed up by stored procedures and views in the RDBMS. Models quite rightly should be able to talk to each other.
2) accepts_nested_attributes_for does not break any OO principles at all. it merely simplifies the amount of code that you would need to write if the method did not exist (as you are finding out). If you accept attributes that are nested inside a params hash for child models all you are doing is allowing the child models to handle that data in the same way that your controller's action would have to do. No business logic is bypassed and you get added benefits.
Lastly
I can afford to care about elegance of code (more than about time)
I can assure you that there is nothing elegant about writing 20 + more lines of code than you need to and adding hundreds of lines more code from a gem where one line of code will do the work for you. As others have stated (including me) accepts_nested_attributes_for is not always an appropriate ActiveRecord method to use and it is a good thing that you are doing by looking at different approaches as ultimately you will be able to make better informed judgements as to when to use built in methods and when to write your own. However I would suggest that to fully understand what is going on (as you state you have the time) you would be better writing your own code to handle form objects and accepts nested attributes alternatives. That way you would find yourself understanding so much more.
Hope that makes sense and good luck with your learning.
UPDATE 2
To finally get to your point and in reference to your own answer plus taking into account of the excellent comments others have made on your own answer form objects backed by the virtus gem is a perfectly reasonable solution especially when dealing with the way data has to be collected. The combination helps to separate the user interface logic from the business logic and so long as you are ultimately passing off the data to the models so that business logic is not bypassed (as you show you are doing exactly this) then you have a great solution.
Just don't rule out accepts_nested_attributes out of hand.
You might also gain some benefit from watching the railscasts by Ryan Bates on form objects.
Railscasts also has an episode on nested form models: http://railscasts.com/episodes/196-nested-model-form-revised?view=asciicast
As shown in the comments, cocoon gem simplifies a lot: https://github.com/nathanvda/cocoon