-
Notifications
You must be signed in to change notification settings - Fork 20
/
associated_form_helper.rb
139 lines (121 loc) · 6.6 KB
/
associated_form_helper.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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:
# <tt>:javascript</tt> - Generate id placeholders for use with Prototype's Template class (this is how attribute_fu's add_associated_link works).
# <tt>:name</tt> - 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
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:
# * <tt>:selector</tt> - The CSS selector with which to find the element to remove.
# * <tt>:function</tt> - 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) || ""
function << "$(this).parents('#{css_selector}').remove()"
@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:
# * <tt>:partial</tt> - specify the name of the partial in which the form is located.
# * <tt>:container</tt> - specify the DOM id of the container in which to insert the new element.
# * <tt>:expression</tt> - specify a javascript expression with which to select the container to insert the new form in to (i.e. $(this).up('.tasks'))
# * <tt>:name</tt> - 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}'"
form_builder = self # because the value of self changes in the block
function = "if (typeof #{variable} == 'undefined') #{variable} = 0;
$(#{container}).append($.template("+[self.render_associated_form(object, :fields_for => { :javascript => true }, :partial => partial)].flatten.first.to_json+"), { number: --#{variable}});"
@template.link_to_function(name, function, opts)
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:
# * <tt>:new</tt> - specify a certain number of new elements to be added to the form. Useful for displaying a
# few blank elements at the bottom.
# * <tt>:name</tt> - override the name of the association, both for the field names, and the name of the partial
# * <tt>:partial</tt> - specify the name of the partial in which the form is located.
# * <tt>:fields_for</tt> - specify additional options for the fields_for_associated call
# * <tt>:locals</tt> - specify additional variables to be passed along to the partial
# * <tt>:render</tt> - 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}[#{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