module AttributeFu
# Methods for building forms that contain fields for associated models.
#
# Refer to the Conventions section in the README for the various expected defaults.
#
module AssociatedFormHelper
# Works similarly to fields_for, but used for building forms for associated objects.
#
# Automatically names fields to be compatible with the association_attributes= created by attribute_fu.
#
# An options hash can be specified to override the default behaviors.
#
# Options are:
# :javascript - Generate id placeholders for use with Prototype's Template class (this is how attribute_fu's add_associated_link works).
# :name - Specify the singular name of the association (in singular form), if it differs from the class name of the object.
#
# Any other supplied parameters are passed along to fields_for.
#
# Note: It is preferable to call render_associated_form, which will automatically wrap your form partial in a fields_for_associated call.
#
def fields_for_associated(associated, *args, &block)
conf = args.last.is_a?(Hash) ? args.last : {}
associated_name = extract_option_or_class_name(conf, :name, associated)
name = associated_base_name(associated_name, conf[:object_name])
unless associated.new_record?
name << "[#{associated.new_record? ? 'new' : associated.id}]"
else
@new_objects ||= {}
@new_objects[associated_name] ||= -1 # we want naming to start at 0
identifier = !conf.nil? && conf[:javascript] ? '#{number}' : @new_objects[associated_name]+=1
name << "[new][#{identifier}]"
end
@template.fields_for(name, *args.unshift(associated), &block)
end
# Creates a link for removing an associated element from the form, by removing its containing element from the DOM.
#
# Must be called from within an associated form.
#
# An options hash can be specified to override the default behaviors.
#
# Options are:
# * :selector - The CSS selector with which to find the element to remove.
# * :function - Additional javascript to be executed before the element is removed.
#
# Any remaining options are passed along to link_to_function
#
def remove_link(name, *args)
options = args.extract_options!
css_selector = options.delete(:selector) || ".#{@object.class.name.split("::").last.underscore}"
function = options.delete(:function) || ""
# HACK - added by sean to allow another javascript function to be called after the removal
after = options.delete(:after) || ""
function << "$(this).up('#{css_selector}').remove()"
function << after
@template.link_to_function(name, function, *args.push(options))
end
# Creates a link that adds a new associated form to the page using Javascript.
#
# Must be called from within an associated form.
#
# Must be provided with a new instance of the associated object.
#
# e.g. f.add_associated_link 'Add Task', @project.tasks.build
#
# An options hash can be specified to override the default behaviors.
#
# Options are:
# * :partial - specify the name of the partial in which the form is located.
# * :container - specify the DOM id of the container in which to insert the new element.
# * :expression - specify a javascript expression with which to select the container to insert the new form in to (i.e. $(this).up('.tasks'))
# * :name - specify an alternate class name for the associated model (underscored)
#
# Any additional options are forwarded to link_to_function. See its documentation for available options.
#
def add_associated_link(name, object, opts = {})
associated_name = extract_option_or_class_name(opts, :name, object)
variable = "attribute_fu_#{associated_name}_count"
opts.symbolize_keys!
partial = opts.delete(:partial) || associated_name
container = opts.delete(:expression) || "'#{opts.delete(:container) || associated_name.pluralize}'"
# Hack by sean
object_name = opts.delete(:object_name)
form_builder = self # because the value of self changes in the block
@template.link_to_function(name, opts) do |page|
page << "if (typeof #{variable} == 'undefined') #{variable} = 0;"
page << "new Insertion.Bottom(#{container}, new Template("+form_builder.render_associated_form(object, :fields_for => { :javascript => true, :object_name => object_name }, :partial => partial).to_json+").evaluate({'number': --#{variable}}).gsub(/__number_/, #{variable}))"
end
end
# Renders the form of an associated object, wrapping it in a fields_for_associated call.
#
# The associated argument can be either an object, or a collection of objects to be rendered.
#
# An options hash can be specified to override the default behaviors.
#
# Options are:
# * :new - specify a certain number of new elements to be added to the form. Useful for displaying a
# few blank elements at the bottom.
# * :name - override the name of the association, both for the field names, and the name of the partial
# * :partial - specify the name of the partial in which the form is located.
# * :fields_for - specify additional options for the fields_for_associated call
# * :locals - specify additional variables to be passed along to the partial
# * :render - specify additional options to be passed along to the render :partial call
#
def render_associated_form(associated, opts = {})
associated = associated.is_a?(Array) ? associated : [associated] # preserve association proxy if this is one
opts.symbolize_keys!
(opts[:new] - associated.select(&:new_record?).length).times { associated.build } if opts[:new]
unless associated.empty?
name = extract_option_or_class_name(opts, :name, associated.first)
partial = opts[:partial] || name
local_assign_name = partial.split('/').last.split('.').first
associated.map do |element|
fields_for_associated(element, (opts[:fields_for] || {}).merge(:name => name)) do |f|
@template.render({:partial => "#{partial}", :locals => {local_assign_name.to_sym => element, :f => f}.merge(opts[:locals] || {})}.merge(opts[:render] || {}))
end
end
end
end
private
def associated_base_name(associated_name, object_name)
return "#{object_name}[#{associated_name}_attributes]" if object_name #HACK - Added by sean to allow attribute_fu to use prepopulated objects
"#{@object_name}[#{associated_name}_attributes]"
end
def extract_option_or_class_name(hash, option, object)
(hash.delete(option) || object.class.name.split('::').last.underscore).to_s
end
end
end