forme 0.9.2 → 0.10.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.
data/Rakefile CHANGED
@@ -25,7 +25,6 @@ end
25
25
  RDOC_DEFAULT_OPTS = ["--line-numbers", "--inline-source", '--title', 'Forme']
26
26
 
27
27
  begin
28
- gem 'rdoc', '= 3.12.2'
29
28
  gem 'hanna-nouveau'
30
29
  RDOC_DEFAULT_OPTS.concat(['-f', 'hanna'])
31
30
  rescue Gem::LoadError
@@ -74,9 +73,11 @@ begin
74
73
 
75
74
  spec_with_cov = lambda do |name, files, d|
76
75
  spec.call(name, files, d)
77
- t = spec.call("#{name}_cov", files, "#{d} with coverage")
78
- t.rcov = true
79
- t.rcov_opts = File.read("spec/rcov.opts").split("\n") if File.file?("spec/rcov.opts")
76
+ desc "#{d} with coverage"
77
+ task "#{name}_cov" do
78
+ ENV['COVERAGE'] = '1'
79
+ Rake::Task[name].invoke
80
+ end
80
81
  end
81
82
 
82
83
  task :default => [:spec]
data/lib/forme.rb CHANGED
@@ -2,82 +2,28 @@ require 'date'
2
2
  require 'bigdecimal'
3
3
  require 'forme/version'
4
4
 
5
- # Forme is designed to make creating HTML forms easier. Flexibility and
6
- # simplicity are primary objectives. The basic usage involves creating
7
- # a <tt>Forme::Form</tt> instance, and calling +input+ and +tag+ methods
8
- # to return html strings for widgets, but it could also be used for
9
- # serializing to other formats, or even as a DSL for a GUI application.
10
- #
11
- # In order to be flexible, Forme stores tags in abstract form until
12
- # output is requested. There are two separate abstract <i>forms</i> that Forme
13
- # uses. One is <tt>Forme::Input</tt>, and the other is <tt>Forme::Tag</tt>.
14
- # <tt>Forme::Input</tt> is a high level abstract form, while <tt>Forme::Tag</tt>
15
- # is a low level abstract form.
16
- #
17
- # The difference between <tt>Forme::Input</tt> and <tt>Forme::Tag</tt>
18
- # is that <tt>Forme::Tag</tt> directly represents the underlying html
19
- # tag, containing a type, optional attributes, and children, while the
20
- # <tt>Forme::Input</tt> is more abstract and attempts to be user friendly.
21
- # For example, these both compile by default to the same select tag:
22
- #
23
- # f.input(:select, :options=>[['foo', 1]])
24
- # # or
25
- # f.tag(:select, {}, [f.tag(:option, {:value=>1}, ['foo'])])
26
- #
27
- # The processing of high level <tt>Forme::Input</tt>s into raw html
28
- # data is broken down to the following steps (called transformers):
29
- #
30
- # 1. +Formatter+: converts a <tt>Forme::Input</tt> instance into a
31
- # <tt>Forme::Tag</tt> instance (or array of them).
32
- # 2. +ErrorHandler+: If the <tt>Forme::Input</tt> instance has a error,
33
- # takes the formatted tag and marks it as having the error.
34
- # 2. +Labeler+: If the <tt>Forme::Input</tt> instance has a label,
35
- # takes the formatted output and labels it.
36
- # 3. +Wrapper+: Takes the output of the labeler (or formatter if
37
- # no label), and wraps it in another tag (or just returns it
38
- # directly).
39
- # 4. +Serializer+: converts a <tt>Forme::Tag</tt> instance into a
40
- # string.
41
- #
42
- # Technically, only the +Serializer+ is necessary. The +input+
43
- # and +tag+ methods return +Input+ and +Tag+ objects. These objects
44
- # both have +to_s+ defined to call the appropriate +Serializer+ with
45
- # themselves. The +Serializer+ calls the appropriate +Formatter+ if
46
- # it encounters an +Input+ instance, and attempts to serialize the
47
- # output of that (which is usually a +Tag+ instance). It is up to
48
- # the +Formatter+ to call the +Labeler+ and/or +ErrorHandler+ (if
49
- # necessary) and the +Wrapper+.
50
- #
51
- # There is also an +InputsWrapper+ transformer, that is called by
52
- # <tt>Forme::Form#inputs</tt>. It's used to wrap up a group of
53
- # related options (in a fieldset by default).
54
- #
55
- # The <tt>Forme::Form</tt> object takes the 6 transformers as options (:formatter,
56
- # :labeler, :error_handler, :wrapper, :inputs_wrapper, and :serializer), all of which
57
- # should be objects responding to +call+ (so you can use +Proc+s) or be symbols
58
- # registered with the library using <tt>Forme.register_transformer</tt>:
59
- #
60
- # Forme.register_transformer(:wrapper, :p){|t| t.tag(:p, {}, t)}
61
- #
62
- # Most of the transformers can be overridden on a per instance basis by
63
- # passing the appopriate option to +input+ or +inputs+:
64
- #
65
- # f.input(:name, :wrapper=>:p)
66
5
  module Forme
67
6
  # Exception class for exceptions raised by Forme.
68
7
  class Error < StandardError
69
8
  end
70
-
9
+
10
+ @default_add_blank_prompt = nil
71
11
  @default_config = :default
72
12
  class << self
73
13
  # Set the default configuration to use if none is explicitly
74
14
  # specified (default: :default).
75
15
  attr_accessor :default_config
16
+
17
+ # The default prompt to use for the :add_blank option (default: nil).
18
+ attr_accessor :default_add_blank_prompt
76
19
  end
77
20
 
78
21
  # Array of all supported transformer types.
79
22
  TRANSFORMER_TYPES = [:formatter, :serializer, :wrapper, :error_handler, :labeler, :inputs_wrapper]
80
23
 
24
+ # Transformer symbols shared by wrapper and inputs_wrapper
25
+ SHARED_WRAPPERS = [:tr, :table, :ol, :fieldset_ol]
26
+
81
27
  # Hash storing all configurations. Configurations are groups of related transformers,
82
28
  # so that you can specify a single :config option when creating a +Form+ and have
83
29
  # all of the transformers set from that.
@@ -131,66 +77,77 @@ module Forme
131
77
  classes.compact.join(' ')
132
78
  end
133
79
 
80
+ # If there is a related transformer, call it with the given +args+ and +block+.
81
+ # Otherwise, attempt to return the initial input without modifying it.
82
+ def self.transform(type, trans_name, default_opts, *args, &block)
83
+ if trans = transformer(type, trans_name, default_opts)
84
+ trans.call(*args, &block)
85
+ else
86
+ case type
87
+ when :inputs_wrapper
88
+ yield
89
+ when :labeler, :error_handler, :wrapper
90
+ args.first
91
+ else
92
+ raise Error, "No matching #{type}: #{trans_name.inspect}"
93
+ end
94
+ end
95
+ end
96
+
97
+ # Get the related transformer for the given transformer type. Output depends on the type
98
+ # of +trans+:
99
+ # +Symbol+ :: Assume a request for a registered transformer, so look it up in the +TRANSFORRMERS+ hash.
100
+ # +Hash+ :: If +type+ is also a key in +trans+, return the related value from +trans+, unless the related
101
+ # value is +nil+, in which case, return +nil+. If +type+ is not a key in +trans+, use the
102
+ # default transformer for the receiver.
103
+ # +nil+ :: Assume the default transformer for this receiver.
104
+ # otherwise :: return +trans+ directly if it responds to +call+, and raise an +Error+ if not.
105
+ def self.transformer(type, trans, default_opts)
106
+ case trans
107
+ when Symbol
108
+ TRANSFORMERS[type][trans] || raise(Error, "invalid #{type}: #{trans.inspect} (valid #{type}s: #{TRANSFORMERS[type].keys.map{|k| k.inspect}.join(', ')})")
109
+ when Hash
110
+ if trans.has_key?(type)
111
+ if v = trans[type]
112
+ transformer(type, v, default_opts)
113
+ end
114
+ else
115
+ transformer(type, nil, default_opts)
116
+ end
117
+ when nil
118
+ transformer(type, default_opts[type], nil) if default_opts
119
+ else
120
+ if trans.respond_to?(:call)
121
+ trans
122
+ else
123
+ raise Error, "#{type} #{trans.inspect} must respond to #call"
124
+ end
125
+ end
126
+ end
127
+
134
128
  # The +Form+ class is the main entry point to the library.
135
129
  # Using the +form+, +input+, +tag+, and +inputs+ methods, one can easily build
136
130
  # an abstract syntax tree of +Tag+ and +Input+ instances, which can be serialized
137
131
  # to a string using +to_s+.
138
132
  class Form
139
- # The object related to the receiver, if any. If the +Form+ has an associated
140
- # obj, then calls to +input+ are assumed to be accessing fields of the object
141
- # instead to directly representing input types.
142
- attr_reader :obj
143
-
144
- # A hash of options for the receiver. Currently, the following are recognized by
145
- # default:
146
- # :obj :: Sets the +obj+ attribute
147
- # :error_handler :: Sets the +error_handler+ for the form
148
- # :formatter :: Sets the +formatter+ for the form
149
- # :hidden_tags :: Sets the hidden tags to automatically add to this form.
150
- # :input_defaults :: Sets the default options for each input type
151
- # :inputs_wrapper :: Sets the +inputs_wrapper+ for the form
152
- # :labeler :: Sets the +labeler+ for the form
153
- # :wrapper :: Sets the +wrapper+ for the form
154
- # :serializer :: Sets the +serializer+ for the form
133
+ # A hash of options for the form.
155
134
  attr_reader :opts
156
135
 
157
- # The +formatter+ determines how the +Input+s created are transformed into
158
- # +Tag+ objects. Must respond to +call+ or be a registered symbol.
159
- attr_reader :formatter
160
-
161
- # The +error_handler+ determines how to to mark tags as containing errors.
162
- # Must respond to +call+ or be a registered symbol.
163
- attr_reader :error_handler
164
-
165
- # The +labeler+ determines how to label tags. Must respond to +call+ or be
166
- # a registered symbol.
167
- attr_reader :labeler
168
-
169
- # The +wrapper+ determines how (potentially labeled) tags are wrapped. Must
170
- # respond to +call+ or be a registered symbol.
171
- attr_reader :wrapper
172
-
173
136
  # Set the default options for inputs by type. This should be a hash with
174
- # input type string keys and values that are hashes of input options.
137
+ # input type keys and values that are hashes of input options.
175
138
  attr_reader :input_defaults
176
139
 
177
- # The +inputs_wrapper+ determines how calls to +inputs+ are wrapped. Must
178
- # respond to +call+ or be a registered symbol.
179
- attr_reader :inputs_wrapper
140
+ # The hidden tags to automatically add to the form.
141
+ attr_reader :hidden_tags
142
+
143
+ # The namespaces if any for the receiver's inputs. This can be used to
144
+ # automatically setup namespaced class and id attributes.
145
+ attr_accessor :namespaces
180
146
 
181
147
  # The +serializer+ determines how +Tag+ objects are transformed into strings.
182
148
  # Must respond to +call+ or be a registered symbol.
183
149
  attr_reader :serializer
184
150
 
185
- # The hidden tags to automatically add to the form. If set, this should be an
186
- # array, where elements are one of the following types:
187
- # String, Array, Forme::Tag :: Added directly as a child of the form tag.
188
- # Hash :: Adds a hidden tag for each entry, with keys as the name of the hidden
189
- # tag and values as the value of the hidden tag.
190
- # Proc :: Will be called with the form tag object, and should return an instance
191
- # of one of the handled types (or nil to not add a tag).
192
- attr_reader :hidden_tags
193
-
194
151
  # Create a +Form+ instance and yield it to the block,
195
152
  # injecting the opening form tag before yielding and
196
153
  # the closing form tag after yielding.
@@ -233,72 +190,31 @@ module Forme
233
190
  # Creates a +Form+ object. Arguments:
234
191
  # obj :: Sets the obj for the form. If a hash, is merged with the +opts+ argument
235
192
  # to set the opts.
236
- # opts :: A hash of options for the form, see +opts+ attribute for details on
237
- # available options.
193
+ # opts :: A hash of options for the form
238
194
  def initialize(obj=nil, opts={})
239
- if obj.is_a?(Hash)
240
- @opts = obj.merge(opts)
241
- @obj = @opts.delete(:obj)
242
- else
243
- @obj = obj
244
- @opts = opts
245
- end
246
- if @obj && @obj.respond_to?(:forme_config)
247
- @obj.forme_config(self)
248
- end
249
- config = CONFIGURATIONS[@opts[:config]||Forme.default_config]
250
- TRANSFORMER_TYPES.each{|k| instance_variable_set(:"@#{k}", transformer(k, @opts.fetch(k, config[k])))}
251
- @input_defaults = @opts[:input_defaults] || {}
252
- @hidden_tags = @opts[:hidden_tags]
253
- @nesting = []
254
- end
195
+ @opts = opts.merge(obj.is_a?(Hash) ? obj : {:obj=>obj})
196
+ @opts[:namespace] = Array(@opts[:namespace])
255
197
 
256
- # If there is a related transformer, call it with the given +args+ and +block+.
257
- # Otherwise, attempt to return the initial input without modifying it.
258
- def transform(type, trans_name, *args, &block)
259
- if trans = transformer(type, trans_name)
260
- trans.call(*args, &block)
261
- else
262
- case type
263
- when :inputs_wrapper
264
- yield
265
- when :labeler, :error_handler, :wrapper
266
- args.first
267
- else
268
- raise Error, "No matching #{type}: #{trans_name.inspect}"
269
- end
198
+ if obj && obj.respond_to?(:forme_config)
199
+ obj.forme_config(self)
270
200
  end
271
- end
272
201
 
273
- # Get the related transformer for the given transformer type. Output depends on the type
274
- # of +trans+:
275
- # +Symbol+ :: Assume a request for a registered transformer, so look it up in the +TRANSFORRMERS+ hash.
276
- # +Hash+ :: If +type+ is also a key in +trans+, return the related value from +trans+, unless the related
277
- # value is +nil+, in which case, return +nil+. If +type+ is not a key in +trans+, use the
278
- # default transformer for the receiver.
279
- # +nil+ :: Assume the default transformer for this receiver.
280
- # otherwise :: return +trans+ directly if it responds to +call+, and raise an +Error+ if not.
281
- def transformer(type, trans)
282
- case trans
283
- when Symbol
284
- TRANSFORMERS[type][trans] || raise(Error, "invalid #{type}: #{trans.inspect} (valid #{type}s: #{TRANSFORMERS[type].keys.map{|k| k.inspect}.join(', ')})")
285
- when Hash
286
- if trans.has_key?(type)
287
- if v = trans[type]
288
- transformer(type, v)
289
- end
290
- else
291
- transformer(type, nil)
292
- end
293
- when nil
294
- send(type)
295
- else
296
- if trans.respond_to?(:call)
297
- trans
298
- else
299
- raise Error, "#{type} #{trans.inspect} must respond to #call"
202
+ config = CONFIGURATIONS[@opts[:config]||Forme.default_config]
203
+ copy_inputs_wrapper_from_wrapper(@opts)
204
+
205
+ TRANSFORMER_TYPES.each do |t|
206
+ case @opts[t]
207
+ when Symbol
208
+ @opts[t] = Forme.transformer(t, @opts[t], @opts)
209
+ when nil
210
+ @opts[t] = Forme.transformer(t, config, @opts)
300
211
  end
301
212
  end
213
+
214
+ @serializer = @opts[:serializer]
215
+ @input_defaults = @opts[:input_defaults] || {}
216
+ @hidden_tags = @opts[:hidden_tags]
217
+ @nesting = []
302
218
  end
303
219
 
304
220
  # Create a form tag with the given attributes.
@@ -306,11 +222,6 @@ module Forme
306
222
  tag(:form, attr, method(:hidden_form_tags), &block)
307
223
  end
308
224
 
309
- # Formats the +input+ using the +formatter+.
310
- def format(input)
311
- transform(:formatter, input.opts, input)
312
- end
313
-
314
225
  # Empty method designed to ease integration with other libraries where
315
226
  # Forme is used in template code and some output implicitly
316
227
  # created by Forme needs to be injected into the template output.
@@ -342,15 +253,19 @@ module Forme
342
253
  obj.forme_input(self, field, opts.dup)
343
254
  else
344
255
  opts = opts.dup
345
- opts[:name] = field unless opts.has_key?(:name)
346
- opts[:id] = field unless opts.has_key?(:id)
347
- opts[:value] = obj.send(field) unless opts.has_key?(:value)
256
+ opts[:key] = field unless opts.has_key?(:key)
257
+ unless opts.has_key?(:value)
258
+ opts[:value] = if obj.is_a?(Hash)
259
+ obj[field]
260
+ else
261
+ obj.send(field)
262
+ end
263
+ end
348
264
  _input(:text, opts)
349
265
  end
350
266
  else
351
267
  _input(field, opts)
352
268
  end
353
- use_serializer(input) if input.is_a?(Array)
354
269
  self << input
355
270
  input
356
271
  end
@@ -362,7 +277,7 @@ module Forme
362
277
  end
363
278
 
364
279
  # Creates a tag using the +inputs_wrapper+ (a fieldset by default), calls
365
- # input on each element of +inputs+, and yields to if given a block.
280
+ # input on each element of +inputs+, and yields if given a block.
366
281
  # You can use array arguments if you want inputs to be created with specific
367
282
  # options:
368
283
  #
@@ -372,22 +287,47 @@ module Forme
372
287
  # The given +opts+ are passed to the +inputs_wrapper+, and the default
373
288
  # +inputs_wrapper+ supports a <tt>:legend</tt> option that is used to
374
289
  # set the legend for the fieldset.
375
- def inputs(*a, &block)
376
- _inputs(*a, &block)
290
+ #
291
+ # +opts+ can also include transformer options itself (e.g. :wrapper), which
292
+ # override the form's current transformer options for the duration of the block.
293
+ # The exception is the :inputs_wrapper transformer option, which affects the
294
+ # wrapper to use for this inputs call. You can use the :nested_inputs_wrapper
295
+ # option to set the default :inputs_wrapper option for the duration of the block.
296
+ #
297
+ # This can also be called with a single hash argument to just use an options hash:
298
+ #
299
+ # inputs(:legend=>'Foo'){...}
300
+ #
301
+ # or even without any arguments:
302
+ #
303
+ # inputs{...}
304
+ def inputs(inputs=[], opts={}, &block)
305
+ _inputs(inputs, opts, &block)
377
306
  end
378
307
 
379
308
  # Internals of #inputs, should be used internally by the library, where #inputs
380
- # is designed for external use.
381
- def _inputs(inputs=[], opts={})
309
+ # is designed for external use.
310
+ def _inputs(inputs=[], opts={}) # :nodoc:
382
311
  if inputs.is_a?(Hash)
383
312
  opts = inputs.merge(opts)
384
313
  inputs = []
385
314
  end
386
- transform(:inputs_wrapper, opts, self, opts) do
387
- inputs.each do |i|
388
- emit(input(*i))
315
+
316
+ form_opts = {}
317
+ form_opts[:inputs_wrapper] = opts[:nested_inputs_wrapper] if opts[:nested_inputs_wrapper]
318
+ TRANSFORMER_TYPES.each do |t|
319
+ if opts.has_key?(t) && t != :inputs_wrapper
320
+ form_opts[t] = opts[t]
321
+ end
322
+ end
323
+
324
+ Forme.transform(:inputs_wrapper, opts, @opts, self, opts) do
325
+ with_opts(form_opts) do
326
+ inputs.each do |i|
327
+ emit(input(*i))
328
+ end
329
+ yield if block_given?
389
330
  end
390
- yield if block_given?
391
331
  end
392
332
  end
393
333
 
@@ -409,6 +349,18 @@ module Forme
409
349
  tag = Tag.new(self, *a, &block)
410
350
  end
411
351
 
352
+ # The object associated with this form, if any. If the +Form+ has an associated
353
+ # obj, then calls to +input+ are assumed to be accessing fields of the object
354
+ # instead to directly representing input types.
355
+ def obj
356
+ @opts[:obj]
357
+ end
358
+
359
+ # The current namespaces for the form, if any.
360
+ def namespaces
361
+ @opts[:namespace]
362
+ end
363
+
412
364
  # Creates a +Tag+ associated to the receiver with the given arguments.
413
365
  # Add the tag to the the list of children for the currently open tag.
414
366
  # If a block is given, make this tag the currently open tag while inside
@@ -420,7 +372,8 @@ module Forme
420
372
  tag
421
373
  end
422
374
 
423
- def tag_(*a, &block)
375
+ # Aliased for tag. Workaround for issue with rails plugin.
376
+ def tag_(*a, &block) # :nodoc:
424
377
  tag(*a, &block)
425
378
  end
426
379
 
@@ -440,13 +393,46 @@ module Forme
440
393
  end
441
394
  end
442
395
 
443
- # Serializes the +tag+ using the +serializer+.
444
- def serialize(tag)
445
- serializer.call(tag)
396
+ # Calls the block for each object in objs, using with_obj with the given namespace
397
+ # and an index namespace (starting at 0).
398
+ def each_obj(objs, namespace=nil)
399
+ objs.each_with_index do |obj, i|
400
+ with_obj(obj, Array(namespace) + [i]) do
401
+ yield obj, i
402
+ end
403
+ end
404
+ end
405
+
406
+ # Temporarily override the given object and namespace for the form. Any given
407
+ # namespaces are appended to the form's current namespace.
408
+ def with_obj(obj, namespace=nil)
409
+ with_opts(:obj=>obj, :namespace=>@opts[:namespace]+Array(namespace)) do
410
+ yield obj
411
+ end
412
+ end
413
+
414
+ # Temporarily override the opts for the form for the duration of the block.
415
+ # This merges the given opts with the form's current opts, restoring
416
+ # the previous opts before returning.
417
+ def with_opts(opts)
418
+ orig_opts = @opts
419
+ @opts = orig_opts.merge(opts)
420
+ copy_inputs_wrapper_from_wrapper(opts, @opts)
421
+ yield
422
+ ensure
423
+ @opts = orig_opts if orig_opts
446
424
  end
447
425
 
448
426
  private
449
427
 
428
+ # Copy the :wrapper option to :inputs_wrapper in output_opts if only :wrapper
429
+ # is present in input_opts and the :wrapper option value is a shared wrapper.
430
+ def copy_inputs_wrapper_from_wrapper(input_opts, output_opts=input_opts)
431
+ if input_opts[:wrapper] && !input_opts[:inputs_wrapper] && SHARED_WRAPPERS.include?(input_opts[:wrapper])
432
+ output_opts[:inputs_wrapper] = output_opts[:wrapper]
433
+ end
434
+ end
435
+
450
436
  # Return array of hidden tags to use for this form,
451
437
  # or nil if the form does not have hidden tags added automatically.
452
438
  def hidden_form_tags(form_tag)
@@ -476,15 +462,6 @@ module Forme
476
462
  end
477
463
  end
478
464
 
479
- # Extend +obj+ with +Serialized+ and associate it with the receiver, such
480
- # that calling +to_s+ on the object will use the receiver's serializer
481
- # to generate the resulting string.
482
- def use_serializer(obj)
483
- obj.extend(Serialized)
484
- obj._form = self
485
- obj
486
- end
487
-
488
465
  # Make the given tag the currently open tag, and yield. After the
489
466
  # block returns, make the previously open tag the currently open
490
467
  # tag.
@@ -505,35 +482,18 @@ module Forme
505
482
  # The type of input, should be a symbol (e.g. :submit, :text, :select).
506
483
  attr_reader :type
507
484
 
508
- # The options hash for the receiver. Here are some of the supported options
509
- # used by the built-in formatter transformers:
510
- #
511
- # :error :: Set an error message, invoking the error_handler
512
- # :label :: Set a label, invoking the labeler
513
- # :wrapper :: Set a custom wrapper, overriding the form's default
514
- # :labeler :: Set a custom labeler, overriding the form's default
515
- # :error_handler :: Set a custom error_handler, overriding the form's default
516
- # :attr :: The attributes hash to use for the given tag, takes precedence over
517
- # other options that set attributes.
518
- # :data :: A hash of data-* attributes for the resulting tag. Keys in this hash
519
- # will have attributes created with data- prepended to the attribute name.
520
- # :name :: The name attribute to use
521
- # :id :: The id attribute to use
522
- # :placeholder :: The placeholder attribute to use
523
- # :value :: The value attribute to use for input tags, the content of the textarea
524
- # for textarea tags, or the selected option(s) for select tags.
525
- # :class :: A class to use. Unlike other options, this is combined with the
526
- # classes set in the :attr hash.
527
- # :disabled :: Set the disabled attribute if true
528
- # :required :: Set the required attribute if true
529
- #
530
- # For other supported options, see the private methods in +Formatter+.
485
+ # The options hash for the Input.
531
486
  attr_reader :opts
532
487
 
488
+ # The options hash in use by the form at the time of the Input's instantiation.
489
+ attr_reader :form_opts
490
+
533
491
  # Set the +form+, +type+, and +opts+.
534
492
  def initialize(form, type, opts={})
535
493
  @form, @type = form, type
536
- @opts = (form.input_defaults[type.to_s] || {}).merge(opts)
494
+ defaults = form.input_defaults
495
+ @opts = (defaults.fetch(type){defaults[type.to_s]} || {}).merge(opts)
496
+ @form_opts = form.opts
537
497
  end
538
498
 
539
499
  # Replace the +opts+ by merging the given +hash+ into +opts+,
@@ -550,13 +510,13 @@ module Forme
550
510
 
551
511
  # Return a string containing the serialized content of the receiver.
552
512
  def to_s
553
- form.serialize(self)
513
+ Forme.transform(:serializer, @opts, @form_opts, self)
554
514
  end
555
515
 
556
516
  # Transform the receiver into a lower level +Tag+ form (or an array
557
517
  # of them).
558
518
  def format
559
- form.format(self)
519
+ Forme.transform(:formatter, @opts, @form_opts, self)
560
520
  end
561
521
  end
562
522
 
@@ -599,7 +559,7 @@ module Forme
599
559
 
600
560
  # Return a string containing the serialized content of the receiver.
601
561
  def to_s
602
- form.serialize(self)
562
+ Forme.transform(:serializer, @opts, @form.opts, self)
603
563
  end
604
564
 
605
565
  private
@@ -619,19 +579,6 @@ module Forme
619
579
  end
620
580
  end
621
581
 
622
- # Module that can extend objects associating them with a specific
623
- # +Form+ instance. Calling +to_s+ on the object will then use the
624
- # form's serializer to return a string.
625
- module Serialized
626
- # The +Form+ instance related to the receiver.
627
- attr_accessor :_form
628
-
629
- # Return a string containing the serialized content of the receiver.
630
- def to_s
631
- _form.serialize(self)
632
- end
633
- end
634
-
635
582
  # Empty module for marking objects as "raw", where they will no longer
636
583
  # html escaped by the default serializer.
637
584
  module Raw
@@ -654,6 +601,11 @@ module Forme
654
601
  # the :attr option version takes precedence.
655
602
  ATTRIBUTE_OPTIONS = [:name, :id, :placeholder, :value, :style]
656
603
 
604
+ # Options copied from the options hash into the attributes hash,
605
+ # where a true value in the options hash sets the attribute
606
+ # value to the same name as the key.
607
+ ATTRIBUTE_BOOLEAN_OPTIONS = [:autofocus, :required, :disabled]
608
+
657
609
  # Create a new instance and call it
658
610
  def self.call(input)
659
611
  new.call(input)
@@ -716,10 +668,7 @@ module Forme
716
668
  # same name that comes before this checkbox. That way, if the checkbox
717
669
  # is checked, the web app will generally see the value of the checkbox, and
718
670
  # if it is not checked, the web app will generally see the value of the hidden
719
- # input tag. Recognizes the following options:
720
- # :checked :: checkbox is set to checked if so.
721
- # :hidden_value :: sets the value of the hidden input tag.
722
- # :no_hidden :: don't create a hidden input tag
671
+ # input tag.
723
672
  def format_checkbox
724
673
  @attr[:type] = :checkbox
725
674
  @attr[:checked] = :checked if @opts[:checked]
@@ -791,69 +740,76 @@ module Forme
791
740
  end
792
741
 
793
742
  # Takes a select input and turns it into a select tag with (possibly) option
794
- # children tags. Respects the following options:
795
- # :options :: an array of options. Processes each entry. If that entry is
796
- # an array, takes the first entry in the hash as the text child
797
- # of the option, and the last entry as the value of the option.
798
- # if not set, ignores the remaining options.
799
- # :add_blank :: Add a blank option if true. If the value is a string,
800
- # use it as the text content of the blank option. The value of
801
- # the blank option is always the empty string.
802
- # :text_method :: If set, each entry in the array has this option called on
803
- # it to get the text of the object.
804
- # :value_method :: If set (and :text_method is set), each entry in the array
805
- # has this method called on it to get the value of the option.
806
- # :selected :: The value that should be selected. Any options that are equal to
807
- # this value (or included in this value if a multiple select box),
808
- # are set to selected.
809
- # :multiple :: Creates a multiple select box.
810
- # :value :: Same as :selected, but has lower priority.
743
+ # children tags.
811
744
  def format_select
812
- if os = @opts[:options]
813
- vm = @opts[:value_method]
814
- tm = @opts[:text_method]
815
- sel = @opts[:selected] || @attr.delete(:value)
816
- if @opts[:multiple]
817
- @attr[:multiple] = :multiple
818
- sel = Array(sel)
819
- cmp = lambda{|v| sel.include?(v)}
820
- else
821
- cmp = lambda{|v| v == sel}
822
- end
823
- os = os.map do |x|
824
- attr = {}
825
- if tm
826
- text = x.send(tm)
827
- if vm
828
- val = x.send(vm)
829
- attr[:value] = val
830
- attr[:selected] = :selected if cmp.call(val)
831
- else
832
- attr[:selected] = :selected if cmp.call(text)
833
- end
834
- form._tag(:option, attr, [text])
835
- elsif x.is_a?(Array)
836
- val = x.last
837
- if val.is_a?(Hash)
838
- attr.merge!(val)
839
- val = attr[:value]
840
- else
841
- attr[:value] = val
842
- end
843
- attr[:selected] = :selected if attr.has_key?(:value) && cmp.call(val)
844
- tag(:option, attr, [x.first])
845
- else
846
- attr[:selected] = :selected if cmp.call(x)
847
- tag(:option, attr, [x])
745
+ if os = process_select_options(@opts[:options])
746
+ @attr[:multiple] = :multiple if @opts[:multiple]
747
+
748
+ os = os.map do |label, value, sel, attrs|
749
+ if value || sel
750
+ attrs = attrs.dup
751
+ attrs[:value] = value if value
752
+ attrs[:selected] = :selected if sel
848
753
  end
849
- end
850
- if prompt = @opts[:add_blank]
851
- os.unshift(tag(:option, {:value=>''}, prompt.is_a?(String) ? [prompt] : []))
754
+ tag(:option, attrs, [label])
852
755
  end
853
756
  end
854
757
  tag(:select, @attr, os)
855
758
  end
856
759
 
760
+ def format_checkboxset
761
+ @opts[:multiple] = true unless @opts.has_key?(:multiple)
762
+ _format_set(:checkbox, :no_hidden=>true, :multiple=>true)
763
+ end
764
+
765
+ def format_radioset
766
+ _format_set(:radio)
767
+ end
768
+
769
+ def _format_set(type, tag_attrs={})
770
+ raise Error, "can't have radioset with no options" unless os = @opts[:options]
771
+ key = @opts[:key]
772
+ name = @opts[:name]
773
+ id = @opts[:id]
774
+ if @opts[:error]
775
+ @opts[:set_error] = @opts.delete(:error)
776
+ end
777
+ if @opts[:label]
778
+ @opts[:set_label] = @opts.delete(:label)
779
+ end
780
+ tag_wrapper = @opts.delete(:tag_wrapper) || :default
781
+ wrapper = Forme.transformer(:wrapper, @opts, @input.form_opts)
782
+
783
+ tags = process_select_options(os).map do |label, value, sel, attrs|
784
+ value ||= label
785
+ r_opts = attrs.merge(tag_attrs).merge(:label=>label||value, :label_attr=>{:class=>:option}, :wrapper=>tag_wrapper)
786
+ r_opts[:value] ||= value if value
787
+ r_opts[:checked] ||= :checked if sel
788
+
789
+ if name
790
+ r_opts[:name] ||= name
791
+ end
792
+ if id
793
+ r_opts[:id] ||= "#{id}_#{value}"
794
+ end
795
+ if key
796
+ r_opts[:key] ||= key
797
+ r_opts[:key_id] ||= value
798
+ end
799
+
800
+ form._input(type, r_opts)
801
+ end
802
+
803
+ if (last_input = tags.last) && last_input.is_a?(Input)
804
+ last_input.opts[:error] = @opts[:set_error]
805
+ else
806
+ tags << form._tag(:span, {:class=>'error_message'}, [@opts[:set_error]])
807
+ end
808
+ tags.unshift(form._tag(:span, {:class=>:label}, @opts[:set_label])) if @opts[:set_label]
809
+ wrapper.call(tags, form._input(type, opts)) if wrapper
810
+ tags
811
+ end
812
+
857
813
  # Formats a textarea. Respects the following options:
858
814
  # :value :: Sets value as the child of the textarea.
859
815
  def format_textarea
@@ -865,19 +821,31 @@ module Forme
865
821
  end
866
822
  end
867
823
 
868
- def copy_options_to_attributes(attributes)
869
- attributes.each do |k|
824
+ # Copy option values for given keys to the attributes unless the
825
+ # attributes already have a value for the key.
826
+ def copy_options_to_attributes(keys)
827
+ keys.each do |k|
870
828
  if @opts.has_key?(k) && !@attr.has_key?(k)
871
829
  @attr[k] = @opts[k]
872
830
  end
873
831
  end
874
832
  end
875
833
 
876
- # Normalize the options used for all input types. Handles:
877
- # :required :: Sets the +required+ attribute on the resulting tag if true.
878
- # :disabled :: Sets the +disabled+ attribute on the resulting tag if true.
834
+ # Set attribute values for given keys to be the same as the key
835
+ # unless the attributes already have a value for the key.
836
+ def copy_boolean_options_to_attributes(keys)
837
+ keys.each do |k|
838
+ if @opts[k] && !@attr.has_key?(k)
839
+ @attr[k] = k
840
+ end
841
+ end
842
+ end
843
+
844
+ # Normalize the options used for all input types.
879
845
  def normalize_options
880
846
  copy_options_to_attributes(ATTRIBUTE_OPTIONS)
847
+ copy_boolean_options_to_attributes(ATTRIBUTE_BOOLEAN_OPTIONS)
848
+ handle_key_option
881
849
 
882
850
  Forme.attr_classes(@attr, @opts[:class]) if @opts.has_key?(:class)
883
851
  Forme.attr_classes(@attr, 'error') if @opts[:error]
@@ -888,9 +856,110 @@ module Forme
888
856
  @attr[sym] = v unless @attr.has_key?(sym)
889
857
  end
890
858
  end
859
+ end
891
860
 
892
- @attr[:required] = :required if @opts[:required] && !@attr.has_key?(:required)
893
- @attr[:disabled] = :disabled if @opts[:disabled] && !@attr.has_key?(:disabled)
861
+ # Have the :key option possibly set the name, id, and/or value attributes if not already set.
862
+ def handle_key_option
863
+ if key = @opts[:key]
864
+ unless @attr[:name] || @attr['name']
865
+ @attr[:name] = namespaced_name(key, @opts[:array] || @opts[:multiple])
866
+ if !@attr.has_key?(:value) && !@attr.has_key?('value') && (values = @form.opts[:values])
867
+ set_value_from_namespaced_values(namespaces, values, key)
868
+ end
869
+ end
870
+ unless @attr[:id] || @attr['id']
871
+ id = namespaced_id(key)
872
+ if suffix = @opts[:key_id]
873
+ id << '_' << suffix.to_s
874
+ end
875
+ @attr[:id] = id
876
+ end
877
+ end
878
+ end
879
+
880
+ # Array of namespaces to use for the input
881
+ def namespaces
882
+ input.form_opts[:namespace]
883
+ end
884
+
885
+ # Return a unique id attribute for the +field+, based on the current namespaces.
886
+ def namespaced_id(field)
887
+ "#{namespaces.join('_')}#{'_' unless namespaces.empty?}#{field}"
888
+ end
889
+
890
+ # Return a unique name attribute for the +field+, based on the current namespaces.
891
+ # If +multiple+ is true, end the name with [] so that param parsing will treat
892
+ # the name as part of an array.
893
+ def namespaced_name(field, multiple=false)
894
+ if namespaces.empty?
895
+ if multiple
896
+ "#{field}[]"
897
+ else
898
+ field
899
+ end
900
+ else
901
+ root, *nsps = namespaces
902
+ "#{root}#{nsps.map{|n| "[#{n}]"}.join}[#{field}]#{'[]' if multiple}"
903
+ end
904
+ end
905
+
906
+ # Set the values option based on the (possibly nested) values
907
+ # hash given, array of namespaces, and key.
908
+ def set_value_from_namespaced_values(namespaces, values, key)
909
+ namespaces.each do |ns|
910
+ v = values[ns] || values[ns.to_s]
911
+ return unless v
912
+ values = v
913
+ end
914
+
915
+ @attr[:value] = values.fetch(key){values.fetch(key.to_s){return}}
916
+ end
917
+
918
+ # Returns an array of arrays, where each array entry contains the label, value,
919
+ # currently selected flag, and attributes for that tag.
920
+ def process_select_options(os)
921
+ if os
922
+ vm = @opts[:value_method]
923
+ tm = @opts[:text_method]
924
+ sel = @opts[:selected] || @attr.delete(:value)
925
+
926
+ if @opts[:multiple]
927
+ sel = Array(sel)
928
+ cmp = lambda{|v| sel.include?(v)}
929
+ else
930
+ cmp = lambda{|v| v == sel}
931
+ end
932
+
933
+ os = os.map do |x|
934
+ attr = {}
935
+ if tm
936
+ text = x.send(tm)
937
+ val = x.send(vm) if vm
938
+ elsif x.is_a?(Array)
939
+ text = x.first
940
+ val = x.last
941
+
942
+ if val.is_a?(Hash)
943
+ value = val[:value]
944
+ attr.merge!(val)
945
+ val = value
946
+ end
947
+ else
948
+ text = x
949
+ end
950
+
951
+ [text, val, val ? cmp.call(val) : cmp.call(text), attr]
952
+ end
953
+
954
+ if prompt = @opts[:add_blank]
955
+ unless prompt.is_a?(String)
956
+ prompt = Forme.default_add_blank_prompt
957
+ end
958
+ os.unshift([prompt, '', false, {}])
959
+ end
960
+
961
+ os
962
+ end
894
963
  end
895
964
 
896
965
  # Create a +Tag+ instance related to the receiver's +form+ with the given
@@ -901,17 +970,17 @@ module Forme
901
970
 
902
971
  # Wrap the tag with the form's +wrapper+.
903
972
  def wrap_tag(tag)
904
- form.transform(:wrapper, @opts, tag, input)
973
+ Forme.transform(:wrapper, @opts, input.form_opts, tag, input)
905
974
  end
906
975
 
907
976
  # Wrap the tag with the form's +error_handler+.
908
977
  def wrap_tag_with_error(tag)
909
- form.transform(:error_handler, @opts, tag, input)
978
+ Forme.transform(:error_handler, @opts, input.form_opts, tag, input)
910
979
  end
911
980
 
912
981
  # Wrap the tag with the form's +labeler+.
913
982
  def wrap_tag_with_label(tag)
914
- form.transform(:labeler, @opts, tag, input)
983
+ Forme.transform(:labeler, @opts, input.form_opts, tag, input)
915
984
  end
916
985
  end
917
986
 
@@ -1038,14 +1107,24 @@ module Forme
1038
1107
  # :label_for option is used, the label created will not be
1039
1108
  # associated with an input.
1040
1109
  def call(tag, input)
1110
+ unless id = input.opts[:id]
1111
+ if key = input.opts[:key]
1112
+ namespaces = input.form_opts[:namespace]
1113
+ id = "#{namespaces.join('_')}#{'_' unless namespaces.empty?}#{key}"
1114
+ if key_id = input.opts[:key_id]
1115
+ id << "_#{key_id.to_s}"
1116
+ end
1117
+ end
1118
+ end
1041
1119
  if [:radio, :checkbox].include?(input.type)
1042
- t = [tag, input.tag(:label, {:for=>input.opts.fetch(:label_for, input.opts[:id])}.merge(input.opts[:label_attr]||{}), [input.opts[:label]])]
1043
- p = :before
1120
+ t = [tag, input.tag(:label, {:for=>input.opts.fetch(:label_for, id)}.merge(input.opts[:label_attr]||{}), [input.opts[:label]])]
1121
+ pos = :before
1044
1122
  else
1045
- t = [input.tag(:label, {:for=>input.opts.fetch(:label_for, input.opts[:id])}.merge(input.opts[:label_attr]||{}), [input.opts[:label]]), tag]
1046
- p = :after
1123
+ t = [input.tag(:label, {:for=>input.opts.fetch(:label_for, id)}.merge(input.opts[:label_attr]||{}), [input.opts[:label]]), tag]
1124
+ pos = :after
1047
1125
  end
1048
- if input.opts[:label_position] == p
1126
+
1127
+ if input.opts[:label_position] == pos
1049
1128
  t.reverse
1050
1129
  else
1051
1130
  t
@@ -1054,7 +1133,7 @@ module Forme
1054
1133
  end
1055
1134
 
1056
1135
  Forme.register_transformer(:wrapper, :default){|tag, input| tag}
1057
- [:li, :p, :div, :span].each do |x|
1136
+ [:li, :p, :div, :span, :td].each do |x|
1058
1137
  Forme.register_transformer(:wrapper, x){|tag, input| input.tag(x, input.opts[:wrapper_attr], Array(tag))}
1059
1138
  end
1060
1139
  Forme.register_transformer(:wrapper, :trtd) do |tag, input|
@@ -1071,15 +1150,18 @@ module Forme
1071
1150
  end
1072
1151
  input.tag(:tr, input.opts[:wrapper_attr], [input.tag(:td, {}, ltd), input.tag(:td, {}, rtd)])
1073
1152
  end
1153
+ {:tr=>:td, :table=>:trtd, :ol=>:li, :fieldset_ol=>:li}.each do |k, v|
1154
+ Forme.register_transformer(:wrapper, k, TRANSFORMERS[:wrapper][v])
1155
+ end
1074
1156
 
1075
- # Default inputs_wrapper used by the library, uses a fieldset.
1157
+ # Default inputs_wrapper used by the library, uses a <fieldset>.
1076
1158
  #
1077
1159
  # Registered as :default.
1078
1160
  class InputsWrapper
1079
1161
  Forme.register_transformer(:inputs_wrapper, :default, new)
1080
1162
 
1081
- # Wrap the inputs in a fieldset. If the :legend
1082
- # option is given, add a +legend+ tag as the first
1163
+ # Wrap the inputs in a <fieldset>. If the :legend
1164
+ # option is given, add a <legend> tag as the first
1083
1165
  # child of the fieldset.
1084
1166
  def call(form, opts)
1085
1167
  attr = opts[:attr] ? opts[:attr].dup : {}
@@ -1095,51 +1177,74 @@ module Forme
1095
1177
  end
1096
1178
  end
1097
1179
 
1098
- # Use a fieldset and an ol tag to wrap the inputs.
1180
+ # Use a <fieldset> and an <ol> tag to wrap the inputs.
1099
1181
  #
1100
1182
  # Registered as :fieldset_ol.
1101
1183
  class InputsWrapper::FieldSetOL < InputsWrapper
1102
1184
  Forme.register_transformer(:inputs_wrapper, :fieldset_ol, new)
1103
1185
 
1104
- # Wrap the inputs in an ol tag
1186
+ # Wrap the inputs in a <fieldset> and a <ol> tag.
1105
1187
  def call(form, opts)
1106
1188
  super(form, opts){form.tag_(:ol){yield}}
1107
1189
  end
1108
1190
  end
1109
1191
 
1110
- # Use an ol tag to wrap the inputs.
1192
+ # Use an <ol> tag to wrap the inputs.
1111
1193
  #
1112
1194
  # Registered as :ol.
1113
1195
  class InputsWrapper::OL
1114
1196
  Forme.register_transformer(:inputs_wrapper, :ol, new)
1115
1197
 
1116
- # Wrap the inputs in an ol tag
1198
+ # Wrap the inputs in an <ol> tag
1117
1199
  def call(form, opts, &block)
1118
- form.tag(:ol, &block)
1200
+ form.tag(:ol, opts[:attr], &block)
1119
1201
  end
1120
1202
  end
1121
1203
 
1122
- # Use a div tag to wrap the inputs.
1204
+ # Use a <div> tag to wrap the inputs.
1123
1205
  #
1124
1206
  # Registered as :div.
1125
1207
  class InputsWrapper::Div
1126
1208
  Forme.register_transformer(:inputs_wrapper, :div, new)
1127
1209
 
1128
- # Wrap the inputs in an ol tag
1210
+ # Wrap the inputs in an <div> tag
1211
+ def call(form, opts, &block)
1212
+ form.tag(:div, opts[:attr], &block)
1213
+ end
1214
+ end
1215
+
1216
+ # Use a <tr> tag to wrap the inputs.
1217
+ #
1218
+ # Registered as :tr.
1219
+ class InputsWrapper::TR
1220
+ Forme.register_transformer(:inputs_wrapper, :tr, new)
1221
+
1222
+ # Wrap the inputs in an <tr> tag
1129
1223
  def call(form, opts, &block)
1130
- form.tag(:div, &block)
1224
+ form.tag(:tr, opts[:attr], &block)
1131
1225
  end
1132
1226
  end
1133
1227
 
1134
- # Use a table tag to wrap the inputs.
1228
+ # Use a <table> tag to wrap the inputs.
1135
1229
  #
1136
1230
  # Registered as :table.
1137
1231
  class InputsWrapper::Table
1138
1232
  Forme.register_transformer(:inputs_wrapper, :table, new)
1139
1233
 
1140
- # Wrap the inputs in a table tag.
1234
+ # Wrap the inputs in a <table> tag.
1141
1235
  def call(form, opts, &block)
1142
- form.tag(:table, &block)
1236
+ attr = opts[:attr] ? opts[:attr].dup : {}
1237
+ form.tag(:table, attr) do
1238
+ if legend = opts[:legend]
1239
+ form.emit(form.tag(:caption, opts[:legend_attr], legend))
1240
+ end
1241
+
1242
+ if (labels = opts[:labels]) && !labels.empty?
1243
+ form.emit(form.tag(:tr, {}, labels.map{|l| form._tag(:th, {}, l)}))
1244
+ end
1245
+
1246
+ yield
1247
+ end
1143
1248
  end
1144
1249
  end
1145
1250