Common Addresses Using Polymorphism and Nested Attributes in Rails


Have you ever wanted to take an object that is common to a lot of models – like addresses – and DRY up your code? If you’re really concerned about design, or if the object itself is complex, you certainly want to make changes in one place and have them apply to everywhere in your app.

You can use a combination of polymorphism, nested attributes, and shared views to accomplish this easily. If it seems complicated at first, try it a couple times and you’ll see it’s no big deal. Let’s get started.

Polymorphism

This is the concept that something like an address can belong to more than one type of thing. Maybe your app has customers, employees, and locations, which all need addresses and you’d like them to appear uniform from one form to the next. Let’s create a migration and model for a simple address.

In your migration, we need the address fields you plan to use, plus the two fields that make polymorphism possible in rails: `object_id` and `object_type`. This is how rails will know what object type and id each address belongs to:

    create_table :addresses do |t|
      t.string :line1
      t.string :line2
      t.string :city
      t.string :state
      t.string :zip
      t.integer :addressable_id
      t.string :addressable_type

      t.timestamps
    end
 
    add_index :addresses, [:addressable_type, :addressable_id], :unique => true

In your model, we need to tell `address` that is belongs to other things polymorphically:

class Address < ActiveRecord::Base
  belongs_to :addressable, :polymorphic => true
end

Nested Attributes

Now let’s make the customer model “contain” an address. In addition to the `has_one` association, we’re going to tell rails that customer forms might also contain fields for the customer’s address as well:

class Customer < ActiveRecord::Base
  has_one :address, :as => :addressable
  accepts_nested_attributes_for :address
end

Shared Views

Next, we’ll want to make a partial to store the address form, that can be called from our other views. Put this in `app/views/shared/_address.html.erb`:

<p>
  <%= f.label :line1, 'Address 1' %><br />
  <%= f.text_field :line1 %>
</p>

<p>
  <%= f.label :line2, 'Address 2' %><br />
  <%= f.text_field :line2 %>
</p>

<p>
  <%= f.label :city %><br />
  <%= f.text_field :city %>
</p>

<p>
  <%= f.label :state %><br />
  <%= f.text_field :state, :size => 2 %>
</p>

<p>
  <%= f.label :zip, 'Zip Code' %><br />
  <%= f.text_field :zip %>
</p>

Where does the `f` come from in the view above? It’s passed in by any view that wants to use it. Let’s use the new customer form as an example:

<% form_for(@customer) do |f| %>
  <%= f.error_messages %>

  <p>
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </p>
  
  <% f.fields_for :address do |address| %>
    <%= render :partial => 'shared/address', :locals => {:f => address} %>
  <% end %>
  
  <p>
    <%= f.submit 'Create' %>
  </p>
<% end %>

The `fields_for` call above is the secret sauce, calling our shared partial and passing the form object `address` into the partial as the variable `f`. When you view the new customer page, you’ll see the address fields included as if they were part of the customer form directly. The real power is that you can repeat these steps for all other models that have an address, and they’ll use the same database table, model, and shared view for addresses.

…And Other Junk

There’s one last step. In the `new` action of your `customers` controller, you need to build the address object onto the customer object, so rails knows it’s there:

  def new
    @customer = Customer.new
    @customer.build_address
  end

That’s all it takes to get polymorphism, nested attributes, and shared views working together to DRY up your code, data modeling, and views. And DRYer code equals happier coders! For more info on polymorphism and nested attributes, visit your friendly neighborhood documentation:

http://wiki.rubyonrails.org/howtos/db-relationships/polymorphic
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html

About these ads

Tags: , , , ,

8 Responses to “Common Addresses Using Polymorphism and Nested Attributes in Rails”

  1. anon Says:

    I did exactly as you said, but I can’t seem to get the values for the Address (it’s all null in the addresses table). I tried this in the Customer controller craete method among many other things:

    @customer = Customer.new(params[:customer])
    @customer.build_address
    @customer.address.attributes = params[:customer][:address_attributes]

    But nothing seems to work!

  2. anon Says:

    Actually it was a problem that the fields were not marked attr_acessible :)

  3. rdg Says:

    Very clean post, thank you. I think you missed one “=”

    • Jaime Bellmyer Says:

      Thanks! And if you can point out the typo, I’d be happy to fix it.

      • rdg Says:

        The code in my post was cutted. I try again:
        %= form_for(@customer) do |f| %

      • rdg Says:

        Sorry, the previous post is wrong. Here the right place:
        %= f.fields_for :address do |address| %

      • Jaime Bellmyer Says:

        Ah, I see what you’re getting at. This is actually a difference between rails 2.3 and rails 3. Rails 2.3 (which was used when I created this post) doesn’t use the equal signs in these types of blocks. Rails 3 does, though, and it’s a good reason for me to start labeling exactly what version of rails I use for everything. Thanks!

  4. Полиморфизм в Ruby on Rails | Ruby on Rails c нуля! Says:

    […] Оригинальная статьи на английском: Common Addresses Using Polymorphism and Nested Attributes in Rails […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


Follow

Get every new post delivered to your Inbox.

%d bloggers like this: