Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
Fetching contributors…

Cannot retrieve contributors at this time

140 lines (109 sloc) 6.013 kb

ProxyAttributes

ProxyAttributes is designed to “skinny-up” your controller code by moving the creation and management of child associations to the parent object. It also has the side benefit of making it easier to use your association proxies directly within a form_for form.

Let's look at some examples and then I'll point out any features not salient from the examples, okay?

Examples

In the Model

class Document < ActiveRecord::Base
  belongs_to :project
  has_many :categorizations
  has_many :categories, :through => :categorizations
  has_many :taggings
  has_many :tags, :through => :taggings

  validates_presence_of :title

  proxy_attributes do
    # Will provide category_ids= method, in addition to add_category
    by_ids :categories

    # Will provide tags_as_string= method, in addition to add_tag
    by_string :tags => :title

    # There's also just_defaults which simply adds add_foos
    # shown here just for the sake of example [but commented out, natch!]
    # just_defaults :foos

    # Allows categories and tags to 'steal' the document's project_id
    # and correctly associate itself with the document's project
    # [both category and tag <tt>belong_to :project</tt> as well]
    before_creating(:categories, :tags) do |child|
      child.project_id = self.project_id
    end
  end
end

In the Controller

# With params == {
#   :document => {
#     :title => "Document Title",
#     :tags_as_string => "simple, clean, elegant even",
#     :category_ids => [8, 15],
#     :add_category => {
#       :title => "New Category"
#     }
#   }
# }

@document = Document.new(params[:document])
@document.save

In that short code there, you've just:

  1. created a new document [titled: “Document Title”]

  2. added three tags [titled: “simple”, “clean”, and “elegant even”],

  3. associated them with the new document,

  4. created a new category [titled: “New Category”],

  5. associated it with the new document,

  6. and associated two pre-existing categories [those with ids: 8 and 15] with the document.

Not bad, eh?

In the View

Maybe you're thinking all that simplicity comes at some serious expense in your views. Wrong!

<% form_for(@document) do |f| %>
  <p>
    <%= f.label :title, "Document Title" %>
    <%= f.text_field :title %>
  </p>
  <% unless @categories.empty? %>
    <p>
      <label>Categories</label>
      <ul>
        <% @categories.each do |category| %>
          <li>
            <%= category.title %>
            <%= proxy_attributes_check_box_tag :document, :category_ids, category %>
          </li>
        <% end %>
      </ul>
    </p>
  <% end %>
  <p>
    <% fields_for("document[add_category]", @document.add_category) do |ff| %>
      <%= ff.label :title, "New Category Title" %>
      <%= ff.text_field :title %>
    <% end %>
  </p>
  <p>
    <%= f.label :tags_as_string, "Tags" %>
    <%= f.text_field :tags_as_string %>
  </p>
  <p>
    <%= f.submit "Create" %>
  </p>
<% end %>

A few notes on that view…

proxy_attributes_check_box_tag

Read the docs. It's really just that simple. Really.

fields_for(html_name, actual_proxy_object)

Nothing really spectacular to note here either. Except that @document.add_category returns a new Category just to please fields_for. Most of the time you should not be calling add_child directly but using it with an attribute hash as shown in the example for the controller code.

f.text_field :tags_as_string

Using the models in the example, this handy little method [internal to the model, not the view] is shorthand for @document.tags.map(&:title).join(", "). The default is comma-separated tags but you can change this by setting the :separator option on by_string to :space.

If you want multiple add_child fields, simply add an index value to the fields_for arguments like so:

<p>
  <% fields_for("document[add_category][#{index}]", @document.add_category[index]) do |ff| %>
    <%= ff.label :title, "New Category Title" %>
    <%= ff.text_field :title %>
  <% end %>
</p>

You'll need to use manage_child for your edit form needs. You'll probably be doing this in a loop like this:

<% @document.categories.each do |category| %>
  <p>
    <% fields_for("document[manage_category][#{category.id}]", @document.manage_category[category.id]) do |ff| %>
      <%= ff.label :title, "Category #{category.id} Title" %>
      <%= ff.text_field :title %>
    <% end %>
  </p>
<% end %>

But, but…

In order to avoided the dreaded ActiveRecord::HasManyThroughCantAssociateNewRecords exception, ProxyAttributes moves association creations to after_saves. This saves in a lot of frustration for most use cases I can think of but obviously causes a problem with models [in the child associations] that have many validations which can fail. The default settings for ProxyAttributes is to simply swallow child validation errors and either not create the new child or not save the invalid changes. This behavior can be overridden with the dont_swallow_errors! directive inside the proxy_attributes block which will raise LuckySneaks::ProxyAttributes::InvalidChildAssignment. You are responsible for rescuing this exception in your controller. There's no way to cause the parent model [which has already passed validation and been saved] to be invalid. Instead, errors are added to :proxy_attribute_child_errors if you want to parse that for your error messages.

Todo

  • Add one-to-one and one-to-many support? many-to-one support already exists

  • Add tests for STI and/or support for STI as needed

Copyright © 2008 Lucky Sneaks, released under the MIT license

Jump to Line
Something went wrong with that request. Please try again.