hashrocket-formtastic 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.textile +499 -0
- data/Rakefile +58 -0
- data/generators/formtastic_stylesheets/formtastic_stylesheets_generator.rb +21 -0
- data/generators/formtastic_stylesheets/templates/formtastic.css +137 -0
- data/generators/formtastic_stylesheets/templates/formtastic_changes.css +10 -0
- data/lib/formtastic.rb +1325 -0
- data/lib/justin_french/formtastic.rb +10 -0
- data/lib/locale/en.yml +8 -0
- data/rails/init.rb +3 -0
- data/spec/formtastic_spec.rb +3069 -0
- data/spec/test_helper.rb +14 -0
- metadata +66 -0
data/Rakefile
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/rdoctask'
|
3
|
+
require 'spec/rake/spectask'
|
4
|
+
|
5
|
+
begin
|
6
|
+
GEM = "formtastic"
|
7
|
+
AUTHOR = "Justin French"
|
8
|
+
EMAIL = "justin@indent.com.au"
|
9
|
+
SUMMARY = "A Rails form builder plugin/gem with semantically rich and accessible markup"
|
10
|
+
HOMEPAGE = "http://github.com/justinfrench/formtastic/tree/master"
|
11
|
+
|
12
|
+
require 'jeweler'
|
13
|
+
Jeweler::Tasks.new do |s|
|
14
|
+
s.name = GEM
|
15
|
+
s.summary = SUMMARY
|
16
|
+
s.email = EMAIL
|
17
|
+
s.homepage = HOMEPAGE
|
18
|
+
s.description = SUMMARY
|
19
|
+
s.author = AUTHOR
|
20
|
+
|
21
|
+
s.require_path = 'lib'
|
22
|
+
s.autorequire = GEM
|
23
|
+
s.files = %w(MIT-LICENSE README.textile Rakefile) + Dir.glob("{rails,lib,generators,spec}/**/*")
|
24
|
+
end
|
25
|
+
rescue LoadError
|
26
|
+
puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
27
|
+
end
|
28
|
+
|
29
|
+
desc 'Default: run unit specs.'
|
30
|
+
task :default => :spec
|
31
|
+
|
32
|
+
desc 'Test the formtastic plugin.'
|
33
|
+
Spec::Rake::SpecTask.new('spec') do |t|
|
34
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
35
|
+
t.spec_opts = ["-c"]
|
36
|
+
end
|
37
|
+
|
38
|
+
desc 'Test the formtastic plugin with specdoc formatting and colors'
|
39
|
+
Spec::Rake::SpecTask.new('specdoc') do |t|
|
40
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
41
|
+
t.spec_opts = ["--format specdoc", "-c"]
|
42
|
+
end
|
43
|
+
|
44
|
+
desc 'Generate documentation for the formtastic plugin.'
|
45
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
46
|
+
rdoc.rdoc_dir = 'rdoc'
|
47
|
+
rdoc.title = 'Formtastic'
|
48
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
49
|
+
rdoc.rdoc_files.include('README.textile')
|
50
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
51
|
+
end
|
52
|
+
|
53
|
+
desc "Run all examples with RCov"
|
54
|
+
Spec::Rake::SpecTask.new('examples_with_rcov') do |t|
|
55
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
56
|
+
t.rcov = true
|
57
|
+
t.rcov_opts = ['--exclude', 'spec,Library']
|
58
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class FormtasticStylesheetsGenerator < Rails::Generator::Base
|
2
|
+
|
3
|
+
def initialize(*runtime_args)
|
4
|
+
super
|
5
|
+
end
|
6
|
+
|
7
|
+
def manifest
|
8
|
+
record do |m|
|
9
|
+
m.directory File.join('public', 'stylesheets')
|
10
|
+
m.template 'formtastic.css', File.join('public', 'stylesheets', 'formtastic.css')
|
11
|
+
m.template 'formtastic_changes.css', File.join('public', 'stylesheets', 'formtastic_changes.css')
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def banner
|
18
|
+
%{Usage: #{$0} #{spec.name}\nCopies formtastic.css and formtastic_changes.css to public/}
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
/* -------------------------------------------------------------------------------------------------
|
2
|
+
|
3
|
+
It's *strongly* suggested that you don't modify this file. Instead, load a new stylesheet after
|
4
|
+
this one in your layouts (eg formtastic_changes.css) and override the styles to suit your needs.
|
5
|
+
This will allow you to update formtastic.css with new releases without clobbering your own changes.
|
6
|
+
|
7
|
+
This stylesheet forms part of the Formtastic Rails Plugin
|
8
|
+
(c) 2008 Justin French
|
9
|
+
|
10
|
+
--------------------------------------------------------------------------------------------------*/
|
11
|
+
|
12
|
+
|
13
|
+
/* NORMALIZE AND RESET - obviously inspired by Yahoo's reset.css, but scoped to just form.formtastic
|
14
|
+
--------------------------------------------------------------------------------------------------*/
|
15
|
+
form.formtastic, form.formtastic ul, form.formtastic ol, form.formtastic li, form.formtastic fieldset, form.formtastic legend, form.formtastic input, form.formtastic textarea, form.formtastic select, form.formtastic p { margin:0; padding:0; }
|
16
|
+
form.formtastic fieldset { border:0; }
|
17
|
+
form.formtastic em, form.formtastic strong { font-style:normal; font-weight:normal; }
|
18
|
+
form.formtastic ol, form.formtastic ul { list-style:none; }
|
19
|
+
form.formtastic abbr, form.formtastic acronym { border:0; font-variant:normal; }
|
20
|
+
form.formtastic input, form.formtastic textarea, form.formtastic select { font-family:inherit; font-size:inherit; font-weight:inherit; }
|
21
|
+
form.formtastic input, form.formtastic textarea, form.formtastic select { font-size:100%; }
|
22
|
+
form.formtastic legend { color:#000; }
|
23
|
+
|
24
|
+
|
25
|
+
/* FIELDSETS & LISTS
|
26
|
+
--------------------------------------------------------------------------------------------------*/
|
27
|
+
form.formtastic fieldset { }
|
28
|
+
form.formtastic fieldset.inputs { }
|
29
|
+
form.formtastic fieldset.buttons { padding-left:25%; }
|
30
|
+
form.formtastic fieldset ol { }
|
31
|
+
form.formtastic fieldset.buttons li { float:left; padding-right:0.5em; }
|
32
|
+
|
33
|
+
/* clearfixing the fieldsets */
|
34
|
+
form.formtastic fieldset { display: inline-block; }
|
35
|
+
form.formtastic fieldset:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; }
|
36
|
+
html[xmlns] form.formtastic fieldset { display: block; }
|
37
|
+
* html form.formtastic fieldset { height: 1%; }
|
38
|
+
|
39
|
+
|
40
|
+
/* INPUT LIs
|
41
|
+
--------------------------------------------------------------------------------------------------*/
|
42
|
+
form.formtastic fieldset ol li { margin-bottom:1.5em; }
|
43
|
+
|
44
|
+
/* clearfixing the li's */
|
45
|
+
form.formtastic fieldset ol li { display: inline-block; }
|
46
|
+
form.formtastic fieldset ol li:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; }
|
47
|
+
html[xmlns] form.formtastic fieldset ol li { display: block; }
|
48
|
+
* html form.formtastic fieldset ol li { height: 1%; }
|
49
|
+
|
50
|
+
form.formtastic fieldset ol li.required { }
|
51
|
+
form.formtastic fieldset ol li.optional { }
|
52
|
+
form.formtastic fieldset ol li.error { }
|
53
|
+
|
54
|
+
|
55
|
+
/* LABELS
|
56
|
+
--------------------------------------------------------------------------------------------------*/
|
57
|
+
form.formtastic fieldset ol li label { display:block; width:25%; float:left; padding-top:.2em; }
|
58
|
+
form.formtastic fieldset ol li li label { line-height:100%; padding-top:0; }
|
59
|
+
form.formtastic fieldset ol li li label input { line-height:100%; vertical-align:middle; margin-top:-0.1em;}
|
60
|
+
|
61
|
+
|
62
|
+
/* NESTED FIELDSETS AND LEGENDS (radio, check boxes and date/time inputs use nested fieldsets)
|
63
|
+
--------------------------------------------------------------------------------------------------*/
|
64
|
+
form.formtastic fieldset ol li fieldset { position:relative; }
|
65
|
+
form.formtastic fieldset ol li fieldset legend { position:absolute; width:25%; padding-top:0.1em; }
|
66
|
+
form.formtastic fieldset ol li fieldset legend span { position:absolute; }
|
67
|
+
form.formtastic fieldset ol li fieldset ol { float:left; width:74%; margin:0; padding:0 0 0 25%; }
|
68
|
+
form.formtastic fieldset ol li fieldset ol li { padding:0; border:0; }
|
69
|
+
|
70
|
+
|
71
|
+
/* INLINE HINTS
|
72
|
+
--------------------------------------------------------------------------------------------------*/
|
73
|
+
form.formtastic fieldset ol li p.inline-hints { color:#666; margin:0.5em 0 0 25%; }
|
74
|
+
|
75
|
+
|
76
|
+
/* INLINE ERRORS
|
77
|
+
--------------------------------------------------------------------------------------------------*/
|
78
|
+
form.formtastic fieldset ol li p.inline-errors { color:#cc0000; margin:0.5em 0 0 25%; }
|
79
|
+
form.formtastic fieldset ol li ul.errors { color:#cc0000; margin:0.5em 0 0 25%; list-style:square; }
|
80
|
+
form.formtastic fieldset ol li ul.errors li { padding:0; border:none; display:list-item; }
|
81
|
+
|
82
|
+
|
83
|
+
/* STRING & NUMERIC OVERRIDES
|
84
|
+
--------------------------------------------------------------------------------------------------*/
|
85
|
+
form.formtastic fieldset ol li.string input { width:74%; }
|
86
|
+
form.formtastic fieldset ol li.password input { width:74%; }
|
87
|
+
form.formtastic fieldset ol li.numeric input { width:74%; }
|
88
|
+
|
89
|
+
|
90
|
+
/* TEXTAREA OVERRIDES
|
91
|
+
--------------------------------------------------------------------------------------------------*/
|
92
|
+
form.formtastic fieldset ol li.text textarea { width:74%; }
|
93
|
+
|
94
|
+
|
95
|
+
/* HIDDEN OVERRIDES
|
96
|
+
--------------------------------------------------------------------------------------------------*/
|
97
|
+
form.formtastic fieldset ol li.hidden { display:none; }
|
98
|
+
|
99
|
+
|
100
|
+
/* BOOLEAN OVERRIDES
|
101
|
+
--------------------------------------------------------------------------------------------------*/
|
102
|
+
form.formtastic fieldset ol li.boolean label { padding-left:25%; width:auto; }
|
103
|
+
form.formtastic fieldset ol li.boolean label input { margin:0 0.5em 0 0.2em; }
|
104
|
+
|
105
|
+
|
106
|
+
/* RADIO OVERRIDES
|
107
|
+
--------------------------------------------------------------------------------------------------*/
|
108
|
+
form.formtastic fieldset ol li.radio { }
|
109
|
+
form.formtastic fieldset ol li.radio fieldset ol { margin-bottom:-0.6em; }
|
110
|
+
form.formtastic fieldset ol li.radio fieldset ol li { margin:0.1em 0 0.5em 0; }
|
111
|
+
form.formtastic fieldset ol li.radio fieldset ol li label { float:none; width:100%; }
|
112
|
+
form.formtastic fieldset ol li.radio fieldset ol li label input { margin-right:0.2em; }
|
113
|
+
|
114
|
+
|
115
|
+
/* CHECK BOXES (COLLECTION) OVERRIDES
|
116
|
+
--------------------------------------------------------------------------------------------------*/
|
117
|
+
form.formtastic fieldset ol li.check_boxes { }
|
118
|
+
form.formtastic fieldset ol li.check_boxes fieldset ol { margin-bottom:-0.6em; }
|
119
|
+
form.formtastic fieldset ol li.check_boxes fieldset ol li { margin:0.1em 0 0.5em 0; }
|
120
|
+
form.formtastic fieldset ol li.check_boxes fieldset ol li label { float:none; width:100%; }
|
121
|
+
form.formtastic fieldset ol li.check_boxes fieldset ol li label input { margin-right:0.2em; }
|
122
|
+
|
123
|
+
|
124
|
+
|
125
|
+
/* DATE & TIME OVERRIDES
|
126
|
+
--------------------------------------------------------------------------------------------------*/
|
127
|
+
form.formtastic fieldset ol li.date fieldset ol li,
|
128
|
+
form.formtastic fieldset ol li.time fieldset ol li,
|
129
|
+
form.formtastic fieldset ol li.datetime fieldset ol li { float:left; width:auto; margin:0 .3em 0 0; }
|
130
|
+
|
131
|
+
form.formtastic fieldset ol li.date fieldset ol li label,
|
132
|
+
form.formtastic fieldset ol li.time fieldset ol li label,
|
133
|
+
form.formtastic fieldset ol li.datetime fieldset ol li label { display:none; }
|
134
|
+
|
135
|
+
form.formtastic fieldset ol li.date fieldset ol li label input,
|
136
|
+
form.formtastic fieldset ol li.time fieldset ol li label input,
|
137
|
+
form.formtastic fieldset ol li.datetime fieldset ol li label input { display:inline; margin:0; padding:0; }
|
@@ -0,0 +1,10 @@
|
|
1
|
+
/* -------------------------------------------------------------------------------------------------
|
2
|
+
|
3
|
+
Load this stylesheet after formtastic.css in your layouts to override the CSS to suit your needs.
|
4
|
+
This will allow you to update formtastic.css with new releases without clobbering your own changes.
|
5
|
+
|
6
|
+
For example, to make the inline hint paragraphs a little darker in color than the standard #666:
|
7
|
+
|
8
|
+
form.formtastic fieldset ol li p.inline-hints { color:#333; }
|
9
|
+
|
10
|
+
--------------------------------------------------------------------------------------------------*/
|
data/lib/formtastic.rb
ADDED
@@ -0,0 +1,1325 @@
|
|
1
|
+
# Override the default ActiveRecordHelper behaviour of wrapping the input.
|
2
|
+
# This gets taken care of semantically by adding an error class to the LI tag
|
3
|
+
# containing the input.
|
4
|
+
ActionView::Base.field_error_proc = proc do |html_tag, instance_tag|
|
5
|
+
html_tag
|
6
|
+
end
|
7
|
+
|
8
|
+
module Formtastic #:nodoc:
|
9
|
+
|
10
|
+
class SemanticFormBuilder < ActionView::Helpers::FormBuilder
|
11
|
+
|
12
|
+
@@default_text_field_size = 50
|
13
|
+
@@all_fields_required_by_default = true
|
14
|
+
@@required_string = proc { %{<abbr title="#{I18n.t 'formtastic.required', :default => 'required'}">*</abbr>} }
|
15
|
+
@@optional_string = ''
|
16
|
+
@@inline_errors = :sentence
|
17
|
+
@@label_str_method = :humanize
|
18
|
+
@@collection_label_methods = %w[to_label display_name full_name name title username login value to_s]
|
19
|
+
@@inline_order = [ :input, :hints, :errors ]
|
20
|
+
@@file_methods = [ :file?, :public_filename ]
|
21
|
+
@@priority_countries = ["Australia", "Canada", "United Kingdom", "United States"]
|
22
|
+
@@i18n_lookups_by_default = false
|
23
|
+
|
24
|
+
cattr_accessor :default_text_field_size, :all_fields_required_by_default, :required_string,
|
25
|
+
:optional_string, :inline_errors, :label_str_method, :collection_label_methods,
|
26
|
+
:inline_order, :file_methods, :priority_countries, :i18n_lookups_by_default
|
27
|
+
|
28
|
+
I18N_SCOPES = [ '{{model}}.{{action}}.{{attribute}}',
|
29
|
+
'{{model}}.{{attribute}}',
|
30
|
+
'{{attribute}}']
|
31
|
+
|
32
|
+
# Keeps simple mappings in a hash
|
33
|
+
INPUT_MAPPINGS = {
|
34
|
+
:string => :text_field,
|
35
|
+
:password => :password_field,
|
36
|
+
:numeric => :text_field,
|
37
|
+
:text => :text_area,
|
38
|
+
:file => :file_field
|
39
|
+
}
|
40
|
+
STRING_MAPPINGS = [ :string, :password, :numeric ]
|
41
|
+
|
42
|
+
attr_accessor :template
|
43
|
+
|
44
|
+
# Returns a suitable form input for the given +method+, using the database column information
|
45
|
+
# and other factors (like the method name) to figure out what you probably want.
|
46
|
+
#
|
47
|
+
# Options:
|
48
|
+
#
|
49
|
+
# * :as - override the input type (eg force a :string to render as a :password field)
|
50
|
+
# * :label - use something other than the method name as the label text, when false no label is printed
|
51
|
+
# * :required - specify if the column is required (true) or not (false)
|
52
|
+
# * :hint - provide some text to hint or help the user provide the correct information for a field
|
53
|
+
# * :input_html - provide options that will be passed down to the generated input
|
54
|
+
# * :wrapper_html - provide options that will be passed down to the li wrapper
|
55
|
+
#
|
56
|
+
# Input Types:
|
57
|
+
#
|
58
|
+
# Most inputs map directly to one of ActiveRecord's column types by default (eg string_input),
|
59
|
+
# but there are a few special cases and some simplification (:integer, :float and :decimal
|
60
|
+
# columns all map to a single numeric_input, for example).
|
61
|
+
#
|
62
|
+
# * :select (a select menu for associations) - default to association names
|
63
|
+
# * :check_boxes (a set of check_box inputs for associations) - alternative to :select has_many and has_and_belongs_to_many associations
|
64
|
+
# * :radio (a set of radio inputs for associations) - alternative to :select belongs_to associations
|
65
|
+
# * :time_zone (a select menu with time zones)
|
66
|
+
# * :password (a password input) - default for :string column types with 'password' in the method name
|
67
|
+
# * :text (a textarea) - default for :text column types
|
68
|
+
# * :date (a date select) - default for :date column types
|
69
|
+
# * :datetime (a date and time select) - default for :datetime and :timestamp column types
|
70
|
+
# * :time (a time select) - default for :time column types
|
71
|
+
# * :boolean (a checkbox) - default for :boolean column types (you can also have booleans as :select and :radio)
|
72
|
+
# * :string (a text field) - default for :string column types
|
73
|
+
# * :numeric (a text field, like string) - default for :integer, :float and :decimal column types
|
74
|
+
# * :country (a select menu of country names) - requires a country_select plugin to be installed
|
75
|
+
# * :hidden (a hidden field) - creates a hidden field (added for compatibility)
|
76
|
+
#
|
77
|
+
# Example:
|
78
|
+
#
|
79
|
+
# <% semantic_form_for @employee do |form| %>
|
80
|
+
# <% form.inputs do -%>
|
81
|
+
# <%= form.input :name, :label => "Full Name"%>
|
82
|
+
# <%= form.input :manager_id, :as => :radio %>
|
83
|
+
# <%= form.input :hired_at, :as => :date, :label => "Date Hired" %>
|
84
|
+
# <%= form.input :phone, :required => false, :hint => "Eg: +1 555 1234" %>
|
85
|
+
# <% end %>
|
86
|
+
# <% end %>
|
87
|
+
#
|
88
|
+
def input(method, options = {})
|
89
|
+
options[:required] = method_required?(method) unless options.key?(:required)
|
90
|
+
options[:as] ||= default_input_type(method)
|
91
|
+
|
92
|
+
html_class = [ options[:as], (options[:required] ? :required : :optional) ]
|
93
|
+
html_class << 'error' if @object && @object.respond_to?(:errors) && @object.errors[method.to_sym]
|
94
|
+
|
95
|
+
wrapper_html = options.delete(:wrapper_html) || {}
|
96
|
+
wrapper_html[:id] ||= generate_html_id(method)
|
97
|
+
wrapper_html[:class] = (html_class << wrapper_html[:class]).flatten.compact.join(' ')
|
98
|
+
|
99
|
+
if [:boolean_select, :boolean_radio].include?(options[:as])
|
100
|
+
::ActiveSupport::Deprecation.warn(":as => :#{options[:as]} is deprecated, use :as => :#{options[:as].to_s[8..-1]} instead", caller[3..-1])
|
101
|
+
end
|
102
|
+
|
103
|
+
if options[:input_html] && options[:input_html][:id]
|
104
|
+
options[:label_html] ||= {}
|
105
|
+
options[:label_html][:for] ||= options[:input_html][:id]
|
106
|
+
end
|
107
|
+
|
108
|
+
input_parts = @@inline_order.dup
|
109
|
+
input_parts.delete(:errors) if options[:as] == :hidden
|
110
|
+
|
111
|
+
list_item_content = input_parts.map do |type|
|
112
|
+
send(:"inline_#{type}_for", method, options)
|
113
|
+
end.compact.join("\n")
|
114
|
+
|
115
|
+
return template.content_tag(:li, list_item_content, wrapper_html)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Creates an input fieldset and ol tag wrapping for use around a set of inputs. It can be
|
119
|
+
# called either with a block (in which you can do the usual Rails form stuff, HTML, ERB, etc),
|
120
|
+
# or with a list of fields. These two examples are functionally equivalent:
|
121
|
+
#
|
122
|
+
# # With a block:
|
123
|
+
# <% semantic_form_for @post do |form| %>
|
124
|
+
# <% form.inputs do %>
|
125
|
+
# <%= form.input :title %>
|
126
|
+
# <%= form.input :body %>
|
127
|
+
# <% end %>
|
128
|
+
# <% end %>
|
129
|
+
#
|
130
|
+
# # With a list of fields:
|
131
|
+
# <% semantic_form_for @post do |form| %>
|
132
|
+
# <%= form.inputs :title, :body %>
|
133
|
+
# <% end %>
|
134
|
+
#
|
135
|
+
# # Output:
|
136
|
+
# <form ...>
|
137
|
+
# <fieldset class="inputs">
|
138
|
+
# <ol>
|
139
|
+
# <li class="string">...</li>
|
140
|
+
# <li class="text">...</li>
|
141
|
+
# </ol>
|
142
|
+
# </fieldset>
|
143
|
+
# </form>
|
144
|
+
#
|
145
|
+
# === Quick Forms
|
146
|
+
#
|
147
|
+
# When called without a block or a field list, an input is rendered for each column in the
|
148
|
+
# model's database table, just like Rails' scaffolding. You'll obviously want more control
|
149
|
+
# than this in a production application, but it's a great way to get started, then come back
|
150
|
+
# later to customise the form with a field list or a block of inputs. Example:
|
151
|
+
#
|
152
|
+
# <% semantic_form_for @post do |form| %>
|
153
|
+
# <%= form.inputs %>
|
154
|
+
# <% end %>
|
155
|
+
#
|
156
|
+
# === Options
|
157
|
+
#
|
158
|
+
# All options (with the exception of :name) are passed down to the fieldset as HTML
|
159
|
+
# attributes (id, class, style, etc). If provided, the :name option is passed into a
|
160
|
+
# legend tag inside the fieldset (otherwise a legend is not generated).
|
161
|
+
#
|
162
|
+
# # With a block:
|
163
|
+
# <% semantic_form_for @post do |form| %>
|
164
|
+
# <% form.inputs :name => "Create a new post", :style => "border:1px;" do %>
|
165
|
+
# ...
|
166
|
+
# <% end %>
|
167
|
+
# <% end %>
|
168
|
+
#
|
169
|
+
# # With a list (the options must come after the field list):
|
170
|
+
# <% semantic_form_for @post do |form| %>
|
171
|
+
# <%= form.inputs :title, :body, :name => "Create a new post", :style => "border:1px;" %>
|
172
|
+
# <% end %>
|
173
|
+
#
|
174
|
+
# === It's basically a fieldset!
|
175
|
+
#
|
176
|
+
# Instead of hard-coding fieldsets & legends into your form to logically group related fields,
|
177
|
+
# use inputs:
|
178
|
+
#
|
179
|
+
# <% semantic_form_for @post do |f| %>
|
180
|
+
# <% f.inputs do %>
|
181
|
+
# <%= f.input :title %>
|
182
|
+
# <%= f.input :body %>
|
183
|
+
# <% end %>
|
184
|
+
# <% f.inputs :name => "Advanced", :id => "advanced" do %>
|
185
|
+
# <%= f.input :created_at %>
|
186
|
+
# <%= f.input :user_id, :label => "Author" %>
|
187
|
+
# <% end %>
|
188
|
+
# <% end %>
|
189
|
+
#
|
190
|
+
# # Output:
|
191
|
+
# <form ...>
|
192
|
+
# <fieldset class="inputs">
|
193
|
+
# <ol>
|
194
|
+
# <li class="string">...</li>
|
195
|
+
# <li class="text">...</li>
|
196
|
+
# </ol>
|
197
|
+
# </fieldset>
|
198
|
+
# <fieldset class="inputs" id="advanced">
|
199
|
+
# <legend><span>Advanced</span></legend>
|
200
|
+
# <ol>
|
201
|
+
# <li class="datetime">...</li>
|
202
|
+
# <li class="select">...</li>
|
203
|
+
# </ol>
|
204
|
+
# </fieldset>
|
205
|
+
# </form>
|
206
|
+
#
|
207
|
+
# === Nested attributes
|
208
|
+
#
|
209
|
+
# As in Rails, you can use semantic_fields_for to nest attributes:
|
210
|
+
#
|
211
|
+
# <% semantic_form_for @post do |form| %>
|
212
|
+
# <%= form.inputs :title, :body %>
|
213
|
+
#
|
214
|
+
# <% form.semantic_fields_for :author, @bob do |author_form| %>
|
215
|
+
# <% author_form.inputs do %>
|
216
|
+
# <%= author_form.input :first_name, :required => false %>
|
217
|
+
# <%= author_form.input :last_name %>
|
218
|
+
# <% end %>
|
219
|
+
# <% end %>
|
220
|
+
# <% end %>
|
221
|
+
#
|
222
|
+
# But this does not look formtastic! This is equivalent:
|
223
|
+
#
|
224
|
+
# <% semantic_form_for @post do |form| %>
|
225
|
+
# <%= form.inputs :title, :body %>
|
226
|
+
# <% form.inputs :for => [ :author, @bob ] do |author_form| %>
|
227
|
+
# <%= author_form.input :first_name, :required => false %>
|
228
|
+
# <%= author_form.input :last_name %>
|
229
|
+
# <% end %>
|
230
|
+
# <% end %>
|
231
|
+
#
|
232
|
+
# And if you don't need to give options to your input call, you could do it
|
233
|
+
# in just one line:
|
234
|
+
#
|
235
|
+
# <% semantic_form_for @post do |form| %>
|
236
|
+
# <%= form.inputs :title, :body %>
|
237
|
+
# <%= form.inputs :first_name, :last_name, :for => @bob %>
|
238
|
+
# <% end %>
|
239
|
+
#
|
240
|
+
# Just remember that calling inputs generates a new fieldset to wrap your
|
241
|
+
# inputs. If you have two separate models, but, semantically, on the page
|
242
|
+
# they are part of the same fieldset, you should use semantic_fields_for
|
243
|
+
# instead (just as you would do with Rails' form builder).
|
244
|
+
#
|
245
|
+
def inputs(*args, &block)
|
246
|
+
html_options = args.extract_options!
|
247
|
+
html_options[:class] ||= "inputs"
|
248
|
+
|
249
|
+
if html_options[:for]
|
250
|
+
inputs_for_nested_attributes(args, html_options, &block)
|
251
|
+
elsif block_given?
|
252
|
+
field_set_and_list_wrapping(html_options, &block)
|
253
|
+
else
|
254
|
+
if @object && args.empty?
|
255
|
+
args = @object.class.reflections.map { |n,_| n if _.macro == :belongs_to }
|
256
|
+
args += @object.class.content_columns.map(&:name)
|
257
|
+
args -= %w[created_at updated_at created_on updated_on lock_version]
|
258
|
+
args.compact!
|
259
|
+
end
|
260
|
+
contents = args.map { |method| input(method.to_sym) }
|
261
|
+
|
262
|
+
field_set_and_list_wrapping(html_options, contents)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
alias :input_field_set :inputs
|
266
|
+
|
267
|
+
# Creates a fieldset and ol tag wrapping for form buttons / actions as list items.
|
268
|
+
# See inputs documentation for a full example. The fieldset's default class attriute
|
269
|
+
# is set to "buttons".
|
270
|
+
#
|
271
|
+
# See inputs for html attributes and special options.
|
272
|
+
def buttons(*args, &block)
|
273
|
+
html_options = args.extract_options!
|
274
|
+
html_options[:class] ||= "buttons"
|
275
|
+
|
276
|
+
if block_given?
|
277
|
+
field_set_and_list_wrapping(html_options, &block)
|
278
|
+
else
|
279
|
+
args = [:commit] if args.empty?
|
280
|
+
contents = args.map { |button_name| send(:"#{button_name}_button") }
|
281
|
+
field_set_and_list_wrapping(html_options, contents)
|
282
|
+
end
|
283
|
+
end
|
284
|
+
alias :button_field_set :buttons
|
285
|
+
|
286
|
+
# Creates a submit input tag with the value "Save [model name]" (for existing records) or
|
287
|
+
# "Create [model name]" (for new records) by default:
|
288
|
+
#
|
289
|
+
# <%= form.commit_button %> => <input name="commit" type="submit" value="Save Post" />
|
290
|
+
#
|
291
|
+
# The value of the button text can be overridden:
|
292
|
+
#
|
293
|
+
# <%= form.commit_button "Go" %> => <input name="commit" type="submit" value="Go" />
|
294
|
+
#
|
295
|
+
# And you can pass html atributes down to the input, with or without the button text:
|
296
|
+
#
|
297
|
+
# <%= form.commit_button "Go" %> => <input name="commit" type="submit" value="Go" />
|
298
|
+
# <%= form.commit_button :class => "pretty" %> => <input name="commit" type="submit" value="Save Post" class="pretty" />
|
299
|
+
|
300
|
+
def commit_button(*args)
|
301
|
+
value = args.first.is_a?(String) ? args.shift : save_or_create_button_text
|
302
|
+
options = args.shift || {}
|
303
|
+
button_html = options.delete(:button_html) || {}
|
304
|
+
template.content_tag(:li, self.submit(value, button_html), :class => "commit")
|
305
|
+
end
|
306
|
+
|
307
|
+
# A thin wrapper around #fields_for to set :builder => Formtastic::SemanticFormBuilder
|
308
|
+
# for nesting forms:
|
309
|
+
#
|
310
|
+
# # Example:
|
311
|
+
# <% semantic_form_for @post do |post| %>
|
312
|
+
# <% post.semantic_fields_for :author do |author| %>
|
313
|
+
# <% author.inputs :name %>
|
314
|
+
# <% end %>
|
315
|
+
# <% end %>
|
316
|
+
#
|
317
|
+
# # Output:
|
318
|
+
# <form ...>
|
319
|
+
# <fieldset class="inputs">
|
320
|
+
# <ol>
|
321
|
+
# <li class="string"><input type='text' name='post[author][name]' id='post_author_name' /></li>
|
322
|
+
# </ol>
|
323
|
+
# </fieldset>
|
324
|
+
# </form>
|
325
|
+
#
|
326
|
+
def semantic_fields_for(record_or_name_or_array, *args, &block)
|
327
|
+
opts = args.extract_options!
|
328
|
+
opts.merge!(:builder => Formtastic::SemanticFormBuilder)
|
329
|
+
args.push(opts)
|
330
|
+
fields_for(record_or_name_or_array, *args, &block)
|
331
|
+
end
|
332
|
+
|
333
|
+
# Generates the label for the input. It also accepts the same arguments as
|
334
|
+
# Rails label method. It has three options that are not supported by Rails
|
335
|
+
# label method:
|
336
|
+
#
|
337
|
+
# * :required - Appends an abbr tag if :required is true
|
338
|
+
# * :label - An alternative form to give the label content. Whenever label
|
339
|
+
# is false, a blank string is returned.
|
340
|
+
# * :as_span - When true returns a span tag with class label instead of a label element
|
341
|
+
# * :input_name - Gives the input to match for. This is needed when you want to
|
342
|
+
# to call f.label :authors but it should match :author_ids.
|
343
|
+
#
|
344
|
+
# == Examples
|
345
|
+
#
|
346
|
+
# f.label :title # like in rails, except that it searches the label on I18n API too
|
347
|
+
#
|
348
|
+
# f.label :title, "Your post title"
|
349
|
+
# f.label :title, :label => "Your post title" # Added for formtastic API
|
350
|
+
#
|
351
|
+
# f.label :title, :required => true # Returns <label>Title<abbr title="required">*</abbr></label>
|
352
|
+
#
|
353
|
+
def label(method, options_or_text=nil, options=nil)
|
354
|
+
if options_or_text.is_a?(Hash)
|
355
|
+
return "" if options_or_text[:label] == false
|
356
|
+
options = options_or_text
|
357
|
+
text = options.delete(:label)
|
358
|
+
else
|
359
|
+
text = options_or_text
|
360
|
+
options ||= {}
|
361
|
+
end
|
362
|
+
|
363
|
+
text = localized_attribute_string(method, text, :label) || humanized_attribute_name(method)
|
364
|
+
text += required_or_optional_string(options.delete(:required))
|
365
|
+
|
366
|
+
input_name = options.delete(:input_name) || method
|
367
|
+
if options.delete(:as_span)
|
368
|
+
options[:class] ||= 'label'
|
369
|
+
template.content_tag(:span, text, options)
|
370
|
+
else
|
371
|
+
super(input_name, text, options)
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
# Generates error messages for the given method. Errors can be shown as list
|
376
|
+
# or as sentence. If :none is set, no error is shown.
|
377
|
+
#
|
378
|
+
# This method is also aliased as errors_on, so you can call on your custom
|
379
|
+
# inputs as well:
|
380
|
+
#
|
381
|
+
# semantic_form_for :post do |f|
|
382
|
+
# f.text_field(:body)
|
383
|
+
# f.errors_on(:body)
|
384
|
+
# end
|
385
|
+
#
|
386
|
+
def inline_errors_for(method, options=nil) #:nodoc:
|
387
|
+
return nil unless @object && @object.respond_to?(:errors) && [:sentence, :list].include?(@@inline_errors)
|
388
|
+
|
389
|
+
errors = @object.errors[method.to_sym]
|
390
|
+
send("error_#{@@inline_errors}", Array(errors)) unless errors.blank?
|
391
|
+
end
|
392
|
+
alias :errors_on :inline_errors_for
|
393
|
+
|
394
|
+
protected
|
395
|
+
|
396
|
+
# Prepare options to be sent to label
|
397
|
+
#
|
398
|
+
def options_for_label(options)
|
399
|
+
options.slice(:label, :required).merge!(options.fetch(:label_html, {}))
|
400
|
+
end
|
401
|
+
|
402
|
+
# Deals with :for option when it's supplied to inputs methods. Additional
|
403
|
+
# options to be passed down to :for should be supplied using :for_options
|
404
|
+
# key.
|
405
|
+
#
|
406
|
+
# It should raise an error if a block with arity zero is given.
|
407
|
+
#
|
408
|
+
def inputs_for_nested_attributes(args, options, &block)
|
409
|
+
args << options.merge!(:parent => { :builder => self, :for => options[:for] })
|
410
|
+
|
411
|
+
fields_for_block = if block_given?
|
412
|
+
raise ArgumentError, 'You gave :for option with a block to inputs method, ' <<
|
413
|
+
'but the block does not accept any argument.' if block.arity <= 0
|
414
|
+
|
415
|
+
proc { |f| f.inputs(*args){ block.call(f) } }
|
416
|
+
else
|
417
|
+
proc { |f| f.inputs(*args) }
|
418
|
+
end
|
419
|
+
|
420
|
+
fields_for_args = [options.delete(:for), options.delete(:for_options) || {}].flatten
|
421
|
+
semantic_fields_for(*fields_for_args, &fields_for_block)
|
422
|
+
end
|
423
|
+
|
424
|
+
# Remove any Formtastic-specific options before passing the down options.
|
425
|
+
#
|
426
|
+
def set_options(options)
|
427
|
+
options.except(:value_method, :label_method, :collection, :required, :label,
|
428
|
+
:as, :hint, :input_html, :label_html, :value_as_class, :options)
|
429
|
+
end
|
430
|
+
|
431
|
+
# Create a default button text. If the form is working with a object, it
|
432
|
+
# defaults to "Create model" or "Save model" depending if we are working
|
433
|
+
# with a new_record or not.
|
434
|
+
#
|
435
|
+
# When not working with models, it defaults to "Submit object".
|
436
|
+
#
|
437
|
+
def save_or_create_button_text(prefix='Submit') #:nodoc:
|
438
|
+
if @object
|
439
|
+
prefix = @object.new_record? ? 'Create' : 'Save'
|
440
|
+
object_name = @object.class.human_name
|
441
|
+
else
|
442
|
+
object_name = @object_name.to_s.send(@@label_str_method)
|
443
|
+
end
|
444
|
+
|
445
|
+
I18n.t(prefix.downcase, :default => prefix, :scope => [:formtastic]) << ' ' << object_name
|
446
|
+
end
|
447
|
+
|
448
|
+
# Determins if the attribute (eg :title) should be considered required or not.
|
449
|
+
#
|
450
|
+
# * if the :required option was provided in the options hash, the true/false value will be
|
451
|
+
# returned immediately, allowing the view to override any guesswork that follows:
|
452
|
+
#
|
453
|
+
# * if the :required option isn't provided in the options hash, and the ValidationReflection
|
454
|
+
# plugin is installed (http://github.com/redinger/validation_reflection), true is returned
|
455
|
+
# if the validates_presence_of macro has been used in the class for this attribute, or false
|
456
|
+
# otherwise.
|
457
|
+
#
|
458
|
+
# * if the :required option isn't provided, and the plugin isn't available, the value of the
|
459
|
+
# configuration option @@all_fields_required_by_default is used.
|
460
|
+
#
|
461
|
+
def method_required?(attribute) #:nodoc:
|
462
|
+
if @object && @object.class.respond_to?(:reflect_on_all_validations)
|
463
|
+
attribute_sym = attribute.to_s.sub(/_id$/, '').to_sym
|
464
|
+
|
465
|
+
@object.class.reflect_on_all_validations.any? do |validation|
|
466
|
+
validation.macro == :validates_presence_of && validation.name == attribute_sym
|
467
|
+
end
|
468
|
+
else
|
469
|
+
@@all_fields_required_by_default
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
# A method that deals with most of inputs (:string, :password, :file,
|
474
|
+
# :textarea and :numeric). :select, :radio, :boolean and :datetime inputs
|
475
|
+
# are not handled by this method, since they need more detailed approach.
|
476
|
+
#
|
477
|
+
# If input_html is given as option, it's passed down to the input.
|
478
|
+
#
|
479
|
+
def input_simple(type, method, options)
|
480
|
+
html_options = options.delete(:input_html) || {}
|
481
|
+
html_options = default_string_options(method, type).merge(html_options) if STRING_MAPPINGS.include?(type)
|
482
|
+
|
483
|
+
self.label(method, options_for_label(options)) +
|
484
|
+
self.send(INPUT_MAPPINGS[type], method, html_options)
|
485
|
+
end
|
486
|
+
|
487
|
+
# Outputs a hidden field inside the wrapper, which should be hidden with CSS.
|
488
|
+
# Additionals options can be given and will be sent straight to hidden input
|
489
|
+
# element.
|
490
|
+
#
|
491
|
+
def hidden_input(method, options)
|
492
|
+
self.hidden_field(method, set_options(options))
|
493
|
+
end
|
494
|
+
|
495
|
+
# Outputs a label and a select box containing options from the parent
|
496
|
+
# (belongs_to, has_many, has_and_belongs_to_many) association. If an association
|
497
|
+
# is has_many or has_and_belongs_to_many the select box will be set as multi-select
|
498
|
+
# and size = 5
|
499
|
+
#
|
500
|
+
# Example (belongs_to):
|
501
|
+
#
|
502
|
+
# f.input :author
|
503
|
+
#
|
504
|
+
# <label for="book_author_id">Author</label>
|
505
|
+
# <select id="book_author_id" name="book[author_id]">
|
506
|
+
# <option value=""></option>
|
507
|
+
# <option value="1">Justin French</option>
|
508
|
+
# <option value="2">Jane Doe</option>
|
509
|
+
# </select>
|
510
|
+
#
|
511
|
+
# Example (has_many):
|
512
|
+
#
|
513
|
+
# f.input :chapters
|
514
|
+
#
|
515
|
+
# <label for="book_chapter_ids">Chapters</label>
|
516
|
+
# <select id="book_chapter_ids" name="book[chapter_ids]">
|
517
|
+
# <option value=""></option>
|
518
|
+
# <option value="1">Chapter 1</option>
|
519
|
+
# <option value="2">Chapter 2</option>
|
520
|
+
# </select>
|
521
|
+
#
|
522
|
+
# Example (has_and_belongs_to_many):
|
523
|
+
#
|
524
|
+
# f.input :authors
|
525
|
+
#
|
526
|
+
# <label for="book_author_ids">Authors</label>
|
527
|
+
# <select id="book_author_ids" name="book[author_ids]">
|
528
|
+
# <option value=""></option>
|
529
|
+
# <option value="1">Justin French</option>
|
530
|
+
# <option value="2">Jane Doe</option>
|
531
|
+
# </select>
|
532
|
+
#
|
533
|
+
#
|
534
|
+
# You can customize the options available in the select by passing in a collection (an Array or
|
535
|
+
# Hash) through the :collection option. If not provided, the choices are found by inferring the
|
536
|
+
# parent's class name from the method name and simply calling find(:all) on it
|
537
|
+
# (VehicleOwner.find(:all) in the example above).
|
538
|
+
#
|
539
|
+
# Examples:
|
540
|
+
#
|
541
|
+
# f.input :author, :collection => @authors
|
542
|
+
# f.input :author, :collection => Author.find(:all)
|
543
|
+
# f.input :author, :collection => [@justin, @kate]
|
544
|
+
# f.input :author, :collection => {@justin.name => @justin.id, @kate.name => @kate.id}
|
545
|
+
# f.input :author, :collection => ["Justin", "Kate", "Amelia", "Gus", "Meg"]
|
546
|
+
#
|
547
|
+
# Note: This input looks for a label method in the parent association.
|
548
|
+
#
|
549
|
+
# You can customize the text label inside each option tag, by naming the correct method
|
550
|
+
# (:full_name, :display_name, :account_number, etc) to call on each object in the collection
|
551
|
+
# by passing in the :label_method option. By default the :label_method is whichever element of
|
552
|
+
# Formtastic::SemanticFormBuilder.collection_label_methods is found first.
|
553
|
+
#
|
554
|
+
# Examples:
|
555
|
+
#
|
556
|
+
# f.input :author, :label_method => :full_name
|
557
|
+
# f.input :author, :label_method => :display_name
|
558
|
+
# f.input :author, :label_method => :to_s
|
559
|
+
# f.input :author, :label_method => :label
|
560
|
+
#
|
561
|
+
# You can also customize the value inside each option tag, by passing in the :value_method option.
|
562
|
+
# Usage is the same as the :label_method option
|
563
|
+
#
|
564
|
+
# Examples:
|
565
|
+
#
|
566
|
+
# f.input :author, :value_method => :full_name
|
567
|
+
# f.input :author, :value_method => :display_name
|
568
|
+
# f.input :author, :value_method => :to_s
|
569
|
+
# f.input :author, :value_method => :value
|
570
|
+
#
|
571
|
+
# You can pass html_options to the select tag using :input_html => {}
|
572
|
+
#
|
573
|
+
# Examples:
|
574
|
+
#
|
575
|
+
# f.input :authors, :input_html => {:size => 20, :multiple => true}
|
576
|
+
#
|
577
|
+
# By default, all select inputs will have a blank option at the top of the list. You can add
|
578
|
+
# a prompt with the :prompt option, or disable the blank option with :include_blank => false.
|
579
|
+
#
|
580
|
+
def select_input(method, options)
|
581
|
+
collection = if options[:options]
|
582
|
+
use_options_for_select(options)
|
583
|
+
else
|
584
|
+
find_collection_for_column(method, options)
|
585
|
+
end
|
586
|
+
|
587
|
+
html_options = options.delete(:input_html) || {}
|
588
|
+
|
589
|
+
unless options.key?(:include_blank) || options.key?(:prompt)
|
590
|
+
options[:include_blank] = true
|
591
|
+
end
|
592
|
+
|
593
|
+
reflection = find_reflection(method)
|
594
|
+
if reflection && [ :has_many, :has_and_belongs_to_many ].include?(reflection.macro)
|
595
|
+
options[:include_blank] = false
|
596
|
+
html_options[:multiple] ||= true
|
597
|
+
html_options[:size] ||= 5
|
598
|
+
end
|
599
|
+
|
600
|
+
input_name = generate_association_input_name(method)
|
601
|
+
self.label(method, options_for_label(options).merge(:input_name => input_name)) +
|
602
|
+
self.select(input_name, collection, set_options(options), html_options)
|
603
|
+
end
|
604
|
+
alias :boolean_select_input :select_input
|
605
|
+
|
606
|
+
# Outputs a timezone select input as Rails' time_zone_select helper. You
|
607
|
+
# can give priority zones as option.
|
608
|
+
#
|
609
|
+
# Examples:
|
610
|
+
#
|
611
|
+
# f.input :time_zone, :as => :time_zone, :priority_zones => /Australia/
|
612
|
+
#
|
613
|
+
def time_zone_input(method, options)
|
614
|
+
html_options = options.delete(:input_html) || {}
|
615
|
+
|
616
|
+
self.label(method, options_for_label(options)) +
|
617
|
+
self.time_zone_select(method, options.delete(:priority_zones), set_options(options), html_options)
|
618
|
+
end
|
619
|
+
|
620
|
+
# Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list
|
621
|
+
# items, one for each possible choice in the belongs_to association. Each li contains a
|
622
|
+
# label and a radio input.
|
623
|
+
#
|
624
|
+
# Example:
|
625
|
+
#
|
626
|
+
# f.input :author, :as => :radio
|
627
|
+
#
|
628
|
+
# Output:
|
629
|
+
#
|
630
|
+
# <fieldset>
|
631
|
+
# <legend><span>Author</span></legend>
|
632
|
+
# <ol>
|
633
|
+
# <li>
|
634
|
+
# <label for="book_author_id_1"><input id="book_author_id_1" name="book[author_id]" type="radio" value="1" /> Justin French</label>
|
635
|
+
# </li>
|
636
|
+
# <li>
|
637
|
+
# <label for="book_author_id_2"><input id="book_author_id_2" name="book[owner_id]" type="radio" value="2" /> Kate French</label>
|
638
|
+
# </li>
|
639
|
+
# </ol>
|
640
|
+
# </fieldset>
|
641
|
+
#
|
642
|
+
# You can customize the options available in the select by passing in a collection (an Array or
|
643
|
+
# Hash) through the :collection option. If not provided, the choices are found by inferring the
|
644
|
+
# parent's class name from the method name and simply calling find(:all) on it
|
645
|
+
# (Author.find(:all) in the example above).
|
646
|
+
#
|
647
|
+
# Examples:
|
648
|
+
#
|
649
|
+
# f.input :author, :as => :radio, :collection => @authors
|
650
|
+
# f.input :author, :as => :radio, :collection => Author.find(:all)
|
651
|
+
# f.input :author, :as => :radio, :collection => [@justin, @kate]
|
652
|
+
# f.input :author, :collection => ["Justin", "Kate", "Amelia", "Gus", "Meg"]
|
653
|
+
#
|
654
|
+
# You can also customize the text label inside each option tag, by naming the correct method
|
655
|
+
# (:full_name, :display_name, :account_number, etc) to call on each object in the collection
|
656
|
+
# by passing in the :label_method option. By default the :label_method is whichever element of
|
657
|
+
# Formtastic::SemanticFormBuilder.collection_label_methods is found first.
|
658
|
+
#
|
659
|
+
# Examples:
|
660
|
+
#
|
661
|
+
# f.input :author, :as => :radio, :label_method => :full_name
|
662
|
+
# f.input :author, :as => :radio, :label_method => :display_name
|
663
|
+
# f.input :author, :as => :radio, :label_method => :to_s
|
664
|
+
# f.input :author, :as => :radio, :label_method => :label
|
665
|
+
#
|
666
|
+
# Finally, you can set :value_as_class => true if you want that LI wrappers
|
667
|
+
# contains a class with the wrapped radio input value.
|
668
|
+
#
|
669
|
+
def radio_input(method, options)
|
670
|
+
collection = find_collection_for_column(method, options)
|
671
|
+
html_options = set_options(options).merge(options.delete(:input_html) || {})
|
672
|
+
|
673
|
+
input_name = generate_association_input_name(method)
|
674
|
+
value_as_class = options.delete(:value_as_class)
|
675
|
+
|
676
|
+
list_item_content = collection.map do |c|
|
677
|
+
label = c.is_a?(Array) ? c.first : c
|
678
|
+
value = c.is_a?(Array) ? c.last : c
|
679
|
+
|
680
|
+
li_content = template.content_tag(:label,
|
681
|
+
"#{self.radio_button(input_name, value, html_options)} #{label}",
|
682
|
+
:for => generate_html_id(input_name, value.to_s.downcase)
|
683
|
+
)
|
684
|
+
|
685
|
+
li_options = value_as_class ? { :class => value.to_s.downcase } : {}
|
686
|
+
template.content_tag(:li, li_content, li_options)
|
687
|
+
end
|
688
|
+
|
689
|
+
field_set_and_list_wrapping_for_method(method, options, list_item_content)
|
690
|
+
end
|
691
|
+
alias :boolean_radio_input :radio_input
|
692
|
+
|
693
|
+
# Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
|
694
|
+
# items (li), one for each fragment for the date (year, month, day). Each li contains a label
|
695
|
+
# (eg "Year") and a select box. See date_or_datetime_input for a more detailed output example.
|
696
|
+
#
|
697
|
+
# Some of Rails' options for select_date are supported, but not everything yet.
|
698
|
+
def date_input(method, options)
|
699
|
+
date_or_datetime_input(method, options.merge(:discard_hour => true))
|
700
|
+
end
|
701
|
+
|
702
|
+
|
703
|
+
# Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
|
704
|
+
# items (li), one for each fragment for the date (year, month, day, hour, min, sec). Each li
|
705
|
+
# contains a label (eg "Year") and a select box. See date_or_datetime_input for a more
|
706
|
+
# detailed output example.
|
707
|
+
#
|
708
|
+
# Some of Rails' options for select_date are supported, but not everything yet.
|
709
|
+
def datetime_input(method, options)
|
710
|
+
date_or_datetime_input(method, options)
|
711
|
+
end
|
712
|
+
|
713
|
+
|
714
|
+
# Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
|
715
|
+
# items (li), one for each fragment for the time (hour, minute, second). Each li contains a label
|
716
|
+
# (eg "Hour") and a select box. See date_or_datetime_input for a more detailed output example.
|
717
|
+
#
|
718
|
+
# Some of Rails' options for select_time are supported, but not everything yet.
|
719
|
+
def time_input(method, options)
|
720
|
+
date_or_datetime_input(method, options.merge(:discard_year => true, :discard_month => true, :discard_day => true))
|
721
|
+
end
|
722
|
+
|
723
|
+
|
724
|
+
# <fieldset>
|
725
|
+
# <legend>Created At</legend>
|
726
|
+
# <ol>
|
727
|
+
# <li>
|
728
|
+
# <label for="user_created_at_1i">Year</label>
|
729
|
+
# <select id="user_created_at_1i" name="user[created_at(1i)]">
|
730
|
+
# <option value="2003">2003</option>
|
731
|
+
# ...
|
732
|
+
# <option value="2013">2013</option>
|
733
|
+
# </select>
|
734
|
+
# </li>
|
735
|
+
# <li>
|
736
|
+
# <label for="user_created_at_2i">Month</label>
|
737
|
+
# <select id="user_created_at_2i" name="user[created_at(2i)]">
|
738
|
+
# <option value="1">January</option>
|
739
|
+
# ...
|
740
|
+
# <option value="12">December</option>
|
741
|
+
# </select>
|
742
|
+
# </li>
|
743
|
+
# <li>
|
744
|
+
# <label for="user_created_at_3i">Day</label>
|
745
|
+
# <select id="user_created_at_3i" name="user[created_at(3i)]">
|
746
|
+
# <option value="1">1</option>
|
747
|
+
# ...
|
748
|
+
# <option value="31">31</option>
|
749
|
+
# </select>
|
750
|
+
# </li>
|
751
|
+
# </ol>
|
752
|
+
# </fieldset>
|
753
|
+
#
|
754
|
+
# This is an absolute abomination, but so is the official Rails select_date().
|
755
|
+
#
|
756
|
+
def date_or_datetime_input(method, options)
|
757
|
+
position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 }
|
758
|
+
inputs = options.delete(:order) || I18n.translate(:'date.order') || [:year, :month, :day]
|
759
|
+
|
760
|
+
time_inputs = [:hour, :minute]
|
761
|
+
time_inputs << [:second] if options[:include_seconds]
|
762
|
+
|
763
|
+
list_items_capture = ""
|
764
|
+
hidden_fields_capture = ""
|
765
|
+
|
766
|
+
# Gets the datetime object. It can be a Fixnum, Date or Time, or nil.
|
767
|
+
datetime = @object ? @object.send(method) : nil
|
768
|
+
html_options = options.delete(:input_html) || {}
|
769
|
+
|
770
|
+
(inputs + time_inputs).each do |input|
|
771
|
+
html_id = generate_html_id(method, "#{position[input]}i")
|
772
|
+
field_name = "#{method}(#{position[input]}i)"
|
773
|
+
if options["discard_#{input}".intern]
|
774
|
+
break if time_inputs.include?(input)
|
775
|
+
|
776
|
+
hidden_value = datetime.respond_to?(input) ? datetime.send(input) : datetime
|
777
|
+
hidden_fields_capture << template.hidden_field_tag("#{@object_name}[#{field_name}]", (hidden_value || 1), :id => html_id)
|
778
|
+
else
|
779
|
+
opts = set_options(options).merge(:prefix => @object_name, :field_name => field_name)
|
780
|
+
item_label_text = I18n.t(input.to_s, :default => input.to_s.humanize, :scope => [:datetime, :prompts])
|
781
|
+
|
782
|
+
list_items_capture << template.content_tag(:li,
|
783
|
+
template.content_tag(:label, item_label_text, :for => html_id) +
|
784
|
+
template.send("select_#{input}".intern, datetime, opts, html_options.merge(:id => html_id))
|
785
|
+
)
|
786
|
+
end
|
787
|
+
end
|
788
|
+
|
789
|
+
hidden_fields_capture + field_set_and_list_wrapping_for_method(method, options, list_items_capture)
|
790
|
+
end
|
791
|
+
|
792
|
+
|
793
|
+
# Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list
|
794
|
+
# items, one for each possible choice in the belongs_to association. Each li contains a
|
795
|
+
# label and a check_box input.
|
796
|
+
#
|
797
|
+
# This is an alternative for has many and has and belongs to many associations.
|
798
|
+
#
|
799
|
+
# Example:
|
800
|
+
#
|
801
|
+
# f.input :author, :as => :check_boxes
|
802
|
+
#
|
803
|
+
# Output:
|
804
|
+
#
|
805
|
+
# <fieldset>
|
806
|
+
# <legend><span>Authors</span></legend>
|
807
|
+
# <ol>
|
808
|
+
# <li>
|
809
|
+
# <input type="hidden" name="book[author_id][1]" value="">
|
810
|
+
# <label for="book_author_id_1"><input id="book_author_id_1" name="book[author_id][1]" type="checkbox" value="1" /> Justin French</label>
|
811
|
+
# </li>
|
812
|
+
# <li>
|
813
|
+
# <input type="hidden" name="book[author_id][2]" value="">
|
814
|
+
# <label for="book_author_id_2"><input id="book_author_id_2" name="book[owner_id][2]" type="checkbox" value="2" /> Kate French</label>
|
815
|
+
# </li>
|
816
|
+
# </ol>
|
817
|
+
# </fieldset>
|
818
|
+
#
|
819
|
+
# Notice that the value of the checkbox is the same as the id and the hidden
|
820
|
+
# field has empty value. You can override the hidden field value using the
|
821
|
+
# unchecked_value option.
|
822
|
+
#
|
823
|
+
# You can customize the options available in the set by passing in a collection (Array) of
|
824
|
+
# ActiveRecord objects through the :collection option. If not provided, the choices are found
|
825
|
+
# by inferring the parent's class name from the method name and simply calling find(:all) on
|
826
|
+
# it (Author.find(:all) in the example above).
|
827
|
+
#
|
828
|
+
# Examples:
|
829
|
+
#
|
830
|
+
# f.input :author, :as => :check_boxes, :collection => @authors
|
831
|
+
# f.input :author, :as => :check_boxes, :collection => Author.find(:all)
|
832
|
+
# f.input :author, :as => :check_boxes, :collection => [@justin, @kate]
|
833
|
+
#
|
834
|
+
# You can also customize the text label inside each option tag, by naming the correct method
|
835
|
+
# (:full_name, :display_name, :account_number, etc) to call on each object in the collection
|
836
|
+
# by passing in the :label_method option. By default the :label_method is whichever element of
|
837
|
+
# Formtastic::SemanticFormBuilder.collection_label_methods is found first.
|
838
|
+
#
|
839
|
+
# Examples:
|
840
|
+
#
|
841
|
+
# f.input :author, :as => :check_boxes, :label_method => :full_name
|
842
|
+
# f.input :author, :as => :check_boxes, :label_method => :display_name
|
843
|
+
# f.input :author, :as => :check_boxes, :label_method => :to_s
|
844
|
+
# f.input :author, :as => :check_boxes, :label_method => :label
|
845
|
+
#
|
846
|
+
# You can set :value_as_class => true if you want that LI wrappers contains
|
847
|
+
# a class with the wrapped checkbox input value.
|
848
|
+
#
|
849
|
+
def check_boxes_input(method, options)
|
850
|
+
collection = find_collection_for_column(method, options)
|
851
|
+
html_options = options.delete(:input_html) || {}
|
852
|
+
|
853
|
+
input_name = generate_association_input_name(method)
|
854
|
+
value_as_class = options.delete(:value_as_class)
|
855
|
+
unchecked_value = options.delete(:unchecked_value) || ''
|
856
|
+
html_options = { :name => "#{@object_name}[#{input_name}][]" }.merge(html_options)
|
857
|
+
|
858
|
+
list_item_content = collection.map do |c|
|
859
|
+
label = c.is_a?(Array) ? c.first : c
|
860
|
+
value = c.is_a?(Array) ? c.last : c
|
861
|
+
|
862
|
+
html_options.merge!(:id => generate_html_id(input_name, value.to_s.downcase))
|
863
|
+
|
864
|
+
li_content = template.content_tag(:label,
|
865
|
+
"#{self.check_box(input_name, html_options, value, unchecked_value)} #{label}",
|
866
|
+
:for => html_options[:id]
|
867
|
+
)
|
868
|
+
|
869
|
+
li_options = value_as_class ? { :class => value.to_s.downcase } : {}
|
870
|
+
template.content_tag(:li, li_content, li_options)
|
871
|
+
end
|
872
|
+
|
873
|
+
field_set_and_list_wrapping_for_method(method, options, list_item_content)
|
874
|
+
end
|
875
|
+
|
876
|
+
|
877
|
+
# Outputs a country select input, wrapping around a regular country_select helper.
|
878
|
+
# Rails doesn't come with a country_select helper by default any more, so you'll need to install
|
879
|
+
# the "official" plugin, or, if you wish, any other country_select plugin that behaves in the
|
880
|
+
# same way.
|
881
|
+
#
|
882
|
+
# The Rails plugin iso-3166-country-select plugin can be found "here":http://github.com/rails/iso-3166-country-select.
|
883
|
+
#
|
884
|
+
# By default, Formtastic includes a handfull of english-speaking countries as "priority counties",
|
885
|
+
# which you can change to suit your market and user base (see README for more info on config).
|
886
|
+
#
|
887
|
+
# Examples:
|
888
|
+
# f.input :location, :as => :country # use Formtastic::SemanticFormBuilder.priority_countries array for the priority countries
|
889
|
+
# f.input :location, :as => :country, :priority_countries => /Australia/ # set your own
|
890
|
+
#
|
891
|
+
def country_input(method, options)
|
892
|
+
raise "To use the :country input, please install a country_select plugin, like this one: http://github.com/rails/iso-3166-country-select" unless self.respond_to?(:country_select)
|
893
|
+
|
894
|
+
html_options = options.delete(:input_html) || {}
|
895
|
+
priority_countries = options.delete(:priority_countries) || @@priority_countries
|
896
|
+
|
897
|
+
self.label(method, options_for_label(options)) +
|
898
|
+
self.country_select(method, priority_countries, set_options(options), html_options)
|
899
|
+
end
|
900
|
+
|
901
|
+
|
902
|
+
# Outputs a label containing a checkbox and the label text. The label defaults
|
903
|
+
# to the column name (method name) and can be altered with the :label option.
|
904
|
+
# :checked_value and :unchecked_value options are also available.
|
905
|
+
#
|
906
|
+
def boolean_input(method, options)
|
907
|
+
html_options = options.delete(:input_html) || {}
|
908
|
+
|
909
|
+
input = self.check_box(method, set_options(options).merge(html_options),
|
910
|
+
options.delete(:checked_value) || '1', options.delete(:unchecked_value) || '0')
|
911
|
+
|
912
|
+
label = options.delete(:label) || humanized_attribute_name(method)
|
913
|
+
self.label(method, input + label, options_for_label(options))
|
914
|
+
end
|
915
|
+
|
916
|
+
# Generates an input for the given method using the type supplied with :as.
|
917
|
+
#
|
918
|
+
# If the input is included in INPUT_MAPPINGS, it uses input_simple
|
919
|
+
# implementation which maps most of the inputs. All others have specific
|
920
|
+
# code and then a proper handler should be called (like radio_input) for
|
921
|
+
# :radio types.
|
922
|
+
#
|
923
|
+
def inline_input_for(method, options)
|
924
|
+
input_type = options.delete(:as)
|
925
|
+
|
926
|
+
if INPUT_MAPPINGS.key?(input_type)
|
927
|
+
input_simple(input_type, method, options)
|
928
|
+
else
|
929
|
+
send("#{input_type}_input", method, options)
|
930
|
+
end
|
931
|
+
end
|
932
|
+
|
933
|
+
# Generates hints for the given method using the text supplied in :hint.
|
934
|
+
#
|
935
|
+
def inline_hints_for(method, options) #:nodoc:
|
936
|
+
options[:hint] = localized_attribute_string(method, options[:hint], :hint)
|
937
|
+
return if options[:hint].blank?
|
938
|
+
template.content_tag(:p, options[:hint], :class => 'inline-hints')
|
939
|
+
end
|
940
|
+
|
941
|
+
# Creates an error sentence by calling to_sentence on the errors array.
|
942
|
+
#
|
943
|
+
def error_sentence(errors) #:nodoc:
|
944
|
+
template.content_tag(:p, errors.to_sentence.untaint, :class => 'inline-errors')
|
945
|
+
end
|
946
|
+
|
947
|
+
# Creates an error li list.
|
948
|
+
#
|
949
|
+
def error_list(errors) #:nodoc:
|
950
|
+
list_elements = []
|
951
|
+
errors.each do |error|
|
952
|
+
list_elements << template.content_tag(:li, error.untaint)
|
953
|
+
end
|
954
|
+
template.content_tag(:ul, list_elements.join("\n"), :class => 'errors')
|
955
|
+
end
|
956
|
+
|
957
|
+
# Generates the required or optional string. If the value set is a proc,
|
958
|
+
# it evaluates the proc first.
|
959
|
+
#
|
960
|
+
def required_or_optional_string(required) #:nodoc:
|
961
|
+
string_or_proc = case required
|
962
|
+
when true
|
963
|
+
@@required_string
|
964
|
+
when false
|
965
|
+
@@optional_string
|
966
|
+
else
|
967
|
+
required
|
968
|
+
end
|
969
|
+
|
970
|
+
if string_or_proc.is_a?(Proc)
|
971
|
+
string_or_proc.call
|
972
|
+
else
|
973
|
+
string_or_proc.to_s
|
974
|
+
end
|
975
|
+
end
|
976
|
+
|
977
|
+
# Generates a fieldset and wraps the content in an ordered list. When working
|
978
|
+
# with nested attributes (in Rails 2.3), it allows %i as interpolation option
|
979
|
+
# in :name. So you can do:
|
980
|
+
#
|
981
|
+
# f.inputs :name => 'Task #%i', :for => :tasks
|
982
|
+
#
|
983
|
+
# And it will generate a fieldset for each task with legend 'Task #1', 'Task #2',
|
984
|
+
# 'Task #3' and so on.
|
985
|
+
#
|
986
|
+
def field_set_and_list_wrapping(html_options, contents='', &block) #:nodoc:
|
987
|
+
legend = html_options.delete(:name).to_s
|
988
|
+
legend %= parent_child_index(html_options[:parent]) if html_options[:parent]
|
989
|
+
legend = template.content_tag(:legend, template.content_tag(:span, legend)) unless legend.blank?
|
990
|
+
|
991
|
+
contents = template.capture(&block) if block_given?
|
992
|
+
|
993
|
+
# Ruby 1.9: String#to_s behavior changed, need to make an explicit join.
|
994
|
+
contents = contents.join if contents.respond_to?(:join)
|
995
|
+
fieldset = template.content_tag(:fieldset,
|
996
|
+
legend + template.content_tag(:ol, contents),
|
997
|
+
html_options.except(:builder, :parent)
|
998
|
+
)
|
999
|
+
|
1000
|
+
template.concat(fieldset) if block_given?
|
1001
|
+
fieldset
|
1002
|
+
end
|
1003
|
+
|
1004
|
+
# Also generates a fieldset and an ordered list but with label based in
|
1005
|
+
# method. This methods is currently used by radio and datetime inputs.
|
1006
|
+
#
|
1007
|
+
def field_set_and_list_wrapping_for_method(method, options, contents)
|
1008
|
+
contents = contents.join if contents.respond_to?(:join)
|
1009
|
+
|
1010
|
+
template.content_tag(:fieldset,
|
1011
|
+
%{<legend>#{self.label(method, options_for_label(options).merge!(:as_span => true))}</legend>} +
|
1012
|
+
template.content_tag(:ol, contents)
|
1013
|
+
)
|
1014
|
+
end
|
1015
|
+
|
1016
|
+
# For methods that have a database column, take a best guess as to what the input method
|
1017
|
+
# should be. In most cases, it will just return the column type (eg :string), but for special
|
1018
|
+
# cases it will simplify (like the case of :integer, :float & :decimal to :numeric), or do
|
1019
|
+
# something different (like :password and :select).
|
1020
|
+
#
|
1021
|
+
# If there is no column for the method (eg "virtual columns" with an attr_accessor), the
|
1022
|
+
# default is a :string, a similar behaviour to Rails' scaffolding.
|
1023
|
+
#
|
1024
|
+
def default_input_type(method) #:nodoc:
|
1025
|
+
column = @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
|
1026
|
+
|
1027
|
+
if column
|
1028
|
+
# handle the special cases where the column type doesn't map to an input method
|
1029
|
+
return :time_zone if column.type == :string && method.to_s =~ /time_zone/
|
1030
|
+
return :select if column.type == :integer && method.to_s =~ /_id$/
|
1031
|
+
return :datetime if column.type == :timestamp
|
1032
|
+
return :numeric if [:integer, :float, :decimal].include?(column.type)
|
1033
|
+
return :password if column.type == :string && method.to_s =~ /password/
|
1034
|
+
return :country if column.type == :string && method.to_s =~ /country/
|
1035
|
+
|
1036
|
+
# otherwise assume the input name will be the same as the column type (eg string_input)
|
1037
|
+
return column.type
|
1038
|
+
else
|
1039
|
+
if @object
|
1040
|
+
return :select if find_reflection(method)
|
1041
|
+
|
1042
|
+
file = @object.send(method) if @object.respond_to?(method)
|
1043
|
+
return :file if file && @@file_methods.any? { |m| file.respond_to?(m) }
|
1044
|
+
end
|
1045
|
+
|
1046
|
+
return :password if method.to_s =~ /password/
|
1047
|
+
return :string
|
1048
|
+
end
|
1049
|
+
end
|
1050
|
+
|
1051
|
+
# Used by select input to allow use of the Rails option helpers, i.e.
|
1052
|
+
#
|
1053
|
+
# :options => option_groups_from_collection_for_select(@continents, :countries, :name, :id, :name, 3)
|
1054
|
+
#
|
1055
|
+
def use_options_for_select(options)
|
1056
|
+
raise "Can not provide :options and :collection for a select" if options[:collection]
|
1057
|
+
options.delete(:options)
|
1058
|
+
end
|
1059
|
+
|
1060
|
+
# Used by select and radio inputs. The collection can be retrieved by
|
1061
|
+
# three ways:
|
1062
|
+
#
|
1063
|
+
# * Explicitly provided through :collection
|
1064
|
+
# * Retrivied through an association
|
1065
|
+
# * Or a boolean column, which will generate a localized { "Yes" => true, "No" => false } hash.
|
1066
|
+
#
|
1067
|
+
# If the collection is not a hash or an array of strings, fixnums or arrays,
|
1068
|
+
# we use label_method and value_method to retreive an array with the
|
1069
|
+
# appropriate label and value.
|
1070
|
+
#
|
1071
|
+
def find_collection_for_column(column, options)
|
1072
|
+
reflection = find_reflection(column)
|
1073
|
+
|
1074
|
+
collection = if options[:collection]
|
1075
|
+
options.delete(:collection)
|
1076
|
+
elsif reflection || column.to_s =~ /_id$/
|
1077
|
+
parent_class = if reflection
|
1078
|
+
reflection.klass
|
1079
|
+
else
|
1080
|
+
::ActiveSupport::Deprecation.warn("The _id way of doing things is deprecated. Please use the association method (#{column.to_s.sub(/_id$/,'')})", caller[3..-1])
|
1081
|
+
column.to_s.sub(/_id$/,'').camelize.constantize
|
1082
|
+
end
|
1083
|
+
|
1084
|
+
parent_class.find(:all)
|
1085
|
+
else
|
1086
|
+
create_boolean_collection(options)
|
1087
|
+
end
|
1088
|
+
|
1089
|
+
collection = collection.to_a if collection.instance_of?(Hash)
|
1090
|
+
|
1091
|
+
# Return if we have an Array of strings, fixnums or arrays
|
1092
|
+
return collection if collection.instance_of?(Array) &&
|
1093
|
+
[Array, Fixnum, String, Symbol].include?(collection.first.class)
|
1094
|
+
|
1095
|
+
label = options.delete(:label_method) || detect_label_method(collection)
|
1096
|
+
value = options.delete(:value_method) || :id
|
1097
|
+
|
1098
|
+
collection.map { |o| [o.send(label), o.send(value)] }
|
1099
|
+
end
|
1100
|
+
|
1101
|
+
# Detected the label collection method when none is supplied using the
|
1102
|
+
# values set in @@collection_label_methods.
|
1103
|
+
#
|
1104
|
+
def detect_label_method(collection) #:nodoc:
|
1105
|
+
@@collection_label_methods.detect { |m| collection.first.respond_to?(m) }
|
1106
|
+
end
|
1107
|
+
|
1108
|
+
# Returns a hash to be used by radio and select inputs when a boolean field
|
1109
|
+
# is provided.
|
1110
|
+
#
|
1111
|
+
def create_boolean_collection(options)
|
1112
|
+
options[:true] ||= I18n.t('yes', :default => 'Yes', :scope => [:formtastic])
|
1113
|
+
options[:false] ||= I18n.t('no', :default => 'No', :scope => [:formtastic])
|
1114
|
+
options[:value_as_class] = true unless options.key?(:value_as_class)
|
1115
|
+
|
1116
|
+
[ [ options.delete(:true), true], [ options.delete(:false), false ] ]
|
1117
|
+
end
|
1118
|
+
|
1119
|
+
# Used by association inputs (select, radio) to generate the name that should
|
1120
|
+
# be used for the input
|
1121
|
+
#
|
1122
|
+
# belongs_to :author; f.input :author; will generate 'author_id'
|
1123
|
+
# belongs_to :entity, :foreign_key = :owner_id; f.input :author; will generate 'owner_id'
|
1124
|
+
# has_many :authors; f.input :authors; will generate 'author_ids'
|
1125
|
+
# has_and_belongs_to_many will act like has_many
|
1126
|
+
#
|
1127
|
+
def generate_association_input_name(method)
|
1128
|
+
if reflection = find_reflection(method)
|
1129
|
+
if [:has_and_belongs_to_many, :has_many].include?(reflection.macro)
|
1130
|
+
"#{method.to_s.singularize}_ids"
|
1131
|
+
else
|
1132
|
+
reflection.options[:foreign_key] || "#{method}_id"
|
1133
|
+
end
|
1134
|
+
else
|
1135
|
+
method
|
1136
|
+
end
|
1137
|
+
end
|
1138
|
+
|
1139
|
+
# If an association method is passed in (f.input :author) try to find the
|
1140
|
+
# reflection object.
|
1141
|
+
#
|
1142
|
+
def find_reflection(method)
|
1143
|
+
@object.class.reflect_on_association(method) if @object.class.respond_to?(:reflect_on_association)
|
1144
|
+
end
|
1145
|
+
|
1146
|
+
# Generates default_string_options by retrieving column information from
|
1147
|
+
# the database.
|
1148
|
+
#
|
1149
|
+
def default_string_options(method, type) #:nodoc:
|
1150
|
+
column = @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
|
1151
|
+
|
1152
|
+
if type == :numeric || column.nil? || column.limit.nil?
|
1153
|
+
{ :size => @@default_text_field_size }
|
1154
|
+
else
|
1155
|
+
{ :maxlength => column.limit, :size => [column.limit, @@default_text_field_size].min }
|
1156
|
+
end
|
1157
|
+
end
|
1158
|
+
|
1159
|
+
# Generate the html id for the li tag.
|
1160
|
+
# It takes into account options[:index] and @auto_index to generate li
|
1161
|
+
# elements with appropriate index scope. It also sanitizes the object
|
1162
|
+
# and method names.
|
1163
|
+
#
|
1164
|
+
def generate_html_id(method_name, value='input')
|
1165
|
+
if options.has_key?(:index)
|
1166
|
+
index = "_#{options[:index]}"
|
1167
|
+
elsif defined?(@auto_index)
|
1168
|
+
index = "_#{@auto_index}"
|
1169
|
+
else
|
1170
|
+
index = ""
|
1171
|
+
end
|
1172
|
+
sanitized_method_name = method_name.to_s.gsub(/[\?\/\-]$/, '')
|
1173
|
+
|
1174
|
+
"#{sanitized_object_name}#{index}_#{sanitized_method_name}_#{value}"
|
1175
|
+
end
|
1176
|
+
|
1177
|
+
# Gets the nested_child_index value from the parent builder. In Rails 2.3
|
1178
|
+
# it always returns a fixnum. In next versions it returns a hash with each
|
1179
|
+
# association that the parent builds.
|
1180
|
+
#
|
1181
|
+
def parent_child_index(parent)
|
1182
|
+
duck = parent[:builder].instance_variable_get('@nested_child_index')
|
1183
|
+
|
1184
|
+
if duck.is_a?(Hash)
|
1185
|
+
child = parent[:for]
|
1186
|
+
child = child.first if child.respond_to?(:first)
|
1187
|
+
duck[child].to_i + 1
|
1188
|
+
else
|
1189
|
+
duck.to_i + 1
|
1190
|
+
end
|
1191
|
+
end
|
1192
|
+
|
1193
|
+
def sanitized_object_name
|
1194
|
+
@sanitized_object_name ||= @object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
|
1195
|
+
end
|
1196
|
+
|
1197
|
+
def humanized_attribute_name(method)
|
1198
|
+
if @object && @object.class.respond_to?(:human_attribute_name)
|
1199
|
+
@object.class.human_attribute_name(method.to_s)
|
1200
|
+
else
|
1201
|
+
method.to_s.send(@@label_str_method)
|
1202
|
+
end
|
1203
|
+
end
|
1204
|
+
|
1205
|
+
# Internal generic method for looking up localized values within Formtastic
|
1206
|
+
# using I18n, if no explicit value is set and I18n-lookups are enabled.
|
1207
|
+
#
|
1208
|
+
# Enabled/Disable this by setting:
|
1209
|
+
#
|
1210
|
+
# Formtastic::SemanticFormBuilder.i18n_lookups_by_default = true/false
|
1211
|
+
#
|
1212
|
+
# Lookup priority:
|
1213
|
+
#
|
1214
|
+
# 'formtastic.{{type}}.{{model}}.{{action}}.{{attribute}}'
|
1215
|
+
# 'formtastic.{{type}}.{{model}}.{{attribute}}'
|
1216
|
+
# 'formtastic.{{type}}.{{attribute}}'
|
1217
|
+
#
|
1218
|
+
# Example:
|
1219
|
+
#
|
1220
|
+
# 'formtastic.labels.post.edit.title'
|
1221
|
+
# 'formtastic.labels.post.title'
|
1222
|
+
# 'formtastic.labels.title'
|
1223
|
+
#
|
1224
|
+
# NOTE: Generic, but only used for form input labels/hints.
|
1225
|
+
#
|
1226
|
+
def localized_attribute_string(attr_name, attr_value, i18n_key)
|
1227
|
+
if attr_value.is_a?(String)
|
1228
|
+
attr_value
|
1229
|
+
else
|
1230
|
+
use_i18n = attr_value.nil? ? @@i18n_lookups_by_default : attr_value
|
1231
|
+
if use_i18n
|
1232
|
+
model_name = @object.class.name.underscore
|
1233
|
+
action_name = template.params[:action].to_s rescue ''
|
1234
|
+
attribute_name = attr_name.to_s
|
1235
|
+
|
1236
|
+
defaults = I18N_SCOPES.collect do |i18n_scope|
|
1237
|
+
i18n_path = i18n_scope.dup
|
1238
|
+
i18n_path.gsub!('{{action}}', action_name)
|
1239
|
+
i18n_path.gsub!('{{model}}', model_name)
|
1240
|
+
i18n_path.gsub!('{{attribute}}', attribute_name)
|
1241
|
+
i18n_path.gsub!('..', '.')
|
1242
|
+
i18n_path.to_sym
|
1243
|
+
end
|
1244
|
+
defaults << ''
|
1245
|
+
|
1246
|
+
i18n_value = ::I18n.t(defaults.shift, :default => defaults,
|
1247
|
+
:scope => "formtastic.#{i18n_key.to_s.pluralize}")
|
1248
|
+
i18n_value.blank? ? nil : i18n_value
|
1249
|
+
end
|
1250
|
+
end
|
1251
|
+
end
|
1252
|
+
|
1253
|
+
end
|
1254
|
+
|
1255
|
+
# Wrappers around form_for (etc) with :builder => SemanticFormBuilder.
|
1256
|
+
#
|
1257
|
+
# * semantic_form_for(@post)
|
1258
|
+
# * semantic_fields_for(@post)
|
1259
|
+
# * semantic_form_remote_for(@post)
|
1260
|
+
# * semantic_remote_form_for(@post)
|
1261
|
+
#
|
1262
|
+
# Each of which are the equivalent of:
|
1263
|
+
#
|
1264
|
+
# * form_for(@post, :builder => Formtastic::SemanticFormBuilder))
|
1265
|
+
# * fields_for(@post, :builder => Formtastic::SemanticFormBuilder))
|
1266
|
+
# * form_remote_for(@post, :builder => Formtastic::SemanticFormBuilder))
|
1267
|
+
# * remote_form_for(@post, :builder => Formtastic::SemanticFormBuilder))
|
1268
|
+
#
|
1269
|
+
# Example Usage:
|
1270
|
+
#
|
1271
|
+
# <% semantic_form_for @post do |f| %>
|
1272
|
+
# <%= f.input :title %>
|
1273
|
+
# <%= f.input :body %>
|
1274
|
+
# <% end %>
|
1275
|
+
#
|
1276
|
+
# The above examples use a resource-oriented style of form_for() helper where only the @post
|
1277
|
+
# object is given as an argument, but the generic style is also supported if you really want it,
|
1278
|
+
# as is forms with inline objects (Post.new) rather than objects with instance variables (@post):
|
1279
|
+
#
|
1280
|
+
# <% semantic_form_for :post, @post, :url => posts_path do |f| %>
|
1281
|
+
# ...
|
1282
|
+
# <% end %>
|
1283
|
+
#
|
1284
|
+
# <% semantic_form_for :post, Post.new, :url => posts_path do |f| %>
|
1285
|
+
# ...
|
1286
|
+
# <% end %>
|
1287
|
+
#
|
1288
|
+
# The shorter, resource-oriented style is most definitely preferred, and has recieved the most
|
1289
|
+
# testing to date.
|
1290
|
+
#
|
1291
|
+
# Please note: Although it's possible to call Rails' built-in form_for() helper without an
|
1292
|
+
# object, all semantic forms *must* have an object (either Post.new or @post), as Formtastic
|
1293
|
+
# has too many dependencies on an ActiveRecord object being present.
|
1294
|
+
#
|
1295
|
+
module SemanticFormHelper
|
1296
|
+
@@builder = Formtastic::SemanticFormBuilder
|
1297
|
+
|
1298
|
+
# cattr_accessor :builder
|
1299
|
+
def self.builder=(val)
|
1300
|
+
@@builder = val
|
1301
|
+
end
|
1302
|
+
|
1303
|
+
[:form_for, :fields_for, :form_remote_for, :remote_form_for].each do |meth|
|
1304
|
+
src = <<-END_SRC
|
1305
|
+
def semantic_#{meth}(record_or_name_or_array, *args, &proc)
|
1306
|
+
options = args.extract_options!
|
1307
|
+
options[:builder] = @@builder
|
1308
|
+
options[:html] ||= {}
|
1309
|
+
|
1310
|
+
class_names = options[:html][:class] ? options[:html][:class].split(" ") : []
|
1311
|
+
class_names << "formtastic"
|
1312
|
+
class_names << case record_or_name_or_array
|
1313
|
+
when String, Symbol then record_or_name_or_array.to_s # :post => "post"
|
1314
|
+
when Array then record_or_name_or_array.last.class.to_s.underscore # [@post, @comment] # => "comment"
|
1315
|
+
else record_or_name_or_array.class.to_s.underscore # @post => "post"
|
1316
|
+
end
|
1317
|
+
options[:html][:class] = class_names.join(" ")
|
1318
|
+
|
1319
|
+
#{meth}(record_or_name_or_array, *(args << options), &proc)
|
1320
|
+
end
|
1321
|
+
END_SRC
|
1322
|
+
module_eval src, __FILE__, __LINE__
|
1323
|
+
end
|
1324
|
+
end
|
1325
|
+
end
|