forme 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +1 -0
- data/MIT-LICENSE +18 -0
- data/README.rdoc +231 -0
- data/Rakefile +97 -0
- data/lib/forme.rb +1178 -0
- data/lib/forme/sinatra.rb +89 -0
- data/lib/forme/version.rb +9 -0
- data/lib/sequel/plugins/forme.rb +435 -0
- data/spec/forme_spec.rb +579 -0
- data/spec/sequel_plugin_spec.rb +405 -0
- data/spec/sinatra_integration_spec.rb +96 -0
- data/spec/spec_helper.rb +2 -0
- metadata +93 -0
data/CHANGELOG
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
=== HEAD
|
data/MIT-LICENSE
ADDED
@@ -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.
|
data/README.rdoc
ADDED
@@ -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>
|
data/Rakefile
ADDED
@@ -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
|
+
|
data/lib/forme.rb
ADDED
@@ -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 = {"&" => "&", "<" => "<", ">" => ">", "'" => "'", '"' => """}
|
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
|