forme 1.2.0 → 1.3.0

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