forme 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ === HEAD
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2011 Jeremy Evans
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to
5
+ deal in the Software without restriction, including without limitation the
6
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
+ sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,231 @@
1
+ = Forme
2
+
3
+ Forme is a HTML forms library for ruby with the following goals:
4
+
5
+ 1. Have no external dependencies
6
+ 2. Have a simple API
7
+ 3. Support forms both with and without related objects
8
+ 4. Allow compiling down to different types of output
9
+
10
+ = Demo Site
11
+
12
+ A demo site is available at http://forme.heroku.com
13
+
14
+ = Source Code
15
+
16
+ Source code is available on GitHub at https://github.com/jeremyevans/forme
17
+
18
+ = Basic Usage
19
+
20
+ Without an object, Forme is a simple form builder:
21
+
22
+ f = Forme::Form.new
23
+ f.open(:action=>'/foo', :method=>:post) # '<form action="/foo" method="post">'
24
+ f.input(:textarea, :value=>'foo', :name=>'bar') # '<textarea name="bar">foo</textarea>'
25
+ f.input(:text, :value=>'foo', :name=>'bar') # '<input name="bar" type="text" value="foo"/>'
26
+ f.close # '</form>'
27
+
28
+ With an object, <tt>Form#input</tt> calls +forme_input+ on the obj with the form, field, and options, which
29
+ should return a <tt>Forme::Input</tt> or <tt>Forme::Tag</tt> instance. Also, in <tt>Form#initialize</tt>,
30
+ +forme_config+ is called on object with the form if the object responds to it, allowing customization of
31
+ the entire form based on the object.
32
+
33
+ f = Forme::Form.new(obj)
34
+ f.input(:field) # '<input id="obj_field" name="obj[field]" type="text" value="foo"/>'
35
+
36
+ If the object doesn't respond to +forme_input+, it falls back to creating text fields
37
+ with the name and id set to the field name and the value set by calling the given method
38
+ on the object.
39
+
40
+ f = Forme::Form.new([:foo])
41
+ f.input(:first) # '<input id="first" name="first" type="text" value="foo"/>'
42
+
43
+ = DSL
44
+
45
+ Forme comes with a DSL:
46
+
47
+ Forme.form(:action=>'/foo') do |f|
48
+ f.input(:text, :name=>'bar')
49
+ f.tag(:fieldset) do
50
+ f.input(:textarea, :name=>'baz')
51
+ end
52
+ end
53
+ # <form action="/foo">
54
+ # <input name="bar" type="text"/>
55
+ # <fieldset>
56
+ # <textarea name="baz"></textarea>
57
+ # </fieldset>
58
+ # </form>
59
+
60
+ You can wrap up multiple inputs with the <tt>:inputs</tt> method:
61
+
62
+ Forme.form(:action=>'/foo') do |f|
63
+ f.inputs([[:text, {:name=>'bar'}], [:textarea, {:name=>'baz'}]])
64
+ end
65
+ # <form action="/foo">
66
+ # <fieldset class="inputs">
67
+ # <input name="bar" type="text"/>
68
+ # <textarea name="baz"></textarea>
69
+ # </fieldset>
70
+ # </form>
71
+
72
+ You can even do everything in a single method call:
73
+
74
+ Forme.form({:action=>'/foo'},
75
+ :inputs=>[[:text, {:name=>'bar'}], [:textarea, {:name=>'baz'}]])
76
+
77
+ = Basic Design
78
+
79
+ Interally, Forme builds an abstract syntax tree of objects that
80
+ represent the form. The abstract syntax tree goes through a
81
+ series of transformations that convert it from high level
82
+ abstract forms to low level abstract forms and finally to
83
+ strings. Here are the main classes used by the library:
84
+
85
+ <tt>Forme::Form</tt> :: main object
86
+ <tt>Forme::Input</tt> :: high level abstract tag (a single +Input+ could represent a select box with a bunch of options)
87
+ <tt>Forme::Tag</tt> :: low level abstract tag representing an html tag (there would be a separate +Tag+ for each option in a select box)
88
+
89
+ The group of objects that perform the transformations to
90
+ the abstract syntax trees are known as transformers.
91
+ Transformers use a functional style, and all use a +call+-based
92
+ API, so you can use a +Proc+ for any custom transformer.
93
+
94
+ == Transformer Types
95
+
96
+ +serializer+ :: tags input/tag, returns string
97
+ +formatter+ :: takes input, returns tag
98
+ +error_handler+ :: takes tag and input, returns version of tag with errors noted
99
+ +labeler+ :: takes tag and input, returns labeled version of tag
100
+ +wrapper+ :: takes tag and input, returns wrapped version of tag
101
+ +inputs_wrapper+ :: takes form, options hash, and block, wrapping block in a tag
102
+
103
+ The +serializer+ is the base of the transformations. It turns +Tag+ instances into strings. If it comes across
104
+ an +Input+, it calls the +formatter+ on the +Input+ to turn it into a +Tag+, and then serializes
105
+ that +Tag+. The +formatter+ first converts the +Input+ to a +Tag+, and then calls the
106
+ +error_handler+ if the <tt>:error</tt> option is set and the +labeler+ if the <tt>:label</tt>
107
+ option is set. Finally, it calls the +wrapper+ to wrap the resulting tag before returning it.
108
+
109
+ The +inputs_wrapper+ is called by <tt>Forme::Form#inputs</tt> and serves to wrap a bunch
110
+ of related inputs.
111
+
112
+ == Built-in Transformers
113
+
114
+ Forme ships with a bunch of built-in transformers that you can use:
115
+
116
+ === +serializer+
117
+
118
+ :default :: returns HTML strings
119
+ :html_usa :: returns HTML strings, formats dates and times in American format without timezones
120
+ :text :: returns plain text strings
121
+
122
+ === +formatter+
123
+
124
+ :default :: turns Inputs into Tags
125
+ :disabled :: disables all resulting input tags
126
+ :readonly :: uses +span+ tags for most values, good for printable versions of forms
127
+
128
+ === +error_handler+
129
+
130
+ :default :: modifies tag to add an error class and adds a span with the error message
131
+
132
+ === +labeler+
133
+
134
+ :default :: uses implicit labels, where the tag is a child of the label tag
135
+ :explicit :: uses explicit labels with the for attribute, where tag is a sibling of the label tag
136
+
137
+ === +wrapper+
138
+
139
+ :default :: returns tag without wrapping
140
+ :li :: wraps tag in li tag
141
+ :p :: wraps tag in p tag
142
+ :div :: wraps tag in div tag
143
+ :span :: wraps tag in span tag
144
+ :trtd :: wraps tag in a tr tag with a td for the label and a td for the tag, useful for lining up
145
+ inputs with the :explicit labeler without CSS
146
+
147
+ === +inputs_wrapper+
148
+
149
+ :default :: uses a fieldset to wrap inputs
150
+ :ol :: uses an ol tag to wrap inputs, useful with :li wrapper
151
+ :div :: uses a div tag to wrap inputs
152
+ :fieldset_ol :: use both a fieldset and an ol tag to wrap inputs
153
+ :table :: uses a table tag to wrap inputs, useful with :trtd wrapper
154
+
155
+ == Configurations
156
+
157
+ You can associate a group of transformers into a configuration. This allows you to
158
+ specify a single :config option when creating a +Form+ and have it automatically
159
+ set all the related transformers.
160
+
161
+ There are a few configurations supported by default:
162
+
163
+ :default :: All +default+ transformers
164
+ :formtastic :: +fieldset_ol+ inputs_wrapper, +li+ wrapper, +explicit+ labeler
165
+
166
+ You can register and use your own configurations easily:
167
+
168
+ Forme.register_config(:mine, :wrapper=>:li, :inputs_wrapper=>:ol, :serializer=>:html_usa)
169
+ Forme::Form.new(:config=>:mine)
170
+
171
+ If you want to, you can base your configuration on an existing configuration:
172
+
173
+ Forme.register_config(:yours, :base=>:mine, :inputs_wrapper=>:fieldset_ol)
174
+
175
+ You can mark a configuration as the default using:
176
+
177
+ Forme.default_config = :mine
178
+
179
+ = Sequel Support
180
+
181
+ Forme ships with a Sequel plugin (use <tt>Sequel::Model.plugin :forme</tt> to enable), that makes
182
+ Sequel::Model instances support the +forme_config+ and +forme_input+ methods and return customized inputs.
183
+
184
+ It deals with inputs based on database columns, virtual columns, and associations. It also handles
185
+ nested associations using the +subform+ method:
186
+
187
+ Forme.form(Album[1], :action=>'/foo') do |f|
188
+ f.inputs([:name, :copies_sold, :tags]) do
189
+ f.subform(:artist, :inputs=>[:name])
190
+ f.subform(:tracks, :inputs=>[:number, :name])
191
+ end
192
+ end
193
+
194
+ For many_to_one associations, you can use the <tt>:as=>:radio</tt> option to use a series of radio buttons,
195
+ and for one_to_many and many_to_many associations, you can use the <tt>:as=>:checkbox</tt> option to use a
196
+ series of checkboxes. For one_to_many and many_to_many associations, you will probably want to use the
197
+ +association_pks+ plugin that ships with Sequel.
198
+
199
+ The Forme Sequel plugin also integerates with Sequel's validation reflection support with the
200
+ +validation_class_methods+ plugin that ships with Sequel. It will add +pattern+ and +maxlength+ attributes
201
+ based on the format, numericality, and length validations.
202
+
203
+ = Sinatra ERB Support
204
+
205
+ Forme ships with a Sinatra extension that you can get by <tt>require "forme/sinatra"</tt> and using
206
+ <tt>helpers Forme::Sinatra::ERB</tt> in your Sinatra::Base subclass. It allows you to use the
207
+ following API in your Sinatra ERB forms:
208
+
209
+ <% form(@obj, :action=>'/foo') do |f| %>
210
+ <%= f.input(:field) %>
211
+ <% f.tag(:fieldset) do %>
212
+ <%= f.input(:field_two) %>
213
+ <% end %>
214
+ <% end %>
215
+
216
+ In addition to ERB, it also works with Sinatra's Erubis support.
217
+
218
+ = Other Similar Projects
219
+
220
+ All of these have external dependencies:
221
+
222
+ 1. Rails built-in helpers
223
+ 2. Formtastic
224
+ 3. simple_form
225
+ 4. padrino-helpers
226
+
227
+ Forme's API draws a lot of inspiration from both Formtastic and simple_form.
228
+
229
+ = Author
230
+
231
+ Jeremy Evans <code@jeremyevans.net>
@@ -0,0 +1,97 @@
1
+ require "rake"
2
+ require "rake/clean"
3
+ begin
4
+ require "hanna/rdoctask"
5
+ rescue LoadError
6
+ require "rake/rdoctask"
7
+ end
8
+
9
+ NAME = 'forme'
10
+ VERS = lambda do
11
+ require File.expand_path("../lib/forme/version", __FILE__)
12
+ Forme.version
13
+ end
14
+ CLEAN.include ["#{NAME}-*.gem", "rdoc", "coverage", '**/*.rbc']
15
+ RDOC_DEFAULT_OPTS = ["--quiet", "--line-numbers", "--inline-source", '--title', 'Forme']
16
+ RDOC_OPTS = RDOC_DEFAULT_OPTS + ['--main', 'README.rdoc']
17
+
18
+ # Gem Packaging and Release
19
+
20
+ desc "Packages #{NAME}"
21
+ task :package=>[:clean] do |p|
22
+ sh %{gem build #{NAME}.gemspec}
23
+ end
24
+
25
+ desc "Upload #{NAME} gem to rubygems"
26
+ task :release=>[:package] do
27
+ sh %{gem push ./#{NAME}-#{VERS.call}.gem}
28
+ end
29
+
30
+ ### RDoc
31
+
32
+ Rake::RDocTask.new do |rdoc|
33
+ rdoc.rdoc_dir = "rdoc"
34
+ rdoc.options += RDOC_OPTS
35
+ rdoc.rdoc_files.add %w"README.rdoc CHANGELOG MIT-LICENSE lib/**/*.rb"
36
+ end
37
+
38
+ ### Specs
39
+
40
+ begin
41
+ begin
42
+ # RSpec 1
43
+ require "spec/rake/spectask"
44
+ spec_class = Spec::Rake::SpecTask
45
+ spec_files_meth = :spec_files=
46
+ rescue LoadError
47
+ # RSpec 2
48
+ require "rspec/core/rake_task"
49
+ spec_class = RSpec::Core::RakeTask
50
+ spec_files_meth = :pattern=
51
+ end
52
+
53
+ spec = lambda do |name, files, d|
54
+ lib_dir = File.join(File.dirname(File.expand_path(__FILE__)), 'lib')
55
+ ENV['RUBYLIB'] ? (ENV['RUBYLIB'] += ":#{lib_dir}") : (ENV['RUBYLIB'] = lib_dir)
56
+ desc d
57
+ spec_class.new(name) do |t|
58
+ t.send spec_files_meth, files
59
+ t.spec_opts = ENV["#{NAME.upcase}_SPEC_OPTS"].split if ENV["#{NAME.upcase}_SPEC_OPTS"]
60
+ end
61
+ end
62
+
63
+ spec_with_cov = lambda do |name, files, d|
64
+ spec.call(name, files, d)
65
+ t = spec.call("#{name}_cov", files, "#{d} with coverage")
66
+ t.rcov = true
67
+ t.rcov_opts = File.read("spec/rcov.opts").split("\n") if File.file?("spec/rcov.opts")
68
+ end
69
+
70
+ task :default => [:spec]
71
+ spec_with_cov.call("spec", Dir["spec/*_spec.rb"], "Run specs")
72
+ rescue LoadError
73
+ task :default do
74
+ puts "Must install rspec to run the default task (which runs specs)"
75
+ end
76
+ end
77
+
78
+ ### Other
79
+
80
+ desc "Print #{NAME} version"
81
+ task :version do
82
+ puts VERS.call
83
+ end
84
+
85
+ desc "Check syntax of all .rb files"
86
+ task :check_syntax do
87
+ Dir['**/*.rb'].each{|file| print `#{ENV['RUBY'] || :ruby} -c #{file} | fgrep -v "Syntax OK"`}
88
+ end
89
+
90
+ desc "Start an IRB shell using the extension"
91
+ task :irb do
92
+ require 'rbconfig'
93
+ ruby = ENV['RUBY'] || File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'])
94
+ irb = ENV['IRB'] || File.join(RbConfig::CONFIG['bindir'], File.basename(ruby).sub('ruby', 'irb'))
95
+ sh %{#{irb} -I lib -r forme}
96
+ end
97
+
@@ -0,0 +1,1178 @@
1
+ require 'date'
2
+ require 'forme/version'
3
+
4
+ # Forme is designed to make creating HTML forms easier. Flexibility and
5
+ # simplicity are primary objectives. The basic usage involves creating
6
+ # a <tt>Forme::Form</tt> instance, and calling +input+ and +tag+ methods
7
+ # to return html strings for widgets, but it could also be used for
8
+ # serializing to other formats, or even as a DSL for a GUI application.
9
+ #
10
+ # In order to be flexible, Forme stores tags in abstract form until
11
+ # output is requested. There are two separate abstract <i>forms</i> that Forme
12
+ # uses. One is <tt>Forme::Input</tt>, and the other is <tt>Forme::Tag</tt>.
13
+ # <tt>Forme::Input</tt> is a high level abstract form, while <tt>Forme::Tag</tt>
14
+ # is a low level abstract form.
15
+ #
16
+ # The difference between <tt>Forme::Input</tt> and <tt>Forme::Tag</tt>
17
+ # is that <tt>Forme::Tag</tt> directly represents the underlying html
18
+ # tag, containing a type, optional attributes, and children, while the
19
+ # <tt>Forme::Input</tt> is more abstract and attempts to be user friendly.
20
+ # For example, these both compile by default to the same select tag:
21
+ #
22
+ # f.input(:select, :options=>[['foo', 1]])
23
+ # # or
24
+ # f.tag(:select, {}, [f.tag(:option, {:value=>1}, ['foo'])])
25
+ #
26
+ # The processing of high level <tt>Forme::Input</tt>s into raw html
27
+ # data is broken down to the following steps (called transformers):
28
+ #
29
+ # 1. +Formatter+: converts a <tt>Forme::Input</tt> instance into a
30
+ # <tt>Forme::Tag</tt> instance (or array of them).
31
+ # 2. +ErrorHandler+: If the <tt>Forme::Input</tt> instance has a error,
32
+ # takes the formatted tag and marks it as having the error.
33
+ # 2. +Labeler+: If the <tt>Forme::Input</tt> instance has a label,
34
+ # takes the formatted output and labels it.
35
+ # 3. +Wrapper+: Takes the output of the labeler (or formatter if
36
+ # no label), and wraps it in another tag (or just returns it
37
+ # directly).
38
+ # 4. +Serializer+: converts a <tt>Forme::Tag</tt> instance into a
39
+ # string.
40
+ #
41
+ # Technically, only the +Serializer+ is necessary. The +input+
42
+ # and +tag+ methods return +Input+ and +Tag+ objects. These objects
43
+ # both have +to_s+ defined to call the appropriate +Serializer+ with
44
+ # themselves. The +Serializer+ calls the appropriate +Formatter+ if
45
+ # it encounters an +Input+ instance, and attempts to serialize the
46
+ # output of that (which is usually a +Tag+ instance). It is up to
47
+ # the +Formatter+ to call the +Labeler+ and/or +ErrorHandler+ (if
48
+ # necessary) and the +Wrapper+.
49
+ #
50
+ # There is also an +InputsWrapper+ transformer, that is called by
51
+ # <tt>Forme::Form#inputs</tt>. It's used to wrap up a group of
52
+ # related options (in a fieldset by default).
53
+ #
54
+ # The <tt>Forme::Form</tt> object takes the 6 transformers as options (:formatter,
55
+ # :labeler, :error_handler, :wrapper, :inputs_wrapper, and :serializer), all of which
56
+ # should be objects responding to +call+ (so you can use +Proc+s) or be symbols
57
+ # registered with the library using <tt>Forme.register_transformer</tt>:
58
+ #
59
+ # Forme.register_transformer(:wrapper, :p){|t| t.tag(:p, {}, t)}
60
+ #
61
+ # Most of the transformers can be overridden on a per instance basis by
62
+ # passing the appopriate option to +input+ or +inputs+:
63
+ #
64
+ # f.input(:name, :wrapper=>:p)
65
+ module Forme
66
+ # Exception class for exceptions raised by Forme.
67
+ class Error < StandardError
68
+ end
69
+
70
+ @default_config = :default
71
+ class << self
72
+ # Set the default configuration to use if none is explicitly
73
+ # specified (default: :default).
74
+ attr_accessor :default_config
75
+ end
76
+
77
+ # Array of all supported transformer types.
78
+ TRANSFORMER_TYPES = [:formatter, :serializer, :wrapper, :error_handler, :labeler, :inputs_wrapper]
79
+
80
+ # Hash storing all configurations. Configurations are groups of related transformers,
81
+ # so that you can specify a single :config option when creating a +Form+ and have
82
+ # all of the transformers set from that.
83
+ CONFIGURATIONS = {:default=>{}}
84
+
85
+ # Main hash storing the registered transformers. Maps transformer type symbols to subhashes
86
+ # containing the registered transformers for that type. Those subhashes should have symbol
87
+ # keys and values that are either classes or objects that respond to +call+.
88
+ TRANSFORMERS = {}
89
+
90
+ TRANSFORMER_TYPES.each do |t|
91
+ CONFIGURATIONS[:default][t] = :default
92
+ TRANSFORMERS[t] = {}
93
+ end
94
+
95
+ # Register a new transformer with this library. Arguments:
96
+ # +type+ :: Transformer type symbol
97
+ # +sym+ :: Transformer name symbol
98
+ # <tt>obj/block</tt> :: Transformer to associate with this symbol. Should provide either
99
+ # +obj+ or +block+, but not both. If +obj+ is given, should be
100
+ # either a +Class+ instance or it should respond to +call+. If a
101
+ # +Class+ instance is given, instances of that class should respond
102
+ # to +call+, and the a new instance of that class should be used
103
+ # for each transformation.
104
+ def self.register_transformer(type, sym, obj=nil, &block)
105
+ raise Error, "Not a valid transformer type" unless TRANSFORMERS.has_key?(type)
106
+ raise Error, "Must provide either block or obj, not both" if obj && block
107
+ TRANSFORMERS[type][sym] = obj||block
108
+ end
109
+
110
+ # Register a new configuration. Type is the configuration name symbol,
111
+ # and hash maps transformer type symbols to transformer name symbols.
112
+ def self.register_config(type, hash)
113
+ CONFIGURATIONS[type] = CONFIGURATIONS[hash.fetch(:base, :default)].merge(hash)
114
+ end
115
+
116
+ register_config(:formtastic, :wrapper=>:li, :inputs_wrapper=>:fieldset_ol, :labeler=>:explicit)
117
+
118
+ # Call <tt>Forme::Form.form</tt> with the given arguments and block.
119
+ def self.form(*a, &block)
120
+ Form.form(*a, &block)
121
+ end
122
+
123
+ # Update the <tt>:class</tt> entry in the +attr+ hash with the given +classes+.
124
+ def self.attr_classes(attr, *classes)
125
+ attr[:class] = merge_classes(attr[:class], *classes)
126
+ end
127
+
128
+ # Return a string that includes all given class strings
129
+ def self.merge_classes(*classes)
130
+ classes.compact.join(' ')
131
+ end
132
+
133
+ # The +Form+ class is the main entry point to the library.
134
+ # Using the +form+, +input+, +tag+, and +inputs+ methods, one can easily build
135
+ # an abstract syntax tree of +Tag+ and +Input+ instances, which can be serialized
136
+ # to a string using +to_s+.
137
+ class Form
138
+ # The object related to the receiver, if any. If the +Form+ has an associated
139
+ # obj, then calls to +input+ are assumed to be accessing fields of the object
140
+ # instead to directly representing input types.
141
+ attr_reader :obj
142
+
143
+ # A hash of options for the receiver. Currently, the following are recognized by
144
+ # default:
145
+ # :obj :: Sets the +obj+ attribute
146
+ # :error_handler :: Sets the +error_handler+ for the form
147
+ # :formatter :: Sets the +formatter+ for the form
148
+ # :inputs_wrapper :: Sets the +inputs_wrapper+ for the form
149
+ # :labeler :: Sets the +labeler+ for the form
150
+ # :wrapper :: Sets the +wrapper+ for the form
151
+ # :serializer :: Sets the +serializer+ for the form
152
+ attr_reader :opts
153
+
154
+ # The +formatter+ determines how the +Input+s created are transformed into
155
+ # +Tag+ objects. Must respond to +call+ or be a registered symbol.
156
+ attr_reader :formatter
157
+
158
+ # The +error_handler+ determines how to to mark tags as containing errors.
159
+ # Must respond to +call+ or be a registered symbol.
160
+ attr_reader :error_handler
161
+
162
+ # The +labeler+ determines how to label tags. Must respond to +call+ or be
163
+ # a registered symbol.
164
+ attr_reader :labeler
165
+
166
+ # The +wrapper+ determines how (potentially labeled) tags are wrapped. Must
167
+ # respond to +call+ or be a registered symbol.
168
+ attr_reader :wrapper
169
+
170
+ # The +inputs_wrapper+ determines how calls to +inputs+ are wrapped. Must
171
+ # respond to +call+ or be a registered symbol.
172
+ attr_reader :inputs_wrapper
173
+
174
+ # The +serializer+ determines how +Tag+ objects are transformed into strings.
175
+ # Must respond to +call+ or be a registered symbol.
176
+ attr_reader :serializer
177
+
178
+ # Create a +Form+ instance and yield it to the block,
179
+ # injecting the opening form tag before yielding and
180
+ # the closing form tag after yielding.
181
+ #
182
+ # Argument Handling:
183
+ # No args :: Creates a +Form+ object with no options and not associated
184
+ # to an +obj+, and with no attributes in the opening tag.
185
+ # 1 hash arg :: Treated as opening form tag attributes, creating a
186
+ # +Form+ object with no options.
187
+ # 1 non-hash arg :: Treated as the +Form+'s +obj+, with empty options
188
+ # and no attributes in the opening tag.
189
+ # 2 hash args :: First hash is opening attributes, second hash is +Form+
190
+ # options.
191
+ # 1 non-hash arg, 1-2 hash args :: First argument is +Form+'s obj, second is
192
+ # opening attributes, third if provided is
193
+ # +Form+'s options.
194
+ def self.form(obj=nil, attr={}, opts={}, &block)
195
+ f = if obj.is_a?(Hash)
196
+ raise Error, "Can't provide 3 hash arguments to form" unless opts.empty?
197
+ opts = attr
198
+ attr = obj
199
+ new(opts)
200
+ else
201
+ new(obj, opts)
202
+ end
203
+
204
+ ins = opts[:inputs]
205
+ button = opts[:button]
206
+ if ins || button
207
+ block = Proc.new do |form|
208
+ form.inputs(ins, opts) if ins
209
+ yield form if block_given?
210
+ form.emit(form.button(button)) if button
211
+ end
212
+ end
213
+
214
+ f.form(attr, &block)
215
+ end
216
+
217
+ # Creates a +Form+ object. Arguments:
218
+ # obj :: Sets the obj for the form. If a hash, is merged with the +opts+ argument
219
+ # to set the opts.
220
+ # opts :: A hash of options for the form, see +opts+ attribute for details on
221
+ # available options.
222
+ def initialize(obj=nil, opts={})
223
+ if obj.is_a?(Hash)
224
+ @opts = obj.merge(opts)
225
+ @obj = @opts.delete(:obj)
226
+ else
227
+ @obj = obj
228
+ @opts = opts
229
+ end
230
+ if @obj && @obj.respond_to?(:forme_config)
231
+ @obj.forme_config(self)
232
+ end
233
+ config = CONFIGURATIONS[@opts[:config]||Forme.default_config]
234
+ TRANSFORMER_TYPES.each{|k| instance_variable_set(:"@#{k}", transformer(k, @opts.fetch(k, config[k])))}
235
+ @nesting = []
236
+ end
237
+
238
+ # If there is a related transformer, call it with the given +args+ and +block+.
239
+ # Otherwise, attempt to return the initial input without modifying it.
240
+ def transform(type, trans_name, *args, &block)
241
+ if trans = transformer(type, trans_name)
242
+ trans.call(*args, &block)
243
+ else
244
+ case type
245
+ when :inputs_wrapper
246
+ yield
247
+ when :labeler, :error_handler, :wrapper
248
+ args.first
249
+ else
250
+ raise Error, "No matching #{type}: #{trans_name.inspect}"
251
+ end
252
+ end
253
+ end
254
+
255
+ # Get the related transformer for the given transformer type. Output depends on the type
256
+ # of +trans+:
257
+ # +Symbol+ :: Assume a request for a registered transformer, so look it up in the +TRANSFORRMERS+ hash.
258
+ # +Hash+ :: If +type+ is also a key in +trans+, return the related value from +trans+, unless the related
259
+ # value is +nil+, in which case, return +nil+. If +type+ is not a key in +trans+, use the
260
+ # default transformer for the receiver.
261
+ # +nil+ :: Assume the default transformer for this receiver.
262
+ # otherwise :: return +trans+ directly if it responds to +call+, and raise an +Error+ if not.
263
+ def transformer(type, trans)
264
+ case trans
265
+ when Symbol
266
+ TRANSFORMERS[type][trans] || raise(Error, "invalid #{type}: #{trans.inspect} (valid #{type}s: #{TRANSFORMERS[type].keys.map{|k| k.inspect}.join(', ')})")
267
+ when Hash
268
+ if trans.has_key?(type)
269
+ if v = trans[type]
270
+ transformer(type, v)
271
+ end
272
+ else
273
+ transformer(type, nil)
274
+ end
275
+ when nil
276
+ send(type)
277
+ else
278
+ if trans.respond_to?(:call)
279
+ trans
280
+ else
281
+ raise Error, "#{type} #{trans.inspect} must respond to #call"
282
+ end
283
+ end
284
+ end
285
+
286
+ # Create a form tag with the given attributes.
287
+ def form(attr={}, &block)
288
+ tag(:form, attr, &block)
289
+ end
290
+
291
+ # Formats the +input+ using the +formatter+.
292
+ def format(input)
293
+ transform(:formatter, input.opts, input)
294
+ end
295
+
296
+ # Empty method designed to ease integration with other libraries where
297
+ # Forme is used in template code and some output implicitly
298
+ # created by Forme needs to be injected into the template output.
299
+ def emit(tag)
300
+ end
301
+
302
+ # Creates an +Input+ with the given +field+ and +opts+ associated with
303
+ # the receiver, and add it to the list of children to the currently
304
+ # open tag.
305
+ #
306
+ # If the form is associated with an +obj+, or the :obj key exists in
307
+ # the +opts+ argument, treats the +field+ as a call to the +obj+. If
308
+ # +obj+ responds to +forme_input+, that method is called with the +field+
309
+ # and a copy of +opts+. Otherwise, the field is used as a method call
310
+ # on the +obj+ and a text input is created with the result.
311
+ #
312
+ # If no +obj+ is associated with the receiver, +field+ represents an input
313
+ # type (e.g. <tt>:text</tt>, <tt>:textarea</tt>, <tt>:select</tt>), and
314
+ # an input is created directly with the +field+ and +opts+.
315
+ def input(field, opts={})
316
+ if opts.has_key?(:obj)
317
+ opts = opts.dup
318
+ obj = opts.delete(:obj)
319
+ else
320
+ obj = self.obj
321
+ end
322
+ input = if obj
323
+ if obj.respond_to?(:forme_input)
324
+ obj.forme_input(self, field, opts.dup)
325
+ else
326
+ opts = opts.dup
327
+ opts[:name] = field unless opts.has_key?(:name)
328
+ opts[:id] = field unless opts.has_key?(:id)
329
+ opts[:value] = obj.send(field) unless opts.has_key?(:value)
330
+ _input(:text, opts)
331
+ end
332
+ else
333
+ _input(field, opts)
334
+ end
335
+ use_serializer(input) if input.is_a?(Array)
336
+ self << input
337
+ input
338
+ end
339
+
340
+ # Create a new +Input+ associated with the receiver with the given
341
+ # arguments, doing no other processing.
342
+ def _input(*a)
343
+ Input.new(self, *a)
344
+ end
345
+
346
+ # Creates a tag using the +inputs_wrapper+ (a fieldset by default), calls
347
+ # input on each element of +inputs+, and yields to if given a block.
348
+ # You can use array arguments if you want inputs to be created with specific
349
+ # options:
350
+ #
351
+ # inputs([:field1, :field2])
352
+ # inputs([[:field1, {:name=>'foo'}], :field2])
353
+ #
354
+ # The given +opts+ are passed to the +inputs_wrapper+, and the default
355
+ # +inputs_wrapper+ supports a <tt>:legend</tt> option that is used to
356
+ # set the legend for the fieldset.
357
+ def inputs(inputs=[], opts={})
358
+ transform(:inputs_wrapper, opts, self, opts) do
359
+ inputs.each do |i|
360
+ emit(input(*i))
361
+ end
362
+ yield if block_given?
363
+ end
364
+ end
365
+
366
+ # Returns a string representing the opening of the form tag for serializers
367
+ # that support opening tags.
368
+ def open(attr)
369
+ serializer.serialize_open(_tag(:form, attr)) if serializer.respond_to?(:serialize_open)
370
+ end
371
+
372
+ # Returns a string representing the closing of the form tag, for serializers
373
+ # that support closing tags.
374
+ def close
375
+ serializer.serialize_close(_tag(:form)) if serializer.respond_to?(:serialize_close)
376
+ end
377
+
378
+ # Create a +Tag+ associated to the receiver with the given arguments and block,
379
+ # doing no other processing.
380
+ def _tag(*a, &block)
381
+ tag = Tag.new(self, *a, &block)
382
+ end
383
+
384
+ # Creates a +Tag+ associated to the receiver with the given arguments.
385
+ # Add the tag to the the list of children for the currently open tag.
386
+ # If a block is given, make this tag the currently open tag while inside
387
+ # the block.
388
+ def tag(*a, &block)
389
+ tag = _tag(*a)
390
+ self << tag
391
+ nest(tag, &block) if block
392
+ tag
393
+ end
394
+
395
+ # Creates a :submit +Input+ with the given opts, adding it to the list
396
+ # of children for the currently open tag.
397
+ def button(opts={})
398
+ opts = {:value=>opts} if opts.is_a?(String)
399
+ input = _input(:submit, opts)
400
+ self << input
401
+ input
402
+ end
403
+
404
+ # Add the +Input+/+Tag+ instance given to the currently open tag.
405
+ def <<(tag)
406
+ if n = @nesting.last
407
+ n << tag
408
+ end
409
+ end
410
+
411
+ # Serializes the +tag+ using the +serializer+.
412
+ def serialize(tag)
413
+ serializer.call(tag)
414
+ end
415
+
416
+ private
417
+
418
+ # Extend +obj+ with +Serialized+ and associate it with the receiver, such
419
+ # that calling +to_s+ on the object will use the receiver's serializer
420
+ # to generate the resulting string.
421
+ def use_serializer(obj)
422
+ obj.extend(Serialized)
423
+ obj._form = self
424
+ obj
425
+ end
426
+
427
+ # Make the given tag the currently open tag, and yield. After the
428
+ # block returns, make the previously open tag the currently open
429
+ # tag.
430
+ def nest(tag)
431
+ @nesting << tag
432
+ yield self
433
+ ensure
434
+ @nesting.pop
435
+ end
436
+ end
437
+
438
+ # High level abstract tag form, transformed by formatters into the lower
439
+ # level +Tag+ form (or an array of them).
440
+ class Input
441
+ # The +Form+ object related to the receiver.
442
+ attr_reader :form
443
+
444
+ # The type of input, should be a symbol (e.g. :submit, :text, :select).
445
+ attr_reader :type
446
+
447
+ # The options hash for the receiver. Here are some of the supported options
448
+ # used by the built-in formatter transformers:
449
+ #
450
+ # :error :: Set an error message, invoking the error_handler
451
+ # :label :: Set a label, invoking the labeler
452
+ # :wrapper :: Set a custom wrapper, overriding the form's default
453
+ # :labeler :: Set a custom labeler, overriding the form's default
454
+ # :error_handler :: Set a custom error_handler, overriding the form's default
455
+ # :attr :: The attributes hash to use for the given tag, takes precedence over
456
+ # other options that set attributes.
457
+ # :data :: A hash of data-* attributes for the resulting tag. Keys in this hash
458
+ # will have attributes created with data- prepended to the attribute name.
459
+ # :name :: The name attribute to use
460
+ # :id :: The id attribute to use
461
+ # :placeholder :: The placeholder attribute to use
462
+ # :value :: The value attribute to use for input tags, the content of the textarea
463
+ # for textarea tags, or the selected option(s) for select tags.
464
+ # :class :: A class to use. Unlike other options, this is combined with the
465
+ # classes set in the :attr hash.
466
+ # :disabled :: Set the disabled attribute if true
467
+ # :required :: Set the required attribute if true
468
+ #
469
+ # For other supported options, see the private methods in +Formatter+.
470
+ attr_reader :opts
471
+
472
+ # Set the +form+, +type+, and +opts+.
473
+ def initialize(form, type, opts={})
474
+ @form, @type, @opts = form, type, opts
475
+ end
476
+
477
+ # Replace the +opts+ by merging the given +hash+ into +opts+,
478
+ # without modifying +opts+.
479
+ def merge_opts(hash)
480
+ @opts = @opts.merge(hash)
481
+ end
482
+
483
+ # Create a new +Tag+ instance with the given arguments and block
484
+ # related to the receiver's +form+.
485
+ def tag(*a, &block)
486
+ form._tag(*a, &block)
487
+ end
488
+
489
+ # Return a string containing the serialized content of the receiver.
490
+ def to_s
491
+ form.serialize(self)
492
+ end
493
+
494
+ # Transform the receiver into a lower level +Tag+ form (or an array
495
+ # of them).
496
+ def format
497
+ form.format(self)
498
+ end
499
+ end
500
+
501
+ # Low level abstract tag form, where each instance represents a
502
+ # html tag with attributes and children.
503
+ class Tag
504
+ # The +Form+ object related to the receiver.
505
+ attr_reader :form
506
+
507
+ # The type of tag, should be a symbol (e.g. :input, :select).
508
+ attr_reader :type
509
+
510
+ # The attributes hash of this receiver.
511
+ attr_reader :attr
512
+
513
+ # An array instance representing the children of the receiver,
514
+ # or possibly +nil+ if the receiver has no children.
515
+ attr_reader :children
516
+
517
+ # Set the +form+, +type+, +attr+, and +children+.
518
+ def initialize(form, type, attr={}, children=nil)
519
+ case children
520
+ when Array
521
+ @children = children
522
+ when nil
523
+ @children = nil
524
+ else
525
+ @children = [children]
526
+ end
527
+ @form, @type, @attr = form, type, (attr||{})
528
+ end
529
+
530
+ # Adds a child to the array of receiver's children.
531
+ def <<(child)
532
+ if children
533
+ children << child
534
+ else
535
+ @children = [child]
536
+ end
537
+ end
538
+
539
+ # Create a new +Tag+ instance with the given arguments and block
540
+ # related to the receiver's +form+.
541
+ def tag(*a, &block)
542
+ form._tag(*a, &block)
543
+ end
544
+
545
+ # Return a string containing the serialized content of the receiver.
546
+ def to_s
547
+ form.serialize(self)
548
+ end
549
+ end
550
+
551
+ # Module that can extend objects associating them with a specific
552
+ # +Form+ instance. Calling +to_s+ on the object will then use the
553
+ # form's serializer to return a string.
554
+ module Serialized
555
+ # The +Form+ instance related to the receiver.
556
+ attr_accessor :_form
557
+
558
+ # Return a string containing the serialized content of the receiver.
559
+ def to_s
560
+ _form.serialize(self)
561
+ end
562
+ end
563
+
564
+ # Empty module for marking objects as "raw", where they will no longer
565
+ # html escaped by the default serializer.
566
+ module Raw
567
+ end
568
+
569
+ # The default formatter used by the library. Any custom formatters should
570
+ # probably inherit from this formatter unless they have very special needs.
571
+ #
572
+ # Unlike most other transformers which are registered as instances and use
573
+ # a functional style, this class is registered as a class due to the large
574
+ # amount of state it uses.
575
+ #
576
+ # Registered as :default.
577
+ class Formatter
578
+ Forme.register_transformer(:formatter, :default, self)
579
+
580
+ # These options are copied directly from the options hash to the the
581
+ # attributes hash, so they don't need to be specified in the :attr
582
+ # option. However, they can be specified in both places, and if so,
583
+ # the :attr option version takes precedence.
584
+ ATTRIBUTE_OPTIONS = [:name, :id, :placeholder, :value]
585
+
586
+ # Create a new instance and call it
587
+ def self.call(input)
588
+ new.call(input)
589
+ end
590
+
591
+ # The +Form+ instance for the receiver, taken from the +input+.
592
+ attr_reader :form
593
+
594
+ # The +Input+ instance for the receiver. This is what the receiver
595
+ # converts to the lower level +Tag+ form (or an array of them).
596
+ attr_reader :input
597
+
598
+ # The attributes to to set on the lower level +Tag+ form returned.
599
+ # This are derived from the +input+'s +opts+, but some processing is done on
600
+ # them.
601
+ attr_reader :attr
602
+
603
+ # The +opts+ hash of the +input+.
604
+ attr_reader :opts
605
+
606
+ # Used to specify the value of the hidden input created for checkboxes.
607
+ # Since the default for an unspecified checkbox value is 1, the default is
608
+ # 0. If the checkbox value is 't', the hidden value is 'f', since that is
609
+ # common usage for boolean values.
610
+ CHECKBOX_MAP = Hash.new(0)
611
+ CHECKBOX_MAP['t'] = 'f'
612
+
613
+ # Transform the +input+ into a +Tag+ instance (or an array of them),
614
+ # wrapping it with the +form+'s wrapper, and the form's +error_handler+
615
+ # and +labeler+ if the +input+ has an <tt>:error</tt> or <tt>:label</tt>
616
+ # options.
617
+ def call(input)
618
+ @input = input
619
+ @form = input.form
620
+ attr = input.opts[:attr]
621
+ @attr = attr ? attr.dup : {}
622
+ @opts = input.opts
623
+ normalize_options
624
+
625
+ tag = convert_to_tag(input.type)
626
+ tag = wrap_tag_with_error(tag) if input.opts[:error]
627
+ tag = wrap_tag_with_label(tag) if input.opts[:label]
628
+ wrap_tag(tag)
629
+ end
630
+
631
+ private
632
+
633
+ # Dispatch to a format_<i>type</i> method if there is one that matches the
634
+ # type, otherwise, call +_format_input+ with the given +type+.
635
+ def convert_to_tag(type)
636
+ meth = :"format_#{type}"
637
+ if respond_to?(meth, true)
638
+ send(meth)
639
+ else
640
+ _format_input(type)
641
+ end
642
+ end
643
+
644
+ # If the checkbox has a name, will create a hidden input tag with the
645
+ # same name that comes before this checkbox. That way, if the checkbox
646
+ # is checked, the web app will generally see the value of the checkbox, and
647
+ # if it is not checked, the web app will generally see the value of the hidden
648
+ # input tag. Recognizes the following options:
649
+ # :checked :: checkbox is set to checked if so.
650
+ # :hidden_value :: sets the value of the hidden input tag.
651
+ # :no_hidden :: don't create a hidden input tag
652
+ def format_checkbox
653
+ @attr[:type] = :checkbox
654
+ @attr[:checked] = :checked if @opts[:checked]
655
+ if @attr[:name] && !@opts[:no_hidden]
656
+ attr = {:type=>:hidden}
657
+ unless attr[:value] = @opts[:hidden_value]
658
+ attr[:value] = CHECKBOX_MAP[@attr[:value]]
659
+ end
660
+ attr[:id] = "#{@attr[:id]}_hidden" if @attr[:id]
661
+ attr[:name] = @attr[:name]
662
+ [tag(:input, attr), tag(:input)]
663
+ else
664
+ tag(:input)
665
+ end
666
+ end
667
+
668
+ # For radio buttons, recognizes the :checked option and sets the :checked
669
+ # attribute in the tag appropriately.
670
+ def format_radio
671
+ @attr[:checked] = :checked if @opts[:checked]
672
+ @attr[:type] = :radio
673
+ tag(:input)
674
+ end
675
+
676
+ # Use a date input by default. If the :as=>:select option is given,
677
+ # use a multiple select box for the options.
678
+ def format_date
679
+ if @opts[:as] == :select
680
+ name = @attr[:name]
681
+ id = @attr[:id]
682
+ v = @attr[:value]
683
+ if v
684
+ v = Date.parse(v) unless v.is_a?(Date)
685
+ values = {}
686
+ values[:year], values[:month], values[:day] = v.year, v.month, v.day
687
+ end
688
+ ops = {:year=>1900..2050, :month=>1..12, :day=>1..31}
689
+ input.merge_opts(:label_for=>"#{id}_year")
690
+ [:year, '-', :month, '-', :day].map{|x| x.is_a?(String) ? x : form._input(:select, @opts.merge(:label=>nil, :wrapper=>nil, :error=>nil, :name=>"#{name}[#{x}]", :id=>"#{id}_#{x}", :value=>values[x], :options=>ops[x].map{|x| [sprintf("%02i", x), x]})).format}
691
+ else
692
+ _format_input(:date)
693
+ end
694
+ end
695
+
696
+ # Use a datetime input by default. If the :as=>:select option is given,
697
+ # use a multiple select box for the options.
698
+ def format_datetime
699
+ if @opts[:as] == :select
700
+ name = @attr[:name]
701
+ id = @attr[:id]
702
+ v = @attr[:value]
703
+ v = DateTime.parse(v) unless v.is_a?(Time) || v.is_a?(DateTime)
704
+ values = {}
705
+ values[:year], values[:month], values[:day], values[:hour], values[:minute], values[:second] = v.year, v.month, v.day, v.hour, v.min, v.sec
706
+ ops = {:year=>1900..2050, :month=>1..12, :day=>1..31, :hour=>0..23, :minute=>0..59, :second=>0..59}
707
+ input.merge_opts(:label_for=>"#{id}_year")
708
+ [:year, '-', :month, '-', :day, ' ', :hour, ':', :minute, ':', :second].map{|x| x.is_a?(String) ? x : form._input(:select, @opts.merge(:label=>nil, :wrapper=>nil, :error=>nil, :name=>"#{name}[#{x}]", :id=>"#{id}_#{x}", :value=>values[x], :options=>ops[x].map{|x| [sprintf("%02i", x), x]})).format}
709
+ else
710
+ _format_input(:datetime)
711
+ end
712
+ end
713
+
714
+ # The default fallback method for handling inputs. Assumes an input tag
715
+ # with the type attribute set to input.
716
+ def _format_input(type)
717
+ @attr[:type] = type
718
+ tag(:input)
719
+ end
720
+
721
+ # Takes a select input and turns it into a select tag with (possibly) option
722
+ # children tags. Respects the following options:
723
+ # :options :: an array of options. Processes each entry. If that entry is
724
+ # an array, takes the first entry in the hash as the text child
725
+ # of the option, and the last entry as the value of the option.
726
+ # if not set, ignores the remaining options.
727
+ # :add_blank :: Add a blank option if true. If the value is a string,
728
+ # use it as the text content of the blank option. The value of
729
+ # the blank option is always the empty string.
730
+ # :text_method :: If set, each entry in the array has this option called on
731
+ # it to get the text of the object.
732
+ # :value_method :: If set (and :text_method is set), each entry in the array
733
+ # has this method called on it to get the value of the option.
734
+ # :selected :: The value that should be selected. Any options that are equal to
735
+ # this value (or included in this value if a multiple select box),
736
+ # are set to selected.
737
+ # :multiple :: Creates a multiple select box.
738
+ # :value :: Same as :selected, but has lower priority.
739
+ def format_select
740
+ if os = @opts[:options]
741
+ vm = @opts[:value_method]
742
+ tm = @opts[:text_method]
743
+ sel = @opts[:selected] || @attr.delete(:value)
744
+ if @opts[:multiple]
745
+ @attr[:multiple] = :multiple
746
+ sel = Array(sel)
747
+ cmp = lambda{|v| sel.include?(v)}
748
+ else
749
+ cmp = lambda{|v| v == sel}
750
+ end
751
+ os = os.map do |x|
752
+ attr = {}
753
+ if tm
754
+ text = x.send(tm)
755
+ if vm
756
+ val = x.send(vm)
757
+ attr[:value] = val
758
+ attr[:selected] = :selected if cmp.call(val)
759
+ else
760
+ attr[:selected] = :selected if cmp.call(text)
761
+ end
762
+ form._tag(:option, attr, [text])
763
+ elsif x.is_a?(Array)
764
+ val = x.last
765
+ if val.is_a?(Hash)
766
+ attr.merge!(val)
767
+ val = attr[:value]
768
+ else
769
+ attr[:value] = val
770
+ end
771
+ attr[:selected] = :selected if attr.has_key?(:value) && cmp.call(val)
772
+ tag(:option, attr, [x.first])
773
+ else
774
+ attr[:selected] = :selected if cmp.call(x)
775
+ tag(:option, attr, [x])
776
+ end
777
+ end
778
+ if prompt = @opts[:add_blank]
779
+ os.unshift(tag(:option, {:value=>''}, prompt.is_a?(String) ? [prompt] : []))
780
+ end
781
+ end
782
+ tag(:select, @attr, os)
783
+ end
784
+
785
+ # Formats a textarea. Respects the following options:
786
+ # :value :: Sets value as the child of the textarea.
787
+ def format_textarea
788
+ if val = @attr.delete(:value)
789
+ tag(:textarea, @attr, [val])
790
+ else
791
+ tag(:textarea)
792
+ end
793
+ end
794
+
795
+ # Normalize the options used for all input types. Handles:
796
+ # :required :: Sets the +required+ attribute on the resulting tag if true.
797
+ # :disabled :: Sets the +disabled+ attribute on the resulting tag if true.
798
+ def normalize_options
799
+ ATTRIBUTE_OPTIONS.each do |k|
800
+ if @opts.has_key?(k) && !@attr.has_key?(k)
801
+ @attr[k] = @opts[k]
802
+ end
803
+ end
804
+
805
+ Forme.attr_classes(@attr, @opts[:class]) if @opts.has_key?(:class)
806
+
807
+ if data = opts[:data]
808
+ data.each do |k, v|
809
+ sym = :"data-#{k}"
810
+ @attr[sym] = v unless @attr.has_key?(sym)
811
+ end
812
+ end
813
+
814
+ @attr[:required] = :required if @opts[:required] && !@attr.has_key?(:required)
815
+ @attr[:disabled] = :disabled if @opts[:disabled] && !@attr.has_key?(:disabled)
816
+ end
817
+
818
+ # Create a +Tag+ instance related to the receiver's +form+ with the given
819
+ # arguments.
820
+ def tag(type, attr=@attr, children=nil)
821
+ form._tag(type, attr, children)
822
+ end
823
+
824
+ # Wrap the tag with the form's +wrapper+.
825
+ def wrap_tag(tag)
826
+ form.transform(:wrapper, @opts, tag, input)
827
+ end
828
+
829
+ # Wrap the tag with the form's +error_handler+.
830
+ def wrap_tag_with_error(tag)
831
+ form.transform(:error_handler, @opts, tag, input)
832
+ end
833
+
834
+ # Wrap the tag with the form's +labeler+.
835
+ def wrap_tag_with_label(tag)
836
+ form.transform(:labeler, @opts, tag, input)
837
+ end
838
+ end
839
+
840
+ # Formatter that disables all input fields,
841
+ #
842
+ # Registered as :disabled.
843
+ class Formatter::Disabled < Formatter
844
+ Forme.register_transformer(:formatter, :disabled, self)
845
+
846
+ private
847
+
848
+ # Unless the :disabled option is specifically set
849
+ # to +false+, set the :disabled attribute on the
850
+ # resulting tag.
851
+ def normalize_options
852
+ if @opts[:disabled] == false
853
+ super
854
+ else
855
+ super
856
+ @attr[:disabled] = :disabled
857
+ end
858
+ end
859
+ end
860
+
861
+ # Formatter that uses span tags with text for most input types,
862
+ # and disables radio/checkbox inputs.
863
+ #
864
+ # Registered as :readonly.
865
+ class Formatter::ReadOnly < Formatter
866
+ Forme.register_transformer(:formatter, :readonly, self)
867
+
868
+ private
869
+
870
+ # Disabled checkbox inputs.
871
+ def format_checkbox
872
+ @attr[:disabled] = :disabled
873
+ super
874
+ end
875
+
876
+ # Use a span with text instead of an input field.
877
+ def _format_input(type)
878
+ tag(:span, {}, @attr[:value])
879
+ end
880
+
881
+ # Disabled radio button inputs.
882
+ def format_radio
883
+ @attr[:disabled] = :disabled
884
+ super
885
+ end
886
+
887
+ # Use a span with text of the selected values instead of a select box.
888
+ def format_select
889
+ t = super
890
+ children = [t.children.select{|o| o.attr[:selected]}.map{|o| o.children}.join(', ')] if t.children
891
+ tag(:span, {}, children)
892
+ end
893
+
894
+ # Use a span with text instead of a text area.
895
+ def format_textarea
896
+ tag(:span, {}, @attr[:value])
897
+ end
898
+ end
899
+
900
+ # Default error handler used by the library, using an "error" class
901
+ # for the input field and a span tag with an "error_message" class
902
+ # for the error message.
903
+ #
904
+ # Registered as :default.
905
+ class ErrorHandler
906
+ Forme.register_transformer(:error_handler, :default, new)
907
+
908
+ # Return tag with error message span tag after it.
909
+ def call(tag, input)
910
+ msg_tag = tag.tag(:span, {:class=>'error_message'}, input.opts[:error])
911
+ if tag.is_a?(Tag)
912
+ attr = tag.attr
913
+ Forme.attr_classes(attr, 'error')
914
+ end
915
+ [tag, msg_tag]
916
+ end
917
+ end
918
+
919
+ # Default labeler used by the library, using implicit labels (where the
920
+ # label tag encloses the other tag).
921
+ #
922
+ # Registered as :default.
923
+ class Labeler
924
+ Forme.register_transformer(:labeler, :default, new)
925
+
926
+ # Return a label tag wrapping the given tag. For radio and checkbox
927
+ # inputs, the label occurs directly after the tag, for all other types,
928
+ # the label occurs before the tag.
929
+ def call(tag, input)
930
+ label = input.opts[:label]
931
+ t = if [:radio, :checkbox].include?(input.type)
932
+ [tag, ' ', label]
933
+ else
934
+ [label, ": ", tag]
935
+ end
936
+ input.tag(:label, {}, t)
937
+ end
938
+ end
939
+
940
+ # Explicit labeler that creates a separate label tag that references
941
+ # the given tag's id using a +for+ attribute. Requires that all tags
942
+ # with labels have +id+ fields.
943
+ #
944
+ # Registered as :explicit.
945
+ class Labeler::Explicit
946
+ Forme.register_transformer(:labeler, :explicit, new)
947
+
948
+ # Return an array with a label tag as the first entry and +tag+ as
949
+ # a second entry. If the +input+ has a :label_for option, use that,
950
+ # otherwise use the input's :id option. If neither the :id or
951
+ # :label_for option is used, the label created will not be
952
+ # associated with an input.
953
+ def call(tag, input)
954
+ [input.tag(:label, {:for=>input.opts.fetch(:label_for, input.opts[:id])}, [input.opts[:label]]), tag]
955
+ end
956
+ end
957
+
958
+ Forme.register_transformer(:wrapper, :default){|tag, input| tag}
959
+ [:li, :p, :div, :span].each do |x|
960
+ Forme.register_transformer(:wrapper, x){|tag, input| input.tag(x, input.opts[:wrapper_attr], Array(tag))}
961
+ end
962
+ Forme.register_transformer(:wrapper, :trtd) do |tag, input|
963
+ a = Array(tag)
964
+ input.tag(:tr, input.opts[:wrapper_attr], a.length == 1 ? input.tag(:td, {}, a) : [input.tag(:td, {}, [a.first]), input.tag(:td, {}, a[1..-1])])
965
+ end
966
+
967
+ # Default inputs_wrapper used by the library, uses a fieldset.
968
+ #
969
+ # Registered as :default.
970
+ class InputsWrapper
971
+ Forme.register_transformer(:inputs_wrapper, :default, new)
972
+
973
+ # Wrap the inputs in a fieldset. If the :legend
974
+ # option is given, add a +legend+ tag as the first
975
+ # child of the fieldset.
976
+ def call(form, opts)
977
+ attr = opts[:attr] ? opts[:attr].dup : {}
978
+ Forme.attr_classes(attr, 'inputs')
979
+ if legend = opts[:legend]
980
+ form.tag(:fieldset, attr) do
981
+ form.emit(form.tag(:legend, opts[:legend_attr], legend))
982
+ yield
983
+ end
984
+ else
985
+ form.tag(:fieldset, attr, &Proc.new)
986
+ end
987
+ end
988
+ end
989
+
990
+ # Use a fieldset and an ol tag to wrap the inputs.
991
+ #
992
+ # Registered as :fieldset_ol.
993
+ class InputsWrapper::FieldSetOL < InputsWrapper
994
+ Forme.register_transformer(:inputs_wrapper, :fieldset_ol, new)
995
+
996
+ # Wrap the inputs in an ol tag
997
+ def call(form, opts)
998
+ super(form, opts){form.tag(:ol){yield}}
999
+ end
1000
+ end
1001
+
1002
+ # Use an ol tag to wrap the inputs.
1003
+ #
1004
+ # Registered as :ol.
1005
+ class InputsWrapper::OL
1006
+ Forme.register_transformer(:inputs_wrapper, :ol, new)
1007
+
1008
+ # Wrap the inputs in an ol tag
1009
+ def call(form, opts, &block)
1010
+ form.tag(:ol, &block)
1011
+ end
1012
+ end
1013
+
1014
+ # Use a div tag to wrap the inputs.
1015
+ #
1016
+ # Registered as :div.
1017
+ class InputsWrapper::Div
1018
+ Forme.register_transformer(:inputs_wrapper, :div, new)
1019
+
1020
+ # Wrap the inputs in an ol tag
1021
+ def call(form, opts, &block)
1022
+ form.tag(:div, &block)
1023
+ end
1024
+ end
1025
+
1026
+ # Use a table tag to wrap the inputs.
1027
+ #
1028
+ # Registered as :table.
1029
+ class InputsWrapper::Table
1030
+ Forme.register_transformer(:inputs_wrapper, :table, new)
1031
+
1032
+ # Wrap the inputs in a table tag.
1033
+ def call(form, opts, &block)
1034
+ form.tag(:table, &block)
1035
+ end
1036
+ end
1037
+
1038
+ # Default serializer class used by the library. Any other serializer
1039
+ # classes that want to produce html should probably subclass this class.
1040
+ #
1041
+ # Registered as :default.
1042
+ class Serializer
1043
+ Forme.register_transformer(:serializer, :default, new)
1044
+
1045
+ # Borrowed from Rack::Utils, map of single character strings to html escaped versions.
1046
+ ESCAPE_HTML = {"&" => "&amp;", "<" => "&lt;", ">" => "&gt;", "'" => "&#39;", '"' => "&quot;"}
1047
+
1048
+ # A regexp that matches all html characters requiring escaping.
1049
+ ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
1050
+
1051
+ # Which tags are self closing (such tags ignore children).
1052
+ SELF_CLOSING = [:img, :input]
1053
+
1054
+ # Serialize the tag object to an html string. Supports +Tag+ instances,
1055
+ # +Input+ instances (recursing into +call+ with the result of formatting the input),
1056
+ # arrays (recurses into +call+ for each entry and joins the result), and
1057
+ # (html escapes the string version of them, unless they include the +Raw+
1058
+ # module, in which case no escaping is done).
1059
+ def call(tag)
1060
+ case tag
1061
+ when Tag
1062
+ if SELF_CLOSING.include?(tag.type)
1063
+ "<#{tag.type}#{attr_html(tag)}/>"
1064
+ else
1065
+ "#{serialize_open(tag)}#{call(tag.children)}#{serialize_close(tag)}"
1066
+ end
1067
+ when Input
1068
+ call(tag.format)
1069
+ when Array
1070
+ tag.map{|x| call(x)}.join
1071
+ when DateTime, Time
1072
+ format_time(tag)
1073
+ when Date
1074
+ format_date(tag)
1075
+ when Raw
1076
+ tag.to_s
1077
+ else
1078
+ h tag
1079
+ end
1080
+ end
1081
+
1082
+ # Returns the opening part of the given tag.
1083
+ def serialize_open(tag)
1084
+ "<#{tag.type}#{attr_html(tag)}>"
1085
+ end
1086
+
1087
+ # Returns the closing part of the given tag.
1088
+ def serialize_close(tag)
1089
+ "</#{tag.type}>"
1090
+ end
1091
+
1092
+ private
1093
+
1094
+ # Return a string in ISO format representing the +Date+ instance.
1095
+ def format_date(date)
1096
+ date.strftime("%F")
1097
+ end
1098
+
1099
+ # Return a string in ISO format representing the +Time+ or +DateTime+ instance.
1100
+ def format_time(time)
1101
+ time.strftime("%F %H:%M:%S%Z")
1102
+ end
1103
+
1104
+ # Escape ampersands, brackets and quotes to their HTML/XML entities.
1105
+ def h(string)
1106
+ string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
1107
+ end
1108
+
1109
+ # Transforms the +tag+'s attributes into an html string, sorting by the keys
1110
+ # and quoting and html escaping the values.
1111
+ def attr_html(tag)
1112
+ attr = tag.attr.to_a.reject{|k,v| v.nil?}
1113
+ " #{attr.map{|k, v| "#{k}=\"#{call(v)}\""}.sort.join(' ')}" unless attr.empty?
1114
+ end
1115
+ end
1116
+
1117
+ # Overrides formatting of dates and times to use an American format without
1118
+ # timezones.
1119
+ module Serializer::AmericanTime
1120
+ Forme.register_transformer(:serializer, :html_usa, Serializer.new.extend(self))
1121
+
1122
+ private
1123
+
1124
+ # Return a string in American format representing the +Date+ instance.
1125
+ def format_date(date)
1126
+ date.strftime("%m/%d/%Y")
1127
+ end
1128
+
1129
+ # Return a string in American format representing the +Time+ or +DateTime+ instance, without the timezone.
1130
+ def format_time(time)
1131
+ time.strftime("%m/%d/%Y %I:%M:%S%p")
1132
+ end
1133
+ end
1134
+
1135
+ # Serializer class that converts tags to plain text strings.
1136
+ #
1137
+ # Registered at :text.
1138
+ class Serializer::PlainText
1139
+ Forme.register_transformer(:serializer, :text, new)
1140
+
1141
+ # Serialize the tag to plain text string.
1142
+ def call(tag)
1143
+ case tag
1144
+ when Tag
1145
+ case tag.type.to_sym
1146
+ when :input
1147
+ case tag.attr[:type].to_sym
1148
+ when :radio, :checkbox
1149
+ tag.attr[:checked] ? '_X_' : '___'
1150
+ when :submit, :reset, :hidden
1151
+ ''
1152
+ when :password
1153
+ "********\n"
1154
+ else
1155
+ "#{tag.attr[:value].to_s}\n"
1156
+ end
1157
+ when :select
1158
+ "\n#{call(tag.children)}"
1159
+ when :option
1160
+ "#{call([tag.attr[:selected] ? '_X_ ' : '___ ', tag.children])}\n"
1161
+ when :textarea, :label
1162
+ "#{call(tag.children)}\n"
1163
+ when :legend
1164
+ v = call(tag.children)
1165
+ "#{v}\n#{'-' * v.length}\n"
1166
+ else
1167
+ call(tag.children)
1168
+ end
1169
+ when Input
1170
+ call(tag.format)
1171
+ when Array
1172
+ tag.map{|x| call(x)}.join
1173
+ else
1174
+ tag.to_s
1175
+ end
1176
+ end
1177
+ end
1178
+ end