Sometimes you need to create a form whose data is associated with several tables. For example, you have two models: Owner and Car. When adding a new Owner, I would like to be able to immediately add a car. With the advent of Rails 2.3, this has become easier.
# () <br/> def create<br/> @owner = Owner. new ( params [ :owner ] ) <br/> ...<br/> if @owner . save <br/> @car = Car. new ( params [ :car ] ) <br/> if @car . save <br/> ...<br/> end <br/><br/> # , Rails 2.3+ <br/> def create<br/> @owner = Owner. new ( params [ :owner ] ) <br/> ...<br/> end
And the magic in the magic parameter
accepts_nested_attributes_for , which is
written in the model and passes additional parameters when creating the object. All this is created in one line:
Owner. create ( :name => "" , :age => 40 , :car_attributes => <br/> { :model => "Formula 1" , :color => "red" } )
')
Thus, we create two entries at once: about the Owner and about the car.
Consider another example, in more detail: the
Person model must be related to itself — when adding a person, we will make it possible to “add children”, which also apply to this model.
Step 1: We inform the model about the use of nested attributes
The first thing to do for an association is to add the line
accepts_nested_attributes_forlass Person < ActiveRecord::Base <br/> validates_presence_of :name <br/> has_many :children , :class_name => 'Person' <br/> accepts_nested_attributes_for :children , :allow_destroy => true <br/> # has_one <br/> end
After that, you can directly create, edit and delete child relationships with the object:
# : <br/> @person . children_attributes = [ { :name => 'Son' } ] <br/> @person . children #=> [ <#Person: name: 'Son'> ] <br/> @person . children . clear <br/> # : <br/> @person . children_attributes =<br/> [ { :name => 'Son' } , { :name => 'Daughter' } ] <br/> @person . save <br/> @person . children #=> [ <#Person: name: 'Son'>, <#Person: name: 'Daughter'> ] <br/> # ( id == 1) <br/> @person . children_attributes = [ { :id => 1 , :name => 'Lad' } ] <br/> @person . save <br/> #=> 'Lad' <br/> # (id == 2) : <br/> @person . children_attributes =<br/> [ { :id => 2 , :name => 'Lassie' } , { :name => 'Pat' } ] <br/> @person . save <br/> #=> 'Lassie', 'Pat' <br/> # Pat'a (id = 3), <br/> @person . children_attributes = [ :id => 3 , '_destroy' => '1' } ] <br/> @person . save <br/> #=> Pat
To support the creation and editing of objects, we should use an array of hashes when associating one-to-many, or just a hash when associating one-to-one. If the hash parameter
:id
not set, a new object will be created.
To delete an existing linked object, use the following method:
[ { :id => pk, '_destroy' => '1' } ]
, where the '_destroy' parameter should be any true value. Do not forget to set the option
:allow_destroy
in the model, by default it is turned off.
In Rails 2.3.5, the function _destroy was renamed -
previously it was called _delete . Do not forget about it if you work with an outdated version.
Perhaps all this looks like a small hack, but soon we will see that working with multi-model forms has actually become easier.
Step 2. Create a form with a nested model
In the view, simply add the fields_for, and in it we write the fields for this model:
<% form_for @person do | person_form | %> <br/> <% = person_form. label :name %> <br/> <% = person_form. text_field :name %> <br/> <% person_form. fields_for :children do | child_form | %> <br/> <% = child_form. label :name %> <br/> <% = child_form. text_field :name %> <br/> <% unless child_form. object . new_record ? %> <br/> <% = child_form. check_box '_destroy' %> <br/> <% = child_form. label '_destroy' , 'Remove' %> <br/> <% end %> <br/> <% end %> <br/> <% = submit_tag %> <br/> <% end %>
The code will create a form with all the required fields, which will go to the RESTful controller, and from there, the
children_attributes parameters will be imperceptibly transferred to the model. If at creation of “children” errors occur, they will be added to
person .errors - in this case the object will not be saved in the database.
A few helpful notes:
- When using fields_for with a
:has_many
connection :has_many
you can become a victim of recursion by inadvertently adding several nested models to each other, which are associated by associations - If you need to change something in a nested model object, you can access it using child_form.object . In the example above, we used child_form.object.new_record? to determine whether to show the tick "Delete" (for new entries it is not needed)
Step 3. What to register in the controller? .. Nothing
The third step, probably the easiest, because we do everything without violating REST. The beauty of this decision is that our controllers do not clutter up with an extra code that has a place in the model. Just look at these creation and update methods:
class PersonController < ApplicationController<br/> def create<br/> @person = Person. new ( params [ :person ] ) <br/> @person . save ? redirect_to ( person_path ( @person ) ) : render ( :action => :new ) <br/> end <br/> def update <br/> @person = Person. find ( params [ :id ] ) <br/> @person . update_attributes ( params [ :person ] ) ?<br/> redirect_to ( person_path ( @person ) ) : render ( :action => :edit ) <br/> end <br/> end
As you can see, everything that we have registered in the model and in the form just works, without unnecessary processing in the controller.
Additionally
Show all nested fields
Quite often it is required that nested fields be displayed immediately. For example, if the user wants to create a new person and at the same time add children.
Due to the fact that the
person being created is just created, the child_form fields will not be displayed. There are two ways to solve this problem:
- Build a new object in the controller:
def new <br/> @person = Person. new <br/> @person . children . build <br/> # ... <br/> end
- You can add a helper, which does almost the same thing, but is located elsewhere:
module ApplicationHelper<br/> def setup_person ( person ) <br/> returning ( person ) do | p | <br/> p . children . build if p . children . empty ?<br/> end <br/> end <br/> end
After that, you need to change the
form_for person to a slightly different one:
<% form_for setup_person ( @person ) do | person_form | %> <br/> <!-- ... --><br/> <% end %>
Which of these methods to use, everyone decides for himself. Personally, I like the first one better, the second one to the author of the original article.
Specify when we need nested models
If you create a form that has nested fields by default (Person → children), then sooner or later someone will try to send the form with empty parameters (no children). You can make the user get an error - if children are required, and come back. Or just create an object without children.
There is an option for this
:reject_if
:
class Person < ActiveRecord::Base <br/> validates_presence_of :name <br/> has_many :children , :class_name => 'Person' <br/> # , <br/> accepts_nested_attributes_for :children ,<br/> :reject_if => proc { | attrs | attrs. all ? { | k, v | v. blank ? } } <br/> # , <br/> accepts_nested_attributes_for :children , :reject_if => :all_blank <br/> end
# <br/> @person . children_attributes = [ { :name => '' } ] <br/> @person . save <br/> @person . children . count #=> 0
This option will also be useful if you have boolean fields in the model, and a checkbox on the form. If you do not mark it and do not write the child’s name, only the parameter '0' will be transferred to the controller, and
:all_blank
will not help
class Person < ActiveRecord::Base <br/> validates_presence_of :name , :bad <br/> has_many :children , :class_name => 'Person' <br/> # , , <br/> accepts_nested_attributes_for :children ,<br/> :reject_if => proc { | attrs | attrs [ 'bad' ] == '0' && attrs [ 'name' ] . blank ? } <br/> <br/> # :reject_if => proc { |attrs| attrs['name'].blank? } <br/> # , (. .) <br/> end
@person . children_attributes = [ { :name => '' , :bad => '0' } ] <br/> @person . save <br/> @person . children . count #=> 0
Dynamically loaded fields
If you build a complex form, in which there are a large number of models, and even nested in several levels, then you can simply make several nested forms visible by default. Although not entirely rational. A more attractive option is to dynamically add new fields using JavaScript at the user's request.
A great example application can be
viewed on GitHub , sponsored by Eloy. Looking at it, you will understand how the whole system of models and connections between them work.