forme 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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