Sunday, December 5, 2010

Implementing Nested attributes

Nested attributes is a life saver for the developers who wants to save the data in 2-3 different tables on submitting a single form.


For example,
You are creating a profile page where user can enter multiple locations. So for that create 2 models Profile and Locations.

In app/models/profile.rb

class Profile < ActiveRecord::Base
has_many :locations, :dependent => :destroy
accepts_nested_attributes_for :locations, :allow_destroy => true
end

class Location < ActiveRecord::Base
belongs_to :profile
# In this model you can add validations like
validates_format_of :phone_number
end

Now, "accepts_nested_attributes_for" is very important. If you missed it then it will give you Error "nil.errors" when you will try to run the application.

In the profile controller you have to right
class ProfilesController < ApplicationController
def new
@profile.locations.build #(You can add the condition if you dont want to build the location object everytime)
end

def create
@profile = Profile.new(params[:profile])
@profile.save
end
end

In app/views/profiles/new.html.erb

<% form_for :profile, @profile, :url => profile_path(params[:id]), :method => :post do |f| %>
<%= f.text_field :name %>
<% f.fields_for :locations do |location| %>
<%= render "location_fields", :f => location %>
<% end %>
# if you want the user to add the locations dynamically then add this line

<%= link_to_add_fields "Add Location", f, :locations %>

<%= link_to_remove_fields "Remove Location", f %>< /p>

<% end %>


In Application helper add this two methods to add and remove the fields(partials) dynamically

module ApplicationHelper
def link_to_remove_fields(name, f)
f.hidden_field(:_destroy) + link_to_function(name, "remove_fields(this)")
end

def link_to_add_fields(name, f, association)

new_object = f.object.class.reflect_on_association(association).klass.new
fields = f.fields_for(association, new_object, :child_index => "new_#{association}") do |builder|
render(association.to_s.singularize + "_fields", :f => builder)

end


link_to_function(name, h("add_fields(this, \"#{association}\", \"#{escape_javascript(fields)}\")"))
end
end

Create a js file and put these methods in that file
function remove_fields(link) {
$(link).prev("input[type=hidden]").val("1");
$(link).closest(".fields").hide();
}

function add_fields(link, association, content) {
var new_id = new Date().getTime();
var regexp = new RegExp("new_" + association, "g")
$(link).parent().before(content.replace(regexp, new_id));


Now when you Click on "Add location" then actually the helper method render the partial on the view page. When it render the page it creates a unique id it identify the locations fields.
So when you submit the form you will see the parameters as

params : { :profile => {:name => nil, :location_attributes => {"0" => {"address" => "ABC", "_delete" => ""}, "123456789" => {"address" => "XYZ", "_delete" => ""} }}}

This means user has added two locations in the profile.

Now when user click on "Remove Location" then through javascript we set the "_delete" field to "1". So the nested_attributes will remove the relevant record from the database.
The parameters will go as :

params : { :profile => {:name => nil, :location_attributes => {"0" => {"address" => "ABC", "_delete" => ""}, "123456789" => {"address" => "XYZ", "_delete" => "1"} }}}

Also if you inspect the "Add Location" button carefully you will see that the partial is already rendered in the view, but when you click on that button the partial is visible to the user.
If you want to have access to the random number which generated at runtime e.g "123456789" then in the partial you just have to right "new_locations" i.e. "new_#{table_name}"




No comments:

Post a Comment

Followers