Monday, April 7, 2008

AssociationFu shaping and testing

I've got the AssociationFu to the stage of being complete and ready to test. I'm hoping that this give me the impetus to begin testing my code. :P

The plugin is pretty cool, IMHO, in that it incorporates so many excellent people's code in one place. Here are the props:

*Luke Galea* for his SubList plugin which was the inspiration for this plugin
*Ryan Bates* for his excellent "Complex Forms" series on Railscasts
*chrisff* for his code that moves associate error messages to the parent
*Mike Mangino* for his error handling forms recipe in "Advanced Rails Recipes"
*Farooq Ali* for his monkey patches to change a proc execution context
*Rick Olson* for inspiration, the name being a nod to his excellent plugins

I've asked Ryan Bates if he would take a look at the plugin since his code influenced the core design and implementation.

Here is the ReadMe for the plugin, for posterity. I'm sure it will change over time:


= AssociationFu
===============

This plugin makes forms containing associations less painful, not only in
the view code but also the model and controller classes.

EXAMPLE 1
=========

The simplest example:

MODELS

class Task < ActiveRecord::Base
belongs_to :project
end

class Project < ActiveRecord::Base
has_many :tasks
associate :tasks
end

The symbol passed to the associate method can be the singular or plural of
the association. It can be camalized or underscored. It can be a string
instead of a symbol. You can pass multiple associations in an array.

Why doesn't the plugin just reflect upon the parent model's associations?
Each association can take an optional :before_each and/or :after_each
proc to do something unique to each specified associated object. See the
following descriptions and the more complex code example below.

OPTIONS

The #associate method takes options called :before_each and :after_each that
reference a proc. When a form submission adds a new association or updates
an existing association, the :before_each proc is called with the model
attributes before the object is created or updated and the :after_each
proc is called with the model attributes and the associated object that
was created or updated.

The exception is when a associated object is removed. In that case, the
:before_each proc is NOT called and the :after_each proc is called with a
third parameter that indicates whether or not the object was deleted from
the association.

You don't have to build a proc that takes all the parameters. AssociationFu
will determine how many parameters your proc takes and send it just those
parameters. That way, you can define a lambda instead of a proc. But the
parameters passed to an :after_each proc will be in this order:
|assoc_attributes, assoc_object, assoc_delete_flag|

CONTROLLERS

The controller code is not modified, you just have to add the following
line to the ApplicationController:

helper :AssociationFu

VIEWS

If you want to use Mike Mangino's error handling forms, copy or move the
association_fu/forms directory to your app/views directory and then delete
either the erb templates or the haml templates, depending upon which one
you don't want to use.

The view now has some helpers:

fields_for_association :project, :task, @task
Takes a chain of model symbols starting with the parent model through all the
associations down to the child, ending with the child object the fields are for.

error_handling_form_for
Drop-in replacement for the ActiveView::Helpers::FormHelper#form_for method.
Using this helper, you get Mike Mangino's error handling forms. See "Advanced
Rails Recipes" book for more detail.

error_handling_fields_for_association
Creates AssociationFu names for the association fields in the form. Pass it
a chain of symbols representing the hierarchy of associations, followed by the
object for which the fields are being created, then the block. For example:
error_handling_fields_for_association :project, :tasks, task do |task_form|
...
end

fields_for_association
Same as #error_handling_fields_for_association except uses ActiveView's
fields_for instead of Mike's error handling form helper.

add__link
These are dynamically-generated helper methods that create a javascript
function link to create new form field partials for the model identified
by . For example, "add_task_link" from the above example.
When the form is submitted, AssociationFu adds the associated model (assuming
no validation errors).

remove__link
Like the add links, these are dynamically-generated methods that create a
javascript function link to remove form field partials for the model
identified by . When the form is submitted, AssociationFu
deletes the associated model (assuming no validation errors). This does not
delete the object from the database, it just deletes it from the collection.

EXAMPLE 2
=========

A more complicated example:

class Toy < ActiveRecord::Base
belongs_to :toy_box
end

class Game < ActiveRecord::Base
belongs_to :toy_box
end

class ToyBox < ActiveRecord::Base
has_many :toys
has_many :games
belongs_to :child
associate [:toys, :games]
end

class Child < ActiveRecord::Base
has_one :toy_box
has_and_belongs_to_many :friends, :class => 'Child'
has_many :games
belongs_to :Parent
associate :toy_box
associate :friends, :after_each => Proc.new {|a,m,d| check_age_of_friend(a, m, d)}
end

class Parent < ActiveRecord::Base
has_many :children
end

(I could go on...;)

The thing I wanted to show above is the use of the :after_each option. In this example,
maybe we want to increment or decrement a counter that keeps track of the number of
older and younger friends are associated with this child. The "d" parameter indicates
whether or not the association is being deleted. The "a" parameter is the params
hash received from the form for this specific association and the "m" parameter is
an object representing this association. The :after_each is post-deletion for objects
being deleted and post-update for existing objects being updated and post-building for
new objects being associated.

In the view, we would have something that looks like this (haml-style):
(TODO: add html-style example. Dang, I'm a haml cult member.)

- error_handling_form_for parent do |form|
= form.text_field :name, :message => 'Full name'
-# any other stuff...
= render :partial => 'children', :object => parent.children
-# submit button and any other stuff

I can hear you say, "Whoah! Shouldn't that be a collection?" You can make it a
collection but in this case, I'd like to show you how to nest associations.

In the partial '_children.html.haml' file:

#children
= render :partial => 'child', :collection => children
= add_child_link 'Add new child'

In the partial '_child.html.haml' file:

- error_handling_fields_for_association :parent, :children, child do |child_form|
= child_form.text_field :name
= remove_child_link
= render :partial => 'toy_box', :object => child.toy_box
= add_toy_box_link

In the partial '_toy_box.html.haml' file:

#toy_box
- error_handling_fields_for_association :parent, :children, :toy_box, toy_box do |toy_box_form|
= toy_box_form.text_field :style
= remove_toy_box_link
= render :partial => 'games', :object => toy_box.games
= render :partial => 'toys', :object => toy_box.toys
= add_toy_box_link

In the partial '_games.html.haml' file:

#games
= render :partial => 'game', :collection => games
= add_game_link

In the partial '_toys.html.haml' file:

#toys
= render :partial => 'toy', :collection => toys
= add_toy_link

In the partial '_game.html.haml' file:

- error_handling_fields_for_association :parent, :children, :toy_box, :games, game do |game_form|
= game_form.text_field :name
= remove_game_link

In the partial '_toy.html.haml' file:

- error_handling_fields_for_association :parent, :children, :toy_box, :toys, toy do |toy_form|
= toy_form.text_field :name
= remove_toy_link





Copyright (c) 2008 Kevin Triplett, et al., released under the MIT license

0 comments: