📜 ⬆️ ⬇️

Creating multi-model forms

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_for

lass 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:

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.

Source: https://habr.com/ru/post/79595/


All Articles