forme 0.9.2 → 0.10.0

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