forme 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|