From 075e7b23b699470bbdd6ec9438ba3ed0ae676e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 9 Jan 2010 14:34:52 +0100 Subject: [PATCH 1/7] Temporary refactoring. --- lib/simple_form.rb | 16 +- lib/simple_form/components.rb | 4 +- lib/simple_form/components/base.rb | 6 +- lib/simple_form/components/hint.rb | 20 --- lib/simple_form/components/label.rb | 89 ----------- lib/simple_form/form_builder.rb | 224 +++++++++++++++++++++++++++- lib/simple_form/i18n_cache.rb | 8 +- test/components/error_test.rb | 38 +++-- test/components/hint_test.rb | 22 +-- test/components/label_test.rb | 17 +-- 10 files changed, 272 insertions(+), 172 deletions(-) delete mode 100644 lib/simple_form/components/hint.rb delete mode 100644 lib/simple_form/components/label.rb diff --git a/lib/simple_form.rb b/lib/simple_form.rb index 2909a1c5..cc3af6a6 100644 --- a/lib/simple_form.rb +++ b/lib/simple_form.rb @@ -9,17 +9,17 @@ module SimpleForm autoload :MapType, 'simple_form/map_type' autoload :RequiredHelpers, 'simple_form/required_helpers' - # Default tag used in components. - mattr_accessor :component_tag - @@component_tag = :span + # Default tag used in hints + mattr_accessor :hint_tag + @@hint_tag = :span + + # Default tag used in errors + mattr_accessor :error_tag + @@error_tag = :span # Components used by the form builder. mattr_accessor :components - @@components = [ - SimpleForm::Components::Wrapper, SimpleForm::Components::Label, - SimpleForm::Components::Input, SimpleForm::Components::Hint, - SimpleForm::Components::Error - ] + @@components = [ :label, :input, :hint, :error ] # Series of attemps to detect a default label method for collection mattr_accessor :collection_label_methods diff --git a/lib/simple_form/components.rb b/lib/simple_form/components.rb index 0b002b76..e51ef8e1 100644 --- a/lib/simple_form/components.rb +++ b/lib/simple_form/components.rb @@ -2,9 +2,9 @@ module SimpleForm module Components autoload :Base, 'simple_form/components/base' autoload :Error, 'simple_form/components/error' - autoload :Hint, 'simple_form/components/hint' + # autoload :Hint, 'simple_form/components/hint' autoload :Input, 'simple_form/components/input' - autoload :Label, 'simple_form/components/label' + # autoload :Label, 'simple_form/components/label' autoload :Wrapper, 'simple_form/components/wrapper' end end \ No newline at end of file diff --git a/lib/simple_form/components/base.rb b/lib/simple_form/components/base.rb index 2f71901d..010d29c4 100644 --- a/lib/simple_form/components/base.rb +++ b/lib/simple_form/components/base.rb @@ -17,6 +17,10 @@ module SimpleForm :update => :edit } + def __content + valid? ? content : "" + end + def self.basename @basename ||= name.split("::").last.underscore.to_sym end @@ -69,7 +73,7 @@ module SimpleForm def component_tag(content) html_options = component_html_options html_options[:class] = "#{basename} #{html_options[:class]}".strip - template.content_tag(SimpleForm.component_tag, content, html_options) + template.content_tag(SimpleForm.error_tag, content, html_options) end # Lookup translations for components using I18n, based on object name, diff --git a/lib/simple_form/components/hint.rb b/lib/simple_form/components/hint.rb deleted file mode 100644 index 9de8e1d5..00000000 --- a/lib/simple_form/components/hint.rb +++ /dev/null @@ -1,20 +0,0 @@ -module SimpleForm - module Components - # Basic hint component, which verifies whether a user has defined a hint - # either on the input or through i18n lookup. If no hint is found, the - # component is skipped. - class Hint < Base - def valid? - !hidden_input? && !hint.blank? - end - - def hint - @hint ||= options[:hint] || translate - end - - def content - component_tag hint - end - end - end -end diff --git a/lib/simple_form/components/label.rb b/lib/simple_form/components/label.rb deleted file mode 100644 index 1a932057..00000000 --- a/lib/simple_form/components/label.rb +++ /dev/null @@ -1,89 +0,0 @@ -module SimpleForm - module Components - # Responsible for rendering label with default options, such as required - # and translation using i18n. By default all fields are required, and label - # will prepend a default string with a required mark in all labels. You can - # disable the required option just passing :required => false in your - # label/input declaration. You can also use i18n to change the required - # text, mark or entire html string that is generated. - class Label < Base - include RequiredHelpers - extend I18nCache - - def self.translate_required_html - i18n_cache :translate_required_html do - I18n.t(:"simple_form.required.html", :default => - %[#{translate_required_mark}] - ) - end - end - - def self.translate_required_text - I18n.t(:"simple_form.required.text", :default => 'required') - end - - def self.translate_required_mark - I18n.t(:"simple_form.required.mark", :default => '*') - end - - def valid? - !hidden_input? - end - - # Overwrite html for option if the user has changed input id, so the label - # will always point correctly to the input. Renders a default label. - def content - html_options = component_html_options - html_options[:for] = options[:input_html][:id] if options.key?(:input_html) - @builder.label(label_target, text, html_options) - end - - # Map attribute to specific name when dealing with date/time/timestamp, - # ensuring label will always be "clickable". For better accessibility. - def label_target - case input_type - when :date, :datetime - "#{attribute_name}_1i" - when :time - "#{attribute_name}_4i" - else - attribute_name - end - end - - # The method that actually generates the label. This can be overwriten using - # a SimpleForm configuration value: - # - # SimpleForm.label_text = lambda do |label_text, required_text| - # required_text + label_text + ":" - # end - def text - SimpleForm.label_text.call(label_text, required_text) - end - - # The user is able to pass a label with the :label option, or it will - # fallback to label lookup. - def label_text - options[:label] || translate_label - end - - # Default required text when attribute is required. - def required_text - attribute_required? ? self.class.translate_required_html.dup : '' - end - - # Attempts to translate the label using default i18n lookup. If no - # translation is found, fallback to human_attribute_name if it is - # available or just use the attribute itself humanized. - def translate_label - default = if object.class.respond_to?(:human_attribute_name) - object.class.human_attribute_name(reflection_or_attribute_name.to_s) - else - attribute_name.to_s.humanize - end - - translate(default) - end - end - end -end diff --git a/lib/simple_form/form_builder.rb b/lib/simple_form/form_builder.rb index a1dc0ed2..b2628cc9 100644 --- a/lib/simple_form/form_builder.rb +++ b/lib/simple_form/form_builder.rb @@ -70,15 +70,225 @@ module SimpleForm # Some inputs, as :time_zone and :country accepts a :priority option. If none is # given SimpleForm.time_zone_priority and SimpleForm.country_priority are used respectivelly. # + module Labels + def self.included(base) + base.extend ClassMethods + end + + module ClassMethods #:nodoc: + include I18nCache + + def translate_required_html + i18n_cache :translate_required_html do + I18n.t(:"simple_form.required.html", :default => + %[#{translate_required_mark}] + ) + end + end + + def translate_required_text + I18n.t(:"simple_form.required.text", :default => 'required') + end + + def translate_required_mark + I18n.t(:"simple_form.required.mark", :default => '*') + end + end + + def label + @builder.label(label_target, label_text, label_html_options) + end + + def label_text + SimpleForm.label_text.call(raw_label_text, required_label_text) + end + + # TODO Fix me + def label_target + case input_type + when :date, :datetime + "#{attribute_name}_1i" + when :time + "#{attribute_name}_4i" + else + attribute_name + end + end + + # TODO Why default_css_options only in labels? + def label_html_options + label_options = html_options_for(:label, input_type, required_class) + label_options[:for] = options[:input_html][:id] if options.key?(:input_html) + label_options + end + + protected + + def raw_label_text #:nodoc: + options[:label] || label_translation + end + + # Default required text when attribute is required. + def required_label_text #:nodoc: + attribute_required? ? self.class.translate_required_html.dup : '' + end + + # First check human attribute name and then labels. + # TODO Remove me in Rails > 2.3.5 + def label_translation #:nodoc: + default = if object.class.respond_to?(:human_attribute_name) + object.class.human_attribute_name(reflection_or_attribute_name.to_s) + else + attribute_name.to_s.humanize + end + + translate(:labels, default) + end + end + + module Hints + def hint + template.content_tag(hint_tag, hint_text, hint_html_options) unless hint_text.blank? + end + + def hint_tag + options[:hint_tag] || SimpleForm.hint_tag + end + + def hint_text + @hint_text ||= options[:hint] || translate(:hints) + end + + def hint_html_options + html_options_for(:hint, :hint) + end + end + + module Errors + def error + template.content_tag(error_tag, error_text, error_html_options) if object && errors.present? + end + + def error_tag + options[:error_tag] || SimpleForm.error_tag + end + + def error_text + errors.to_sentence + end + + def error_html_options + html_options_for(:error, :error) + end + + protected + + def errors + @errors ||= (errors_on_attribute + errors_on_association).compact + end + + def errors_on_attribute + Array(object.errors[attribute_name]) + end + + def errors_on_association + reflection ? Array(object.errors[reflection.name]) : [] + end + end + + class Input + include Errors + include Hints + include Labels + + delegate :template, :object, :object_name, :attribute_name, :column, + :reflection, :input_type, :options, :to => :@builder + + def initialize(builder) + @builder = builder + end + + def input + SimpleForm::Components::Input.new(@builder, TERMINATOR).__content + end + + def render + pieces = SimpleForm.components.select { |n| n unless @builder.options[n] == false } + terminator = lambda { pieces.map!{ |p| send(p).to_s }.join } + SimpleForm::Components::Wrapper.new(@builder, terminator).call + end + + protected + + # When action is create or update, we still should use new and edit + ACTIONS = { + :create => :new, + :update => :edit + } + + def attribute_required? + options[:required] != false + end + + def required_class + attribute_required? ? :required : :optional + end + + # Find reflection name when available, otherwise use attribute + def reflection_or_attribute_name + reflection ? reflection.name : attribute_name + end + + # Retrieve options for the given namespace from the options hash + def html_options_for(namespace, *extra) + html_options = options[:"#{namespace}_html"] || {} + html_options[:class] = (extra << html_options[:class]).join(' ').strip if extra.present? + html_options + end + + # Lookup translations for the given namespace using I18n, based on object name, + # actual action and attribute name. Lookup priority as follows: + # + # simple_form.{namespace}.{model}.{action}.{attribute} + # simple_form.{namespace}.{model}.{attribute} + # simple_form.{namespace}.{attribute} + # + # Namespace is used for :labels and :hints. + # + # Model is the actual object name, for a @user object you'll have :user. + # Action is the action being rendered, usually :new or :edit. + # And attribute is the attribute itself, :name for example. + # + # Example: + # + # simple_form: + # labels: + # user: + # new: + # email: 'E-mail para efetuar o sign in.' + # edit: + # email: 'E-mail.' + # + # Take a look at our locale example file. + def translate(namespace, default='') + lookups = [] + lookups << :"#{object_name}.#{lookup_action}.#{reflection_or_attribute_name}" + lookups << :"#{object_name}.#{reflection_or_attribute_name}" + lookups << :"#{reflection_or_attribute_name}" + lookups << default + I18n.t(lookups.shift, :scope => :"simple_form.#{namespace}", :default => lookups) + end + + # The action to be used in lookup. + def lookup_action + action = template.controller.action_name.to_sym + ACTIONS[action] || action + end + end + def input(attribute_name, options={}) define_simple_form_attributes(attribute_name, options) - - component = TERMINATOR - SimpleForm.components.reverse.each do |klass| - next if @options[klass.basename] == false - component = klass.new(self, component) - end - component.call + Input.new(self).render end alias :attribute :input diff --git a/lib/simple_form/i18n_cache.rb b/lib/simple_form/i18n_cache.rb index fc8e567d..74f056be 100644 --- a/lib/simple_form/i18n_cache.rb +++ b/lib/simple_form/i18n_cache.rb @@ -8,11 +8,15 @@ module SimpleForm end def get_i18n_cache(key) - instance_variable_get(:"@#{key}") || reset_i18n_cache(key) + if class_variable_defined?(:"@@#{key}") + class_variable_get(:"@@#{key}") + else + reset_i18n_cache(key) + end end def reset_i18n_cache(key) - instance_variable_set(:"@#{key}", {}) + class_variable_set(:"@@#{key}", {}) end end end diff --git a/test/components/error_test.rb b/test/components/error_test.rb index f6bf98d7..1d6a6180 100644 --- a/test/components/error_test.rb +++ b/test/components/error_test.rb @@ -9,29 +9,27 @@ class ErrorTest < ActionView::TestCase f.input_type = type f.options = options - error = SimpleForm::Components::Error.new(f, SimpleForm::FormBuilder::TERMINATOR) - concat(error.call) - yield error if block_given? + concat(SimpleForm::FormBuilder::Input.new(f).error) end end - test 'error should not generate content for hidden fields' do - with_error_for @user, :name, :hidden do |error| - assert error.call.blank? - end - end - - test 'error should not generate content for attribute without errors' do - with_error_for @user, :active, :boolean do |error| - assert error.call.blank? - end - end - - test 'error should not generate messages when object is not present' do - with_error_for :project, :name, :string do |error| - assert error.call.blank? - end - end + # test 'error should not generate content for hidden fields' do + # with_error_for @user, :name, :hidden do |error| + # assert error.call.blank? + # end + # end + # + # test 'error should not generate content for attribute without errors' do + # with_error_for @user, :active, :boolean do |error| + # assert error.call.blank? + # end + # end + # + # test 'error should not generate messages when object is not present' do + # with_error_for :project, :name, :string do |error| + # assert error.call.blank? + # end + # end test 'error should generate messages for attribute with single error' do with_error_for @user, :name, :string diff --git a/test/components/hint_test.rb b/test/components/hint_test.rb index 6f2c713b..c2e9719c 100644 --- a/test/components/hint_test.rb +++ b/test/components/hint_test.rb @@ -9,23 +9,17 @@ class HintTest < ActionView::TestCase f.input_type = type f.options = options - hint = SimpleForm::Components::Hint.new(f, SimpleForm::FormBuilder::TERMINATOR) - concat(hint.call) - yield hint if block_given? + concat(SimpleForm::FormBuilder::Input.new(f).hint) end end - test 'hint should not be generated by default' do - with_hint_for @user, :name, :string do |hint| - assert hint.call.blank? - end - end - - test 'hint should not be generated for hidden fields' do - with_hint_for @user, :name, :hidden, :hint => 'Use with care...' do |hint| - assert hint.call.blank? - end - end + # test 'hint should not be generated by default' do + # assert with_hint_for(@user, :name, :string).blank? + # end + # + # test 'hint should not be generated for hidden fields' do + # assert with_hint_for(@user, :name, :hidden, :hint => 'Use with care...').blank? + # end test 'hint should be generated with input text' do with_hint_for @user, :name, :string, :hint => 'Use with care...' diff --git a/test/components/label_test.rb b/test/components/label_test.rb index 2ade6688..70085346 100644 --- a/test/components/label_test.rb +++ b/test/components/label_test.rb @@ -3,7 +3,7 @@ require 'test_helper' class LabelTest < ActionView::TestCase setup do - SimpleForm::Components::Label.reset_i18n_cache :translate_required_html + SimpleForm::FormBuilder::Input.reset_i18n_cache :translate_required_html end def with_label_for(object, attribute_name, type, options={}) @@ -13,17 +13,16 @@ class LabelTest < ActionView::TestCase f.input_type = type f.options = options - label = SimpleForm::Components::Label.new(f, SimpleForm::FormBuilder::TERMINATOR) - concat(label.call) - yield label if block_given? + concat(SimpleForm::FormBuilder::Input.new(f).label) end end - test 'label should not be generated for hidden inputs' do - with_label_for @user, :name, :hidden do |label| - assert label.call.blank? - end - end + # Fix me! + # test 'label should not be generated for hidden inputs' do + # with_label_for @user, :name, :hidden do |label| + # assert label.call.blank? + # end + # end test 'label should generate a default humanized description' do with_label_for @user, :name, :string From 42830967d74d9f5470fc23ff3f0df6d3600c6b53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 9 Jan 2010 15:39:14 +0100 Subject: [PATCH 2/7] Most of refactoring done. --- .../action_view_extensions/builder.rb | 4 +- lib/simple_form/components.rb | 6 - lib/simple_form/components/base.rb | 117 ---- lib/simple_form/components/error.rb | 28 - lib/simple_form/components/input.rb | 122 ---- lib/simple_form/components/wrapper.rb | 19 - lib/simple_form/form_builder.rb | 194 +++++- lib/simple_form/map_type.rb | 6 +- test/action_view_extensions/builder_test.rb | 16 +- test/components/error_test.rb | 2 +- test/components/hint_test.rb | 2 +- test/components/input_test.rb | 646 +++++++++--------- test/components/label_test.rb | 4 +- 13 files changed, 508 insertions(+), 658 deletions(-) delete mode 100644 lib/simple_form/components/base.rb delete mode 100644 lib/simple_form/components/error.rb delete mode 100644 lib/simple_form/components/input.rb delete mode 100644 lib/simple_form/components/wrapper.rb diff --git a/lib/simple_form/action_view_extensions/builder.rb b/lib/simple_form/action_view_extensions/builder.rb index 7d12d20a..a656bc2b 100644 --- a/lib/simple_form/action_view_extensions/builder.rb +++ b/lib/simple_form/action_view_extensions/builder.rb @@ -68,7 +68,7 @@ module SimpleForm # * disabled => the value or values that should be disabled. Accepts a single # item or an array of items. # - def collection_check_box(attribute, collection, value_method, text_method, options={}, html_options={}) + def collection_check_boxes(attribute, collection, value_method, text_method, options={}, html_options={}) collection.inject('') do |result, item| value = item.send value_method text = item.send text_method @@ -77,7 +77,7 @@ module SimpleForm default_html_options[:multiple] = true result << check_box(attribute, default_html_options, value, '') << - label("#{attribute}_#{value}", text, :class => "collection_check_box") + label("#{attribute}_#{value}", text, :class => "collection_check_boxes") end end diff --git a/lib/simple_form/components.rb b/lib/simple_form/components.rb index e51ef8e1..f0f27f08 100644 --- a/lib/simple_form/components.rb +++ b/lib/simple_form/components.rb @@ -1,10 +1,4 @@ module SimpleForm module Components - autoload :Base, 'simple_form/components/base' - autoload :Error, 'simple_form/components/error' - # autoload :Hint, 'simple_form/components/hint' - autoload :Input, 'simple_form/components/input' - # autoload :Label, 'simple_form/components/label' - autoload :Wrapper, 'simple_form/components/wrapper' end end \ No newline at end of file diff --git a/lib/simple_form/components/base.rb b/lib/simple_form/components/base.rb deleted file mode 100644 index 010d29c4..00000000 --- a/lib/simple_form/components/base.rb +++ /dev/null @@ -1,117 +0,0 @@ -module SimpleForm - module Components - # The component is the core of SimpleForm. SimpleForm can be customized simply - # with the addition of new components to the component stack. A component just - # need to be initialized with two values, the builder and the next component to - # be invoked and respond to call. - # - # The Base component is a raw component with some helpers and a default behavior - # of prepending the content available in the method content. - class Base - delegate :template, :object, :object_name, :attribute_name, :column, - :reflection, :input_type, :options, :to => :@builder - - # When action is create or update, we still should use new and edit - ACTIONS = { - :create => :new, - :update => :edit - } - - def __content - valid? ? content : "" - end - - def self.basename - @basename ||= name.split("::").last.underscore.to_sym - end - - def initialize(builder, component) - @builder = builder - @component = component - end - - # Generate component content and call next component in the stack. When a - # component is invalid it will be skipped. - def call - return @component.call unless valid? - content + @component.call - end - - def valid? - true - end - - def hidden_input? - input_type == :hidden - end - - def basename - self.class.basename - end - - # Find reflection name when available, otherwise use attribute - def reflection_or_attribute_name - reflection ? reflection.name : attribute_name - end - - # Default html options for a component. Passed as a parameter for simple - # form component using component name as follows: - # - # label_html => {} - # input_html => {} - # hint_html => {} - # error_html => {} - # wrapper_html => {} - # - def component_html_options - options[:"#{basename}_html"] || {} - end - - # Renders default content tag for components, using default html class - # and user defined parameters. - # Default component tag can be configured in SimpleForm.component_tag. - def component_tag(content) - html_options = component_html_options - html_options[:class] = "#{basename} #{html_options[:class]}".strip - template.content_tag(SimpleForm.error_tag, content, html_options) - end - - # Lookup translations for components using I18n, based on object name, - # actual action and attribute name. Lookup priority as follows: - # - # simple_form.{type}.{model}.{action}.{attribute} - # simple_form.{type}.{model}.{attribute} - # simple_form.{type}.{attribute} - # - # Type is used for :labels and :hints. - # Model is the actual object name, for a @user object you'll have :user. - # Action is the action being rendered, usually :new or :edit. - # And attribute is the attribute itself, :name for example. - # Example: - # - # simple_form: - # labels: - # user: - # new: - # email: 'E-mail para efetuar o sign in.' - # edit: - # email: 'E-mail.' - # - # Take a look at our locale example file. - def translate(default='') - lookups = [] - lookups << :"#{object_name}.#{lookup_action}.#{reflection_or_attribute_name}" - lookups << :"#{object_name}.#{reflection_or_attribute_name}" - lookups << :"#{reflection_or_attribute_name}" - lookups << default - I18n.t(lookups.shift, :scope => :"simple_form.#{basename.to_s.pluralize}", :default => lookups) - end - - # The action to be used in lookup. - def lookup_action - action = template.controller.action_name.to_sym - ACTIONS[action] || action - end - end - end -end diff --git a/lib/simple_form/components/error.rb b/lib/simple_form/components/error.rb deleted file mode 100644 index 8241fd41..00000000 --- a/lib/simple_form/components/error.rb +++ /dev/null @@ -1,28 +0,0 @@ -module SimpleForm - module Components - # General error component. Responsible for verifying whether an object - # exists and there are errors on the attribute being generated. If errors - # exists than the component will be rendered, otherwise will be skipped. - class Error < Base - def valid? - object && !hidden_input? && !errors.blank? - end - - def errors - @errors ||= (errors_on_attribute + errors_on_association).compact - end - - def errors_on_attribute - Array(object.errors[attribute_name]) - end - - def errors_on_association - reflection ? Array(object.errors[reflection.name]) : [] - end - - def content - component_tag errors.to_sentence - end - end - end -end diff --git a/lib/simple_form/components/input.rb b/lib/simple_form/components/input.rb deleted file mode 100644 index b4912f5b..00000000 --- a/lib/simple_form/components/input.rb +++ /dev/null @@ -1,122 +0,0 @@ -module SimpleForm - module Components - # Default input component, responsible for mapping column attributes from - # database to inputs to be rendered. - class Input < Base - include RequiredHelpers - extend I18nCache - extend MapType - - map_type :boolean, :to => :check_box - map_type :string, :to => :text_field - map_type :password, :to => :password_field - map_type :text, :to => :text_area - map_type :file, :to => :file_field - map_type :hidden, :to => :hidden_field - - # Numeric types - map_type :integer, :float, :decimal, :to => :text_field - - # Date/time types - map_type :datetime, :to => :datetime_select, :options => true - map_type :date, :to => :date_select, :options => true - map_type :time, :to => :time_select, :options => true - - # Collection types - map_type :select, :to => :collection_select, :options => true, :collection => true - map_type :radio, :to => :collection_radio, :options => true, :collection => true - map_type :check_boxes, :to => :collection_check_box, :options => true, :collection => true - - # With priority zones - map_type :country, :to => :country_select, :options => true, :with_priority => true - map_type :time_zone, :to => :time_zone_select, :options => true, :with_priority => true - - # Default boolean collection for use with selects/radios when no - # collection is given. Always fallback to this boolean collection. - # Texts can be translated using i18n in "simple_form.true" and - # "simple_form.false" keys. See the example locale file. - def self.boolean_collection - i18n_cache :boolean_collection do - [ [I18n.t(:"simple_form.yes", :default => 'Yes'), true], - [I18n.t(:"simple_form.no", :default => 'No'), false] ] - end - end - - # Generate the input through the mapped option. Apply correct behaviors - # for collections and add options whenever the input requires it. - def content - mapping = self.class.mappings[input_type] - raise "Invalid input type #{input_type.inspect}" unless mapping - - args = [ attribute_name ] - apply_with_priority_behavior(args) if mapping.with_priority - apply_collection_behavior(args) if mapping.collection - apply_options_behavior(args) if mapping.options - apply_html_options(args) - - @builder.send(mapping.method, *args) - end - - protected - - # Applies priority behavior to configured types. - def apply_with_priority_behavior(args) - priorities = options[:priority] || SimpleForm.send(:"#{input_type}_priority") - args.push(priorities) - end - - # Applies default collection behavior, mapping the default collection to - # boolean collection if it was not set, and defining default include_blank - # option - def apply_collection_behavior(args) - collection = (options[:collection] || self.class.boolean_collection).to_a - detect_collection_methods(collection, options) - args.push(collection, options[:value_method], options[:label_method]) - end - - # Apply default behavior for inputs that need extra options, such as date - # and time selects. - def apply_options_behavior(args) - options[:include_blank] = true unless skip_include_blank? - args << options - end - - # Adds default html options to the input based on db column information. - def apply_html_options(args) - html_options = component_html_options - - if column && [:string, :password, :decimal, :float].include?(input_type) - html_options[:maxlength] ||= column.limit - end - - args << html_options - end - - # Check if :include_blank must be included by default. - def skip_include_blank? - options.key?(:prompt) || options.key?(:include_blank) || options[:input_html].try(:[], :multiple) - end - - # Detect the right method to find the label and value for a collection. - # If no label or value method are defined, will attempt to find them based - # on default label and value methods that can be configured through - # SimpleForm.collection_label_methods and - # SimpleForm.collection_value_methods. - def detect_collection_methods(collection, options) - sample = collection.first || collection.last - - case sample - when Array - label, value = :first, :last - when Integer - label, value = :to_s, :to_i - when String, NilClass - label, value = :to_s, :to_s - end - - options[:label_method] ||= label || SimpleForm.collection_label_methods.find { |m| sample.respond_to?(m) } - options[:value_method] ||= value || SimpleForm.collection_value_methods.find { |m| sample.respond_to?(m) } - end - end - end -end diff --git a/lib/simple_form/components/wrapper.rb b/lib/simple_form/components/wrapper.rb deleted file mode 100644 index 2db6f2ab..00000000 --- a/lib/simple_form/components/wrapper.rb +++ /dev/null @@ -1,19 +0,0 @@ -module SimpleForm - module Components - # Wrapper component. The last the will be executed by default, responsible - # for wrapping the entire stack in a wrapper tag if it is configured. - class Wrapper < Base - include RequiredHelpers - - def call - tag = options[:wrapper] || SimpleForm.wrapper_tag - - if tag - template.content_tag(tag, @component.call, component_html_options) - else - @component.call - end - end - end - end -end diff --git a/lib/simple_form/form_builder.rb b/lib/simple_form/form_builder.rb index b2628cc9..0fe3e1cc 100644 --- a/lib/simple_form/form_builder.rb +++ b/lib/simple_form/form_builder.rb @@ -3,8 +3,6 @@ module SimpleForm attr_reader :template, :object_name, :object, :attribute_name, :column, :reflection, :input_type, :options - TERMINATOR = lambda { "" } - # Basic input helper, combines all components in the stack to generate # input html based on options the user define and some guesses through # database column information. By default a call to input will generate @@ -76,8 +74,6 @@ module SimpleForm end module ClassMethods #:nodoc: - include I18nCache - def translate_required_html i18n_cache :translate_required_html do I18n.t(:"simple_form.required.html", :default => @@ -102,20 +98,11 @@ module SimpleForm def label_text SimpleForm.label_text.call(raw_label_text, required_label_text) end - - # TODO Fix me + def label_target - case input_type - when :date, :datetime - "#{attribute_name}_1i" - when :time - "#{attribute_name}_4i" - else - attribute_name - end + attribute_name end - - # TODO Why default_css_options only in labels? + def label_html_options label_options = html_options_for(:label, input_type, required_class) label_options[:for] = options[:input_html][:id] if options.key?(:input_html) @@ -196,7 +183,9 @@ module SimpleForm end end - class Input + class Base + extend I18nCache + include Errors include Hints include Labels @@ -209,13 +198,38 @@ module SimpleForm end def input - SimpleForm::Components::Input.new(@builder, TERMINATOR).__content + raise NotImplemented + end + + def input_options + options[:include_blank] = true unless skip_include_blank? + options + end + + def input_html_options + html_options_for(:input, input_type, required_class) end def render pieces = SimpleForm.components.select { |n| n unless @builder.options[n] == false } - terminator = lambda { pieces.map!{ |p| send(p).to_s }.join } - SimpleForm::Components::Wrapper.new(@builder, terminator).call + content = pieces.map!{ |p| send(p).to_s }.join + wrap(content) + end + + def wrap(content) + if wrapper_tag && options[:wrapper] != false + template.content_tag(wrapper_tag, content, wrapper_html_options) + else + content + end + end + + def wrapper_tag + options[:wrapper_tag] || SimpleForm.wrapper_tag + end + + def wrapper_html_options + html_options_for(:wrapper, input_type, required_class) end protected @@ -239,6 +253,11 @@ module SimpleForm reflection ? reflection.name : attribute_name end + # Check if :include_blank must be included by default. + def skip_include_blank? + options.key?(:prompt) || options.key?(:include_blank) + end + # Retrieve options for the given namespace from the options hash def html_options_for(namespace, *extra) html_options = options[:"#{namespace}_html"] || {} @@ -286,9 +305,136 @@ module SimpleForm end end + # Uses MapType to handle basic input types. + class MappingInput < Base + extend MapType + + map_type :boolean, :to => :check_box + map_type :password, :to => :password_field + map_type :text, :to => :text_area + map_type :file, :to => :file_field + + def input + @builder.send(input_method, attribute_name, input_html_options) + end + + def input_method + method = self.class.mappings[input_type] + raise "Could not find method for #{input_type.inspect}" unless method + method + end + end + + # Handles common text field inputs, as String, Numeric, Float and Decimal. + class TextFieldInput < Base + def input + @builder.text_field(attribute_name, input_html_options) + end + + def input_html_options + input_options = super + input_options[:max_length] ||= column.limit if column + input_options + end + end + + class DateTimeInput < Base + def input + @builder.send(:"#{input_type}_select", attribute_name, input_options, input_html_options) + end + + def label_target + case input_type + when :date, :datetime + "#{attribute_name}_1i" + when :time + "#{attribute_name}_4i" + end + end + end + + class CollectionInput < Base + # Default boolean collection for use with selects/radios when no + # collection is given. Always fallback to this boolean collection. + # Texts can be translated using i18n in "simple_form.true" and + # "simple_form.false" keys. See the example locale file. + def self.boolean_collection + i18n_cache :boolean_collection do + [ [I18n.t(:"simple_form.yes", :default => 'Yes'), true], + [I18n.t(:"simple_form.no", :default => 'No'), false] ] + end + end + + def input + collection = (options[:collection] || self.class.boolean_collection).to_a + detect_collection_methods(collection, options) + @builder.send(:"collection_#{input_type}", attribute_name, collection, options[:value_method], + options[:label_method], input_options, input_html_options) + end + + protected + + def skip_include_blank? + super || options[:input_html].try(:[], :multiple) + end + + # Detect the right method to find the label and value for a collection. + # If no label or value method are defined, will attempt to find them based + # on default label and value methods that can be configured through + # SimpleForm.collection_label_methods and + # SimpleForm.collection_value_methods. + def detect_collection_methods(collection, options) + sample = collection.first || collection.last + + case sample + when Array + label, value = :first, :last + when Integer + label, value = :to_s, :to_i + when String, NilClass + label, value = :to_s, :to_s + end + + options[:label_method] ||= label || SimpleForm.collection_label_methods.find { |m| sample.respond_to?(m) } + options[:value_method] ||= value || SimpleForm.collection_value_methods.find { |m| sample.respond_to?(m) } + end + end + + # Handles hidden input. + class HiddenInput < Base + def render + @builder.hidden_field(attribute_name, input_html_options) + end + end + + class PriorityInput < Base + def input + @builder.send(:"#{input_type}_select", attribute_name, input_priority, + input_options, input_html_options) + end + + def input_priority + options[:priority] || SimpleForm.send(:"#{input_type}_priority") + end + end + + extend MapType + + map_type :boolean, :password, :text, :file, :to => MappingInput + map_type :hidden, :to => HiddenInput # TODO This should be automatic + map_type :string, :integer, :decimal, :float, :to => TextFieldInput + map_type :select, :radio, :check_boxes, :to => CollectionInput + map_type :date, :time, :datetime, :to => DateTimeInput + map_type :country, :time_zone, :to => PriorityInput + def input(attribute_name, options={}) define_simple_form_attributes(attribute_name, options) - Input.new(self).render + + if klass = self.class.mappings[input_type] + klass.new(self).render + else + const_get(:"#{input_type.to_s.camelize}Input").new(self).render + end end alias :attribute :input @@ -420,7 +566,7 @@ module SimpleForm # def error(attribute_name, options={}) define_simple_form_attributes(attribute_name, :error_html => options) - SimpleForm::Components::Error.new(self, TERMINATOR).call + Base.new(self).error end # Creates a hint tag for the given attribute. Accepts a symbol indicating @@ -436,7 +582,7 @@ module SimpleForm def hint(attribute_name, options={}) attribute_name, options[:hint] = nil, attribute_name if attribute_name.is_a?(String) define_simple_form_attributes(attribute_name, :hint => options.delete(:hint), :hint_html => options) - SimpleForm::Components::Hint.new(self, TERMINATOR).call + Base.new(self).hint end # Creates a default label tag for the given attribute. You can give a label @@ -457,7 +603,7 @@ module SimpleForm options = args.extract_options! define_simple_form_attributes(attribute_name, :label => options.delete(:label), :label_html => options, :required => options.delete(:required)) - SimpleForm::Components::Label.new(self, TERMINATOR).call + Base.new(self).label end private diff --git a/lib/simple_form/map_type.rb b/lib/simple_form/map_type.rb index 6cfa7843..3cb8d3e1 100644 --- a/lib/simple_form/map_type.rb +++ b/lib/simple_form/map_type.rb @@ -1,6 +1,4 @@ module SimpleForm - Mapping = Struct.new(:method, :collection, :options, :with_priority) - module MapType def mappings @mappings ||= {} @@ -9,9 +7,7 @@ module SimpleForm def map_type(*types) options = types.extract_options! raise ArgumentError, "You need to give :to as option to map_type" unless options[:to] - mapping = Mapping.new(options[:to], options[:collection] || false, - options[:options] || false, options[:with_priority] || false) - types.each { |t| mappings[t] = mapping } + types.each { |t| mappings[t] = options[:to] } end end end \ No newline at end of file diff --git a/test/action_view_extensions/builder_test.rb b/test/action_view_extensions/builder_test.rb index 0e228b2b..ac182c16 100644 --- a/test/action_view_extensions/builder_test.rb +++ b/test/action_view_extensions/builder_test.rb @@ -63,7 +63,7 @@ class BuilderTest < ActionView::TestCase test 'collection check box accepts a collection and generate a serie of checkboxes for value method' do collection = [Tag.new(1, 'Tag 1'), Tag.new(2, 'Tag 2')] form_for @user do |f| - concat f.collection_check_box :tag_ids, collection, :id, :name + concat f.collection_check_boxes :tag_ids, collection, :id, :name end assert_select "form input[type=hidden][name='user[tag_ids][]'][value=]" @@ -74,7 +74,7 @@ class BuilderTest < ActionView::TestCase test 'collection check box accepts a collection and generate a serie of checkboxes with labels for label method' do collection = [Tag.new(1, 'Tag 1'), Tag.new(2, 'Tag 2')] form_for @user do |f| - concat f.collection_check_box :tag_ids, collection, :id, :name + concat f.collection_check_boxes :tag_ids, collection, :id, :name end assert_select 'form label.collection_check_box[for=user_tag_ids_1]', 'Tag 1' @@ -84,7 +84,7 @@ class BuilderTest < ActionView::TestCase test 'collection check box accepts selected values as :checked option' do collection = (1..3).map{|i| [i, "Tag #{i}"] } form_for @user do |f| - concat f.collection_check_box :tag_ids, collection, :first, :last, :checked => [1, 3] + concat f.collection_check_boxes :tag_ids, collection, :first, :last, :checked => [1, 3] end assert_select 'form input[type=checkbox][value=1][checked=checked]' @@ -95,7 +95,7 @@ class BuilderTest < ActionView::TestCase test 'collection check box accepts a single checked value' do collection = (1..3).map{|i| [i, "Tag #{i}"] } form_for @user do |f| - concat f.collection_check_box :tag_ids, collection, :first, :last, :checked => 3 + concat f.collection_check_boxes :tag_ids, collection, :first, :last, :checked => 3 end assert_select 'form input[type=checkbox][value=3][checked=checked]' @@ -106,7 +106,7 @@ class BuilderTest < ActionView::TestCase test 'collection check box accepts multiple disabled items' do collection = (1..3).map{|i| [i, "Tag #{i}"] } form_for @user do |f| - concat f.collection_check_box :tag_ids, collection, :first, :last, :disabled => [1, 3] + concat f.collection_check_boxes :tag_ids, collection, :first, :last, :disabled => [1, 3] end assert_select 'form input[type=checkbox][value=1][disabled=disabled]' @@ -117,7 +117,7 @@ class BuilderTest < ActionView::TestCase test 'collection check box accepts single disable item' do collection = (1..3).map{|i| [i, "Tag #{i}"] } form_for @user do |f| - concat f.collection_check_box :tag_ids, collection, :first, :last, :disabled => 1 + concat f.collection_check_boxes :tag_ids, collection, :first, :last, :disabled => 1 end assert_select 'form input[type=checkbox][value=1][disabled=disabled]' @@ -128,7 +128,7 @@ class BuilderTest < ActionView::TestCase test 'collection check box accepts a proc to disabled items' do collection = (1..3).map{|i| [i, "Tag #{i}"] } form_for @user do |f| - concat f.collection_check_box :tag_ids, collection, :first, :last, :disabled => proc { |i| i.first == 1 } + concat f.collection_check_boxes :tag_ids, collection, :first, :last, :disabled => proc { |i| i.first == 1 } end assert_select 'form input[type=checkbox][value=1][disabled=disabled]' @@ -139,7 +139,7 @@ class BuilderTest < ActionView::TestCase test 'collection check box accepts html options' do collection = [[1, 'Tag 1'], [2, 'Tag 2']] form_for @user do |f| - concat f.collection_check_box :tag_ids, collection, :first, :last, {}, :class => 'check' + concat f.collection_check_boxes :tag_ids, collection, :first, :last, {}, :class => 'check' end assert_select 'form input.check[type=checkbox][value=1]' diff --git a/test/components/error_test.rb b/test/components/error_test.rb index 1d6a6180..7bdb9758 100644 --- a/test/components/error_test.rb +++ b/test/components/error_test.rb @@ -9,7 +9,7 @@ class ErrorTest < ActionView::TestCase f.input_type = type f.options = options - concat(SimpleForm::FormBuilder::Input.new(f).error) + concat(SimpleForm::FormBuilder::Base.new(f).error) end end diff --git a/test/components/hint_test.rb b/test/components/hint_test.rb index c2e9719c..78012c57 100644 --- a/test/components/hint_test.rb +++ b/test/components/hint_test.rb @@ -9,7 +9,7 @@ class HintTest < ActionView::TestCase f.input_type = type f.options = options - concat(SimpleForm::FormBuilder::Input.new(f).hint) + concat(SimpleForm::FormBuilder::Base.new(f).hint) end end diff --git a/test/components/input_test.rb b/test/components/input_test.rb index 6e141f78..afff87b4 100644 --- a/test/components/input_test.rb +++ b/test/components/input_test.rb @@ -5,327 +5,327 @@ class InputTest < ActionView::TestCase setup do SimpleForm::Components::Input.reset_i18n_cache :boolean_collection end - - def with_input_for(object, attribute_name, type, options={}) - simple_form_for object do |f| - f.attribute_name = attribute_name - f.column = object.column_for_attribute(attribute_name) if object.respond_to?(:column_for_attribute) - f.input_type = type - f.options = options - - input = SimpleForm::Components::Input.new(f, SimpleForm::FormBuilder::TERMINATOR) - concat(input.call) - yield input if block_given? - end - end - - test 'input should map text field to string attribute' do - with_input_for @user, :name, :string - assert_select 'input[name=\'user[name]\'][id=user_name][value=New in Simple Form!]' - end - - test 'input should generate css class based on default input type' do - with_input_for @user, :name, :string - assert_select 'input.string' - with_input_for @user, :description, :text - assert_select 'textarea.text' - with_input_for @user, :age, :integer - assert_select 'input.integer' - with_input_for @user, :born_at, :date - assert_select 'select.date' - with_input_for @user, :created_at, :datetime - assert_select 'select.datetime' - end - - test 'input should allow passing options to text field' do - with_input_for @user, :name, :string, :input_html => { :class => 'my_input', :id => 'my_input' } - assert_select 'input#my_input.my_input' - end - - test 'input should generate a text area for text attributes' do - with_input_for @user, :description, :text - assert_select 'textarea.text#user_description' - end - - test 'input should generate an integer text field for integer attributes ' do - with_input_for @user, :age, :integer - assert_select 'input.integer#user_age' - end - - test 'input should generate a float text field for float attributes ' do - with_input_for @user, :age, :float - assert_select 'input.float#user_age' - end - - test 'input should generate a decimal text field for decimal attributes ' do - with_input_for @user, :age, :decimal - assert_select 'input.decimal#user_age' - end - - test 'input should generate a checkbox by default for boolean attributes' do - with_input_for @user, :active, :boolean - assert_select 'input[type=checkbox].boolean#user_active' - end - - test 'input should generate a password field for password attributes' do - with_input_for @user, :password, :password - assert_select 'input[type=password].password#user_password' - end - - test 'input should generate a hidden field' do - with_input_for @user, :name, :hidden - assert_no_select 'input[type=text]' - assert_select 'input#user_name[type=hidden]' - end - - test 'input should generate a file field' do - with_input_for @user, :name, :file - assert_select 'input#user_name[type=file]' - end - - test 'input should generate a country select field' do - with_input_for @user, :country, :country - assert_select 'select#user_country' - assert_select 'select option[value=Brazil]', 'Brazil' - assert_no_select 'select option[value=][disabled=disabled]' - end - - test 'input should generate a country select with simple form default' do - swap SimpleForm, :country_priority => [ 'Brazil' ] do - with_input_for @user, :country, :country - assert_select 'select option[value=][disabled=disabled]' - end - end - - test 'input should generate a time zone select field' do - with_input_for @user, :time_zone, :time_zone - assert_select 'select#user_time_zone' - assert_select 'select option[value=Brasilia]', '(GMT-03:00) Brasilia' - assert_no_select 'select option[value=][disabled=disabled]' - end - - test 'input should generate a time zone select field with default' do - with_input_for @user, :time_zone, :time_zone, :default => 'Brasilia' - assert_select 'select option[value=Brasilia][selected=selected]' - end - - test 'input should generate a time zone select using options priority' do - with_input_for @user, :time_zone, :time_zone, :priority => /Brasilia/ - assert_select 'select option[value=][disabled=disabled]' - end - - test 'input should generate a datetime select by default for datetime attributes' do - with_input_for @user, :created_at, :datetime - 1.upto(5) do |i| - assert_select "form select.datetime#user_created_at_#{i}i" - end - end - - test 'input should be able to pass options to datetime select' do - with_input_for @user, :created_at, :datetime, - :disabled => true, :prompt => { :year => 'ano', :month => 'mês', :day => 'dia' } - - assert_select 'select.datetime[disabled=disabled]' - assert_select 'select.datetime option', 'ano' - assert_select 'select.datetime option', 'mês' - assert_select 'select.datetime option', 'dia' - end - - test 'input should generate a date select for date attributes' do - with_input_for @user, :born_at, :date - assert_select 'select.date#user_born_at_1i' - assert_select 'select.date#user_born_at_2i' - assert_select 'select.date#user_born_at_3i' - assert_no_select 'select.date#user_born_at_4i' - end - - test 'input should be able to pass options to date select' do - with_input_for @user, :born_at, :date, :as => :date, - :disabled => true, :prompt => { :year => 'ano', :month => 'mês', :day => 'dia' } - - assert_select 'select.date[disabled=disabled]' - assert_select 'select.date option', 'ano' - assert_select 'select.date option', 'mês' - assert_select 'select.date option', 'dia' - end - - test 'input should generate a time select for time attributes' do - with_input_for @user, :delivery_time, :time - assert_select 'input[type=hidden]#user_delivery_time_1i' - assert_select 'input[type=hidden]#user_delivery_time_2i' - assert_select 'input[type=hidden]#user_delivery_time_3i' - assert_select 'select.time#user_delivery_time_4i' - assert_select 'select.time#user_delivery_time_5i' - end - - test 'input should be able to pass options to time select' do - with_input_for @user, :delivery_time, :time, :required => true, - :disabled => true, :prompt => { :hour => 'hora', :minute => 'minuto' } - - assert_select 'select.time[disabled=disabled]' - assert_select 'select.time option', 'hora' - assert_select 'select.time option', 'minuto' - end - - test 'input should generate boolean radio buttons by default for radio types' do - with_input_for @user, :active, :radio - assert_select 'input[type=radio][value=true].radio#user_active_true' - assert_select 'input[type=radio][value=false].radio#user_active_false' - end - - test 'input as radio should generate internal labels by default' do - with_input_for @user, :active, :radio - assert_select 'label[for=user_active_true]', 'Yes' - assert_select 'label[for=user_active_false]', 'No' - end - - test 'input as radio should use i18n to translate internal labels' do - store_translations(:en, :simple_form => { :yes => 'Sim', :no => 'Não' }) do - with_input_for @user, :active, :radio - assert_select 'label[for=user_active_true]', 'Sim' - assert_select 'label[for=user_active_false]', 'Não' - end - end - - test 'input should generate a boolean select with options by default for select types' do - with_input_for @user, :active, :select - assert_select 'select.select#user_active' - assert_select 'select option[value=true]', 'Yes' - assert_select 'select option[value=false]', 'No' - end - - test 'input as select should use i18n to translate select boolean options' do - store_translations(:en, :simple_form => { :yes => 'Sim', :no => 'Não' }) do - with_input_for @user, :active, :select - assert_select 'select option[value=true]', 'Sim' - assert_select 'select option[value=false]', 'Não' - end - end - - test 'input should allow overriding collection for select types' do - with_input_for @user, :name, :select, :collection => ['Jose', 'Carlos'] - assert_select 'select.select#user_name' - assert_select 'select option', 'Jose' - assert_select 'select option', 'Carlos' - end - - test 'input should mark the selected value by default' do - @user.name = "Carlos" - with_input_for @user, :name, :select, :collection => ['Jose', 'Carlos'] - assert_select 'select option[selected=selected]', 'Carlos' - end - - test 'input should mark the selected value also when using integers' do - @user.age = 18 - with_input_for @user, :age, :select, :collection => 18..60 - assert_select 'select option[selected=selected]', '18' - end - - test 'input should automatically set include blank' do - with_input_for @user, :age, :select, :collection => 18..30 - assert_select 'select option[value=]', '' - end - - test 'input should not set include blank if otherwise is told' do - with_input_for @user, :age, :select, :collection => 18..30, :include_blank => false - assert_no_select 'select option[value=]', '' - end - - test 'input should not set include blank if prompt is given' do - with_input_for @user, :age, :select, :collection => 18..30, :prompt => "Please select foo" - assert_no_select 'select option[value=]', '' - end - - test 'input should not set include blank if multiple is given' do - with_input_for @user, :age, :select, :collection => 18..30, :input_html => { :multiple => true } - assert_no_select 'select option[value=]', '' - end - - test 'input should detect label and value on collections' do - users = [ setup_new_user(:id => 1, :name => "Jose"), setup_new_user(:id => 2, :name => "Carlos") ] - with_input_for @user, :description, :select, :collection => users - assert_select 'select option[value=1]', 'Jose' - assert_select 'select option[value=2]', 'Carlos' - end - - test 'input should allow overriding collection for radio types' do - with_input_for @user, :name, :radio, :collection => ['Jose', 'Carlos'] - assert_select 'input[type=radio][value=Jose]' - assert_select 'input[type=radio][value=Carlos]' - assert_select 'label.collection_radio', 'Jose' - assert_select 'label.collection_radio', 'Carlos' - end - - test 'input should mark the current radio value by default' do - @user.name = "Carlos" - with_input_for @user, :name, :radio, :collection => ['Jose', 'Carlos'] - assert_select 'input[type=radio][value=Carlos][checked=checked]' - end - - test 'input should allow using a collection with text/value arrays' do - with_input_for @user, :name, :radio, :collection => [['Jose', 'jose'], ['Carlos', 'carlos']] - assert_select 'input[type=radio][value=jose]' - assert_select 'input[type=radio][value=carlos]' - assert_select 'label.collection_radio', 'Jose' - assert_select 'label.collection_radio', 'Carlos' - end - - test 'input should allow overriding label and value method for collections' do - with_input_for @user, :name, :radio, - :collection => ['Jose' , 'Carlos'], - :label_method => :upcase, - :value_method => :downcase - assert_select 'input[type=radio][value=jose]' - assert_select 'input[type=radio][value=carlos]' - assert_select 'label.collection_radio', 'JOSE' - assert_select 'label.collection_radio', 'CARLOS' - end - - test 'input should be required by default' do - with_input_for @user, :name, :string - assert_select 'input.required#user_name' - end - - test 'input should allow disabling required' do - with_input_for @user, :name, :string, :required => false - assert_no_select 'input.required' - assert_select 'input.optional#user_name' - end - - test 'input should get options from column definition for string attributes' do - with_input_for @user, :name, :string - assert_select 'input.string[maxlength=100]' - end - - test 'input should get options from column definition for decimal attributes' do - with_input_for @user, :credit_limit, :decimal - assert_select 'input.decimal[maxlength=15]' - end - - test 'input should get options from column definition for password attributes' do - with_input_for @user, :password, :password - assert_select 'input.password[maxlength=100]' - end - - test 'input should not generate options for different attributes' do - with_input_for @user, :description, :text - assert_select 'textarea' - assert_no_select 'textarea[maxlength]' - end - - test 'input should be generated properly when object is not present' do - with_input_for :project, :name, :string - assert_select 'input.string.required#project_name' - end - - test 'input as radio should be generated properly when object is not present ' do - with_input_for :project, :name, :radio - assert_select 'input.radio#project_name_true' - assert_select 'input.radio#project_name_false' - end - - test 'input as select with collection should be generated properly when object is not present' do - with_input_for :project, :name, :select, :collection => ['Jose', 'Carlos'] - assert_select 'select.select#project_name' - end + # + # def with_input_for(object, attribute_name, type, options={}) + # simple_form_for object do |f| + # f.attribute_name = attribute_name + # f.column = object.column_for_attribute(attribute_name) if object.respond_to?(:column_for_attribute) + # f.input_type = type + # f.options = options + # + # input = SimpleForm::Components::Input.new(f, SimpleForm::FormBuilder::TERMINATOR) + # concat(input.call) + # yield input if block_given? + # end + # end + # + # test 'input should map text field to string attribute' do + # with_input_for @user, :name, :string + # assert_select 'input[name=\'user[name]\'][id=user_name][value=New in Simple Form!]' + # end + # + # test 'input should generate css class based on default input type' do + # with_input_for @user, :name, :string + # assert_select 'input.string' + # with_input_for @user, :description, :text + # assert_select 'textarea.text' + # with_input_for @user, :age, :integer + # assert_select 'input.integer' + # with_input_for @user, :born_at, :date + # assert_select 'select.date' + # with_input_for @user, :created_at, :datetime + # assert_select 'select.datetime' + # end + # + # test 'input should allow passing options to text field' do + # with_input_for @user, :name, :string, :input_html => { :class => 'my_input', :id => 'my_input' } + # assert_select 'input#my_input.my_input' + # end + # + # test 'input should generate a text area for text attributes' do + # with_input_for @user, :description, :text + # assert_select 'textarea.text#user_description' + # end + # + # test 'input should generate an integer text field for integer attributes ' do + # with_input_for @user, :age, :integer + # assert_select 'input.integer#user_age' + # end + # + # test 'input should generate a float text field for float attributes ' do + # with_input_for @user, :age, :float + # assert_select 'input.float#user_age' + # end + # + # test 'input should generate a decimal text field for decimal attributes ' do + # with_input_for @user, :age, :decimal + # assert_select 'input.decimal#user_age' + # end + # + # test 'input should generate a checkbox by default for boolean attributes' do + # with_input_for @user, :active, :boolean + # assert_select 'input[type=checkbox].boolean#user_active' + # end + # + # test 'input should generate a password field for password attributes' do + # with_input_for @user, :password, :password + # assert_select 'input[type=password].password#user_password' + # end + # + # test 'input should generate a hidden field' do + # with_input_for @user, :name, :hidden + # assert_no_select 'input[type=text]' + # assert_select 'input#user_name[type=hidden]' + # end + # + # test 'input should generate a file field' do + # with_input_for @user, :name, :file + # assert_select 'input#user_name[type=file]' + # end + # + # test 'input should generate a country select field' do + # with_input_for @user, :country, :country + # assert_select 'select#user_country' + # assert_select 'select option[value=Brazil]', 'Brazil' + # assert_no_select 'select option[value=][disabled=disabled]' + # end + # + # test 'input should generate a country select with simple form default' do + # swap SimpleForm, :country_priority => [ 'Brazil' ] do + # with_input_for @user, :country, :country + # assert_select 'select option[value=][disabled=disabled]' + # end + # end + # + # test 'input should generate a time zone select field' do + # with_input_for @user, :time_zone, :time_zone + # assert_select 'select#user_time_zone' + # assert_select 'select option[value=Brasilia]', '(GMT-03:00) Brasilia' + # assert_no_select 'select option[value=][disabled=disabled]' + # end + # + # test 'input should generate a time zone select field with default' do + # with_input_for @user, :time_zone, :time_zone, :default => 'Brasilia' + # assert_select 'select option[value=Brasilia][selected=selected]' + # end + # + # test 'input should generate a time zone select using options priority' do + # with_input_for @user, :time_zone, :time_zone, :priority => /Brasilia/ + # assert_select 'select option[value=][disabled=disabled]' + # end + # + # test 'input should generate a datetime select by default for datetime attributes' do + # with_input_for @user, :created_at, :datetime + # 1.upto(5) do |i| + # assert_select "form select.datetime#user_created_at_#{i}i" + # end + # end + # + # test 'input should be able to pass options to datetime select' do + # with_input_for @user, :created_at, :datetime, + # :disabled => true, :prompt => { :year => 'ano', :month => 'mês', :day => 'dia' } + # + # assert_select 'select.datetime[disabled=disabled]' + # assert_select 'select.datetime option', 'ano' + # assert_select 'select.datetime option', 'mês' + # assert_select 'select.datetime option', 'dia' + # end + # + # test 'input should generate a date select for date attributes' do + # with_input_for @user, :born_at, :date + # assert_select 'select.date#user_born_at_1i' + # assert_select 'select.date#user_born_at_2i' + # assert_select 'select.date#user_born_at_3i' + # assert_no_select 'select.date#user_born_at_4i' + # end + # + # test 'input should be able to pass options to date select' do + # with_input_for @user, :born_at, :date, :as => :date, + # :disabled => true, :prompt => { :year => 'ano', :month => 'mês', :day => 'dia' } + # + # assert_select 'select.date[disabled=disabled]' + # assert_select 'select.date option', 'ano' + # assert_select 'select.date option', 'mês' + # assert_select 'select.date option', 'dia' + # end + # + # test 'input should generate a time select for time attributes' do + # with_input_for @user, :delivery_time, :time + # assert_select 'input[type=hidden]#user_delivery_time_1i' + # assert_select 'input[type=hidden]#user_delivery_time_2i' + # assert_select 'input[type=hidden]#user_delivery_time_3i' + # assert_select 'select.time#user_delivery_time_4i' + # assert_select 'select.time#user_delivery_time_5i' + # end + # + # test 'input should be able to pass options to time select' do + # with_input_for @user, :delivery_time, :time, :required => true, + # :disabled => true, :prompt => { :hour => 'hora', :minute => 'minuto' } + # + # assert_select 'select.time[disabled=disabled]' + # assert_select 'select.time option', 'hora' + # assert_select 'select.time option', 'minuto' + # end + # + # test 'input should generate boolean radio buttons by default for radio types' do + # with_input_for @user, :active, :radio + # assert_select 'input[type=radio][value=true].radio#user_active_true' + # assert_select 'input[type=radio][value=false].radio#user_active_false' + # end + # + # test 'input as radio should generate internal labels by default' do + # with_input_for @user, :active, :radio + # assert_select 'label[for=user_active_true]', 'Yes' + # assert_select 'label[for=user_active_false]', 'No' + # end + # + # test 'input as radio should use i18n to translate internal labels' do + # store_translations(:en, :simple_form => { :yes => 'Sim', :no => 'Não' }) do + # with_input_for @user, :active, :radio + # assert_select 'label[for=user_active_true]', 'Sim' + # assert_select 'label[for=user_active_false]', 'Não' + # end + # end + # + # test 'input should generate a boolean select with options by default for select types' do + # with_input_for @user, :active, :select + # assert_select 'select.select#user_active' + # assert_select 'select option[value=true]', 'Yes' + # assert_select 'select option[value=false]', 'No' + # end + # + # test 'input as select should use i18n to translate select boolean options' do + # store_translations(:en, :simple_form => { :yes => 'Sim', :no => 'Não' }) do + # with_input_for @user, :active, :select + # assert_select 'select option[value=true]', 'Sim' + # assert_select 'select option[value=false]', 'Não' + # end + # end + # + # test 'input should allow overriding collection for select types' do + # with_input_for @user, :name, :select, :collection => ['Jose', 'Carlos'] + # assert_select 'select.select#user_name' + # assert_select 'select option', 'Jose' + # assert_select 'select option', 'Carlos' + # end + # + # test 'input should mark the selected value by default' do + # @user.name = "Carlos" + # with_input_for @user, :name, :select, :collection => ['Jose', 'Carlos'] + # assert_select 'select option[selected=selected]', 'Carlos' + # end + # + # test 'input should mark the selected value also when using integers' do + # @user.age = 18 + # with_input_for @user, :age, :select, :collection => 18..60 + # assert_select 'select option[selected=selected]', '18' + # end + # + # test 'input should automatically set include blank' do + # with_input_for @user, :age, :select, :collection => 18..30 + # assert_select 'select option[value=]', '' + # end + # + # test 'input should not set include blank if otherwise is told' do + # with_input_for @user, :age, :select, :collection => 18..30, :include_blank => false + # assert_no_select 'select option[value=]', '' + # end + # + # test 'input should not set include blank if prompt is given' do + # with_input_for @user, :age, :select, :collection => 18..30, :prompt => "Please select foo" + # assert_no_select 'select option[value=]', '' + # end + # + # test 'input should not set include blank if multiple is given' do + # with_input_for @user, :age, :select, :collection => 18..30, :input_html => { :multiple => true } + # assert_no_select 'select option[value=]', '' + # end + # + # test 'input should detect label and value on collections' do + # users = [ setup_new_user(:id => 1, :name => "Jose"), setup_new_user(:id => 2, :name => "Carlos") ] + # with_input_for @user, :description, :select, :collection => users + # assert_select 'select option[value=1]', 'Jose' + # assert_select 'select option[value=2]', 'Carlos' + # end + # + # test 'input should allow overriding collection for radio types' do + # with_input_for @user, :name, :radio, :collection => ['Jose', 'Carlos'] + # assert_select 'input[type=radio][value=Jose]' + # assert_select 'input[type=radio][value=Carlos]' + # assert_select 'label.collection_radio', 'Jose' + # assert_select 'label.collection_radio', 'Carlos' + # end + # + # test 'input should mark the current radio value by default' do + # @user.name = "Carlos" + # with_input_for @user, :name, :radio, :collection => ['Jose', 'Carlos'] + # assert_select 'input[type=radio][value=Carlos][checked=checked]' + # end + # + # test 'input should allow using a collection with text/value arrays' do + # with_input_for @user, :name, :radio, :collection => [['Jose', 'jose'], ['Carlos', 'carlos']] + # assert_select 'input[type=radio][value=jose]' + # assert_select 'input[type=radio][value=carlos]' + # assert_select 'label.collection_radio', 'Jose' + # assert_select 'label.collection_radio', 'Carlos' + # end + # + # test 'input should allow overriding label and value method for collections' do + # with_input_for @user, :name, :radio, + # :collection => ['Jose' , 'Carlos'], + # :label_method => :upcase, + # :value_method => :downcase + # assert_select 'input[type=radio][value=jose]' + # assert_select 'input[type=radio][value=carlos]' + # assert_select 'label.collection_radio', 'JOSE' + # assert_select 'label.collection_radio', 'CARLOS' + # end + # + # test 'input should be required by default' do + # with_input_for @user, :name, :string + # assert_select 'input.required#user_name' + # end + # + # test 'input should allow disabling required' do + # with_input_for @user, :name, :string, :required => false + # assert_no_select 'input.required' + # assert_select 'input.optional#user_name' + # end + # + # test 'input should get options from column definition for string attributes' do + # with_input_for @user, :name, :string + # assert_select 'input.string[maxlength=100]' + # end + # + # test 'input should get options from column definition for decimal attributes' do + # with_input_for @user, :credit_limit, :decimal + # assert_select 'input.decimal[maxlength=15]' + # end + # + # test 'input should get options from column definition for password attributes' do + # with_input_for @user, :password, :password + # assert_select 'input.password[maxlength=100]' + # end + # + # test 'input should not generate options for different attributes' do + # with_input_for @user, :description, :text + # assert_select 'textarea' + # assert_no_select 'textarea[maxlength]' + # end + # + # test 'input should be generated properly when object is not present' do + # with_input_for :project, :name, :string + # assert_select 'input.string.required#project_name' + # end + # + # test 'input as radio should be generated properly when object is not present ' do + # with_input_for :project, :name, :radio + # assert_select 'input.radio#project_name_true' + # assert_select 'input.radio#project_name_false' + # end + # + # test 'input as select with collection should be generated properly when object is not present' do + # with_input_for :project, :name, :select, :collection => ['Jose', 'Carlos'] + # assert_select 'select.select#project_name' + # end end diff --git a/test/components/label_test.rb b/test/components/label_test.rb index 70085346..e4038b6d 100644 --- a/test/components/label_test.rb +++ b/test/components/label_test.rb @@ -3,7 +3,7 @@ require 'test_helper' class LabelTest < ActionView::TestCase setup do - SimpleForm::FormBuilder::Input.reset_i18n_cache :translate_required_html + SimpleForm::FormBuilder::Base.reset_i18n_cache :translate_required_html end def with_label_for(object, attribute_name, type, options={}) @@ -13,7 +13,7 @@ class LabelTest < ActionView::TestCase f.input_type = type f.options = options - concat(SimpleForm::FormBuilder::Input.new(f).label) + concat(SimpleForm::FormBuilder::Base.new(f).label) end end From 7d7c6cda729953ece75c45cb6699aa7f9dd26b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 9 Jan 2010 16:05:02 +0100 Subject: [PATCH 3/7] Code refactoring finished. Now tests one. --- lib/simple_form.rb | 18 +- lib/simple_form/components.rb | 4 + lib/simple_form/components/errors.rb | 35 ++ lib/simple_form/components/hints.rb | 21 + lib/simple_form/components/labels.rb | 68 +++ lib/simple_form/components/wrapper.rb | 21 + lib/simple_form/form_builder.rb | 374 +----------- lib/simple_form/inputs.rb | 11 + lib/simple_form/inputs/base.rb | 113 ++++ lib/simple_form/inputs/collection_input.rb | 50 ++ lib/simple_form/inputs/date_time_input.rb | 18 + lib/simple_form/inputs/hidden_input.rb | 11 + lib/simple_form/inputs/mapping_input.rb | 23 + lib/simple_form/inputs/priority_input.rb | 14 + lib/simple_form/inputs/text_field_input.rb | 16 + lib/simple_form/required_helpers.rb | 26 - test/components/error_test.rb | 2 +- test/components/hint_test.rb | 2 +- test/components/input_test.rb | 648 ++++++++++----------- test/components/label_test.rb | 4 +- 20 files changed, 754 insertions(+), 725 deletions(-) create mode 100644 lib/simple_form/components/errors.rb create mode 100644 lib/simple_form/components/hints.rb create mode 100644 lib/simple_form/components/labels.rb create mode 100644 lib/simple_form/components/wrapper.rb create mode 100644 lib/simple_form/inputs.rb create mode 100644 lib/simple_form/inputs/base.rb create mode 100644 lib/simple_form/inputs/collection_input.rb create mode 100644 lib/simple_form/inputs/date_time_input.rb create mode 100644 lib/simple_form/inputs/hidden_input.rb create mode 100644 lib/simple_form/inputs/mapping_input.rb create mode 100644 lib/simple_form/inputs/priority_input.rb create mode 100644 lib/simple_form/inputs/text_field_input.rb delete mode 100644 lib/simple_form/required_helpers.rb diff --git a/lib/simple_form.rb b/lib/simple_form.rb index cc3af6a6..cabb4856 100644 --- a/lib/simple_form.rb +++ b/lib/simple_form.rb @@ -3,17 +3,17 @@ require 'simple_form/action_view_extensions/builder' require 'simple_form/action_view_extensions/instance_tag' module SimpleForm - autoload :Components, 'simple_form/components' - autoload :FormBuilder, 'simple_form/form_builder' - autoload :I18nCache, 'simple_form/i18n_cache' - autoload :MapType, 'simple_form/map_type' - autoload :RequiredHelpers, 'simple_form/required_helpers' + autoload :Components, 'simple_form/components' + autoload :FormBuilder, 'simple_form/form_builder' + autoload :I18nCache, 'simple_form/i18n_cache' + autoload :Inputs, 'simple_form/inputs' + autoload :MapType, 'simple_form/map_type' - # Default tag used in hints + # Default tag used in hints. mattr_accessor :hint_tag @@hint_tag = :span - # Default tag used in errors + # Default tag used in errors. mattr_accessor :error_tag @@error_tag = :span @@ -21,11 +21,11 @@ module SimpleForm mattr_accessor :components @@components = [ :label, :input, :hint, :error ] - # Series of attemps to detect a default label method for collection + # Series of attemps to detect a default label method for collection. mattr_accessor :collection_label_methods @@collection_label_methods = [ :to_label, :name, :title, :to_s ] - # Series of attemps to detect a default value method for collection + # Series of attemps to detect a default value method for collection. mattr_accessor :collection_value_methods @@collection_value_methods = [ :id, :to_s ] diff --git a/lib/simple_form/components.rb b/lib/simple_form/components.rb index f0f27f08..0588aeeb 100644 --- a/lib/simple_form/components.rb +++ b/lib/simple_form/components.rb @@ -1,4 +1,8 @@ module SimpleForm module Components + autoload :Errors, 'simple_form/components/errors' + autoload :Hints, 'simple_form/components/hints' + autoload :Labels, 'simple_form/components/labels' + autoload :Wrapper, 'simple_form/components/wrapper' end end \ No newline at end of file diff --git a/lib/simple_form/components/errors.rb b/lib/simple_form/components/errors.rb new file mode 100644 index 00000000..09c70e44 --- /dev/null +++ b/lib/simple_form/components/errors.rb @@ -0,0 +1,35 @@ +module SimpleForm + module Components + module Errors + def error + template.content_tag(error_tag, error_text, error_html_options) if object && errors.present? + end + + def error_tag + options[:error_tag] || SimpleForm.error_tag + end + + def error_text + errors.to_sentence + end + + def error_html_options + html_options_for(:error, :error) + end + + protected + + def errors + @errors ||= (errors_on_attribute + errors_on_association).compact + end + + def errors_on_attribute + Array(object.errors[attribute_name]) + end + + def errors_on_association + reflection ? Array(object.errors[reflection.name]) : [] + end + end + end +end \ No newline at end of file diff --git a/lib/simple_form/components/hints.rb b/lib/simple_form/components/hints.rb new file mode 100644 index 00000000..ebcee571 --- /dev/null +++ b/lib/simple_form/components/hints.rb @@ -0,0 +1,21 @@ +module SimpleForm + module Components + module Hints + def hint + template.content_tag(hint_tag, hint_text, hint_html_options) unless hint_text.blank? + end + + def hint_tag + options[:hint_tag] || SimpleForm.hint_tag + end + + def hint_text + @hint_text ||= options[:hint] || translate(:hints) + end + + def hint_html_options + html_options_for(:hint, :hint) + end + end + end +end \ No newline at end of file diff --git a/lib/simple_form/components/labels.rb b/lib/simple_form/components/labels.rb new file mode 100644 index 00000000..26898fdf --- /dev/null +++ b/lib/simple_form/components/labels.rb @@ -0,0 +1,68 @@ +module SimpleForm + module Components + module Labels + def self.included(base) + base.extend ClassMethods + end + + module ClassMethods #:nodoc: + def translate_required_html + i18n_cache :translate_required_html do + I18n.t(:"simple_form.required.html", :default => + %[#{translate_required_mark}] + ) + end + end + + def translate_required_text + I18n.t(:"simple_form.required.text", :default => 'required') + end + + def translate_required_mark + I18n.t(:"simple_form.required.mark", :default => '*') + end + end + + def label + @builder.label(label_target, label_text, label_html_options) + end + + def label_text + SimpleForm.label_text.call(raw_label_text, required_label_text) + end + + def label_target + attribute_name + end + + def label_html_options + label_options = html_options_for(:label, input_type, required_class) + label_options[:for] = options[:input_html][:id] if options.key?(:input_html) + label_options + end + + protected + + def raw_label_text #:nodoc: + options[:label] || label_translation + end + + # Default required text when attribute is required. + def required_label_text #:nodoc: + attribute_required? ? self.class.translate_required_html.dup : '' + end + + # First check human attribute name and then labels. + # TODO Remove me in Rails > 2.3.5 + def label_translation #:nodoc: + default = if object.class.respond_to?(:human_attribute_name) + object.class.human_attribute_name(reflection_or_attribute_name.to_s) + else + attribute_name.to_s.humanize + end + + translate(:labels, default) + end + end + end +end \ No newline at end of file diff --git a/lib/simple_form/components/wrapper.rb b/lib/simple_form/components/wrapper.rb new file mode 100644 index 00000000..3d75f364 --- /dev/null +++ b/lib/simple_form/components/wrapper.rb @@ -0,0 +1,21 @@ +module SimpleForm + module Components + module Wrapper + def wrap(content) + if wrapper_tag && options[:wrapper] != false + template.content_tag(wrapper_tag, content, wrapper_html_options) + else + content + end + end + + def wrapper_tag + options[:wrapper_tag] || SimpleForm.wrapper_tag + end + + def wrapper_html_options + html_options_for(:wrapper, input_type, required_class) + end + end + end +end \ No newline at end of file diff --git a/lib/simple_form/form_builder.rb b/lib/simple_form/form_builder.rb index 0fe3e1cc..4ea546cd 100644 --- a/lib/simple_form/form_builder.rb +++ b/lib/simple_form/form_builder.rb @@ -3,6 +3,15 @@ module SimpleForm attr_reader :template, :object_name, :object, :attribute_name, :column, :reflection, :input_type, :options + extend MapType + include SimpleForm::Inputs + + map_type :boolean, :password, :text, :file, :to => SimpleForm::Inputs::MappingInput + map_type :string, :integer, :decimal, :float, :to => SimpleForm::Inputs::TextFieldInput + map_type :select, :radio, :check_boxes, :to => SimpleForm::Inputs::CollectionInput + map_type :date, :time, :datetime, :to => SimpleForm::Inputs::DateTimeInput + map_type :country, :time_zone, :to => SimpleForm::Inputs::PriorityInput + # Basic input helper, combines all components in the stack to generate # input html based on options the user define and some guesses through # database column information. By default a call to input will generate @@ -68,365 +77,6 @@ module SimpleForm # Some inputs, as :time_zone and :country accepts a :priority option. If none is # given SimpleForm.time_zone_priority and SimpleForm.country_priority are used respectivelly. # - module Labels - def self.included(base) - base.extend ClassMethods - end - - module ClassMethods #:nodoc: - def translate_required_html - i18n_cache :translate_required_html do - I18n.t(:"simple_form.required.html", :default => - %[#{translate_required_mark}] - ) - end - end - - def translate_required_text - I18n.t(:"simple_form.required.text", :default => 'required') - end - - def translate_required_mark - I18n.t(:"simple_form.required.mark", :default => '*') - end - end - - def label - @builder.label(label_target, label_text, label_html_options) - end - - def label_text - SimpleForm.label_text.call(raw_label_text, required_label_text) - end - - def label_target - attribute_name - end - - def label_html_options - label_options = html_options_for(:label, input_type, required_class) - label_options[:for] = options[:input_html][:id] if options.key?(:input_html) - label_options - end - - protected - - def raw_label_text #:nodoc: - options[:label] || label_translation - end - - # Default required text when attribute is required. - def required_label_text #:nodoc: - attribute_required? ? self.class.translate_required_html.dup : '' - end - - # First check human attribute name and then labels. - # TODO Remove me in Rails > 2.3.5 - def label_translation #:nodoc: - default = if object.class.respond_to?(:human_attribute_name) - object.class.human_attribute_name(reflection_or_attribute_name.to_s) - else - attribute_name.to_s.humanize - end - - translate(:labels, default) - end - end - - module Hints - def hint - template.content_tag(hint_tag, hint_text, hint_html_options) unless hint_text.blank? - end - - def hint_tag - options[:hint_tag] || SimpleForm.hint_tag - end - - def hint_text - @hint_text ||= options[:hint] || translate(:hints) - end - - def hint_html_options - html_options_for(:hint, :hint) - end - end - - module Errors - def error - template.content_tag(error_tag, error_text, error_html_options) if object && errors.present? - end - - def error_tag - options[:error_tag] || SimpleForm.error_tag - end - - def error_text - errors.to_sentence - end - - def error_html_options - html_options_for(:error, :error) - end - - protected - - def errors - @errors ||= (errors_on_attribute + errors_on_association).compact - end - - def errors_on_attribute - Array(object.errors[attribute_name]) - end - - def errors_on_association - reflection ? Array(object.errors[reflection.name]) : [] - end - end - - class Base - extend I18nCache - - include Errors - include Hints - include Labels - - delegate :template, :object, :object_name, :attribute_name, :column, - :reflection, :input_type, :options, :to => :@builder - - def initialize(builder) - @builder = builder - end - - def input - raise NotImplemented - end - - def input_options - options[:include_blank] = true unless skip_include_blank? - options - end - - def input_html_options - html_options_for(:input, input_type, required_class) - end - - def render - pieces = SimpleForm.components.select { |n| n unless @builder.options[n] == false } - content = pieces.map!{ |p| send(p).to_s }.join - wrap(content) - end - - def wrap(content) - if wrapper_tag && options[:wrapper] != false - template.content_tag(wrapper_tag, content, wrapper_html_options) - else - content - end - end - - def wrapper_tag - options[:wrapper_tag] || SimpleForm.wrapper_tag - end - - def wrapper_html_options - html_options_for(:wrapper, input_type, required_class) - end - - protected - - # When action is create or update, we still should use new and edit - ACTIONS = { - :create => :new, - :update => :edit - } - - def attribute_required? - options[:required] != false - end - - def required_class - attribute_required? ? :required : :optional - end - - # Find reflection name when available, otherwise use attribute - def reflection_or_attribute_name - reflection ? reflection.name : attribute_name - end - - # Check if :include_blank must be included by default. - def skip_include_blank? - options.key?(:prompt) || options.key?(:include_blank) - end - - # Retrieve options for the given namespace from the options hash - def html_options_for(namespace, *extra) - html_options = options[:"#{namespace}_html"] || {} - html_options[:class] = (extra << html_options[:class]).join(' ').strip if extra.present? - html_options - end - - # Lookup translations for the given namespace using I18n, based on object name, - # actual action and attribute name. Lookup priority as follows: - # - # simple_form.{namespace}.{model}.{action}.{attribute} - # simple_form.{namespace}.{model}.{attribute} - # simple_form.{namespace}.{attribute} - # - # Namespace is used for :labels and :hints. - # - # Model is the actual object name, for a @user object you'll have :user. - # Action is the action being rendered, usually :new or :edit. - # And attribute is the attribute itself, :name for example. - # - # Example: - # - # simple_form: - # labels: - # user: - # new: - # email: 'E-mail para efetuar o sign in.' - # edit: - # email: 'E-mail.' - # - # Take a look at our locale example file. - def translate(namespace, default='') - lookups = [] - lookups << :"#{object_name}.#{lookup_action}.#{reflection_or_attribute_name}" - lookups << :"#{object_name}.#{reflection_or_attribute_name}" - lookups << :"#{reflection_or_attribute_name}" - lookups << default - I18n.t(lookups.shift, :scope => :"simple_form.#{namespace}", :default => lookups) - end - - # The action to be used in lookup. - def lookup_action - action = template.controller.action_name.to_sym - ACTIONS[action] || action - end - end - - # Uses MapType to handle basic input types. - class MappingInput < Base - extend MapType - - map_type :boolean, :to => :check_box - map_type :password, :to => :password_field - map_type :text, :to => :text_area - map_type :file, :to => :file_field - - def input - @builder.send(input_method, attribute_name, input_html_options) - end - - def input_method - method = self.class.mappings[input_type] - raise "Could not find method for #{input_type.inspect}" unless method - method - end - end - - # Handles common text field inputs, as String, Numeric, Float and Decimal. - class TextFieldInput < Base - def input - @builder.text_field(attribute_name, input_html_options) - end - - def input_html_options - input_options = super - input_options[:max_length] ||= column.limit if column - input_options - end - end - - class DateTimeInput < Base - def input - @builder.send(:"#{input_type}_select", attribute_name, input_options, input_html_options) - end - - def label_target - case input_type - when :date, :datetime - "#{attribute_name}_1i" - when :time - "#{attribute_name}_4i" - end - end - end - - class CollectionInput < Base - # Default boolean collection for use with selects/radios when no - # collection is given. Always fallback to this boolean collection. - # Texts can be translated using i18n in "simple_form.true" and - # "simple_form.false" keys. See the example locale file. - def self.boolean_collection - i18n_cache :boolean_collection do - [ [I18n.t(:"simple_form.yes", :default => 'Yes'), true], - [I18n.t(:"simple_form.no", :default => 'No'), false] ] - end - end - - def input - collection = (options[:collection] || self.class.boolean_collection).to_a - detect_collection_methods(collection, options) - @builder.send(:"collection_#{input_type}", attribute_name, collection, options[:value_method], - options[:label_method], input_options, input_html_options) - end - - protected - - def skip_include_blank? - super || options[:input_html].try(:[], :multiple) - end - - # Detect the right method to find the label and value for a collection. - # If no label or value method are defined, will attempt to find them based - # on default label and value methods that can be configured through - # SimpleForm.collection_label_methods and - # SimpleForm.collection_value_methods. - def detect_collection_methods(collection, options) - sample = collection.first || collection.last - - case sample - when Array - label, value = :first, :last - when Integer - label, value = :to_s, :to_i - when String, NilClass - label, value = :to_s, :to_s - end - - options[:label_method] ||= label || SimpleForm.collection_label_methods.find { |m| sample.respond_to?(m) } - options[:value_method] ||= value || SimpleForm.collection_value_methods.find { |m| sample.respond_to?(m) } - end - end - - # Handles hidden input. - class HiddenInput < Base - def render - @builder.hidden_field(attribute_name, input_html_options) - end - end - - class PriorityInput < Base - def input - @builder.send(:"#{input_type}_select", attribute_name, input_priority, - input_options, input_html_options) - end - - def input_priority - options[:priority] || SimpleForm.send(:"#{input_type}_priority") - end - end - - extend MapType - - map_type :boolean, :password, :text, :file, :to => MappingInput - map_type :hidden, :to => HiddenInput # TODO This should be automatic - map_type :string, :integer, :decimal, :float, :to => TextFieldInput - map_type :select, :radio, :check_boxes, :to => CollectionInput - map_type :date, :time, :datetime, :to => DateTimeInput - map_type :country, :time_zone, :to => PriorityInput - def input(attribute_name, options={}) define_simple_form_attributes(attribute_name, options) @@ -566,7 +216,7 @@ module SimpleForm # def error(attribute_name, options={}) define_simple_form_attributes(attribute_name, :error_html => options) - Base.new(self).error + SimpleForm::Inputs::Base.new(self).error end # Creates a hint tag for the given attribute. Accepts a symbol indicating @@ -582,7 +232,7 @@ module SimpleForm def hint(attribute_name, options={}) attribute_name, options[:hint] = nil, attribute_name if attribute_name.is_a?(String) define_simple_form_attributes(attribute_name, :hint => options.delete(:hint), :hint_html => options) - Base.new(self).hint + SimpleForm::Inputs::Base.new(self).hint end # Creates a default label tag for the given attribute. You can give a label @@ -603,7 +253,7 @@ module SimpleForm options = args.extract_options! define_simple_form_attributes(attribute_name, :label => options.delete(:label), :label_html => options, :required => options.delete(:required)) - Base.new(self).label + SimpleForm::Inputs::Base.new(self).label end private diff --git a/lib/simple_form/inputs.rb b/lib/simple_form/inputs.rb new file mode 100644 index 00000000..1cd44896 --- /dev/null +++ b/lib/simple_form/inputs.rb @@ -0,0 +1,11 @@ +module SimpleForm + module Inputs + autoload :Base, 'simple_form/inputs/base' + autoload :CollectionInput, 'simple_form/inputs/collection_input' + autoload :DateTimeInput, 'simple_form/inputs/date_time_input' + autoload :HiddenInput, 'simple_form/inputs/hidden_input' + autoload :MappingInput, 'simple_form/inputs/mapping_input' + autoload :PriorityInput, 'simple_form/inputs/priority_input' + autoload :TextFieldInput, 'simple_form/inputs/text_field_input' + end +end \ No newline at end of file diff --git a/lib/simple_form/inputs/base.rb b/lib/simple_form/inputs/base.rb new file mode 100644 index 00000000..4fdb5f9b --- /dev/null +++ b/lib/simple_form/inputs/base.rb @@ -0,0 +1,113 @@ +module SimpleForm + module Inputs + class Base + extend I18nCache + + # When action is create or update, we still should use new and edit + ACTIONS = { + :create => :new, + :update => :edit + } + + include SimpleForm::Components::Errors + include SimpleForm::Components::Hints + include SimpleForm::Components::Labels + include SimpleForm::Components::Wrapper + + delegate :template, :object, :object_name, :attribute_name, :column, + :reflection, :input_type, :options, :to => :@builder + + def initialize(builder) + @builder = builder + end + + def input + raise NotImplementedError + end + + def input_options + options[:include_blank] = true unless skip_include_blank? + options + end + + def input_html_options + html_options_for(:input, input_type, required_class) + end + + def render + content = SimpleForm.components.map do |component| + next if options[component] == false + send(component) + end + content.compact! + wrap(content.join) + end + + protected + + def attribute_required? + options[:required] != false + end + + def required_class + attribute_required? ? :required : :optional + end + + # Find reflection name when available, otherwise use attribute + def reflection_or_attribute_name + reflection ? reflection.name : attribute_name + end + + # Check if :include_blank must be included by default. + def skip_include_blank? + options.key?(:prompt) || options.key?(:include_blank) + end + + # Retrieve options for the given namespace from the options hash + def html_options_for(namespace, *extra) + html_options = options[:"#{namespace}_html"] || {} + html_options[:class] = (extra << html_options[:class]).join(' ').strip if extra.present? + html_options + end + + # Lookup translations for the given namespace using I18n, based on object name, + # actual action and attribute name. Lookup priority as follows: + # + # simple_form.{namespace}.{model}.{action}.{attribute} + # simple_form.{namespace}.{model}.{attribute} + # simple_form.{namespace}.{attribute} + # + # Namespace is used for :labels and :hints. + # + # Model is the actual object name, for a @user object you'll have :user. + # Action is the action being rendered, usually :new or :edit. + # And attribute is the attribute itself, :name for example. + # + # Example: + # + # simple_form: + # labels: + # user: + # new: + # email: 'E-mail para efetuar o sign in.' + # edit: + # email: 'E-mail.' + # + # Take a look at our locale example file. + def translate(namespace, default='') + lookups = [] + lookups << :"#{object_name}.#{lookup_action}.#{reflection_or_attribute_name}" + lookups << :"#{object_name}.#{reflection_or_attribute_name}" + lookups << :"#{reflection_or_attribute_name}" + lookups << default + I18n.t(lookups.shift, :scope => :"simple_form.#{namespace}", :default => lookups) + end + + # The action to be used in lookup. + def lookup_action + action = template.controller.action_name.to_sym + ACTIONS[action] || action + end + end + end +end \ No newline at end of file diff --git a/lib/simple_form/inputs/collection_input.rb b/lib/simple_form/inputs/collection_input.rb new file mode 100644 index 00000000..526bfd12 --- /dev/null +++ b/lib/simple_form/inputs/collection_input.rb @@ -0,0 +1,50 @@ +module SimpleForm + module Inputs + class CollectionInput < Base + # Default boolean collection for use with selects/radios when no + # collection is given. Always fallback to this boolean collection. + # Texts can be translated using i18n in "simple_form.yes" and + # "simple_form.no" keys. See the example locale file. + def self.boolean_collection + i18n_cache :boolean_collection do + [ [I18n.t(:"simple_form.yes", :default => 'Yes'), true], + [I18n.t(:"simple_form.no", :default => 'No'), false] ] + end + end + + def input + collection = (options[:collection] || self.class.boolean_collection).to_a + detect_collection_methods(collection, options) + @builder.send(:"collection_#{input_type}", attribute_name, collection, options[:value_method], + options[:label_method], input_options, input_html_options) + end + + protected + + def skip_include_blank? + super || options[:input_html].try(:[], :multiple) + end + + # Detect the right method to find the label and value for a collection. + # If no label or value method are defined, will attempt to find them based + # on default label and value methods that can be configured through + # SimpleForm.collection_label_methods and + # SimpleForm.collection_value_methods. + def detect_collection_methods(collection, options) + sample = collection.first || collection.last + + case sample + when Array + label, value = :first, :last + when Integer + label, value = :to_s, :to_i + when String, NilClass + label, value = :to_s, :to_s + end + + options[:label_method] ||= label || SimpleForm.collection_label_methods.find { |m| sample.respond_to?(m) } + options[:value_method] ||= value || SimpleForm.collection_value_methods.find { |m| sample.respond_to?(m) } + end + end + end +end \ No newline at end of file diff --git a/lib/simple_form/inputs/date_time_input.rb b/lib/simple_form/inputs/date_time_input.rb new file mode 100644 index 00000000..d23c3cba --- /dev/null +++ b/lib/simple_form/inputs/date_time_input.rb @@ -0,0 +1,18 @@ +module SimpleForm + module Inputs + class DateTimeInput < Base + def input + @builder.send(:"#{input_type}_select", attribute_name, input_options, input_html_options) + end + + def label_target + case input_type + when :date, :datetime + "#{attribute_name}_1i" + when :time + "#{attribute_name}_4i" + end + end + end + end +end \ No newline at end of file diff --git a/lib/simple_form/inputs/hidden_input.rb b/lib/simple_form/inputs/hidden_input.rb new file mode 100644 index 00000000..dff79cbb --- /dev/null +++ b/lib/simple_form/inputs/hidden_input.rb @@ -0,0 +1,11 @@ +module SimpleForm + module Inputs + # Handles hidden input. + class HiddenInput < Base + def render + @builder.hidden_field(attribute_name, input_html_options) + end + alias :input :render + end + end +end \ No newline at end of file diff --git a/lib/simple_form/inputs/mapping_input.rb b/lib/simple_form/inputs/mapping_input.rb new file mode 100644 index 00000000..dac7d186 --- /dev/null +++ b/lib/simple_form/inputs/mapping_input.rb @@ -0,0 +1,23 @@ +module SimpleForm + module Inputs + # Uses MapType to handle basic input types. + class MappingInput < Base + extend MapType + + map_type :boolean, :to => :check_box + map_type :password, :to => :password_field + map_type :text, :to => :text_area + map_type :file, :to => :file_field + + def input + @builder.send(input_method, attribute_name, input_html_options) + end + + def input_method + method = self.class.mappings[input_type] + raise "Could not find method for #{input_type.inspect}" unless method + method + end + end + end +end diff --git a/lib/simple_form/inputs/priority_input.rb b/lib/simple_form/inputs/priority_input.rb new file mode 100644 index 00000000..6af6798e --- /dev/null +++ b/lib/simple_form/inputs/priority_input.rb @@ -0,0 +1,14 @@ +module SimpleForm + module Inputs + class PriorityInput < Base + def input + @builder.send(:"#{input_type}_select", attribute_name, input_priority, + input_options, input_html_options) + end + + def input_priority + options[:priority] || SimpleForm.send(:"#{input_type}_priority") + end + end + end +end \ No newline at end of file diff --git a/lib/simple_form/inputs/text_field_input.rb b/lib/simple_form/inputs/text_field_input.rb new file mode 100644 index 00000000..d06a65fa --- /dev/null +++ b/lib/simple_form/inputs/text_field_input.rb @@ -0,0 +1,16 @@ +module SimpleForm + module Inputs + # Handles common text field inputs, as String, Numeric, Float and Decimal. + class TextFieldInput < Base + def input + @builder.text_field(attribute_name, input_html_options) + end + + def input_html_options + input_options = super + input_options[:max_length] ||= column.limit if column + input_options + end + end + end +end \ No newline at end of file diff --git a/lib/simple_form/required_helpers.rb b/lib/simple_form/required_helpers.rb deleted file mode 100644 index c782fa21..00000000 --- a/lib/simple_form/required_helpers.rb +++ /dev/null @@ -1,26 +0,0 @@ -module SimpleForm - module RequiredHelpers - # Attribute is always required, unless the user has defined the opposite. - def attribute_required? - options[:required] != false - end - - def required_class - attribute_required? ? :required : :optional - end - - # Creates default required classes for attributes, such as .string and - # .decimal, based on input type, and required class - def default_css_classes(merge_class=nil) - "#{input_type} #{required_class} #{merge_class}".strip - end - - # When components may be required, default component html options always - # must include default css classes. - def component_html_options - html_options = super - html_options[:class] = default_css_classes(html_options[:class]) - html_options - end - end -end diff --git a/test/components/error_test.rb b/test/components/error_test.rb index 7bdb9758..0a745a17 100644 --- a/test/components/error_test.rb +++ b/test/components/error_test.rb @@ -9,7 +9,7 @@ class ErrorTest < ActionView::TestCase f.input_type = type f.options = options - concat(SimpleForm::FormBuilder::Base.new(f).error) + concat(SimpleForm::Inputs::Base.new(f).error) end end diff --git a/test/components/hint_test.rb b/test/components/hint_test.rb index 78012c57..cab67b6b 100644 --- a/test/components/hint_test.rb +++ b/test/components/hint_test.rb @@ -9,7 +9,7 @@ class HintTest < ActionView::TestCase f.input_type = type f.options = options - concat(SimpleForm::FormBuilder::Base.new(f).hint) + concat(SimpleForm::Inputs::Base.new(f).hint) end end diff --git a/test/components/input_test.rb b/test/components/input_test.rb index afff87b4..948e00e6 100644 --- a/test/components/input_test.rb +++ b/test/components/input_test.rb @@ -3,329 +3,329 @@ require 'test_helper' class InputTest < ActionView::TestCase setup do - SimpleForm::Components::Input.reset_i18n_cache :boolean_collection + SimpleForm::Inputs::Base.reset_i18n_cache :boolean_collection + end + + def with_input_for(object, attribute_name, type, options={}) + simple_form_for object do |f| + f.attribute_name = attribute_name + f.column = object.column_for_attribute(attribute_name) if object.respond_to?(:column_for_attribute) + f.input_type = type + f.options = options + + klass = SimpleForm::FormBuilder.mappings[type] + klass ||= SimpleForm::Inputs.const_get(:"#{type.to_s.camelize}Input") + concat(klass.new(f).input) + end + end + + test 'input should map text field to string attribute' do + with_input_for @user, :name, :string + assert_select 'input[name=\'user[name]\'][id=user_name][value=New in Simple Form!]' + end + + test 'input should generate css class based on default input type' do + with_input_for @user, :name, :string + assert_select 'input.string' + with_input_for @user, :description, :text + assert_select 'textarea.text' + with_input_for @user, :age, :integer + assert_select 'input.integer' + with_input_for @user, :born_at, :date + assert_select 'select.date' + with_input_for @user, :created_at, :datetime + assert_select 'select.datetime' + end + + test 'input should allow passing options to text field' do + with_input_for @user, :name, :string, :input_html => { :class => 'my_input', :id => 'my_input' } + assert_select 'input#my_input.my_input' + end + + test 'input should generate a text area for text attributes' do + with_input_for @user, :description, :text + assert_select 'textarea.text#user_description' + end + + test 'input should generate an integer text field for integer attributes ' do + with_input_for @user, :age, :integer + assert_select 'input.integer#user_age' + end + + test 'input should generate a float text field for float attributes ' do + with_input_for @user, :age, :float + assert_select 'input.float#user_age' + end + + test 'input should generate a decimal text field for decimal attributes ' do + with_input_for @user, :age, :decimal + assert_select 'input.decimal#user_age' + end + + test 'input should generate a checkbox by default for boolean attributes' do + with_input_for @user, :active, :boolean + assert_select 'input[type=checkbox].boolean#user_active' + end + + test 'input should generate a password field for password attributes' do + with_input_for @user, :password, :password + assert_select 'input[type=password].password#user_password' + end + + test 'input should generate a hidden field' do + with_input_for @user, :name, :hidden + assert_no_select 'input[type=text]' + assert_select 'input#user_name[type=hidden]' + end + + test 'input should generate a file field' do + with_input_for @user, :name, :file + assert_select 'input#user_name[type=file]' + end + + test 'input should generate a country select field' do + with_input_for @user, :country, :country + assert_select 'select#user_country' + assert_select 'select option[value=Brazil]', 'Brazil' + assert_no_select 'select option[value=][disabled=disabled]' + end + + test 'input should generate a country select with simple form default' do + swap SimpleForm, :country_priority => [ 'Brazil' ] do + with_input_for @user, :country, :country + assert_select 'select option[value=][disabled=disabled]' + end + end + + test 'input should generate a time zone select field' do + with_input_for @user, :time_zone, :time_zone + assert_select 'select#user_time_zone' + assert_select 'select option[value=Brasilia]', '(GMT-03:00) Brasilia' + assert_no_select 'select option[value=][disabled=disabled]' + end + + test 'input should generate a time zone select field with default' do + with_input_for @user, :time_zone, :time_zone, :default => 'Brasilia' + assert_select 'select option[value=Brasilia][selected=selected]' + end + + test 'input should generate a time zone select using options priority' do + with_input_for @user, :time_zone, :time_zone, :priority => /Brasilia/ + assert_select 'select option[value=][disabled=disabled]' + end + + test 'input should generate a datetime select by default for datetime attributes' do + with_input_for @user, :created_at, :datetime + 1.upto(5) do |i| + assert_select "form select.datetime#user_created_at_#{i}i" + end + end + + test 'input should be able to pass options to datetime select' do + with_input_for @user, :created_at, :datetime, + :disabled => true, :prompt => { :year => 'ano', :month => 'mês', :day => 'dia' } + + assert_select 'select.datetime[disabled=disabled]' + assert_select 'select.datetime option', 'ano' + assert_select 'select.datetime option', 'mês' + assert_select 'select.datetime option', 'dia' + end + + test 'input should generate a date select for date attributes' do + with_input_for @user, :born_at, :date + assert_select 'select.date#user_born_at_1i' + assert_select 'select.date#user_born_at_2i' + assert_select 'select.date#user_born_at_3i' + assert_no_select 'select.date#user_born_at_4i' + end + + test 'input should be able to pass options to date select' do + with_input_for @user, :born_at, :date, :as => :date, + :disabled => true, :prompt => { :year => 'ano', :month => 'mês', :day => 'dia' } + + assert_select 'select.date[disabled=disabled]' + assert_select 'select.date option', 'ano' + assert_select 'select.date option', 'mês' + assert_select 'select.date option', 'dia' + end + + test 'input should generate a time select for time attributes' do + with_input_for @user, :delivery_time, :time + assert_select 'input[type=hidden]#user_delivery_time_1i' + assert_select 'input[type=hidden]#user_delivery_time_2i' + assert_select 'input[type=hidden]#user_delivery_time_3i' + assert_select 'select.time#user_delivery_time_4i' + assert_select 'select.time#user_delivery_time_5i' + end + + test 'input should be able to pass options to time select' do + with_input_for @user, :delivery_time, :time, :required => true, + :disabled => true, :prompt => { :hour => 'hora', :minute => 'minuto' } + + assert_select 'select.time[disabled=disabled]' + assert_select 'select.time option', 'hora' + assert_select 'select.time option', 'minuto' + end + + test 'input should generate boolean radio buttons by default for radio types' do + with_input_for @user, :active, :radio + assert_select 'input[type=radio][value=true].radio#user_active_true' + assert_select 'input[type=radio][value=false].radio#user_active_false' + end + + test 'input as radio should generate internal labels by default' do + with_input_for @user, :active, :radio + assert_select 'label[for=user_active_true]', 'Yes' + assert_select 'label[for=user_active_false]', 'No' + end + + test 'input as radio should use i18n to translate internal labels' do + store_translations(:en, :simple_form => { :yes => 'Sim', :no => 'Não' }) do + with_input_for @user, :active, :radio + assert_select 'label[for=user_active_true]', 'Sim' + assert_select 'label[for=user_active_false]', 'Não' + end + end + + test 'input should generate a boolean select with options by default for select types' do + with_input_for @user, :active, :select + assert_select 'select.select#user_active' + assert_select 'select option[value=true]', 'Yes' + assert_select 'select option[value=false]', 'No' + end + + test 'input as select should use i18n to translate select boolean options' do + store_translations(:en, :simple_form => { :yes => 'Sim', :no => 'Não' }) do + with_input_for @user, :active, :select + assert_select 'select option[value=true]', 'Sim' + assert_select 'select option[value=false]', 'Não' + end + end + + test 'input should allow overriding collection for select types' do + with_input_for @user, :name, :select, :collection => ['Jose', 'Carlos'] + assert_select 'select.select#user_name' + assert_select 'select option', 'Jose' + assert_select 'select option', 'Carlos' + end + + test 'input should mark the selected value by default' do + @user.name = "Carlos" + with_input_for @user, :name, :select, :collection => ['Jose', 'Carlos'] + assert_select 'select option[selected=selected]', 'Carlos' + end + + test 'input should mark the selected value also when using integers' do + @user.age = 18 + with_input_for @user, :age, :select, :collection => 18..60 + assert_select 'select option[selected=selected]', '18' + end + + test 'input should automatically set include blank' do + with_input_for @user, :age, :select, :collection => 18..30 + assert_select 'select option[value=]', '' + end + + test 'input should not set include blank if otherwise is told' do + with_input_for @user, :age, :select, :collection => 18..30, :include_blank => false + assert_no_select 'select option[value=]', '' + end + + test 'input should not set include blank if prompt is given' do + with_input_for @user, :age, :select, :collection => 18..30, :prompt => "Please select foo" + assert_no_select 'select option[value=]', '' + end + + test 'input should not set include blank if multiple is given' do + with_input_for @user, :age, :select, :collection => 18..30, :input_html => { :multiple => true } + assert_no_select 'select option[value=]', '' + end + + test 'input should detect label and value on collections' do + users = [ setup_new_user(:id => 1, :name => "Jose"), setup_new_user(:id => 2, :name => "Carlos") ] + with_input_for @user, :description, :select, :collection => users + assert_select 'select option[value=1]', 'Jose' + assert_select 'select option[value=2]', 'Carlos' + end + + test 'input should allow overriding collection for radio types' do + with_input_for @user, :name, :radio, :collection => ['Jose', 'Carlos'] + assert_select 'input[type=radio][value=Jose]' + assert_select 'input[type=radio][value=Carlos]' + assert_select 'label.collection_radio', 'Jose' + assert_select 'label.collection_radio', 'Carlos' + end + + test 'input should mark the current radio value by default' do + @user.name = "Carlos" + with_input_for @user, :name, :radio, :collection => ['Jose', 'Carlos'] + assert_select 'input[type=radio][value=Carlos][checked=checked]' + end + + test 'input should allow using a collection with text/value arrays' do + with_input_for @user, :name, :radio, :collection => [['Jose', 'jose'], ['Carlos', 'carlos']] + assert_select 'input[type=radio][value=jose]' + assert_select 'input[type=radio][value=carlos]' + assert_select 'label.collection_radio', 'Jose' + assert_select 'label.collection_radio', 'Carlos' + end + + test 'input should allow overriding label and value method for collections' do + with_input_for @user, :name, :radio, + :collection => ['Jose' , 'Carlos'], + :label_method => :upcase, + :value_method => :downcase + assert_select 'input[type=radio][value=jose]' + assert_select 'input[type=radio][value=carlos]' + assert_select 'label.collection_radio', 'JOSE' + assert_select 'label.collection_radio', 'CARLOS' + end + + test 'input should be required by default' do + with_input_for @user, :name, :string + assert_select 'input.required#user_name' + end + + test 'input should allow disabling required' do + with_input_for @user, :name, :string, :required => false + assert_no_select 'input.required' + assert_select 'input.optional#user_name' + end + + test 'input should get options from column definition for string attributes' do + with_input_for @user, :name, :string + assert_select 'input.string[maxlength=100]' + end + + test 'input should get options from column definition for decimal attributes' do + with_input_for @user, :credit_limit, :decimal + assert_select 'input.decimal[maxlength=15]' + end + + test 'input should get options from column definition for password attributes' do + with_input_for @user, :password, :password + assert_select 'input.password[maxlength=100]' + end + + test 'input should not generate options for different attributes' do + with_input_for @user, :description, :text + assert_select 'textarea' + assert_no_select 'textarea[maxlength]' + end + + test 'input should be generated properly when object is not present' do + with_input_for :project, :name, :string + assert_select 'input.string.required#project_name' + end + + test 'input as radio should be generated properly when object is not present ' do + with_input_for :project, :name, :radio + assert_select 'input.radio#project_name_true' + assert_select 'input.radio#project_name_false' + end + + test 'input as select with collection should be generated properly when object is not present' do + with_input_for :project, :name, :select, :collection => ['Jose', 'Carlos'] + assert_select 'select.select#project_name' end - # - # def with_input_for(object, attribute_name, type, options={}) - # simple_form_for object do |f| - # f.attribute_name = attribute_name - # f.column = object.column_for_attribute(attribute_name) if object.respond_to?(:column_for_attribute) - # f.input_type = type - # f.options = options - # - # input = SimpleForm::Components::Input.new(f, SimpleForm::FormBuilder::TERMINATOR) - # concat(input.call) - # yield input if block_given? - # end - # end - # - # test 'input should map text field to string attribute' do - # with_input_for @user, :name, :string - # assert_select 'input[name=\'user[name]\'][id=user_name][value=New in Simple Form!]' - # end - # - # test 'input should generate css class based on default input type' do - # with_input_for @user, :name, :string - # assert_select 'input.string' - # with_input_for @user, :description, :text - # assert_select 'textarea.text' - # with_input_for @user, :age, :integer - # assert_select 'input.integer' - # with_input_for @user, :born_at, :date - # assert_select 'select.date' - # with_input_for @user, :created_at, :datetime - # assert_select 'select.datetime' - # end - # - # test 'input should allow passing options to text field' do - # with_input_for @user, :name, :string, :input_html => { :class => 'my_input', :id => 'my_input' } - # assert_select 'input#my_input.my_input' - # end - # - # test 'input should generate a text area for text attributes' do - # with_input_for @user, :description, :text - # assert_select 'textarea.text#user_description' - # end - # - # test 'input should generate an integer text field for integer attributes ' do - # with_input_for @user, :age, :integer - # assert_select 'input.integer#user_age' - # end - # - # test 'input should generate a float text field for float attributes ' do - # with_input_for @user, :age, :float - # assert_select 'input.float#user_age' - # end - # - # test 'input should generate a decimal text field for decimal attributes ' do - # with_input_for @user, :age, :decimal - # assert_select 'input.decimal#user_age' - # end - # - # test 'input should generate a checkbox by default for boolean attributes' do - # with_input_for @user, :active, :boolean - # assert_select 'input[type=checkbox].boolean#user_active' - # end - # - # test 'input should generate a password field for password attributes' do - # with_input_for @user, :password, :password - # assert_select 'input[type=password].password#user_password' - # end - # - # test 'input should generate a hidden field' do - # with_input_for @user, :name, :hidden - # assert_no_select 'input[type=text]' - # assert_select 'input#user_name[type=hidden]' - # end - # - # test 'input should generate a file field' do - # with_input_for @user, :name, :file - # assert_select 'input#user_name[type=file]' - # end - # - # test 'input should generate a country select field' do - # with_input_for @user, :country, :country - # assert_select 'select#user_country' - # assert_select 'select option[value=Brazil]', 'Brazil' - # assert_no_select 'select option[value=][disabled=disabled]' - # end - # - # test 'input should generate a country select with simple form default' do - # swap SimpleForm, :country_priority => [ 'Brazil' ] do - # with_input_for @user, :country, :country - # assert_select 'select option[value=][disabled=disabled]' - # end - # end - # - # test 'input should generate a time zone select field' do - # with_input_for @user, :time_zone, :time_zone - # assert_select 'select#user_time_zone' - # assert_select 'select option[value=Brasilia]', '(GMT-03:00) Brasilia' - # assert_no_select 'select option[value=][disabled=disabled]' - # end - # - # test 'input should generate a time zone select field with default' do - # with_input_for @user, :time_zone, :time_zone, :default => 'Brasilia' - # assert_select 'select option[value=Brasilia][selected=selected]' - # end - # - # test 'input should generate a time zone select using options priority' do - # with_input_for @user, :time_zone, :time_zone, :priority => /Brasilia/ - # assert_select 'select option[value=][disabled=disabled]' - # end - # - # test 'input should generate a datetime select by default for datetime attributes' do - # with_input_for @user, :created_at, :datetime - # 1.upto(5) do |i| - # assert_select "form select.datetime#user_created_at_#{i}i" - # end - # end - # - # test 'input should be able to pass options to datetime select' do - # with_input_for @user, :created_at, :datetime, - # :disabled => true, :prompt => { :year => 'ano', :month => 'mês', :day => 'dia' } - # - # assert_select 'select.datetime[disabled=disabled]' - # assert_select 'select.datetime option', 'ano' - # assert_select 'select.datetime option', 'mês' - # assert_select 'select.datetime option', 'dia' - # end - # - # test 'input should generate a date select for date attributes' do - # with_input_for @user, :born_at, :date - # assert_select 'select.date#user_born_at_1i' - # assert_select 'select.date#user_born_at_2i' - # assert_select 'select.date#user_born_at_3i' - # assert_no_select 'select.date#user_born_at_4i' - # end - # - # test 'input should be able to pass options to date select' do - # with_input_for @user, :born_at, :date, :as => :date, - # :disabled => true, :prompt => { :year => 'ano', :month => 'mês', :day => 'dia' } - # - # assert_select 'select.date[disabled=disabled]' - # assert_select 'select.date option', 'ano' - # assert_select 'select.date option', 'mês' - # assert_select 'select.date option', 'dia' - # end - # - # test 'input should generate a time select for time attributes' do - # with_input_for @user, :delivery_time, :time - # assert_select 'input[type=hidden]#user_delivery_time_1i' - # assert_select 'input[type=hidden]#user_delivery_time_2i' - # assert_select 'input[type=hidden]#user_delivery_time_3i' - # assert_select 'select.time#user_delivery_time_4i' - # assert_select 'select.time#user_delivery_time_5i' - # end - # - # test 'input should be able to pass options to time select' do - # with_input_for @user, :delivery_time, :time, :required => true, - # :disabled => true, :prompt => { :hour => 'hora', :minute => 'minuto' } - # - # assert_select 'select.time[disabled=disabled]' - # assert_select 'select.time option', 'hora' - # assert_select 'select.time option', 'minuto' - # end - # - # test 'input should generate boolean radio buttons by default for radio types' do - # with_input_for @user, :active, :radio - # assert_select 'input[type=radio][value=true].radio#user_active_true' - # assert_select 'input[type=radio][value=false].radio#user_active_false' - # end - # - # test 'input as radio should generate internal labels by default' do - # with_input_for @user, :active, :radio - # assert_select 'label[for=user_active_true]', 'Yes' - # assert_select 'label[for=user_active_false]', 'No' - # end - # - # test 'input as radio should use i18n to translate internal labels' do - # store_translations(:en, :simple_form => { :yes => 'Sim', :no => 'Não' }) do - # with_input_for @user, :active, :radio - # assert_select 'label[for=user_active_true]', 'Sim' - # assert_select 'label[for=user_active_false]', 'Não' - # end - # end - # - # test 'input should generate a boolean select with options by default for select types' do - # with_input_for @user, :active, :select - # assert_select 'select.select#user_active' - # assert_select 'select option[value=true]', 'Yes' - # assert_select 'select option[value=false]', 'No' - # end - # - # test 'input as select should use i18n to translate select boolean options' do - # store_translations(:en, :simple_form => { :yes => 'Sim', :no => 'Não' }) do - # with_input_for @user, :active, :select - # assert_select 'select option[value=true]', 'Sim' - # assert_select 'select option[value=false]', 'Não' - # end - # end - # - # test 'input should allow overriding collection for select types' do - # with_input_for @user, :name, :select, :collection => ['Jose', 'Carlos'] - # assert_select 'select.select#user_name' - # assert_select 'select option', 'Jose' - # assert_select 'select option', 'Carlos' - # end - # - # test 'input should mark the selected value by default' do - # @user.name = "Carlos" - # with_input_for @user, :name, :select, :collection => ['Jose', 'Carlos'] - # assert_select 'select option[selected=selected]', 'Carlos' - # end - # - # test 'input should mark the selected value also when using integers' do - # @user.age = 18 - # with_input_for @user, :age, :select, :collection => 18..60 - # assert_select 'select option[selected=selected]', '18' - # end - # - # test 'input should automatically set include blank' do - # with_input_for @user, :age, :select, :collection => 18..30 - # assert_select 'select option[value=]', '' - # end - # - # test 'input should not set include blank if otherwise is told' do - # with_input_for @user, :age, :select, :collection => 18..30, :include_blank => false - # assert_no_select 'select option[value=]', '' - # end - # - # test 'input should not set include blank if prompt is given' do - # with_input_for @user, :age, :select, :collection => 18..30, :prompt => "Please select foo" - # assert_no_select 'select option[value=]', '' - # end - # - # test 'input should not set include blank if multiple is given' do - # with_input_for @user, :age, :select, :collection => 18..30, :input_html => { :multiple => true } - # assert_no_select 'select option[value=]', '' - # end - # - # test 'input should detect label and value on collections' do - # users = [ setup_new_user(:id => 1, :name => "Jose"), setup_new_user(:id => 2, :name => "Carlos") ] - # with_input_for @user, :description, :select, :collection => users - # assert_select 'select option[value=1]', 'Jose' - # assert_select 'select option[value=2]', 'Carlos' - # end - # - # test 'input should allow overriding collection for radio types' do - # with_input_for @user, :name, :radio, :collection => ['Jose', 'Carlos'] - # assert_select 'input[type=radio][value=Jose]' - # assert_select 'input[type=radio][value=Carlos]' - # assert_select 'label.collection_radio', 'Jose' - # assert_select 'label.collection_radio', 'Carlos' - # end - # - # test 'input should mark the current radio value by default' do - # @user.name = "Carlos" - # with_input_for @user, :name, :radio, :collection => ['Jose', 'Carlos'] - # assert_select 'input[type=radio][value=Carlos][checked=checked]' - # end - # - # test 'input should allow using a collection with text/value arrays' do - # with_input_for @user, :name, :radio, :collection => [['Jose', 'jose'], ['Carlos', 'carlos']] - # assert_select 'input[type=radio][value=jose]' - # assert_select 'input[type=radio][value=carlos]' - # assert_select 'label.collection_radio', 'Jose' - # assert_select 'label.collection_radio', 'Carlos' - # end - # - # test 'input should allow overriding label and value method for collections' do - # with_input_for @user, :name, :radio, - # :collection => ['Jose' , 'Carlos'], - # :label_method => :upcase, - # :value_method => :downcase - # assert_select 'input[type=radio][value=jose]' - # assert_select 'input[type=radio][value=carlos]' - # assert_select 'label.collection_radio', 'JOSE' - # assert_select 'label.collection_radio', 'CARLOS' - # end - # - # test 'input should be required by default' do - # with_input_for @user, :name, :string - # assert_select 'input.required#user_name' - # end - # - # test 'input should allow disabling required' do - # with_input_for @user, :name, :string, :required => false - # assert_no_select 'input.required' - # assert_select 'input.optional#user_name' - # end - # - # test 'input should get options from column definition for string attributes' do - # with_input_for @user, :name, :string - # assert_select 'input.string[maxlength=100]' - # end - # - # test 'input should get options from column definition for decimal attributes' do - # with_input_for @user, :credit_limit, :decimal - # assert_select 'input.decimal[maxlength=15]' - # end - # - # test 'input should get options from column definition for password attributes' do - # with_input_for @user, :password, :password - # assert_select 'input.password[maxlength=100]' - # end - # - # test 'input should not generate options for different attributes' do - # with_input_for @user, :description, :text - # assert_select 'textarea' - # assert_no_select 'textarea[maxlength]' - # end - # - # test 'input should be generated properly when object is not present' do - # with_input_for :project, :name, :string - # assert_select 'input.string.required#project_name' - # end - # - # test 'input as radio should be generated properly when object is not present ' do - # with_input_for :project, :name, :radio - # assert_select 'input.radio#project_name_true' - # assert_select 'input.radio#project_name_false' - # end - # - # test 'input as select with collection should be generated properly when object is not present' do - # with_input_for :project, :name, :select, :collection => ['Jose', 'Carlos'] - # assert_select 'select.select#project_name' - # end end diff --git a/test/components/label_test.rb b/test/components/label_test.rb index e4038b6d..2f8acbad 100644 --- a/test/components/label_test.rb +++ b/test/components/label_test.rb @@ -3,7 +3,7 @@ require 'test_helper' class LabelTest < ActionView::TestCase setup do - SimpleForm::FormBuilder::Base.reset_i18n_cache :translate_required_html + SimpleForm::Inputs::Base.reset_i18n_cache :translate_required_html end def with_label_for(object, attribute_name, type, options={}) @@ -13,7 +13,7 @@ class LabelTest < ActionView::TestCase f.input_type = type f.options = options - concat(SimpleForm::FormBuilder::Base.new(f).label) + concat(SimpleForm::Inputs::Base.new(f).label) end end From 0c0e63243318f631643720f9740e032225509f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 9 Jan 2010 18:12:46 +0100 Subject: [PATCH 4/7] Tests pass. --- lib/simple_form/form_builder.rb | 2 +- lib/simple_form/inputs/text_field_input.rb | 2 +- test/action_view_extensions/builder_test.rb | 10 +- test/components/error_test.rb | 28 ++-- test/components/hint_test.rb | 15 +- test/components/label_test.rb | 22 --- test/form_builder_test.rb | 19 +-- .../input_test.rb => inputs_test.rb} | 137 ++++++++++-------- test/simple_form_test.rb | 1 - 9 files changed, 109 insertions(+), 127 deletions(-) rename test/{components/input_test.rb => inputs_test.rb} (91%) diff --git a/lib/simple_form/form_builder.rb b/lib/simple_form/form_builder.rb index 4ea546cd..f293d52b 100644 --- a/lib/simple_form/form_builder.rb +++ b/lib/simple_form/form_builder.rb @@ -83,7 +83,7 @@ module SimpleForm if klass = self.class.mappings[input_type] klass.new(self).render else - const_get(:"#{input_type.to_s.camelize}Input").new(self).render + self.class.const_get(:"#{input_type.to_s.camelize}Input").new(self).render end end alias :attribute :input diff --git a/lib/simple_form/inputs/text_field_input.rb b/lib/simple_form/inputs/text_field_input.rb index d06a65fa..bc8aeb38 100644 --- a/lib/simple_form/inputs/text_field_input.rb +++ b/lib/simple_form/inputs/text_field_input.rb @@ -8,7 +8,7 @@ module SimpleForm def input_html_options input_options = super - input_options[:max_length] ||= column.limit if column + input_options[:maxlength] ||= column.limit if column input_options end end diff --git a/test/action_view_extensions/builder_test.rb b/test/action_view_extensions/builder_test.rb index ac182c16..a45b9b20 100644 --- a/test/action_view_extensions/builder_test.rb +++ b/test/action_view_extensions/builder_test.rb @@ -77,8 +77,8 @@ class BuilderTest < ActionView::TestCase concat f.collection_check_boxes :tag_ids, collection, :id, :name end - assert_select 'form label.collection_check_box[for=user_tag_ids_1]', 'Tag 1' - assert_select 'form label.collection_check_box[for=user_tag_ids_2]', 'Tag 2' + assert_select 'form label.collection_check_boxes[for=user_tag_ids_1]', 'Tag 1' + assert_select 'form label.collection_check_boxes[for=user_tag_ids_2]', 'Tag 2' end test 'collection check box accepts selected values as :checked option' do @@ -150,15 +150,15 @@ class BuilderTest < ActionView::TestCase collection = [Tag.new(1, 'Tag 1'), Tag.new(2, 'Tag 2')] form_for @user do |f| f.fields_for :post do |p| - concat p.collection_check_box :tag_ids, collection, :id, :name + concat p.collection_check_boxes :tag_ids, collection, :id, :name end end assert_select 'form input#user_post_tag_ids_1[type=checkbox][value=1]' assert_select 'form input#user_post_tag_ids_2[type=checkbox][value=2]' - assert_select 'form label.collection_check_box[for=user_post_tag_ids_1]', 'Tag 1' - assert_select 'form label.collection_check_box[for=user_post_tag_ids_2]', 'Tag 2' + assert_select 'form label.collection_check_boxes[for=user_post_tag_ids_1]', 'Tag 1' + assert_select 'form label.collection_check_boxes[for=user_post_tag_ids_2]', 'Tag 2' end # SIMPLE FIELDS diff --git a/test/components/error_test.rb b/test/components/error_test.rb index 0a745a17..941563f1 100644 --- a/test/components/error_test.rb +++ b/test/components/error_test.rb @@ -9,27 +9,19 @@ class ErrorTest < ActionView::TestCase f.input_type = type f.options = options - concat(SimpleForm::Inputs::Base.new(f).error) + concat(SimpleForm::Inputs::Base.new(f).error.to_s) end end - # test 'error should not generate content for hidden fields' do - # with_error_for @user, :name, :hidden do |error| - # assert error.call.blank? - # end - # end - # - # test 'error should not generate content for attribute without errors' do - # with_error_for @user, :active, :boolean do |error| - # assert error.call.blank? - # end - # end - # - # test 'error should not generate messages when object is not present' do - # with_error_for :project, :name, :string do |error| - # assert error.call.blank? - # end - # end + test 'error should not generate content for attribute without errors' do + with_error_for @user, :active, :boolean + assert_no_select 'span.error' + end + + test 'error should not generate messages when object is not present' do + with_error_for :project, :name, :string + assert_no_select 'span.error' + end test 'error should generate messages for attribute with single error' do with_error_for @user, :name, :string diff --git a/test/components/hint_test.rb b/test/components/hint_test.rb index cab67b6b..d8e97ca3 100644 --- a/test/components/hint_test.rb +++ b/test/components/hint_test.rb @@ -9,17 +9,14 @@ class HintTest < ActionView::TestCase f.input_type = type f.options = options - concat(SimpleForm::Inputs::Base.new(f).hint) + concat(SimpleForm::Inputs::Base.new(f).hint.to_s) end end - # test 'hint should not be generated by default' do - # assert with_hint_for(@user, :name, :string).blank? - # end - # - # test 'hint should not be generated for hidden fields' do - # assert with_hint_for(@user, :name, :hidden, :hint => 'Use with care...').blank? - # end + test 'hint should not be generated by default' do + with_hint_for @user, :name, :string + assert_no_select 'span.hint' + end test 'hint should be generated with input text' do with_hint_for @user, :name, :string, :hint => 'Use with care...' @@ -27,7 +24,7 @@ class HintTest < ActionView::TestCase end test 'hint uses the current component tag set' do - swap SimpleForm, :component_tag => :p do + swap SimpleForm, :hint_tag => :p do with_hint_for @user, :name, :string, :hint => 'Use with care...' assert_select 'p.hint', 'Use with care...' end diff --git a/test/components/label_test.rb b/test/components/label_test.rb index 2f8acbad..fca05f43 100644 --- a/test/components/label_test.rb +++ b/test/components/label_test.rb @@ -17,13 +17,6 @@ class LabelTest < ActionView::TestCase end end - # Fix me! - # test 'label should not be generated for hidden inputs' do - # with_label_for @user, :name, :hidden do |label| - # assert label.call.blank? - # end - # end - test 'label should generate a default humanized description' do with_label_for @user, :name, :string assert_select 'label[for=user_name]', /Name/ @@ -174,19 +167,4 @@ class LabelTest < ActionView::TestCase with_label_for :project, :description, :string, :required => false assert_no_select 'label.required[for=project_description]' end - - test 'label should point to first option when date input type' do - with_label_for :project, :created_at, :date - assert_select 'label[for=project_created_at_1i]' - end - - test 'label should point to first option when datetime input type' do - with_label_for :project, :created_at, :datetime - assert_select 'label[for=project_created_at_1i]' - end - - test 'label should point to first option when time input type' do - with_label_for :project, :created_at, :time - assert_select 'label[for=project_created_at_4i]' - end end diff --git a/test/form_builder_test.rb b/test/form_builder_test.rb index 57d82b15..88591e5c 100644 --- a/test/form_builder_test.rb +++ b/test/form_builder_test.rb @@ -38,6 +38,15 @@ class FormBuilderTest < ActionView::TestCase end end + # All + test 'nested simple fields should yields an instance of FormBuilder' do + simple_form_for :user do |f| + f.simple_fields_for :posts do |posts_form| + assert posts_form.instance_of?(SimpleForm::FormBuilder) + end + end + end + # INPUT TYPES test 'builder should generate text fields for string columns' do with_form_for @user, :name @@ -209,19 +218,11 @@ class FormBuilderTest < ActionView::TestCase test 'builder allows wrapper tag to be given on demand' do simple_form_for @user do |f| - concat f.input :name, :wrapper => :b + concat f.input :name, :wrapper_tag => :b end assert_select 'form b.required.string' end - test 'nested simple fields should yields an instance of FormBuilder' do - simple_form_for :user do |f| - f.simple_fields_for :posts do |posts_form| - assert posts_form.instance_of?(SimpleForm::FormBuilder) - end - end - end - # WITHOUT OBJECT test 'builder should generate properly when object is not present' do with_form_for :project, :name diff --git a/test/components/input_test.rb b/test/inputs_test.rb similarity index 91% rename from test/components/input_test.rb rename to test/inputs_test.rb index 948e00e6..0740b3f5 100644 --- a/test/components/input_test.rb +++ b/test/inputs_test.rb @@ -3,27 +3,16 @@ require 'test_helper' class InputTest < ActionView::TestCase setup do - SimpleForm::Inputs::Base.reset_i18n_cache :boolean_collection + SimpleForm::Inputs::CollectionInput.reset_i18n_cache :boolean_collection end def with_input_for(object, attribute_name, type, options={}) simple_form_for object do |f| - f.attribute_name = attribute_name - f.column = object.column_for_attribute(attribute_name) if object.respond_to?(:column_for_attribute) - f.input_type = type - f.options = options - - klass = SimpleForm::FormBuilder.mappings[type] - klass ||= SimpleForm::Inputs.const_get(:"#{type.to_s.camelize}Input") - concat(klass.new(f).input) + concat f.input(attribute_name, options.merge(:as => type)) end end - test 'input should map text field to string attribute' do - with_input_for @user, :name, :string - assert_select 'input[name=\'user[name]\'][id=user_name][value=New in Simple Form!]' - end - + # ALL test 'input should generate css class based on default input type' do with_input_for @user, :name, :string assert_select 'input.string' @@ -36,17 +25,29 @@ class InputTest < ActionView::TestCase with_input_for @user, :created_at, :datetime assert_select 'select.datetime' end - - test 'input should allow passing options to text field' do + + test 'input should allow passing options to input field' do with_input_for @user, :name, :string, :input_html => { :class => 'my_input', :id => 'my_input' } assert_select 'input#my_input.my_input' end - - test 'input should generate a text area for text attributes' do - with_input_for @user, :description, :text - assert_select 'textarea.text#user_description' + + test 'input should be required by default' do + with_input_for @user, :name, :string + assert_select 'input.required#user_name' end + test 'input should allow disabling required' do + with_input_for @user, :name, :string, :required => false + assert_no_select 'input.required' + assert_select 'input.optional#user_name' + end + + # TextFieldInput + test 'input should map text field to string attribute' do + with_input_for @user, :name, :string + assert_select 'input[name=\'user[name]\'][id=user_name][value=New in Simple Form!]' + end + test 'input should generate an integer text field for integer attributes ' do with_input_for @user, :age, :integer assert_select 'input.integer#user_age' @@ -61,7 +62,23 @@ class InputTest < ActionView::TestCase with_input_for @user, :age, :decimal assert_select 'input.decimal#user_age' end + + test 'input should get options from column definition for string attributes' do + with_input_for @user, :name, :string + assert_select 'input.string[maxlength=100]' + end + test 'input should get options from column definition for decimal attributes' do + with_input_for @user, :credit_limit, :decimal + assert_select 'input.decimal[maxlength=15]' + end + + # MappingInput + test 'input should generate a text area for text attributes' do + with_input_for @user, :description, :text + assert_select 'textarea.text#user_description' + end + test 'input should generate a checkbox by default for boolean attributes' do with_input_for @user, :active, :boolean assert_select 'input[type=checkbox].boolean#user_active' @@ -71,18 +88,30 @@ class InputTest < ActionView::TestCase with_input_for @user, :password, :password assert_select 'input[type=password].password#user_password' end - + + test 'input should generate a file field' do + with_input_for @user, :name, :file + assert_select 'input#user_name[type=file]' + end + + # HiddenInput test 'input should generate a hidden field' do with_input_for @user, :name, :hidden assert_no_select 'input[type=text]' assert_select 'input#user_name[type=hidden]' end - - test 'input should generate a file field' do - with_input_for @user, :name, :file - assert_select 'input#user_name[type=file]' + + test 'hint should not be generated for hidden fields' do + with_input_for @user, :name, :hidden, :hint => 'Use with care...' + assert_no_select 'span.hint' end - + + test 'label should not be generated for hidden inputs' do + with_input_for @user, :name, :hidden + assert_no_select 'label' + end + + # PriorityInput test 'input should generate a country select field' do with_input_for @user, :country, :country assert_select 'select#user_country' @@ -96,7 +125,7 @@ class InputTest < ActionView::TestCase assert_select 'select option[value=][disabled=disabled]' end end - + test 'input should generate a time zone select field' do with_input_for @user, :time_zone, :time_zone assert_select 'select#user_time_zone' @@ -113,7 +142,8 @@ class InputTest < ActionView::TestCase with_input_for @user, :time_zone, :time_zone, :priority => /Brasilia/ assert_select 'select option[value=][disabled=disabled]' end - + + # DateTime input test 'input should generate a datetime select by default for datetime attributes' do with_input_for @user, :created_at, :datetime 1.upto(5) do |i| @@ -166,7 +196,23 @@ class InputTest < ActionView::TestCase assert_select 'select.time option', 'hora' assert_select 'select.time option', 'minuto' end - + + test 'label should point to first option when date input type' do + with_input_for :project, :created_at, :date + assert_select 'label[for=project_created_at_1i]' + end + + test 'label should point to first option when datetime input type' do + with_input_for :project, :created_at, :datetime + assert_select 'label[for=project_created_at_1i]' + end + + test 'label should point to first option when time input type' do + with_input_for :project, :created_at, :time + assert_select 'label[for=project_created_at_4i]' + end + + # CollectionInput test 'input should generate boolean radio buttons by default for radio types' do with_input_for @user, :active, :radio assert_select 'input[type=radio][value=true].radio#user_active_true' @@ -281,38 +327,7 @@ class InputTest < ActionView::TestCase assert_select 'label.collection_radio', 'CARLOS' end - test 'input should be required by default' do - with_input_for @user, :name, :string - assert_select 'input.required#user_name' - end - - test 'input should allow disabling required' do - with_input_for @user, :name, :string, :required => false - assert_no_select 'input.required' - assert_select 'input.optional#user_name' - end - - test 'input should get options from column definition for string attributes' do - with_input_for @user, :name, :string - assert_select 'input.string[maxlength=100]' - end - - test 'input should get options from column definition for decimal attributes' do - with_input_for @user, :credit_limit, :decimal - assert_select 'input.decimal[maxlength=15]' - end - - test 'input should get options from column definition for password attributes' do - with_input_for @user, :password, :password - assert_select 'input.password[maxlength=100]' - end - - test 'input should not generate options for different attributes' do - with_input_for @user, :description, :text - assert_select 'textarea' - assert_no_select 'textarea[maxlength]' - end - + # With no object test 'input should be generated properly when object is not present' do with_input_for :project, :name, :string assert_select 'input.string.required#project_name' diff --git a/test/simple_form_test.rb b/test/simple_form_test.rb index 0209d775..662e13d9 100644 --- a/test/simple_form_test.rb +++ b/test/simple_form_test.rb @@ -1,7 +1,6 @@ require 'test_helper' class SimpleFormTest < ActiveSupport::TestCase - test 'setup block yields self' do SimpleForm.setup do |config| assert_equal SimpleForm, config From b1df90790eacc81da4a1af1b3d7797add1b43720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 10 Jan 2010 05:12:57 +0100 Subject: [PATCH 5/7] Add block support. --- TODO.rdoc | 1 - lib/simple_form/form_builder.rb | 27 ++++++++++--- lib/simple_form/inputs.rb | 1 + lib/simple_form/inputs/block_input.rb | 13 ++++++ test/form_builder_test.rb | 58 ++++++++++++++++++++++----- test/inputs_test.rb | 16 -------- test/support/models.rb | 7 ++++ 7 files changed, 91 insertions(+), 32 deletions(-) create mode 100644 lib/simple_form/inputs/block_input.rb diff --git a/TODO.rdoc b/TODO.rdoc index 7f276451..0d856858 100644 --- a/TODO.rdoc +++ b/TODO.rdoc @@ -2,7 +2,6 @@ * Sample CSS * Add default size support -* Allow block to be given to overwrite content == Validations diff --git a/lib/simple_form/form_builder.rb b/lib/simple_form/form_builder.rb index f293d52b..9e7b27ce 100644 --- a/lib/simple_form/form_builder.rb +++ b/lib/simple_form/form_builder.rb @@ -77,13 +77,15 @@ module SimpleForm # Some inputs, as :time_zone and :country accepts a :priority option. If none is # given SimpleForm.time_zone_priority and SimpleForm.country_priority are used respectivelly. # - def input(attribute_name, options={}) + def input(attribute_name, options={}, &block) define_simple_form_attributes(attribute_name, options) - if klass = self.class.mappings[input_type] - klass.new(self).render + if block_given? + SimpleForm::Inputs::BlockInput.new(self, block).render else - self.class.const_get(:"#{input_type.to_s.camelize}Input").new(self).render + klass = self.class.mappings[input_type] || + self.class.const_get(:"#{input_type.to_s.camelize}Input") + klass.new(self).render end end alias :attribute :input @@ -123,7 +125,22 @@ module SimpleForm # f.association :company, :scope => [ :public, :not_broken ] # # Same as doing Company.public.not_broken.all # - def association(association, options={}) + # == Block + # + # When a block is given, association simple behaves as a proxy to + # simple_fields_for: + # + # f.association :company do |c| + # c.input :name + # c.input :type + # end + # + # From the options above, only :collection can also be supplied. + # + def association(association, options={}, &block) + return simple_fields_for(*[association, + options.delete(:collection), options].compact, &block) if block_given? + raise ArgumentError, "Association cannot be used in forms not associated with an object" unless @object options[:as] ||= :select diff --git a/lib/simple_form/inputs.rb b/lib/simple_form/inputs.rb index 1cd44896..1f422f54 100644 --- a/lib/simple_form/inputs.rb +++ b/lib/simple_form/inputs.rb @@ -1,6 +1,7 @@ module SimpleForm module Inputs autoload :Base, 'simple_form/inputs/base' + autoload :BlockInput, 'simple_form/inputs/block_input' autoload :CollectionInput, 'simple_form/inputs/collection_input' autoload :DateTimeInput, 'simple_form/inputs/date_time_input' autoload :HiddenInput, 'simple_form/inputs/hidden_input' diff --git a/lib/simple_form/inputs/block_input.rb b/lib/simple_form/inputs/block_input.rb new file mode 100644 index 00000000..d75b1cfe --- /dev/null +++ b/lib/simple_form/inputs/block_input.rb @@ -0,0 +1,13 @@ +module SimpleForm + module Inputs + class BlockInput < Base + def initialize(builder, block) + @builder, @block = builder, block + end + + def input + template.capture(&@block) + end + end + end +end \ No newline at end of file diff --git a/test/form_builder_test.rb b/test/form_builder_test.rb index 88591e5c..44b7c438 100644 --- a/test/form_builder_test.rb +++ b/test/form_builder_test.rb @@ -2,39 +2,39 @@ require 'test_helper' class FormBuilderTest < ActionView::TestCase - def with_form_for(object, attribute, options={}) + def with_form_for(object, *args, &block) simple_form_for object do |f| - concat f.input attribute, options + concat f.input(*args, &block) end end def with_button_for(object, *args) simple_form_for object do |f| - concat f.button *args + concat f.button(*args) end end def with_error_for(object, *args) simple_form_for object do |f| - concat f.error *args + concat f.error(*args) end end def with_hint_for(object, *args) simple_form_for object do |f| - concat f.hint *args + concat f.hint(*args) end end def with_label_for(object, *args) simple_form_for object do |f| - concat f.label *args + concat f.label(*args) end end def with_association_for(object, *args) simple_form_for object do |f| - concat f.association *args + concat f.association(*args) end end @@ -190,6 +190,25 @@ class FormBuilderTest < ActionView::TestCase assert_no_select 'span.error' end + test 'builder input should be required by default' do + with_form_for @user, :name + assert_select 'input.required#user_name' + end + + test 'builder input should allow disabling required' do + with_form_for @user, :name, :required => false + assert_no_select 'input.required' + assert_select 'input.optional#user_name' + end + + test 'builder input should allow a block to configure input' do + with_form_for @user, :name do + concat text_field_tag :foo, :bar, :id => :cool + end + assert_no_select 'input.string' + assert_select 'input#cool' + end + # WRAPPERS test 'builder support wrapping around an specific tag' do swap SimpleForm, :wrapper_tag => :p do @@ -249,7 +268,7 @@ class FormBuilderTest < ActionView::TestCase end # ERRORS - test 'builder should generate an error component tag for the attribute' do + test 'builder should generate an error tag for the attribute' do with_error_for @user, :name assert_select 'span.error', "can't be blank" end @@ -260,7 +279,7 @@ class FormBuilderTest < ActionView::TestCase end # HINTS - test 'builder should generate a hint component tag for the attribute' do + test 'builder should generate a hint tag for the attribute' do store_translations(:en, :simple_form => { :hints => { :user => { :name => "Add your name" }}}) do with_hint_for @user, :name assert_select 'span.hint', 'Add your name' @@ -278,7 +297,7 @@ class FormBuilderTest < ActionView::TestCase end # LABELS - test 'builder should generate a label component tag for the attribute' do + test 'builder should generate a label for the attribute' do with_label_for @user, :name assert_select 'label.string[for=user_name]', /Name/ end @@ -370,6 +389,25 @@ class FormBuilderTest < ActionView::TestCase end end + test 'builder association with a block call simple_fields_for' do + simple_form_for @user do |f| + f.association :posts do |posts_form| + assert posts_form.instance_of?(SimpleForm::FormBuilder) + end + end + end + + test 'builder association forwards collection to simple_fields_for' do + calls = 0 + simple_form_for @user do |f| + f.association :company, :collection => Company.all do |c| + calls += 1 + end + end + + assert_equal calls, 3 + end + # ASSOCIATIONS - BELONGS TO test 'builder creates a select for belongs_to associations' do with_association_for @user, :company diff --git a/test/inputs_test.rb b/test/inputs_test.rb index 0740b3f5..ddfababe 100644 --- a/test/inputs_test.rb +++ b/test/inputs_test.rb @@ -26,22 +26,6 @@ class InputTest < ActionView::TestCase assert_select 'select.datetime' end - test 'input should allow passing options to input field' do - with_input_for @user, :name, :string, :input_html => { :class => 'my_input', :id => 'my_input' } - assert_select 'input#my_input.my_input' - end - - test 'input should be required by default' do - with_input_for @user, :name, :string - assert_select 'input.required#user_name' - end - - test 'input should allow disabling required' do - with_input_for @user, :name, :string, :required => false - assert_no_select 'input.required' - assert_select 'input.optional#user_name' - end - # TextFieldInput test 'input should map text field to string attribute' do with_input_for @user, :name, :string diff --git a/test/support/models.rb b/test/support/models.rb index a1f90fd4..7027d2a0 100644 --- a/test/support/models.rb +++ b/test/support/models.rb @@ -12,6 +12,10 @@ class Company < Struct.new(:id, :name) return all[1..2] if options[:joins] all end + + def new_record? + false + end end class Tag < Struct.new(:id, :name) @@ -32,6 +36,9 @@ class User < OpenStruct @new_record || false end + def company_attributes=(foo) + end + def column_for_attribute(attribute) column_type, limit = case attribute.to_sym when :name, :status, :password then [:string, 100] From 2af0377bb43b94e69093f341691bd61c4d3e8182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 10 Jan 2010 05:18:10 +0100 Subject: [PATCH 6/7] Ensure output is html safe. --- lib/simple_form/inputs/base.rb | 2 +- test/form_builder_test.rb | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/simple_form/inputs/base.rb b/lib/simple_form/inputs/base.rb index 4fdb5f9b..adc4773b 100644 --- a/lib/simple_form/inputs/base.rb +++ b/lib/simple_form/inputs/base.rb @@ -40,7 +40,7 @@ module SimpleForm send(component) end content.compact! - wrap(content.join) + wrap(content.join).html_safe! end protected diff --git a/test/form_builder_test.rb b/test/form_builder_test.rb index 44b7c438..b2927993 100644 --- a/test/form_builder_test.rb +++ b/test/form_builder_test.rb @@ -47,6 +47,12 @@ class FormBuilderTest < ActionView::TestCase end end + test 'builder input is html safe' do + simple_form_for @user do |f| + assert f.input(:name).html_safe? + end + end + # INPUT TYPES test 'builder should generate text fields for string columns' do with_form_for @user, :name From 753476050bc2ae68e68e5d6f2c3d3adad29a6042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 10 Jan 2010 05:24:53 +0100 Subject: [PATCH 7/7] More tests. --- test/form_builder_test.rb | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/test/form_builder_test.rb b/test/form_builder_test.rb index b2927993..45fd63ff 100644 --- a/test/form_builder_test.rb +++ b/test/form_builder_test.rb @@ -53,6 +53,14 @@ class FormBuilderTest < ActionView::TestCase end end + test 'builder input should allow a block to configure input' do + with_form_for @user, :name do + concat text_field_tag :foo, :bar, :id => :cool + end + assert_no_select 'input.string' + assert_select 'input#cool' + end + # INPUT TYPES test 'builder should generate text fields for string columns' do with_form_for @user, :name @@ -151,7 +159,7 @@ class FormBuilderTest < ActionView::TestCase test 'builder should generate a input with label' do with_form_for @user, :name - assert_select 'form label.string[for=user_name]' + assert_select 'form label.string[for=user_name]', /Name/ end test 'builder should be able to disable the label for a input' do @@ -161,7 +169,12 @@ class FormBuilderTest < ActionView::TestCase test 'builder should use custom label' do with_form_for @user, :name, :label => 'Yay!' - assert_no_select 'form label', 'Yay!' + assert_select 'form label', /Yay!/ + end + + test 'builder should pass options to label' do + with_form_for @user, :name, :label_html => { :id => "cool" } + assert_select 'form label#cool', /Name/ end test 'builder should not generate hints for a input' do @@ -181,6 +194,11 @@ class FormBuilderTest < ActionView::TestCase end end + test 'builder should pass options to hint' do + with_form_for @user, :name, :hint => 'test', :hint_html => { :id => "cool" } + assert_select 'span.hint#cool', 'test' + end + test 'builder should generate errors for attribute without errors' do with_form_for @user, :credit_limit assert_no_select 'span.errors' @@ -196,6 +214,11 @@ class FormBuilderTest < ActionView::TestCase assert_no_select 'span.error' end + test 'builder should pass options to errors' do + with_form_for @user, :name, :error_html => { :id => "cool" } + assert_select 'span.error#cool', "can't be blank" + end + test 'builder input should be required by default' do with_form_for @user, :name assert_select 'input.required#user_name' @@ -207,14 +230,6 @@ class FormBuilderTest < ActionView::TestCase assert_select 'input.optional#user_name' end - test 'builder input should allow a block to configure input' do - with_form_for @user, :name do - concat text_field_tag :foo, :bar, :id => :cool - end - assert_no_select 'input.string' - assert_select 'input#cool' - end - # WRAPPERS test 'builder support wrapping around an specific tag' do swap SimpleForm, :wrapper_tag => :p do