Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge branch 'master' of git@github.com:justinfrench/formtastic

  • Loading branch information...
commit 1a229ca60d9a9ccc21a7a90e642635da8fecd6f1 2 parents c764539 + 38bfba0
@justinfrench authored
View
1  README.textile
@@ -202,6 +202,7 @@ h2. Contributors
* "Justin French":http://justinfrench.com
* "Xavier Shay":http://rhnh.net
+* Bin Dong
h2. Project Info
View
67 lib/justin_french/formtastic.rb
@@ -21,6 +21,25 @@ module Formtastic #:nodoc:
# <%= f.input :title %>
# <%= f.input :body %>
# <% end %>
+ #
+ # The above examples use a resource-oriented style of form_for() helper where only the @post
+ # object is given as an argument, but the generic style is also supported if you really want it,
+ # as is forms with inline objects (Post.new) rather than objects with instance variables (@post):
+ #
+ # <% semantic_form_for :post, @post, :url => posts_path do |f| %>
+ # ...
+ # <% end %>
+ #
+ # <% semantic_form_for :post, Post.new, :url => posts_path do |f| %>
+ # ...
+ # <% end %>
+ #
+ # The shorter, resource-oriented style is most definitely preferred, and has recieved the most
+ # testing to date.
+ #
+ # Please note: Although it's possible to call Rails' built-in form_for() helper without an
+ # object, all semantic forms *must* have an object (either Post.new or @post), as Formtastic
+ # has too many dependencies on an ActiveRecord object being present.
module SemanticFormHelper
[:form_for, :fields_for, :form_remote_for, :remote_form_for].each do |meth|
src = <<-END_SRC
@@ -84,14 +103,12 @@ class SemanticFormBuilder < ActionView::Helpers::FormBuilder
# <% end %>
# <% end %>
def input(method, options = {})
- raise "@#{@object_name} doesn't respond to the method #{method}" unless @template.instance_eval("@#{@object_name}").respond_to?(method)
+ raise NoMethodError unless @object.respond_to?(method)
options[:required] = @@all_fields_required_by_default if options[:required].nil?
options[:label] ||= method.to_s.humanize
- options[:as] ||= default_input_type(@object_name, method)
-
+ options[:as] ||= default_input_type(@object, method)
input_method = "#{options[:as]}_input"
- raise("Cannot guess an input type for '#{method}' - please set :as option") unless respond_to?(input_method)
content = ''
content += send(input_method, method, options) # eg string_input or select_input
content += inline_errors(method, options)
@@ -166,7 +183,7 @@ def error_messages
def save_or_create_commit_button_text #:nodoc:
- prefix = @template.instance_eval("@#{@object_name}").new_record? ? "Create" : "Save"
+ prefix = @object.new_record? ? "Create" : "Save"
"#{prefix} #{@object_name.humanize}"
end
@@ -342,11 +359,11 @@ def date_or_datetime_input(method, options)
(inputs + time_inputs).each do |input|
if options["discard_#{input}".intern]
break if time_inputs.include?(input)
- list_items_capture << @template.hidden_field_tag("#{@object_name}[#{method}(#{position[input]}i)]", @template.instance_eval("@#{@object_name}").send(method), :id => "#{@object_name}_#{method}_#{position[input]}i")
+ list_items_capture << @template.hidden_field_tag("#{@object_name}[#{method}(#{position[input]}i)]", @object.send(method), :id => "#{@object_name}_#{method}_#{position[input]}i")
else
list_items_capture << @template.content_tag(:li,
@template.content_tag(:label, input.to_s.humanize, :for => "#{@object_name}_#{method}_#{position[input]}i") +
- @template.send("select_#{input}".intern, @template.instance_eval("@#{@object_name}").send(method), :prefix => @object_name, :field_name => "#{method}(#{position[input]}i)")
+ @template.send("select_#{input}".intern, @object.send(method), :prefix => @object_name, :field_name => "#{method}(#{position[input]}i)")
)
end
end
@@ -441,7 +458,7 @@ def boolean_radio_input(method, options)
end
def inline_errors(method, options) #:nodoc:
- errors = @template.instance_eval("@#{@object_name}").errors.on(method).to_a
+ errors = @object.errors.on(method).to_a
errors.empty? ? '' : @template.content_tag(:p, errors.to_sentence, :class => 'inline-errors')
end
@@ -475,22 +492,30 @@ def field_set_and_list_wrapping(field_set_html_options, &block) #:nodoc:
)
end
- def default_input_type(object_name, method) #:nodoc:
- if type = @template.instance_eval("@#{object_name}").send("column_for_attribute", method).type
-
+ # For methods that have a database column, take a best guess as to what the inout method
+ # should be. In most cases, it will just return the column type (eg :string), but for special
+ # cases it will simplify (like the case of :integer, :float & :decimal to :numeric), or do
+ # something different (like :password and :select).
+ #
+ # If there is no column for the method (eg "virtual columns" with an attr_accessor), an error
+ # is raised asking you to specify the :as option for the input.
+ def default_input_type(object, method) #:nodoc:
+ column = object.send("column_for_attribute", method)
+ if column
# handle the special cases where the column type doesn't map to an input method
- return :select if type == :integer && method.to_s =~ /_id$/
- return :password if type == :string && method =~ /password/
- return :datetime if type == :timestamp
- return :numeric if [:integer, :float, :decimal].include?(type)
-
+ return :select if column.type == :integer && method.to_s =~ /_id$/
+ return :datetime if column.type == :timestamp
+ return :numeric if [:integer, :float, :decimal].include?(column.type)
+ return :password if column.type == :string && method.to_s =~ /password/
# otherwise assume the input name will be the same as the column type (eg string_input)
- return type
- end
+ return column.type
+ else
+ raise("Cannot guess an input type for '#{method}' - please set :as option")
+ end
end
-
+
def default_string_options(method) #:nodoc:
- column = @template.instance_eval("@#{@object_name}").column_for_attribute(method)
+ column = @object.column_for_attribute(method)
if column.nil? || column.limit.nil?
{ :size => DEFAULT_TEXT_FIELD_SIZE }
else
@@ -501,7 +526,7 @@ def default_string_options(method) #:nodoc:
def list_item_html_attributes(method, options) #:nodoc:
classes = [options[:as].to_s]
classes << (options[:required] ? 'required' : 'optional')
- classes << 'error' if @template.instance_eval("@#{@object_name}").errors.on(method)
+ classes << 'error' if @object.errors.on(method)
return { :id => "#{@object_name}_#{method}_input", :class => classes.join(" ") }
end
View
362 spec/formtastic_spec.rb
@@ -9,6 +9,7 @@
include ActionView::Helpers::TextHelper
include ActionView::Helpers::ActiveRecordHelper
include ActionView::Helpers::RecordIdentificationHelper
+ include ActiveSupport
include ActionController::PolymorphicRoutes
include JustinFrench::Formtastic::SemanticFormHelper
@@ -16,50 +17,337 @@
def protect_against_forgery?
false
end
-
- describe '#semantic_form_for' do
- it 'yields an instance of SemanticFormBuilder' do
- _erbout = ''
- semantic_form_for(:post, Object.new, :url => '/hello') do |builder|
- builder.class.should == JustinFrench::Formtastic::SemanticFormBuilder
+
+ setup do
+ # Resource-oriented styles like form_for(@post) will expect a path method for the object,
+ # so we're defining some here.
+ def post_path(o); "/posts/1"; end
+ def posts_path; "/posts"; end
+ def new_post_path; "/posts/new"; end
+
+ # Sometimes we need a Post class
+ class Post; end
+
+ # Sometimes we need a mock @post object
+ @new_post = mock('post')
+ @new_post.stub!(:class).and_return(Post)
+ @new_post.stub!(:id).and_return(nil)
+ @new_post.stub!(:new_record?).and_return(true)
+ end
+
+ describe 'SemanticFormHelper' do
+
+ describe '#semantic_form_for' do
+
+ it 'yields an instance of SemanticFormBuilder' do
+ _erbout = ''
+ semantic_form_for(:post, Post.new, :url => '/hello') do |builder|
+ builder.class.should == JustinFrench::Formtastic::SemanticFormBuilder
+ end
+ end
+
+ it 'adds a class of "formtastic" to generated form' do
+ _erbout = ''
+ semantic_form_for(:post, Post.new, :url => '/hello') do |builder|
+ end
+ _erbout.should have_tag("form.formtastic")
+ end
+
+ it 'can be called with a resource-oriented style' do
+ _erbout = ''
+ semantic_form_for(@new_post) do |builder|
+ builder.object.class.should == Post
+ builder.object_name.should == "post"
+ end
end
+
+ xit 'can be called with a resource-oriented style with an inline object' do
+ _erbout = ''
+ semantic_form_for(Post.new) do |builder|
+ builder.object.class.should == Post
+ builder.object_name.should == "post"
+ end
+ end
+
+ it 'can be called with a generic style and instance variable' do
+ _erbout = ''
+ semantic_form_for(:post, @new_post, :url => new_post_path) do |builder|
+ builder.object.class.should == Post
+ builder.object_name.to_s.should == "post" # TODO: is this forced .to_s a bad assumption somewhere?
+ end
+ end
+
+ it 'can be called with a generic style and inline object' do
+ _erbout = ''
+ semantic_form_for(:post, Post.new, :url => new_post_path) do |builder|
+ builder.object.class.should == Post
+ builder.object_name.to_s.should == "post" # TODO: is this forced .to_s a bad assumption somewhere?
+ end
+ end
+
+ xit 'cannot be called without an object' do
+ _erbout = ''
+ lambda {
+ semantic_form_for(:post, :url => new_post_path) do |builder|
+ end
+ }.should raise_error
+ end
+
end
-
- it 'adds a class of "formtastic" to generated form' do
- _erbout = ''
- semantic_form_for(:post, Object.new, :url => '/hello') do |builder|
+
+ describe '#semantic_fields_for' do
+ it 'yields an instance of SemanticFormBuilder' do
+ _erbout = ''
+ semantic_fields_for(:post, Post.new, :url => '/hello') do |builder|
+ builder.class.should == JustinFrench::Formtastic::SemanticFormBuilder
+ end
+ end
+ end
+
+ describe '#semantic_form_remote_for' do
+ it 'yields an instance of SemanticFormBuilder' do
+ _erbout = ''
+ semantic_form_remote_for(:post, Post.new, :url => '/hello') do |builder|
+ builder.class.should == JustinFrench::Formtastic::SemanticFormBuilder
+ end
end
- _erbout.should match_xpath("form/@class", /\bformtastic\b/)
end
+
+ describe '#semantic_form_for_remote' do
+ it 'yields an instance of SemanticFormBuilder' do
+ _erbout = ''
+ semantic_form_remote_for(:post, Post.new, :url => '/hello') do |builder|
+ builder.class.should == JustinFrench::Formtastic::SemanticFormBuilder
+ end
+ end
+ end
+
end
- describe '#input' do
- it 'generates a text field with label' do
- @post = mock('post')
- @post.stub!(:title).and_return('hello')
- @post.stub!(:errors).and_return(mock('errors', :on => nil))
- @post.stub!(:column_for_attribute).and_return(mock('column', :type => :string, :limit => 255))
- _erbout = ''
- semantic_form_for(:post, @post, :url => '/hello') do |builder|
- _erbout += builder.input :title
- end
- _erbout.should have_xpath("form/li/label")
- _erbout.should have_xpath("form/li/input")
- _erbout.should match_xpath("form/li/input/@value", "hello")
- end
+ describe 'SemanticFormBuilder' do
- it 'generates a text area with label' do
- @post = mock('post')
- @post.stub!(:body).and_return('hello')
- @post.stub!(:errors).and_return(mock('errors', :on => nil))
- @post.stub!(:column_for_attribute).and_return(mock('column', :type => :text))
- _erbout = ''
- semantic_form_for(:post, @post, :url => '/hello') do |builder|
- _erbout += builder.input :body
- end
- _erbout.should have_xpath("form/li/label")
- _erbout.should have_xpath("form/li/textarea")
- _erbout.should match_xpath("form/li/textarea", "hello")
+ describe '#input' do
+
+ setup do
+ @new_post.stub!(:title)
+ @new_post.stub!(:body)
+ @new_post.stub!(:errors).and_return(mock('errors', :on => nil))
+ @new_post.stub!(:column_for_attribute).and_return(mock('column', :type => :string, :limit => 255))
+ end
+
+ it 'should require the first argument (the method on form\'s object)' do
+ _erbout = ''
+ lambda {
+ semantic_form_for(@new_post) do |builder|
+ builder.input # no args passed in at all
+ end
+ }.should raise_error(ArgumentError)
+ end
+
+ it 'should raise an error when the object does not respond to the method' do
+ _erbout = ''
+ semantic_form_for(@new_post) do |builder|
+ lambda { builder.input :method_on_post_that_doesnt_exist }.should raise_error(NoMethodError)
+ end
+ end
+
+ it 'should create a list item for each input' do
+ _erbout = ''
+ semantic_form_for(@new_post) do |builder|
+ _erbout += builder.input(:title)
+ _erbout += builder.input(:body)
+ end
+ _erbout.should have_tag('form li', :count => 2)
+ end
+
+ describe ':required option' do
+
+ it 'should set a "required" class when true' do
+ _erbout = ''
+ semantic_form_for(@new_post) do |builder|
+ _erbout += builder.input(:title, :required => true)
+ end
+ _erbout.should_not have_tag('form li.optional')
+ _erbout.should have_tag('form li.required')
+ end
+
+ it 'should set an "optional" class when false' do
+ _erbout = ''
+ semantic_form_for(@new_post) do |builder|
+ _erbout += builder.input(:title, :required => false)
+ end
+ _erbout.should_not have_tag('form li.required')
+ _erbout.should have_tag('form li.optional')
+ end
+
+ it 'should use the default value when none is provided' do
+ JustinFrench::Formtastic::SemanticFormBuilder.all_fields_required_by_default.should == true
+ JustinFrench::Formtastic::SemanticFormBuilder.all_fields_required_by_default = false
+ _erbout = ''
+ semantic_form_for(@new_post) do |builder|
+ _erbout += builder.input(:title)
+ end
+ _erbout.should_not have_tag('form li.required')
+ _erbout.should have_tag('form li.optional')
+ end
+
+ it 'should append the "required" string to the label when true' do
+ string = JustinFrench::Formtastic::SemanticFormBuilder.required_string = " required yo!" # ensure there's something in the string
+ _erbout = ''
+ semantic_form_for(@new_post) do |builder|
+ _erbout += builder.input(:title, :required => true)
+ end
+ _erbout.should have_tag('form li.required label', /#{string}$/)
+ end
+
+ it 'should append the "optional" string to the label when false' do
+ string = JustinFrench::Formtastic::SemanticFormBuilder.optional_string = " optional yo!" # ensure there's something in the string
+ _erbout = ''
+ semantic_form_for(@new_post) do |builder|
+ _erbout += builder.input(:title, :required => false)
+ end
+ _erbout.should have_tag('form li.optional label', /#{string}$/)
+ end
+
+ end
+
+ describe ':as option' do
+
+ def default_input_type(column_type, column_name = :generic_column_name)
+ _erbout = ''
+ @new_post.stub!(column_name)
+ @new_post.stub!(:column_for_attribute).and_return(mock('column', :type => column_type))
+ semantic_form_for(@new_post) do |builder|
+ @default_type = builder.send(:default_input_type, @new_post, column_name)
+ end
+ return @default_type
+ end
+
+ it 'should raise an error for methods that don\'t have a db column' do
+ _erbout = ''
+ @new_post.stub!(:method_without_a_database_column)
+ @new_post.stub!(:column_for_attribute).and_return(nil)
+ semantic_form_for(@new_post) do |builder|
+ lambda {
+ builder.send(:default_input_type, @new_post, :method_without_a_database_column)
+ }.should raise_error("Cannot guess an input type for 'method_without_a_database_column' - please set :as option")
+ end
+ end
+
+ it 'should default to :select for column names ending in "_id"' do
+ default_input_type(:integer, :user_id).should == :select
+ default_input_type(:integer, :section_id).should == :select
+ end
+
+ it 'should default to :password for :string column types with "password" in the method name' do
+ default_input_type(:string, :password).should == :password
+ default_input_type(:string, :hashed_password).should == :password
+ default_input_type(:string, :password_hash).should == :password
+ end
+
+ it 'should default to :text for :text column types' do
+ default_input_type(:text).should == :text
+ end
+
+ it 'should default to :date for :date column types' do
+ default_input_type(:date).should == :date
+ end
+
+ it 'should default to :datetime for :datetime and :timestamp column types' do
+ default_input_type(:datetime).should == :datetime
+ default_input_type(:timestamp).should == :datetime
+ end
+
+ it 'should default to :time for :time column types' do
+ default_input_type(:time).should == :time
+ end
+
+ it 'should default to :boolean for :boolean column types' do
+ default_input_type(:boolean).should == :boolean
+ end
+
+ it 'should default to :string for :string column types' do
+ default_input_type(:string).should == :string
+ end
+
+ it 'should default to :numeric for :integer, :float and :decimal column types' do
+ default_input_type(:integer).should == :numeric
+ default_input_type(:float).should == :numeric
+ default_input_type(:decimal).should == :numeric
+ end
+
+ it 'should call the corresponding input method' do
+ [:select, :radio, :password, :text, :date, :datetime, :time, :boolean, :boolean_select, :string, :numeric].each do |input_style|
+ _erbout = ''
+ @new_post.stub!(:generic_column_name)
+ @new_post.stub!(:column_for_attribute).and_return(mock('column', :type => :string, :limit => 255))
+ semantic_form_for(@new_post) do |builder|
+ builder.should_receive(:"#{input_style}_input").once.and_return("fake HTML output from #input")
+ _erbout += builder.input(:generic_column_name, :as => input_style)
+ end
+ end
+ end
+
+ end
+
+ describe ':label option' do
+
+ it 'should default the method name when not specified and pass it down to the label tag' do
+ _erbout = ''
+ @new_post.stub!(:meta_description) # a two word method name
+ semantic_form_for(@new_post) do |builder|
+ _erbout += builder.input(:meta_description)
+ end
+ _erbout.should have_tag("form li label", /#{'meta_description'.humanize}/)
+ _erbout.should have_tag("form li label", /Meta description/)
+ end
+
+ it 'should be passed down to the label tag when specified' do
+ _erbout = ''
+ semantic_form_for(@new_post) do |builder|
+ _erbout += builder.input(:title, :label => "Kustom")
+ end
+ _erbout.should have_tag("form li label", /Kustom/)
+ end
+
+ end
+
+ describe ':hint option' do
+
+ it 'should be passed down to the paragraph tag when specified' do
+ _erbout = ''
+ hint_text = "this is the title of the post"
+ semantic_form_for(@new_post) do |builder|
+ _erbout += builder.input(:title, :hint => hint_text)
+ end
+ _erbout.should have_tag("form li p.inline-hints", hint_text)
+ end
+
+ end
+
+ # these original specs will eventually go away, once the coverage is up in the new stuff
+ it 'generates a text field with label' do
+ _erbout = ''
+ semantic_form_for(@new_post) do |builder|
+ _erbout += builder.input :title
+ end
+ _erbout.should have_tag("form li label")
+ _erbout.should have_tag("form li input")
+ end
+
+ it 'generates a textarea with label' do
+ _erbout = ''
+ @new_post.stub!(:column_for_attribute).and_return(mock('column', :type => :text, :limit => nil))
+
+ semantic_form_for(@new_post) do |builder|
+ _erbout += builder.input :body
+ end
+ _erbout.should have_tag("form li label")
+ _erbout.should have_tag("form li textarea")
+ end
+
end
+
end
+
end
View
5 spec/test_helper.rb
@@ -6,6 +6,9 @@
require 'action_controller'
require 'action_view'
require 'rexml/document'
-require File.dirname(__FILE__) + '/xpath_matchers'
+require 'rspec_hpricot_matchers'
+Spec::Runner.configure do |config|
+ config.include(RspecHpricotMatchers)
+end
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib')
View
109 spec/xpath_matchers.rb
@@ -1,109 +0,0 @@
-# http://blog.wolfman.com/articles/2008/01/02/xpath-matchers-for-rspec
-
-module Spec
- module Matchers
-
- # check if the xpath exists one or more times
- class HaveXpath
- def initialize(xpath)
- @xpath = xpath
- end
-
- def matches?(response)
- @response = response
- doc = response.is_a?(REXML::Document) ? response : REXML::Document.new(@response)
- match = REXML::XPath.match(doc, @xpath)
- not match.empty?
- end
-
- def failure_message
- "Did not find expected xpath #{@xpath}"
- end
-
- def negative_failure_message
- "Did find unexpected xpath #{@xpath}"
- end
-
- def description
- "match the xpath expression #{@xpath}"
- end
- end
-
- def have_xpath(xpath)
- HaveXpath.new(xpath)
- end
-
- # check if the xpath has the specified value
- # value is a string and there must be a single result to match its
- # equality against
- class MatchXpath
- def initialize(xpath, val)
- @xpath = xpath
- @val= val
- end
-
- def matches?(response)
- @response = response
- doc = response.is_a?(REXML::Document) ? response : REXML::Document.new(@response)
- ok= true
- REXML::XPath.each(doc, @xpath) do |e|
- @actual_val= case e
- when REXML::Attribute
- e.to_s
- when REXML::Element
- e.text
- else
- e.to_s
- end
- if @val.is_a?(String)
- return false unless @val == @actual_val
- else
- return false unless @actual_val =~ @val
- end
- end
- return ok
- end
-
- def failure_message
- "The xpath #{@xpath} did not have the value '#{@val}'\nIt was '#{@actual_val}'"
- end
-
- def description
- "match the xpath expression #{@xpath} with #{@val}"
- end
- end
-
- def match_xpath(xpath, val)
- MatchXpath.new(xpath, val)
- end
-
- # checks if the given xpath occurs num times
- class HaveNodes #:nodoc:
- def initialize(xpath, num)
- @xpath= xpath
- @num = num
- end
-
- def matches?(response)
- @response = response
- doc = response.is_a?(REXML::Document) ? response : REXML::Document.new(@response)
- match = REXML::XPath.match(doc, @xpath)
- @num_found= match.size
- @num_found == @num
- end
-
- def failure_message
- "Did not find expected number of nodes #{@num} in xpath #{@xpath}\nFound #{@num_found}"
- end
-
- def description
- "match the number of nodes #{@num}"
- end
- end
-
- def have_nodes(xpath, num)
- HaveNodes.new(xpath, num)
- end
-
- end
-end
Please sign in to comment.
Something went wrong with that request. Please try again.