forme 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,387 @@
1
+ module Forme
2
+ # The +Form+ class is the main entry point to the library.
3
+ # Using the +form+, +input+, +tag+, and +inputs+ methods, one can easily build
4
+ # an abstract syntax tree of +Tag+ and +Input+ instances, which can be serialized
5
+ # to a string using +to_s+.
6
+ class Form
7
+ # A hash of options for the form.
8
+ attr_reader :opts
9
+
10
+ # Set the default options for inputs by type. This should be a hash with
11
+ # input type keys and values that are hashes of input options.
12
+ attr_reader :input_defaults
13
+
14
+ # The hidden tags to automatically add to the form.
15
+ attr_reader :hidden_tags
16
+
17
+ # The namespaces if any for the receiver's inputs. This can be used to
18
+ # automatically setup namespaced class and id attributes.
19
+ attr_accessor :namespaces
20
+
21
+ # The +serializer+ determines how +Tag+ objects are transformed into strings.
22
+ # Must respond to +call+ or be a registered symbol.
23
+ attr_reader :serializer
24
+
25
+ # Use appropriate Form subclass for object based on the current class, if the
26
+ # object responds to +forme_form_class+.
27
+ def self.new(obj=nil, opts={})
28
+ if obj && obj.respond_to?(:forme_form_class) && !opts[:_forme_form_class_set]
29
+ obj.forme_form_class(self).new(obj, opts.merge(:_forme_form_class_set=>true))
30
+ else
31
+ super
32
+ end
33
+ end
34
+
35
+ # Create a +Form+ instance and yield it to the block,
36
+ # injecting the opening form tag before yielding and
37
+ # the closing form tag after yielding.
38
+ #
39
+ # Argument Handling:
40
+ # No args :: Creates a +Form+ object with no options and not associated
41
+ # to an +obj+, and with no attributes in the opening tag.
42
+ # 1 hash arg :: Treated as opening form tag attributes, creating a
43
+ # +Form+ object with no options.
44
+ # 1 non-hash arg :: Treated as the +Form+'s +obj+, with empty options
45
+ # and no attributes in the opening tag.
46
+ # 2 hash args :: First hash is opening attributes, second hash is +Form+
47
+ # options.
48
+ # 1 non-hash arg, 1-2 hash args :: First argument is +Form+'s obj, second is
49
+ # opening attributes, third if provided is
50
+ # +Form+'s options.
51
+ def self.form(obj=nil, attr={}, opts={}, &block)
52
+ f = if obj.is_a?(Hash)
53
+ raise Error, "Can't provide 3 hash arguments to form" unless opts.empty?
54
+ opts = attr
55
+ attr = obj
56
+ new(opts)
57
+ else
58
+ new(obj, opts)
59
+ end
60
+
61
+ ins = opts[:inputs]
62
+ button = opts[:button]
63
+ if ins || button
64
+ block = Proc.new do |form|
65
+ form._inputs(ins, opts) if ins
66
+ yield form if block_given?
67
+ form.emit(form.button(button)) if button
68
+ end
69
+ end
70
+
71
+ f.form(attr, &block)
72
+ end
73
+
74
+ # Creates a +Form+ object. Arguments:
75
+ # obj :: Sets the obj for the form. If a hash, is merged with the +opts+ argument
76
+ # to set the opts.
77
+ # opts :: A hash of options for the form
78
+ def initialize(obj=nil, opts={})
79
+ @opts = opts.merge(obj.is_a?(Hash) ? obj : {:obj=>obj})
80
+ @opts[:namespace] = Array(@opts[:namespace])
81
+
82
+ if obj && obj.respond_to?(:forme_config)
83
+ obj.forme_config(self)
84
+ end
85
+
86
+ config = CONFIGURATIONS[@opts[:config]||Forme.default_config]
87
+ copy_inputs_wrapper_from_wrapper(@opts)
88
+
89
+ TRANSFORMER_TYPES.each do |t|
90
+ case @opts[t]
91
+ when Symbol
92
+ @opts[t] = Forme.transformer(t, @opts[t], @opts)
93
+ when nil
94
+ unless @opts.has_key?(t)
95
+ @opts[t] = Forme.transformer(t, config, @opts)
96
+ end
97
+ end
98
+ end
99
+
100
+ @serializer = @opts[:serializer]
101
+ @input_defaults = @opts[:input_defaults] || {}
102
+ @hidden_tags = @opts[:hidden_tags]
103
+ @nesting = []
104
+ end
105
+
106
+ # Create a form tag with the given attributes.
107
+ def form(attr={}, &block)
108
+ tag(:form, attr, method(:hidden_form_tags), &block)
109
+ end
110
+
111
+ # Empty method designed to ease integration with other libraries where
112
+ # Forme is used in template code and some output implicitly
113
+ # created by Forme needs to be injected into the template output.
114
+ def emit(tag)
115
+ end
116
+
117
+ # Creates an +Input+ with the given +field+ and +opts+ associated with
118
+ # the receiver, and add it to the list of children to the currently
119
+ # open tag.
120
+ #
121
+ # If the form is associated with an +obj+, or the :obj key exists in
122
+ # the +opts+ argument, treats the +field+ as a call to the +obj+. If
123
+ # +obj+ responds to +forme_input+, that method is called with the +field+
124
+ # and a copy of +opts+. Otherwise, the field is used as a method call
125
+ # on the +obj+ and a text input is created with the result.
126
+ #
127
+ # If no +obj+ is associated with the receiver, +field+ represents an input
128
+ # type (e.g. <tt>:text</tt>, <tt>:textarea</tt>, <tt>:select</tt>), and
129
+ # an input is created directly with the +field+ and +opts+.
130
+ def input(field, opts={})
131
+ if opts.has_key?(:obj)
132
+ opts = opts.dup
133
+ obj = opts.delete(:obj)
134
+ else
135
+ obj = self.obj
136
+ end
137
+ input = if obj
138
+ if obj.respond_to?(:forme_input)
139
+ obj.forme_input(self, field, opts.dup)
140
+ else
141
+ opts = opts.dup
142
+ opts[:key] = field unless opts.has_key?(:key)
143
+ unless opts.has_key?(:value)
144
+ opts[:value] = if obj.is_a?(Hash)
145
+ obj[field]
146
+ else
147
+ obj.send(field)
148
+ end
149
+ end
150
+ _input(:text, opts)
151
+ end
152
+ else
153
+ _input(field, opts)
154
+ end
155
+ self << input
156
+ input
157
+ end
158
+
159
+ # Create a new +Input+ associated with the receiver with the given
160
+ # arguments, doing no other processing.
161
+ def _input(*a)
162
+ Input.new(self, *a)
163
+ end
164
+
165
+ # Creates a tag using the +inputs_wrapper+ (a fieldset by default), calls
166
+ # input on each element of +inputs+, and yields if given a block.
167
+ # You can use array arguments if you want inputs to be created with specific
168
+ # options:
169
+ #
170
+ # f.inputs([:field1, :field2])
171
+ # f.inputs([[:field1, {:name=>'foo'}], :field2])
172
+ #
173
+ # The given +opts+ are passed to the +inputs_wrapper+, and the default
174
+ # +inputs_wrapper+ supports a <tt>:legend</tt> option that is used to
175
+ # set the legend for the fieldset.
176
+ #
177
+ # +opts+ can also include transformer options itself (e.g. :wrapper), which
178
+ # override the form's current transformer options for the duration of the block.
179
+ # The exception is the :inputs_wrapper transformer option, which affects the
180
+ # wrapper to use for this inputs call. You can use the :nested_inputs_wrapper
181
+ # option to set the default :inputs_wrapper option for the duration of the block.
182
+ #
183
+ # This can also be called with a single hash argument to just use an options hash:
184
+ #
185
+ # f.inputs(:legend=>'Foo') do
186
+ # # ...
187
+ # end
188
+ #
189
+ # or even without any arguments:
190
+ #
191
+ # f.inputs do
192
+ # # ...
193
+ # end
194
+ def inputs(inputs=[], opts={}, &block)
195
+ _inputs(inputs, opts, &block)
196
+ end
197
+
198
+ # Internals of #inputs, should be used internally by the library, where #inputs
199
+ # is designed for external use.
200
+ def _inputs(inputs=[], opts={}) # :nodoc:
201
+ if inputs.is_a?(Hash)
202
+ opts = inputs.merge(opts)
203
+ inputs = []
204
+ end
205
+
206
+ form_opts = {}
207
+ form_opts[:inputs_wrapper] = opts[:nested_inputs_wrapper] if opts[:nested_inputs_wrapper]
208
+ TRANSFORMER_TYPES.each do |t|
209
+ if opts.has_key?(t) && t != :inputs_wrapper
210
+ form_opts[t] = opts[t]
211
+ end
212
+ end
213
+
214
+ Forme.transform(:inputs_wrapper, opts, @opts, self, opts) do
215
+ with_opts(form_opts) do
216
+ inputs.each do |i|
217
+ emit(input(*i))
218
+ end
219
+ yield if block_given?
220
+ end
221
+ end
222
+ end
223
+
224
+ # Returns a string representing the opening of the form tag for serializers
225
+ # that support opening tags.
226
+ def open(attr)
227
+ serializer.serialize_open(_tag(:form, attr)) if serializer.respond_to?(:serialize_open)
228
+ end
229
+
230
+ # Returns a string representing the closing of the form tag, for serializers
231
+ # that support closing tags.
232
+ def close
233
+ serializer.serialize_close(_tag(:form)) if serializer.respond_to?(:serialize_close)
234
+ end
235
+
236
+ # Create a +Tag+ associated to the receiver with the given arguments and block,
237
+ # doing no other processing.
238
+ def _tag(*a, &block)
239
+ tag = Tag.new(self, *a, &block)
240
+ end
241
+
242
+ # The object associated with this form, if any. If the +Form+ has an associated
243
+ # obj, then calls to +input+ are assumed to be accessing fields of the object
244
+ # instead to directly representing input types.
245
+ def obj
246
+ @opts[:obj]
247
+ end
248
+
249
+ # The current namespaces for the form, if any.
250
+ def namespaces
251
+ @opts[:namespace]
252
+ end
253
+
254
+ # Creates a +Tag+ associated to the receiver with the given arguments.
255
+ # Add the tag to the the list of children for the currently open tag.
256
+ # If a block is given, make this tag the currently open tag while inside
257
+ # the block.
258
+ def tag(*a, &block)
259
+ tag = _tag(*a)
260
+ self << tag
261
+ nest(tag, &block) if block
262
+ tag
263
+ end
264
+
265
+ # Aliased for tag. Workaround for issue with rails plugin.
266
+ def tag_(*a, &block) # :nodoc:
267
+ tag(*a, &block)
268
+ end
269
+
270
+ # Creates a :submit +Input+ with the given opts, adding it to the list
271
+ # of children for the currently open tag.
272
+ def button(opts={})
273
+ opts = {:value=>opts} if opts.is_a?(String)
274
+ input = _input(:submit, opts)
275
+ self << input
276
+ input
277
+ end
278
+
279
+ # Add the +Input+/+Tag+ instance given to the currently open tag.
280
+ def <<(tag)
281
+ if n = @nesting.last
282
+ n << tag
283
+ end
284
+ end
285
+
286
+ # Calls the block for each object in objs, using with_obj with the given namespace
287
+ # and an index namespace (starting at 0).
288
+ def each_obj(objs, namespace=nil)
289
+ objs.each_with_index do |obj, i|
290
+ with_obj(obj, Array(namespace) + [i]) do
291
+ yield obj, i
292
+ end
293
+ end
294
+ end
295
+
296
+ # Return a new string that will not be html escaped by the default serializer.
297
+ def raw(s)
298
+ Forme.raw(s)
299
+ end
300
+
301
+ # Marks the string as containing already escaped output. Returns string given
302
+ # by default, but subclasses for specific web frameworks can handle automatic
303
+ # html escaping by overriding this.
304
+ def raw_output(s)
305
+ s
306
+ end
307
+
308
+ # Temporarily override the given object and namespace for the form. Any given
309
+ # namespaces are appended to the form's current namespace.
310
+ def with_obj(obj, namespace=nil)
311
+ with_opts(:obj=>obj, :namespace=>@opts[:namespace]+Array(namespace)) do
312
+ yield obj
313
+ end
314
+ end
315
+
316
+ # Temporarily override the opts for the form for the duration of the block.
317
+ # This merges the given opts with the form's current opts, restoring
318
+ # the previous opts before returning.
319
+ def with_opts(opts)
320
+ orig_opts = @opts
321
+ @opts = orig_opts.merge(opts)
322
+ copy_inputs_wrapper_from_wrapper(opts, @opts)
323
+ yield
324
+ ensure
325
+ @opts = orig_opts if orig_opts
326
+ end
327
+
328
+ private
329
+
330
+ # Copy the :wrapper option to :inputs_wrapper in output_opts if only :wrapper
331
+ # is present in input_opts and the :wrapper option value is a shared wrapper.
332
+ def copy_inputs_wrapper_from_wrapper(input_opts, output_opts=input_opts)
333
+ if input_opts[:wrapper] && !input_opts[:inputs_wrapper] && SHARED_WRAPPERS.include?(input_opts[:wrapper])
334
+ output_opts[:inputs_wrapper] = output_opts[:wrapper]
335
+ end
336
+ end
337
+
338
+ # Return array of hidden tags to use for this form,
339
+ # or nil if the form does not have hidden tags added automatically.
340
+ def hidden_form_tags(form_tag)
341
+ if hidden_tags
342
+ tags = []
343
+ hidden_tags.each do |hidden_tag|
344
+ hidden_tag = hidden_tag.call(form_tag) if hidden_tag.respond_to?(:call)
345
+ tags.concat(parse_hidden_tags(hidden_tag))
346
+ end
347
+ tags
348
+ end
349
+ end
350
+
351
+ # Handle various types of hidden tags for the form.
352
+ def parse_hidden_tags(hidden_tag)
353
+ case hidden_tag
354
+ when Array
355
+ hidden_tag
356
+ when Tag, String
357
+ [hidden_tag]
358
+ when Hash
359
+ hidden_tag.map{|k,v| _tag(:input, :type=>:hidden, :name=>k, :value=>v)}
360
+ when nil
361
+ []
362
+ else
363
+ raise Error, "unhandled hidden_tag response: #{hidden_tag.inspect}"
364
+ end
365
+ end
366
+
367
+ # Make the given tag the currently open tag, and yield. After the
368
+ # block returns, make the previously open tag the currently open
369
+ # tag.
370
+ def nest(tag)
371
+ @nesting << tag
372
+ yield self
373
+ ensure
374
+ @nesting.pop
375
+ end
376
+
377
+ # Return a serialized opening tag for the given tag.
378
+ def serialize_open(tag)
379
+ raw_output(serializer.serialize_open(tag)) if serializer.respond_to?(:serialize_open)
380
+ end
381
+
382
+ # Return a serialized closing tag for the given tag.
383
+ def serialize_close(tag)
384
+ raw_output(serializer.serialize_close(tag)) if serializer.respond_to?(:serialize_close)
385
+ end
386
+ end
387
+ end
@@ -0,0 +1,48 @@
1
+ module Forme
2
+ # High level abstract tag form, transformed by formatters into the lower
3
+ # level +Tag+ form (or an array of them).
4
+ class Input
5
+ # The +Form+ object related to the receiver.
6
+ attr_reader :form
7
+
8
+ # The type of input, should be a symbol (e.g. :submit, :text, :select).
9
+ attr_reader :type
10
+
11
+ # The options hash for the Input.
12
+ attr_reader :opts
13
+
14
+ # The options hash in use by the form at the time of the Input's instantiation.
15
+ attr_reader :form_opts
16
+
17
+ # Set the +form+, +type+, and +opts+.
18
+ def initialize(form, type, opts={})
19
+ @form, @type = form, type
20
+ defaults = form.input_defaults
21
+ @opts = (defaults.fetch(type){defaults[type.to_s]} || {}).merge(opts)
22
+ @form_opts = form.opts
23
+ end
24
+
25
+ # Replace the +opts+ by merging the given +hash+ into +opts+,
26
+ # without modifying +opts+.
27
+ def merge_opts(hash)
28
+ @opts = @opts.merge(hash)
29
+ end
30
+
31
+ # Create a new +Tag+ instance with the given arguments and block
32
+ # related to the receiver's +form+.
33
+ def tag(*a, &block)
34
+ form._tag(*a, &block)
35
+ end
36
+
37
+ # Return a string containing the serialized content of the receiver.
38
+ def to_s
39
+ form.raw_output(Forme.transform(:serializer, @opts, @form_opts, self))
40
+ end
41
+
42
+ # Transform the receiver into a lower level +Tag+ form (or an array
43
+ # of them).
44
+ def format
45
+ Forme.transform(:formatter, @opts, @form_opts, self)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,12 @@
1
+ module Forme
2
+ # Empty module for marking objects as "raw", where they will no longer
3
+ # html escaped by the default serializer.
4
+ module Raw
5
+ end
6
+
7
+ # A String subclass that includes Raw, which will cause the default
8
+ # serializer to no longer html escape the string.
9
+ class RawString < ::String
10
+ include Raw
11
+ end
12
+ end
@@ -0,0 +1,60 @@
1
+ module Forme
2
+ # Low level abstract tag form, where each instance represents a
3
+ # html tag with attributes and children.
4
+ class Tag
5
+ # The +Form+ object related to the receiver.
6
+ attr_reader :form
7
+
8
+ # The type of tag, should be a symbol (e.g. :input, :select).
9
+ attr_reader :type
10
+
11
+ # The attributes hash of this receiver.
12
+ attr_reader :attr
13
+
14
+ # An array instance representing the children of the receiver,
15
+ # or possibly +nil+ if the receiver has no children.
16
+ attr_reader :children
17
+
18
+ # Set the +form+, +type+, +attr+, and +children+.
19
+ def initialize(form, type, attr={}, children=nil)
20
+ @form, @type, @attr = form, type, (attr||{})
21
+ @children = parse_children(children)
22
+ end
23
+
24
+ # Adds a child to the array of receiver's children.
25
+ def <<(child)
26
+ if children
27
+ children << child
28
+ else
29
+ @children = [child]
30
+ end
31
+ end
32
+
33
+ # Create a new +Tag+ instance with the given arguments and block
34
+ # related to the receiver's +form+.
35
+ def tag(*a, &block)
36
+ form._tag(*a, &block)
37
+ end
38
+
39
+ # Return a string containing the serialized content of the receiver.
40
+ def to_s
41
+ form.raw_output(Forme.transform(:serializer, @opts, @form.opts, self))
42
+ end
43
+
44
+ private
45
+
46
+ # Convert children constructor argument into the children to use for the tag.
47
+ def parse_children(children)
48
+ case children
49
+ when Array
50
+ children
51
+ when Proc, Method
52
+ parse_children(children.call(self))
53
+ when nil
54
+ nil
55
+ else
56
+ [children]
57
+ end
58
+ end
59
+ end
60
+ end