formtastic 0.2.5

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