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.
@@ -0,0 +1,18 @@
1
+ module Forme
2
+ # Default error handler used by the library, using an "error" class
3
+ # for the input field and a span tag with an "error_message" class
4
+ # for the error message.
5
+ #
6
+ # Registered as :default.
7
+ class ErrorHandler
8
+ Forme.register_transformer(:error_handler, :default, new)
9
+
10
+ # Return tag with error message span tag after it.
11
+ def call(tag, input)
12
+ attr = input.opts[:error_attr]
13
+ attr = attr ? attr.dup : {}
14
+ Forme.attr_classes(attr, 'error_message')
15
+ [tag, input.tag(:span, attr, input.opts[:error])]
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,511 @@
1
+ module Forme
2
+ # The default formatter used by the library. Any custom formatters should
3
+ # probably inherit from this formatter unless they have very special needs.
4
+ #
5
+ # Unlike most other transformers which are registered as instances and use
6
+ # a functional style, this class is registered as a class due to the large
7
+ # amount of state it uses.
8
+ #
9
+ # Registered as :default.
10
+ class Formatter
11
+ Forme.register_transformer(:formatter, :default, self)
12
+
13
+ # These options are copied directly from the options hash to the the
14
+ # attributes hash, so they don't need to be specified in the :attr
15
+ # option. However, they can be specified in both places, and if so,
16
+ # the :attr option version takes precedence.
17
+ ATTRIBUTE_OPTIONS = [:name, :id, :placeholder, :value, :style]
18
+
19
+ # Options copied from the options hash into the attributes hash,
20
+ # where a true value in the options hash sets the attribute
21
+ # value to the same name as the key.
22
+ ATTRIBUTE_BOOLEAN_OPTIONS = [:autofocus, :required, :disabled]
23
+
24
+ # Create a new instance and call it
25
+ def self.call(input)
26
+ new.call(input)
27
+ end
28
+
29
+ # The +Form+ instance for the receiver, taken from the +input+.
30
+ attr_reader :form
31
+
32
+ # The +Input+ instance for the receiver. This is what the receiver
33
+ # converts to the lower level +Tag+ form (or an array of them).
34
+ attr_reader :input
35
+
36
+ # The attributes to to set on the lower level +Tag+ form returned.
37
+ # This are derived from the +input+'s +opts+, but some processing is done on
38
+ # them.
39
+ attr_reader :attr
40
+
41
+ # The +opts+ hash of the +input+.
42
+ attr_reader :opts
43
+
44
+ # Used to specify the value of the hidden input created for checkboxes.
45
+ # Since the default for an unspecified checkbox value is 1, the default is
46
+ # 0. If the checkbox value is 't', the hidden value is 'f', since that is
47
+ # common usage for boolean values.
48
+ CHECKBOX_MAP = Hash.new(0)
49
+ CHECKBOX_MAP['t'] = 'f'
50
+
51
+ # Transform the +input+ into a +Tag+ instance (or an array of them),
52
+ # wrapping it with the +form+'s wrapper, and the form's +error_handler+
53
+ # and +labeler+ if the +input+ has an <tt>:error</tt> or <tt>:label</tt>
54
+ # options.
55
+ def call(input)
56
+ @input = input
57
+ @form = input.form
58
+ attr = input.opts[:attr]
59
+ @attr = attr ? attr.dup : {}
60
+ @opts = input.opts
61
+ normalize_options
62
+
63
+ tag = convert_to_tag(input.type)
64
+ tag = wrap_tag_with_label(tag) if input.opts[:label]
65
+ tag = wrap_tag_with_error(tag) if input.opts[:error]
66
+ tag = wrap(:helper, tag) if input.opts[:help]
67
+ wrap_tag(tag)
68
+ end
69
+
70
+ private
71
+
72
+ # Dispatch to a format_<i>type</i> method if there is one that matches the
73
+ # type, otherwise, call +_format_input+ with the given +type+.
74
+ def convert_to_tag(type)
75
+ meth = :"format_#{type}"
76
+ if respond_to?(meth, true)
77
+ send(meth)
78
+ else
79
+ _format_input(type)
80
+ end
81
+ end
82
+
83
+ # If the checkbox has a name, will create a hidden input tag with the
84
+ # same name that comes before this checkbox. That way, if the checkbox
85
+ # is checked, the web app will generally see the value of the checkbox, and
86
+ # if it is not checked, the web app will generally see the value of the hidden
87
+ # input tag.
88
+ def format_checkbox
89
+ @attr[:type] = :checkbox
90
+ @attr[:checked] = :checked if @opts[:checked]
91
+ if @attr[:name] && !@opts[:no_hidden]
92
+ attr = {:type=>:hidden}
93
+ unless attr[:value] = @opts[:hidden_value]
94
+ attr[:value] = CHECKBOX_MAP[@attr[:value]]
95
+ end
96
+ attr[:id] = "#{@attr[:id]}_hidden" if @attr[:id]
97
+ attr[:name] = @attr[:name]
98
+ [tag(:input, attr), tag(:input)]
99
+ else
100
+ tag(:input)
101
+ end
102
+ end
103
+
104
+ # For radio buttons, recognizes the :checked option and sets the :checked
105
+ # attribute in the tag appropriately.
106
+ def format_radio
107
+ @attr[:checked] = :checked if @opts[:checked]
108
+ @attr[:type] = :radio
109
+ tag(:input)
110
+ end
111
+
112
+ DEFAULT_DATE_ORDER = [:year, '-'.freeze, :month, '-'.freeze, :day].freeze
113
+ # Use a date input by default. If the :as=>:select option is given,
114
+ # use a multiple select box for the options.
115
+ def format_date
116
+ if @opts[:as] == :select
117
+ values = {}
118
+ if v = @attr[:value]
119
+ v = Date.parse(v) unless v.is_a?(Date)
120
+ values[:year], values[:month], values[:day] = v.year, v.month, v.day
121
+ end
122
+ _format_date_select(values, @opts[:order] || DEFAULT_DATE_ORDER)
123
+ else
124
+ _format_input(:date)
125
+ end
126
+ end
127
+
128
+ DEFAULT_DATETIME_ORDER = [:year, '-'.freeze, :month, '-'.freeze, :day, ' '.freeze, :hour, ':'.freeze, :minute, ':'.freeze, :second].freeze
129
+ # Use a datetime input by default. If the :as=>:select option is given,
130
+ # use a multiple select box for the options.
131
+ def format_datetime
132
+ if @opts[:as] == :select
133
+ values = {}
134
+ if v = @attr[:value]
135
+ v = DateTime.parse(v) unless v.is_a?(Time) || v.is_a?(DateTime)
136
+ values[:year], values[:month], values[:day], values[:hour], values[:minute], values[:second] = v.year, v.month, v.day, v.hour, v.min, v.sec
137
+ end
138
+ _format_date_select(values, @opts[:order] || DEFAULT_DATETIME_ORDER)
139
+ else
140
+ _format_input('datetime-local')
141
+ end
142
+ end
143
+
144
+ DEFAULT_DATE_SELECT_OPS = {:year=>1900..2050, :month=>1..12, :day=>1..31, :hour=>0..23, :minute=>0..59, :second=>0..59}.freeze
145
+ DATE_SELECT_FORMAT = '%02i'.freeze
146
+ # Shared code for formatting dates/times as select boxes
147
+ def _format_date_select(values, order)
148
+ name = @attr[:name]
149
+ id = @attr[:id]
150
+ ops = DEFAULT_DATE_SELECT_OPS
151
+ ops = ops.merge(@opts[:select_options]) if @opts[:select_options]
152
+ first_input = true
153
+ format = DATE_SELECT_FORMAT
154
+ order.map do |x|
155
+ next x if x.is_a?(String)
156
+ opts = @opts.merge(:label=>nil, :wrapper=>nil, :error=>nil, :name=>"#{name}[#{x}]", :value=>values[x], :options=>ops[x].map{|y| [sprintf(format, y), y]})
157
+ opts[:id] = if first_input
158
+ first_input = false
159
+ id
160
+ else
161
+ "#{id}_#{x}"
162
+ end
163
+ form._input(:select, opts).format
164
+ end
165
+ end
166
+
167
+ # The default fallback method for handling inputs. Assumes an input tag
168
+ # with the type attribute set to input.
169
+ def _format_input(type)
170
+ @attr[:type] = type
171
+ copy_options_to_attributes([:size, :maxlength])
172
+ tag(:input)
173
+ end
174
+
175
+ # Takes a select input and turns it into a select tag with (possibly) option
176
+ # children tags.
177
+ def format_select
178
+ @attr[:multiple] = :multiple if @opts[:multiple]
179
+ copy_options_to_attributes([:size])
180
+
181
+ os = process_select_optgroups(:_format_select_optgroup) do |label, value, sel, attrs|
182
+ if value || sel
183
+ attrs = attrs.dup
184
+ attrs[:value] = value if value
185
+ attrs[:selected] = :selected if sel
186
+ end
187
+ tag(:option, attrs, [label])
188
+ end
189
+ tag(:select, @attr, os)
190
+ end
191
+
192
+ # Use an optgroup around related options in a select tag.
193
+ def _format_select_optgroup(group, options)
194
+ group = {:label=>group} unless group.is_a?(Hash)
195
+ tag(:optgroup, group, options)
196
+ end
197
+
198
+ # Use a fieldset/legend around related options in a checkbox or radio button set.
199
+ def _format_set_optgroup(group, options)
200
+ tag(:fieldset, {}, [tag(:legend, {}, [group])] + options)
201
+ end
202
+
203
+ def format_checkboxset
204
+ @opts[:multiple] = true unless @opts.has_key?(:multiple)
205
+ _format_set(:checkbox, :no_hidden=>true, :multiple=>true)
206
+ end
207
+
208
+ def format_radioset
209
+ _format_set(:radio)
210
+ end
211
+
212
+ def _format_set(type, tag_attrs={})
213
+ raise Error, "can't have radioset with no options" unless @opts[:optgroups] || @opts[:options]
214
+ key = @opts[:key]
215
+ name = @opts[:name]
216
+ id = @opts[:id]
217
+ if @opts[:error]
218
+ @opts[:set_error] = @opts.delete(:error)
219
+ end
220
+ if @opts[:label]
221
+ @opts[:set_label] = @opts.delete(:label)
222
+ end
223
+ tag_wrapper = @opts.delete(:tag_wrapper) || :default
224
+ wrapper = Forme.transformer(:wrapper, @opts, @input.form_opts)
225
+
226
+ tags = process_select_optgroups(:_format_set_optgroup) do |label, value, sel, attrs|
227
+ value ||= label
228
+ r_opts = attrs.merge(tag_attrs).merge(:label=>label||value, :label_attr=>{:class=>:option}, :wrapper=>tag_wrapper)
229
+ r_opts[:value] ||= value if value
230
+ r_opts[:checked] ||= :checked if sel
231
+
232
+ if name
233
+ r_opts[:name] ||= name
234
+ end
235
+ if id
236
+ r_opts[:id] ||= "#{id}_#{value}"
237
+ end
238
+ if key
239
+ r_opts[:key] ||= key
240
+ r_opts[:key_id] ||= value
241
+ end
242
+
243
+ form._input(type, r_opts)
244
+ end
245
+
246
+ if @opts[:set_error]
247
+ if (last_input = tags.last) && last_input.is_a?(Input)
248
+ last_input.opts[:error] = @opts[:set_error]
249
+ else
250
+ tags << form._tag(:span, {:class=>'error_message'}, [@opts[:set_error]])
251
+ end
252
+ end
253
+ tags.unshift(form._tag(:span, {:class=>:label}, @opts[:set_label])) if @opts[:set_label]
254
+ wrapper.call(tags, form._input(type, opts)) if wrapper
255
+ tags
256
+ end
257
+
258
+ # Formats a textarea. Respects the following options:
259
+ # :value :: Sets value as the child of the textarea.
260
+ def format_textarea
261
+ copy_options_to_attributes([:cols, :rows])
262
+ if val = @attr.delete(:value)
263
+ tag(:textarea, @attr, [val])
264
+ else
265
+ tag(:textarea)
266
+ end
267
+ end
268
+
269
+ # Copy option values for given keys to the attributes unless the
270
+ # attributes already have a value for the key.
271
+ def copy_options_to_attributes(keys)
272
+ keys.each do |k|
273
+ if @opts.has_key?(k) && !@attr.has_key?(k)
274
+ @attr[k] = @opts[k]
275
+ end
276
+ end
277
+ end
278
+
279
+ # Set attribute values for given keys to be the same as the key
280
+ # unless the attributes already have a value for the key.
281
+ def copy_boolean_options_to_attributes(keys)
282
+ keys.each do |k|
283
+ if @opts[k] && !@attr.has_key?(k)
284
+ @attr[k] = k
285
+ end
286
+ end
287
+ end
288
+
289
+ # Normalize the options used for all input types.
290
+ def normalize_options
291
+ copy_options_to_attributes(ATTRIBUTE_OPTIONS)
292
+ copy_boolean_options_to_attributes(ATTRIBUTE_BOOLEAN_OPTIONS)
293
+ handle_key_option
294
+
295
+ Forme.attr_classes(@attr, @opts[:class]) if @opts.has_key?(:class)
296
+ Forme.attr_classes(@attr, 'error') if @opts[:error]
297
+
298
+ if data = opts[:data]
299
+ data.each do |k, v|
300
+ sym = :"data-#{k}"
301
+ @attr[sym] = v unless @attr.has_key?(sym)
302
+ end
303
+ end
304
+ end
305
+
306
+ # Have the :key option possibly set the name, id, and/or value attributes if not already set.
307
+ def handle_key_option
308
+ if key = @opts[:key]
309
+ unless @attr[:name] || @attr['name']
310
+ @attr[:name] = namespaced_name(key, @opts[:array] || @opts[:multiple])
311
+ if !@attr.has_key?(:value) && !@attr.has_key?('value') && (values = @form.opts[:values])
312
+ set_value_from_namespaced_values(namespaces, values, key)
313
+ end
314
+ end
315
+ unless @attr[:id] || @attr['id']
316
+ id = namespaced_id(key)
317
+ if suffix = @opts[:key_id]
318
+ id << '_' << suffix.to_s
319
+ end
320
+ @attr[:id] = id
321
+ end
322
+ end
323
+ end
324
+
325
+ # Array of namespaces to use for the input
326
+ def namespaces
327
+ input.form_opts[:namespace]
328
+ end
329
+
330
+ # Return a unique id attribute for the +field+, based on the current namespaces.
331
+ def namespaced_id(field)
332
+ "#{namespaces.join('_')}#{'_' unless namespaces.empty?}#{field}"
333
+ end
334
+
335
+ # Return a unique name attribute for the +field+, based on the current namespaces.
336
+ # If +multiple+ is true, end the name with [] so that param parsing will treat
337
+ # the name as part of an array.
338
+ def namespaced_name(field, multiple=false)
339
+ if namespaces.empty?
340
+ if multiple
341
+ "#{field}[]"
342
+ else
343
+ field
344
+ end
345
+ else
346
+ root, *nsps = namespaces
347
+ "#{root}#{nsps.map{|n| "[#{n}]"}.join}[#{field}]#{'[]' if multiple}"
348
+ end
349
+ end
350
+
351
+ # Set the values option based on the (possibly nested) values
352
+ # hash given, array of namespaces, and key.
353
+ def set_value_from_namespaced_values(namespaces, values, key)
354
+ namespaces.each do |ns|
355
+ v = values[ns] || values[ns.to_s]
356
+ return unless v
357
+ values = v
358
+ end
359
+
360
+ @attr[:value] = values.fetch(key){values.fetch(key.to_s){return}}
361
+ end
362
+
363
+ # If :optgroups option is present, iterate over each of the groups
364
+ # inside of it and create options for each group. Otherwise, if
365
+ # :options option present, iterate over it and create options.
366
+ def process_select_optgroups(grouper, &block)
367
+ os = if groups = @opts[:optgroups]
368
+ groups.map do |group, options|
369
+ send(grouper, group, process_select_options(options, &block))
370
+ end
371
+ else
372
+ return unless @opts[:options]
373
+ process_select_options(@opts[:options], &block)
374
+ end
375
+
376
+ if prompt = @opts[:add_blank]
377
+ unless prompt.is_a?(String)
378
+ prompt = Forme.default_add_blank_prompt
379
+ end
380
+ blank_attr = @opts[:blank_attr] || {}
381
+ os.send(@opts[:blank_position] == :after ? :push : :unshift, yield([prompt, '', false, blank_attr]))
382
+ end
383
+
384
+ os
385
+ end
386
+
387
+ # Iterate over the given options, yielding the option text, value, whether it is selected, and any attributes.
388
+ # The block should return an appropriate tag object.
389
+ def process_select_options(os)
390
+ if os
391
+ vm = @opts[:value_method]
392
+ tm = @opts[:text_method]
393
+ sel = @opts[:selected] || @attr.delete(:value)
394
+
395
+ if @opts[:multiple]
396
+ sel = Array(sel)
397
+ cmp = lambda{|v| sel.include?(v)}
398
+ else
399
+ cmp = lambda{|v| v == sel}
400
+ end
401
+
402
+ os.map do |x|
403
+ attr = {}
404
+ if tm
405
+ text = x.send(tm)
406
+ val = x.send(vm) if vm
407
+ elsif x.is_a?(Array)
408
+ text = x.first
409
+ val = x.last
410
+
411
+ if val.is_a?(Hash)
412
+ value = val[:value]
413
+ attr.merge!(val)
414
+ val = value
415
+ end
416
+ else
417
+ text = x
418
+ end
419
+
420
+ yield [text, val, val ? cmp.call(val) : cmp.call(text), attr]
421
+ end
422
+ end
423
+ end
424
+
425
+ # Create a +Tag+ instance related to the receiver's +form+ with the given
426
+ # arguments.
427
+ def tag(type, attr=@attr, children=nil)
428
+ form._tag(type, attr, children)
429
+ end
430
+
431
+ # Wrap the tag for the given transformer type.
432
+ def wrap(type, tag)
433
+ Forme.transform(type, @opts, input.form_opts, tag, input)
434
+ end
435
+
436
+ # Wrap the tag with the form's +wrapper+.
437
+ def wrap_tag(tag)
438
+ wrap(:wrapper, tag)
439
+ end
440
+
441
+ # Wrap the tag with the form's +error_handler+.
442
+ def wrap_tag_with_error(tag)
443
+ wrap(:error_handler, tag)
444
+ end
445
+
446
+ # Wrap the tag with the form's +labeler+.
447
+ def wrap_tag_with_label(tag)
448
+ wrap(:labeler, tag)
449
+ end
450
+ end
451
+
452
+ # Formatter that disables all input fields,
453
+ #
454
+ # Registered as :disabled.
455
+ class Formatter::Disabled < Formatter
456
+ Forme.register_transformer(:formatter, :disabled, self)
457
+
458
+ private
459
+
460
+ # Unless the :disabled option is specifically set
461
+ # to +false+, set the :disabled attribute on the
462
+ # resulting tag.
463
+ def normalize_options
464
+ if @opts[:disabled] == false
465
+ super
466
+ else
467
+ super
468
+ @attr[:disabled] = :disabled
469
+ end
470
+ end
471
+ end
472
+
473
+ # Formatter that uses span tags with text for most input types,
474
+ # and disables radio/checkbox inputs.
475
+ #
476
+ # Registered as :readonly.
477
+ class Formatter::ReadOnly < Formatter
478
+ Forme.register_transformer(:formatter, :readonly, self)
479
+
480
+ private
481
+
482
+ # Disabled checkbox inputs.
483
+ def format_checkbox
484
+ @attr[:disabled] = :disabled
485
+ super
486
+ end
487
+
488
+ # Use a span with text instead of an input field.
489
+ def _format_input(type)
490
+ tag(:span, {}, @attr[:value])
491
+ end
492
+
493
+ # Disabled radio button inputs.
494
+ def format_radio
495
+ @attr[:disabled] = :disabled
496
+ super
497
+ end
498
+
499
+ # Use a span with text of the selected values instead of a select box.
500
+ def format_select
501
+ t = super
502
+ children = [t.children.select{|o| o.attr[:selected]}.map(&:children).join(', ')] if t.children
503
+ tag(:span, {}, children)
504
+ end
505
+
506
+ # Use a span with text instead of a text area.
507
+ def format_textarea
508
+ tag(:span, {}, @attr[:value])
509
+ end
510
+ end
511
+ end