Good Ideas

Rails 3: ajaxed nested form

Posted by:

|

On:

|

In these days I’m developing a rails 3 application for a customer. A tast was to complete a ajaxed nested form with fields added through ajax. I’ll show you how I’ve implemented that.

In the following rows you’ll read a part of the developed application. In particular I’ve extracted for you the lines that implement a form to save a “candidate” which has many “nationalities” (a relation table) joined to the “countries” table. The nationalities are added via ajax and binded to the candidate.

Models

The models are the following:

[code lang=”ruby”]
class Candidate < ActiveRecord::Base
attr_accessible :name, :surname, :gender
attr_accessible :nationalities_attribute

has_many :nationalities, :dependent => :delete_all
has_many :countries, :through => :nationalities, :foreign_key => :country_id
accepts_nested_attributes_for :nationalities, :allow_destroy => true
end

class Nationality < ActiveRecord::Base
attr_accessible :candidate_id, :country_id, :created_at, :updated_at

belongs_to :country
belongs_to :candidate

validates :country_id,
:presence => true,
:uniqueness => {:case_sensitive => false, :scope => :candidate_id}
end

class Country < ActiveRecord::Base
attr_accessible :name, :code, :nationality

has_many :candidates
end

[/code]

Controller

The controller has the normal actions and you haven’t to do nothing else than generate a standard CRUD restful controller. The only method to be added is the following that manage the ajax call:

[code lang=”ruby”]

def nationality
id = params[:id] unless params[:id].blank?
candidate_id = params[:candidate_id] unless params[:candidate_id].blank?
country_id = params[:country_id] unless params[:country_id].blank?

@item = Candidate.where(:id => params[:candidate_id]).first unless params[:candidate_id].blank?
@item = Candidate.new if @item.nil?

@nationality = Nationality.where(:id => params[:id]).first unless params[:id].blank?
if @nationality.nil?
@nationality = @item.nationalities.build
end

respond_to do |format|
format.js do
if !params[:clone].nil?
render :layout => false, :partial => "candidates/nationalities/row.html", :locals => {:f=> nil, :id => Time.now.to_f.to_s.gsub(‘.’, ”)}
end
end
end

end

[/code]

The code is quite simple: create the two instance variable @item and @nationality and pass them to the _row.html.erb partial.

Routes

The routes have to be changed in the following way

[code lang=”ruby”]

resources :candidates do
get ‘nationality’, :on => :collection
end

[/code]

Views

The views are the critical part of the implementation. Here are a screenshot of the folder structure:

I’ll show the views for the “new” action. The new.html.erb is simple:

[code lang=”ruby”]

<% title @title_new_label, false %>

<%= render :partial=>’candidates/form’, :locals => {:title_box => @title_new_label} %>

<p><%= link_to "Vai alla lista", :action =>"index" %></p>

[/code]

Nothing special. The _form.html.erb partial is coded as following:

[code lang=”ruby”]
<%= form_for @item do |f| %>

<%= f.fields_for :nationalities, @item.nationalities.order(‘created_at ASC’) do |nationalities_fields| %>
<%= render :partial => ‘candidates/nationalities/row’, :locals => {:f => nationalities_fields} %>
<% end %>
</div>
<%= add_new_row("nationality", @item.id, ‘#nationalities’) %>
<% end %>
[/code]

The _row.html.erb partial is so coded and is the more complicated part:

[code lang=”ruby”]

<%
def step_fields(f, id, country_id)
html = flat_area(3, true)
html += link_to theme_icon("Trashcan"), nationality_candidates_path, :remote =>true, :class=> ‘alert_red round_all button_display link_button’, :id=>"del_#{id}"
html += _flat_area

html += flat_area(13)
html += f.select :country_id, options_for_select(Country.order(:nationality).all.collect { |p| [p.nationality, p.id] }, country_id), {:include_blank => true}, {:title=>"Indica la nazionalità", :id => "nationality_#{id}", :name => "candidate[nationalities_attributes][#{id}][country_id]"}
html += _flat_area

html += f.hidden_field :id, {:name => "candidate[nationalities_attributes][#{id}][id]"}
end
%>

<% id = f.object.id unless f.nil? %>

<fieldset id="nationalities_row_<%= id %>" class="grid_16 alpha omega">

<% unless f.nil? %>
<% candidate_id = f.object.candidate_id %>
<% country_id = f.object.country.id %>
<%= step_fields(f, id, country_id) %>
<% else %>
<%= fields_for :candidate, Candidate.where(:id => candidate_id).first do |form_candidate| %>
<% form_candidate.fields_for :nationalities, @nationality do |f| %>
<%= step_fields(f, id, nil) %>
<% end %>
<% end %>
<% end %>

<script type="text/javascript">

$(‘#nationalities #del_<%=id%>’).click(function() {
if (confirm(‘Sicuro di voler cancellare la riga?’)) {
<% unless f.nil? %>
$(‘#nationalities’).append(‘<input type="hidden" name="candidate[nationalities_attributes][<%= id %>][_destroy]" value="1" />’)
$(‘#nationalities’).append(‘<input type="hidden" name="candidate[nationalities_attributes][<%= id %>][id]" value="<%= id %>" />’)
<% end %>
$("#nationalities_row_<%= id %>").remove()
}
return false;
})

</script>
</fieldset>

[/code]

The above partial is composed by 3 parts: the first one contains a method “step_fields“, the second one contains a “if statement“, and the last one a is a javascript function for deleting the row.
Let’s look at the middle part. In normal conditions (p.e. when showing the already saved candidate, edit action), this form will show the already saved nationalities. So, the partial could use the “f” variable (is a FormBuilder instance) that is passed from the callee (_form.html.erb partial).
When the _row.html.erb is called by ajax, “f” is nil and a FormBuilder object should be instantiated. Once the form builder is created, the form fields could be generated by the “step_fields” method.
The javascript part implement the trashcan button. It hide the row and set an hidden variable named “[_destroy]” telling the controller to automatically remove it from database.

Helper

There’s a helper that create a new “nationality row” in the view. Here is the code:

[code lang=”ruby”]
def add_new_row(action, candidate_id, container)

html = link_to "[aggiungi nuovo]", ‘#’, :class=> ‘add_row’, :id => ‘add_’ + action
html += (<<eos).html_safe
<script type="text/javascript">
$(‘#add_#{action}’).click(function() {
$.get(‘#{url_for(:action => action)}’,
"clone=true&candidate_id=#{candidate_id}",
function(data) {
$(‘#{ container }’).append( data )
$(‘#{ container } fieldset’).last().find(‘select’).uniform();
},
‘html’
)
return false;
})
</script>
eos
html
end
[/code]

I hope this code could help many of you. Write me any comment that could extend this implementation.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *