rsl / proxy_attributes
- Source
- Commits
- Network (0)
- Issues (0)
- Downloads (0)
- Wiki (1)
- Graphs
-
Branch:
master
| name | age | message | |
|---|---|---|---|
| |
.gitignore | Sat Apr 19 15:30:53 -0700 2008 | |
| |
CHANGELOG | Sat Apr 19 15:41:23 -0700 2008 | |
| |
MIT-LICENSE | Fri Mar 28 05:20:27 -0700 2008 | |
| |
README.rdoc | Mon Jun 30 10:04:26 -0700 2008 | |
| |
Rakefile | Sat Apr 19 15:31:10 -0700 2008 | |
| |
init.rb | Sun Sep 07 08:00:35 -0700 2008 | |
| |
lib/ | Thu Jan 29 09:18:08 -0800 2009 | |
| |
test/ | Tue Sep 30 12:49:20 -0700 2008 |
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:
- created a new document [titled: "Document Title"]
- added three tags [titled: "simple", "clean", and "elegant even"],
- associated them with the new document,
- created a new category [titled: "New Category"],
- associated it with the new document,
- 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
