forme 1.2.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +24 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +37 -18
- data/Rakefile +6 -5
- data/lib/forme.rb +6 -1348
- data/lib/forme/form.rb +387 -0
- data/lib/forme/input.rb +48 -0
- data/lib/forme/raw.rb +12 -0
- data/lib/forme/tag.rb +60 -0
- data/lib/forme/transformers/error_handler.rb +18 -0
- data/lib/forme/transformers/formatter.rb +511 -0
- data/lib/forme/transformers/helper.rb +17 -0
- data/lib/forme/transformers/inputs_wrapper.rb +95 -0
- data/lib/forme/transformers/labeler.rb +78 -0
- data/lib/forme/transformers/serializer.rb +171 -0
- data/lib/forme/transformers/wrapper.rb +52 -0
- data/lib/forme/version.rb +1 -1
- data/lib/sequel/plugins/forme.rb +7 -4
- data/spec/forme_spec.rb +65 -5
- data/spec/sequel_plugin_spec.rb +18 -3
- metadata +15 -4
@@ -0,0 +1,17 @@
|
|
1
|
+
module Forme
|
2
|
+
# Default helper used by the library, using a spam with "helper" class
|
3
|
+
#
|
4
|
+
# Registered as :default.
|
5
|
+
class Helper
|
6
|
+
Forme.register_transformer(:helper, :default, new)
|
7
|
+
|
8
|
+
# Return tag with error message span tag after it.
|
9
|
+
def call(tag, input)
|
10
|
+
attr = input.opts[:helper_attr]
|
11
|
+
attr = attr ? attr.dup : {}
|
12
|
+
Forme.attr_classes(attr, 'helper')
|
13
|
+
[tag, input.tag(:span, attr, input.opts[:help])]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Forme
|
2
|
+
# Default inputs_wrapper used by the library, uses a <fieldset>.
|
3
|
+
#
|
4
|
+
# Registered as :default.
|
5
|
+
class InputsWrapper
|
6
|
+
Forme.register_transformer(:inputs_wrapper, :default, new)
|
7
|
+
|
8
|
+
# Wrap the inputs in a <fieldset>. If the :legend
|
9
|
+
# option is given, add a <legend> tag as the first
|
10
|
+
# child of the fieldset.
|
11
|
+
def call(form, opts)
|
12
|
+
attr = opts[:attr] ? opts[:attr].dup : {}
|
13
|
+
Forme.attr_classes(attr, 'inputs')
|
14
|
+
if legend = opts[:legend]
|
15
|
+
form.tag(:fieldset, attr) do
|
16
|
+
form.emit(form.tag(:legend, opts[:legend_attr], legend))
|
17
|
+
yield
|
18
|
+
end
|
19
|
+
else
|
20
|
+
form.tag(:fieldset, attr, &Proc.new)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Use a <fieldset> and an <ol> tag to wrap the inputs.
|
26
|
+
#
|
27
|
+
# Registered as :fieldset_ol.
|
28
|
+
class InputsWrapper::FieldSetOL < InputsWrapper
|
29
|
+
Forme.register_transformer(:inputs_wrapper, :fieldset_ol, new)
|
30
|
+
|
31
|
+
# Wrap the inputs in a <fieldset> and a <ol> tag.
|
32
|
+
def call(form, opts)
|
33
|
+
super(form, opts){form.tag_(:ol){yield}}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Use an <ol> tag to wrap the inputs.
|
38
|
+
#
|
39
|
+
# Registered as :ol.
|
40
|
+
class InputsWrapper::OL
|
41
|
+
Forme.register_transformer(:inputs_wrapper, :ol, new)
|
42
|
+
|
43
|
+
# Wrap the inputs in an <ol> tag
|
44
|
+
def call(form, opts, &block)
|
45
|
+
form.tag(:ol, opts[:attr], &block)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Use a <div> tag to wrap the inputs.
|
50
|
+
#
|
51
|
+
# Registered as :div.
|
52
|
+
class InputsWrapper::Div
|
53
|
+
Forme.register_transformer(:inputs_wrapper, :div, new)
|
54
|
+
|
55
|
+
# Wrap the inputs in an <div> tag
|
56
|
+
def call(form, opts, &block)
|
57
|
+
form.tag(:div, opts[:attr], &block)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Use a <tr> tag to wrap the inputs.
|
62
|
+
#
|
63
|
+
# Registered as :tr.
|
64
|
+
class InputsWrapper::TR
|
65
|
+
Forme.register_transformer(:inputs_wrapper, :tr, new)
|
66
|
+
|
67
|
+
# Wrap the inputs in an <tr> tag
|
68
|
+
def call(form, opts, &block)
|
69
|
+
form.tag(:tr, opts[:attr], &block)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Use a <table> tag to wrap the inputs.
|
74
|
+
#
|
75
|
+
# Registered as :table.
|
76
|
+
class InputsWrapper::Table
|
77
|
+
Forme.register_transformer(:inputs_wrapper, :table, new)
|
78
|
+
|
79
|
+
# Wrap the inputs in a <table> tag.
|
80
|
+
def call(form, opts, &block)
|
81
|
+
attr = opts[:attr] ? opts[:attr].dup : {}
|
82
|
+
form.tag(:table, attr) do
|
83
|
+
if legend = opts[:legend]
|
84
|
+
form.emit(form.tag(:caption, opts[:legend_attr], legend))
|
85
|
+
end
|
86
|
+
|
87
|
+
if (labels = opts[:labels]) && !labels.empty?
|
88
|
+
form.emit(form.tag(:tr, {}, labels.map{|l| form._tag(:th, {}, l)}))
|
89
|
+
end
|
90
|
+
|
91
|
+
yield
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Forme
|
2
|
+
# Default labeler used by the library, using implicit labels (where the
|
3
|
+
# label tag encloses the other tag).
|
4
|
+
#
|
5
|
+
# Registered as :default.
|
6
|
+
class Labeler
|
7
|
+
Forme.register_transformer(:labeler, :default, new)
|
8
|
+
|
9
|
+
# Return a label tag wrapping the given tag. For radio and checkbox
|
10
|
+
# inputs, the label occurs directly after the tag, for all other types,
|
11
|
+
# the label occurs before the tag.
|
12
|
+
def call(tag, input)
|
13
|
+
label = input.opts[:label]
|
14
|
+
label_position = input.opts[:label_position]
|
15
|
+
if [:radio, :checkbox].include?(input.type)
|
16
|
+
if input.type == :checkbox && tag.is_a?(Array) && tag.length == 2 && tag.first.attr[:type].to_s == 'hidden'
|
17
|
+
t = if label_position == :before
|
18
|
+
[label, ' ', tag.last]
|
19
|
+
else
|
20
|
+
[tag.last, ' ', label]
|
21
|
+
end
|
22
|
+
return [tag.first , input.tag(:label, input.opts[:label_attr]||{}, t)]
|
23
|
+
elsif label_position == :before
|
24
|
+
t = [label, ' ', tag]
|
25
|
+
else
|
26
|
+
t = [tag, ' ', label]
|
27
|
+
end
|
28
|
+
elsif label_position == :after
|
29
|
+
t = [tag, ' ', label]
|
30
|
+
else
|
31
|
+
t = [label, ": ", tag]
|
32
|
+
end
|
33
|
+
input.tag(:label, input.opts[:label_attr]||{}, t)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Explicit labeler that creates a separate label tag that references
|
38
|
+
# the given tag's id using a +for+ attribute. Requires that all tags
|
39
|
+
# with labels have +id+ fields.
|
40
|
+
#
|
41
|
+
# Registered as :explicit.
|
42
|
+
class Labeler::Explicit
|
43
|
+
Forme.register_transformer(:labeler, :explicit, new)
|
44
|
+
|
45
|
+
# Return an array with a label tag as the first entry and +tag+ as
|
46
|
+
# a second entry. If the +input+ has a :label_for option, use that,
|
47
|
+
# otherwise use the input's :id option. If neither the :id or
|
48
|
+
# :label_for option is used, the label created will not be
|
49
|
+
# associated with an input.
|
50
|
+
def call(tag, input)
|
51
|
+
unless id = input.opts[:id]
|
52
|
+
if key = input.opts[:key]
|
53
|
+
namespaces = input.form_opts[:namespace]
|
54
|
+
id = "#{namespaces.join('_')}#{'_' unless namespaces.empty?}#{key}"
|
55
|
+
if key_id = input.opts[:key_id]
|
56
|
+
id << "_#{key_id.to_s}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
label_attr = input.opts[:label_attr]
|
62
|
+
label_attr = label_attr ? label_attr.dup : {}
|
63
|
+
label_attr[:for] ||= input.opts.fetch(:label_for, id)
|
64
|
+
lpos = input.opts[:label_position] || ([:radio, :checkbox].include?(input.type) ? :after : :before)
|
65
|
+
|
66
|
+
Forme.attr_classes(label_attr, "label-#{lpos}")
|
67
|
+
label = input.tag(:label, label_attr, [input.opts[:label]])
|
68
|
+
|
69
|
+
t = if lpos == :before
|
70
|
+
[label, tag]
|
71
|
+
else
|
72
|
+
[tag, label]
|
73
|
+
end
|
74
|
+
|
75
|
+
t
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
module Forme
|
2
|
+
# Default serializer class used by the library. Any other serializer
|
3
|
+
# classes that want to produce html should probably subclass this class.
|
4
|
+
#
|
5
|
+
# Registered as :default.
|
6
|
+
class Serializer
|
7
|
+
Forme.register_transformer(:serializer, :default, new)
|
8
|
+
|
9
|
+
# Borrowed from Rack::Utils, map of single character strings to html escaped versions.
|
10
|
+
ESCAPE_HTML = {"&" => "&", "<" => "<", ">" => ">", "'" => "'", '"' => """}
|
11
|
+
|
12
|
+
# A regexp that matches all html characters requiring escaping.
|
13
|
+
ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
|
14
|
+
|
15
|
+
# Which tags are self closing (such tags ignore children).
|
16
|
+
SELF_CLOSING = [:img, :input]
|
17
|
+
|
18
|
+
# Serialize the tag object to an html string. Supports +Tag+ instances,
|
19
|
+
# +Input+ instances (recursing into +call+ with the result of formatting the input),
|
20
|
+
# arrays (recurses into +call+ for each entry and joins the result), and
|
21
|
+
# (html escapes the string version of them, unless they include the +Raw+
|
22
|
+
# module, in which case no escaping is done).
|
23
|
+
def call(tag)
|
24
|
+
case tag
|
25
|
+
when Tag
|
26
|
+
if SELF_CLOSING.include?(tag.type)
|
27
|
+
"<#{tag.type}#{attr_html(tag.attr)}/>"
|
28
|
+
else
|
29
|
+
"#{serialize_open(tag)}#{call(tag.children)}#{serialize_close(tag)}"
|
30
|
+
end
|
31
|
+
when Input
|
32
|
+
call(tag.format)
|
33
|
+
when Array
|
34
|
+
tag.map{|x| call(x)}.join
|
35
|
+
when DateTime, Time
|
36
|
+
format_time(tag)
|
37
|
+
when Date
|
38
|
+
format_date(tag)
|
39
|
+
when BigDecimal
|
40
|
+
tag.to_s('F')
|
41
|
+
when Raw
|
42
|
+
tag.to_s
|
43
|
+
else
|
44
|
+
h tag
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns the opening part of the given tag.
|
49
|
+
def serialize_open(tag)
|
50
|
+
"<#{tag.type}#{attr_html(tag.attr)}>"
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns the closing part of the given tag.
|
54
|
+
def serialize_close(tag)
|
55
|
+
"</#{tag.type}>"
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# Return a string in ISO format representing the +Date+ instance.
|
61
|
+
def format_date(date)
|
62
|
+
date.strftime("%F")
|
63
|
+
end
|
64
|
+
|
65
|
+
# Return a string in ISO format representing the +Time+ or +DateTime+ instance.
|
66
|
+
def format_time(time)
|
67
|
+
time.is_a?(Time) ? (time.strftime('%Y-%m-%dT%H:%M:%S') + sprintf(".%06d", time.usec)) : (time.strftime('%Y-%m-%dT%H:%M:%S.') + time.strftime('%N')[0...6])
|
68
|
+
end
|
69
|
+
|
70
|
+
# Escape ampersands, brackets and quotes to their HTML/XML entities.
|
71
|
+
def h(string)
|
72
|
+
string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
|
73
|
+
end
|
74
|
+
|
75
|
+
# Join attribute values that are arrays with spaces instead of an empty
|
76
|
+
# string.
|
77
|
+
def attr_value(v)
|
78
|
+
if v.is_a?(Array)
|
79
|
+
v.map{|c| attr_value(c)}.join(' ')
|
80
|
+
else
|
81
|
+
call(v)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Transforms the +tag+'s attributes into an html string, sorting by the keys
|
86
|
+
# and quoting and html escaping the values.
|
87
|
+
def attr_html(attr)
|
88
|
+
attr = attr.to_a.reject{|k,v| v.nil?}
|
89
|
+
" #{attr.map{|k, v| "#{k}=\"#{attr_value(v)}\""}.sort.join(' ')}" unless attr.empty?
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Overrides formatting of dates and times to use an American format without
|
94
|
+
# timezones.
|
95
|
+
class Serializer::AmericanTime < Serializer
|
96
|
+
Forme.register_transformer(:serializer, :html_usa, new)
|
97
|
+
|
98
|
+
def call(tag)
|
99
|
+
case tag
|
100
|
+
when Tag
|
101
|
+
if tag.type.to_s == 'input' && %w'date datetime datetime-local'.include?((tag.attr[:type] || tag.attr['type']).to_s)
|
102
|
+
attr = tag.attr.dup
|
103
|
+
attr.delete(:type)
|
104
|
+
attr.delete('type')
|
105
|
+
attr['type'] = 'text'
|
106
|
+
"<#{tag.type}#{attr_html(attr)}/>"
|
107
|
+
else
|
108
|
+
super
|
109
|
+
end
|
110
|
+
else
|
111
|
+
super
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
# Return a string in American format representing the +Date+ instance.
|
118
|
+
def format_date(date)
|
119
|
+
date.strftime("%m/%d/%Y")
|
120
|
+
end
|
121
|
+
|
122
|
+
# Return a string in American format representing the +Time+ or +DateTime+ instance, without the timezone.
|
123
|
+
def format_time(time)
|
124
|
+
time.strftime("%m/%d/%Y %I:%M:%S%p")
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Serializer class that converts tags to plain text strings.
|
129
|
+
#
|
130
|
+
# Registered at :text.
|
131
|
+
class Serializer::PlainText
|
132
|
+
Forme.register_transformer(:serializer, :text, new)
|
133
|
+
|
134
|
+
# Serialize the tag to plain text string.
|
135
|
+
def call(tag)
|
136
|
+
case tag
|
137
|
+
when Tag
|
138
|
+
case tag.type.to_sym
|
139
|
+
when :input
|
140
|
+
case tag.attr[:type].to_sym
|
141
|
+
when :radio, :checkbox
|
142
|
+
tag.attr[:checked] ? '_X_' : '___'
|
143
|
+
when :submit, :reset, :hidden
|
144
|
+
''
|
145
|
+
when :password
|
146
|
+
"********\n"
|
147
|
+
else
|
148
|
+
"#{tag.attr[:value].to_s}\n"
|
149
|
+
end
|
150
|
+
when :select
|
151
|
+
"\n#{call(tag.children)}"
|
152
|
+
when :option
|
153
|
+
"#{call([tag.attr[:selected] ? '_X_ ' : '___ ', tag.children])}\n"
|
154
|
+
when :textarea, :label
|
155
|
+
"#{call(tag.children)}\n"
|
156
|
+
when :legend
|
157
|
+
v = call(tag.children)
|
158
|
+
"#{v}\n#{'-' * v.length}\n"
|
159
|
+
else
|
160
|
+
call(tag.children)
|
161
|
+
end
|
162
|
+
when Input
|
163
|
+
call(tag.format)
|
164
|
+
when Array
|
165
|
+
tag.map{|x| call(x)}.join
|
166
|
+
else
|
167
|
+
tag.to_s
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Forme
|
2
|
+
# Default wrapper doesn't wrap input in any tag
|
3
|
+
class Wrapper
|
4
|
+
# Return an array containing the tag
|
5
|
+
def call(tag, input)
|
6
|
+
Array(tag)
|
7
|
+
end
|
8
|
+
|
9
|
+
Forme.register_transformer(:wrapper, :default, new)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Wraps inputs using the given tag type.
|
13
|
+
class Wrapper::Tag < Wrapper
|
14
|
+
# Set the tag type to use.
|
15
|
+
def initialize(type)
|
16
|
+
@type = type
|
17
|
+
end
|
18
|
+
|
19
|
+
# Wrap the input in the tag of the given type.
|
20
|
+
def call(tag, input)
|
21
|
+
input.tag(@type, input.opts[:wrapper_attr], super)
|
22
|
+
end
|
23
|
+
|
24
|
+
[:li, :p, :div, :span, :td].each do |x|
|
25
|
+
Forme.register_transformer(:wrapper, x, new(x))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Wrapper::TableRow < Wrapper
|
30
|
+
# Wrap the input in tr and td tags.
|
31
|
+
def call(tag, input)
|
32
|
+
a = super.flatten
|
33
|
+
labels, other = a.partition{|e| e.is_a?(Tag) && e.type.to_s == 'label'}
|
34
|
+
if labels.length == 1
|
35
|
+
ltd = labels
|
36
|
+
rtd = other
|
37
|
+
elsif a.length == 1
|
38
|
+
ltd = [a.first]
|
39
|
+
rtd = a[1..-1]
|
40
|
+
else
|
41
|
+
ltd = a
|
42
|
+
end
|
43
|
+
input.tag(:tr, input.opts[:wrapper_attr], [input.tag(:td, {}, ltd), input.tag(:td, {}, rtd)])
|
44
|
+
end
|
45
|
+
|
46
|
+
Forme.register_transformer(:wrapper, :trtd, new)
|
47
|
+
end
|
48
|
+
|
49
|
+
{:tr=>:td, :table=>:trtd, :ol=>:li, :fieldset_ol=>:li}.each do |k, v|
|
50
|
+
register_transformer(:wrapper, k, TRANSFORMERS[:wrapper][v])
|
51
|
+
end
|
52
|
+
end
|
data/lib/forme/version.rb
CHANGED
data/lib/sequel/plugins/forme.rb
CHANGED
@@ -53,6 +53,8 @@ module Sequel # :nodoc:
|
|
53
53
|
# be created via the :inputs option. If you are not providing
|
54
54
|
# an :inputs option or are using a block with additional inputs,
|
55
55
|
# you should specify this option.
|
56
|
+
# :skip_primary_key :: Skip adding a hidden primary key field for existing
|
57
|
+
# objects.
|
56
58
|
def subform(association, opts={}, &block)
|
57
59
|
nested_obj = opts.has_key?(:obj) ? opts[:obj] : obj.send(association)
|
58
60
|
ref = obj.class.association_reflection(association)
|
@@ -62,7 +64,7 @@ module Sequel # :nodoc:
|
|
62
64
|
|
63
65
|
contents = proc do
|
64
66
|
send(multiple ? :each_obj : :with_obj, nested_obj, ns) do |no, i|
|
65
|
-
emit(input(ref.associated_class.primary_key, :type=>:hidden, :label=>nil, :wrapper=>nil)) unless no.new?
|
67
|
+
emit(input(ref.associated_class.primary_key, :type=>:hidden, :label=>nil, :wrapper=>nil)) unless no.new? || opts[:skip_primary_key]
|
66
68
|
options = opts.dup
|
67
69
|
if grid
|
68
70
|
options.delete(:legend)
|
@@ -245,7 +247,7 @@ module Sequel # :nodoc:
|
|
245
247
|
if meth = opts.delete(:name_method)
|
246
248
|
meth
|
247
249
|
else
|
248
|
-
meths = FORME_NAME_METHODS & ref.associated_class.instance_methods.map
|
250
|
+
meths = FORME_NAME_METHODS & ref.associated_class.instance_methods.map(&:to_sym)
|
249
251
|
if meths.empty?
|
250
252
|
raise Error, "No suitable name method found for association #{ref[:name]}"
|
251
253
|
else
|
@@ -289,7 +291,8 @@ module Sequel # :nodoc:
|
|
289
291
|
label = klass.send(:singularize, ref[:name])
|
290
292
|
|
291
293
|
field = if ref[:type] == :pg_array_to_many
|
292
|
-
|
294
|
+
handle_errors(key)
|
295
|
+
key
|
293
296
|
else
|
294
297
|
"#{label}_pks"
|
295
298
|
end
|
@@ -361,7 +364,7 @@ module Sequel # :nodoc:
|
|
361
364
|
when :select
|
362
365
|
v = opts[:value] || obj.send(field)
|
363
366
|
opts[:value] = (v ? 't' : 'f') unless v.nil?
|
364
|
-
opts[:add_blank] = true
|
367
|
+
opts[:add_blank] = true unless opts.has_key?(:add_blank)
|
365
368
|
opts[:options] = [[opts[:true_label]||'True', opts[:true_value]||'t'], [opts[:false_label]||'False', opts[:false_value]||'f']]
|
366
369
|
_input(:select, opts)
|
367
370
|
else
|