formular 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.travis.yml +29 -0
  4. data/CHANGELOG.md +3 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +105 -0
  8. data/Rakefile +12 -0
  9. data/formular.gemspec +33 -0
  10. data/lib/formular.rb +8 -0
  11. data/lib/formular/attributes.rb +45 -0
  12. data/lib/formular/builder.rb +52 -0
  13. data/lib/formular/builders/basic.rb +64 -0
  14. data/lib/formular/builders/bootstrap3.rb +28 -0
  15. data/lib/formular/builders/bootstrap3_horizontal.rb +32 -0
  16. data/lib/formular/builders/bootstrap3_inline.rb +10 -0
  17. data/lib/formular/builders/bootstrap4.rb +36 -0
  18. data/lib/formular/builders/bootstrap4_horizontal.rb +39 -0
  19. data/lib/formular/builders/bootstrap4_inline.rb +15 -0
  20. data/lib/formular/builders/foundation6.rb +28 -0
  21. data/lib/formular/element.rb +135 -0
  22. data/lib/formular/element/bootstrap3.rb +70 -0
  23. data/lib/formular/element/bootstrap3/checkable_control.rb +105 -0
  24. data/lib/formular/element/bootstrap3/column_control.rb +45 -0
  25. data/lib/formular/element/bootstrap3/horizontal.rb +146 -0
  26. data/lib/formular/element/bootstrap3/input_group.rb +83 -0
  27. data/lib/formular/element/bootstrap4.rb +49 -0
  28. data/lib/formular/element/bootstrap4/checkable_control.rb +94 -0
  29. data/lib/formular/element/bootstrap4/custom_control.rb +120 -0
  30. data/lib/formular/element/bootstrap4/horizontal.rb +87 -0
  31. data/lib/formular/element/foundation6.rb +69 -0
  32. data/lib/formular/element/foundation6/checkable_control.rb +72 -0
  33. data/lib/formular/element/foundation6/input_group.rb +88 -0
  34. data/lib/formular/element/foundation6/wrapped_control.rb +21 -0
  35. data/lib/formular/element/module.rb +35 -0
  36. data/lib/formular/element/modules/checkable.rb +96 -0
  37. data/lib/formular/element/modules/collection.rb +17 -0
  38. data/lib/formular/element/modules/container.rb +60 -0
  39. data/lib/formular/element/modules/control.rb +42 -0
  40. data/lib/formular/element/modules/error.rb +48 -0
  41. data/lib/formular/element/modules/hint.rb +36 -0
  42. data/lib/formular/element/modules/label.rb +30 -0
  43. data/lib/formular/element/modules/wrapped_control.rb +73 -0
  44. data/lib/formular/elements.rb +295 -0
  45. data/lib/formular/helper.rb +53 -0
  46. data/lib/formular/html_block.rb +70 -0
  47. data/lib/formular/path.rb +30 -0
  48. data/lib/formular/version.rb +3 -0
  49. metadata +247 -0
@@ -0,0 +1,17 @@
1
+ require 'formular/element/module'
2
+ module Formular
3
+ class Element
4
+ module Modules
5
+ # This module adds the relevant option keys and defaults
6
+ # for elements that support collections
7
+ module Collection
8
+ include Formular::Element::Module
9
+
10
+ add_option_keys :collection, :label_method, :value_method
11
+
12
+ set_default :label_method, 'first'
13
+ set_default :value_method, 'last'
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,60 @@
1
+ require 'formular/element/module'
2
+ module Formular
3
+ class Element
4
+ module Modules
5
+ # this module is used for element that contain something
6
+ # e.g. <label>Label Words</label>
7
+ # it is designed to accept content as a string (via the :content option)
8
+ # or as a block
9
+ # Alternatively you can produce blockless content by making use of the
10
+ # #start and #end methods e.g.
11
+ #
12
+ # element = Container.()
13
+ # element.start
14
+ # add your own content
15
+ # element.end
16
+ module Container
17
+ include Formular::Element::Module
18
+
19
+ add_option_keys :content
20
+
21
+ html do |element|
22
+ concat start_tag
23
+ concat element.content
24
+ concat end_tag
25
+ end
26
+
27
+ html(:start) { start_tag }
28
+
29
+ html(:end) { end_tag }
30
+
31
+ module InstanceMethods
32
+ def content
33
+ @block ? HtmlBlock.new(self, @block).call : options[:content].to_s
34
+ end
35
+
36
+ def has_content?
37
+ @block || options[:content]
38
+ end
39
+
40
+ def end
41
+ to_html(context: :end)
42
+ end
43
+
44
+ def start
45
+ to_html(context: :start)
46
+ end
47
+
48
+ # Delegate missing methods to the builder
49
+ # TODO:: @apotonick is going to do something fancy here to delegate
50
+ # the builder methods rather then using this method missing.
51
+ def method_missing(method, *args, &block)
52
+ return super unless builder
53
+
54
+ builder.send(method, *args, &block)
55
+ end
56
+ end # module InstanceMethods
57
+ end # module Container
58
+ end # module Modules
59
+ end # class Element
60
+ end # module Formular
@@ -0,0 +1,42 @@
1
+ require 'formular/element/module'
2
+ module Formular
3
+ class Element
4
+ module Modules
5
+ # include this module in an element to set the id,
6
+ # name & value based on the attribute name
7
+ module Control
8
+ include Formular::Element::Module
9
+
10
+ add_option_keys :attribute_name
11
+
12
+ set_default :name, :form_encoded_name
13
+ set_default :id, :form_encoded_id
14
+ set_default :value, :reader_value
15
+
16
+ module InstanceMethods
17
+ def attribute_name
18
+ options[:attribute_name]
19
+ end
20
+
21
+ private
22
+
23
+ def path
24
+ @path ||= builder.path(attribute_name) if attribute_name && builder
25
+ end
26
+
27
+ def form_encoded_name
28
+ path.to_encoded_name if path
29
+ end
30
+
31
+ def form_encoded_id
32
+ path.to_encoded_id if path
33
+ end
34
+
35
+ def reader_value
36
+ builder.reader_value(attribute_name) if attribute_name && builder
37
+ end
38
+ end # model InstanceMethods
39
+ end # module Control
40
+ end # module Modules
41
+ end # class Element
42
+ end # module Formular
@@ -0,0 +1,48 @@
1
+ require 'formular/element/module'
2
+ module Formular
3
+ class Element
4
+ module Modules
5
+ # this module provides error methods and options to a control when included
6
+ module Error
7
+ include Formular::Element::Module
8
+ add_option_keys :error
9
+
10
+ # options functionality (same as SimpleForm):
11
+ # options[:error] == false NO ERROR regardless of model errors
12
+ # options[:error] == String return the string, regardless of model errors
13
+ module InstanceMethods
14
+ def error_text
15
+ has_custom_error? ? options[:error] : errors_on_attribute.send(error_method) if has_errors?
16
+ end
17
+
18
+ def has_errors?
19
+ options[:error] != false && (has_custom_error? || has_attribute_errors?)
20
+ end
21
+
22
+ protected
23
+
24
+ # attribute_errors is an array, what method should we use to return a
25
+ # string? (:first, :last, :join etc.)
26
+ # ideally this should be configurable via the builder...
27
+ def error_method
28
+ :first
29
+ end
30
+
31
+ # I bet we could clean this up alot but it needs to be flexible
32
+ # enough not to error with nils
33
+ def has_attribute_errors?
34
+ builder != nil && builder.errors != nil && errors_on_attribute != nil && errors_on_attribute.size > 0
35
+ end
36
+
37
+ def has_custom_error?
38
+ options[:error].is_a?(String)
39
+ end
40
+
41
+ def errors_on_attribute
42
+ @errors ||= builder.errors[options[:attribute_name]]
43
+ end
44
+ end # module InstanceMethods
45
+ end # module Error
46
+ end # module Modules
47
+ end # class Element
48
+ end # module Formular
@@ -0,0 +1,36 @@
1
+ require 'formular/element/module'
2
+ module Formular
3
+ class Element
4
+ module Modules
5
+ # this module provides hints to a control when included.
6
+ module Hint
7
+ include Formular::Element::Module
8
+ add_option_keys :hint, :hint_options
9
+
10
+ # options functionality (same as SimpleForm):
11
+ # options[:hint] == String return the string
12
+ module InstanceMethods
13
+ def hint_text
14
+ options[:hint] if has_hint?
15
+ end
16
+
17
+ def has_hint?
18
+ options[:hint].is_a?(String)
19
+ end
20
+
21
+ private
22
+ def hint_id
23
+ return hint_options[:id] if hint_options[:id]
24
+
25
+ id = attributes[:id] || form_encoded_id
26
+ "#{id}_hint" if id
27
+ end
28
+
29
+ def hint_options
30
+ @hint_options ||= Attributes[options[:hint_options]]
31
+ end
32
+ end # module InstanceMethods
33
+ end # module Hint
34
+ end # module Modules
35
+ end # class Element
36
+ end # module Formular
@@ -0,0 +1,30 @@
1
+ require 'formular/element/module'
2
+ module Formular
3
+ class Element
4
+ module Modules
5
+ # this module provides label options and methods to a control when included.
6
+ module Label
7
+ include Formular::Element::Module
8
+ add_option_keys :label, :label_options
9
+
10
+ # options functionality:
11
+ # options[:label] == String return the string
12
+ # currently we don't infer label text so if you don't include
13
+ # label as an option, you wont get one rendered
14
+ module InstanceMethods
15
+ def label_text
16
+ options[:label]
17
+ end
18
+
19
+ def has_label?
20
+ !label_text.nil? && label_text != false
21
+ end
22
+
23
+ def label_options
24
+ @label_options ||= Attributes[options[:label_options]]
25
+ end
26
+ end # module InstanceMethods
27
+ end # module Label
28
+ end # module Modules
29
+ end # class Element
30
+ end # module Formular
@@ -0,0 +1,73 @@
1
+ require 'formular/element/module'
2
+ require 'formular/element/modules/control'
3
+ require 'formular/element/modules/hint'
4
+ require 'formular/element/modules/error'
5
+ require 'formular/element/modules/label'
6
+ module Formular
7
+ class Element
8
+ module Modules
9
+ # include this module to enable an element to render the entire wrapped input
10
+ # e.g. wrapper{label+control+hint+error}
11
+ module WrappedControl
12
+ include Formular::Element::Module
13
+ include Control
14
+ include Hint
15
+ include Error
16
+ include Label
17
+
18
+ add_option_keys :error_options, :wrapper_options
19
+ set_default :aria_describedby, :hint_id, if: :has_hint?
20
+
21
+ self.html_context = :wrapped
22
+
23
+ html(:wrapped) do |input|
24
+ input.wrapper do
25
+ concat input.label
26
+ concat input.to_html(context: :default)
27
+ concat input.hint
28
+ concat input.error
29
+ end
30
+ end
31
+
32
+ module InstanceMethods
33
+ def wrapper(&block)
34
+ wrapper_element = has_errors? ? :error_wrapper : :wrapper
35
+ builder.send(wrapper_element, Attributes[options[:wrapper_options]], &block)
36
+ end
37
+
38
+ def label
39
+ return '' unless has_label?
40
+
41
+ label_options[:content] = label_text
42
+ label_options[:labeled_control] = self
43
+ builder.label(label_options).to_s
44
+ end
45
+
46
+ def error
47
+ return '' unless has_errors?
48
+
49
+ error_options[:content] = error_text
50
+ builder.error(error_options).to_s
51
+ end
52
+
53
+ def hint
54
+ return '' unless has_hint?
55
+
56
+ hint_options[:content] = hint_text
57
+ hint_options[:id] ||= hint_id
58
+ builder.hint(hint_options).to_s
59
+ end
60
+
61
+ private
62
+ def error_options
63
+ @error_options ||= Attributes[options[:error_options]]
64
+ end
65
+
66
+ def wrapper_options
67
+ @wrapper_options ||= Attributes[options[:wrapper_options]]
68
+ end
69
+ end # module InstanceMethods
70
+ end # module WrappedControl
71
+ end # module Modules
72
+ end # class Element
73
+ end # module Formular
@@ -0,0 +1,295 @@
1
+ require 'formular/element'
2
+ require 'formular/element/module'
3
+ require 'formular/element/modules/container'
4
+ require 'formular/element/modules/wrapped_control'
5
+ require 'formular/element/modules/control'
6
+ require 'formular/element/modules/checkable'
7
+ require 'formular/element/modules/error'
8
+
9
+ module Formular
10
+ class Element
11
+ # These three are really just provided for convenience when creating other elements
12
+ Container = Class.new(Formular::Element) { include Formular::Element::Modules::Container }
13
+ Control = Class.new(Formular::Element) { include Formular::Element::Modules::Control }
14
+ WrappedControl = Class.new(Formular::Element) { include Formular::Element::Modules::WrappedControl }
15
+
16
+ # define some base classes to build from or easily use elsewhere
17
+ Option = Class.new(Container) { tag :option }
18
+ OptGroup = Class.new(Container) { tag :optgroup }
19
+ Fieldset = Class.new(Container) { tag :fieldset }
20
+ Legend = Class.new(Container) { tag :legend }
21
+ Div = Class.new(Container) { tag :div }
22
+ P = Class.new(Container) { tag :p }
23
+ Span = Class.new(Container) { tag :span }
24
+ Small = Class.new(Container) { tag :small }
25
+
26
+ class Hidden < Control
27
+ tag :input
28
+ set_default :type, 'hidden'
29
+
30
+ html { closed_start_tag }
31
+ end
32
+
33
+ class Form < Container
34
+ tag :form
35
+
36
+ add_option_keys :enforce_utf8, :csrf_token, :csrf_token_name
37
+
38
+ set_default :method, 'post'
39
+ set_default :accept_charset, 'utf-8'
40
+ set_default :enforce_utf8, true
41
+
42
+ html(:start) do |form|
43
+ hidden_tags = element.extra_hidden_tags
44
+ concat start_tag
45
+ concat hidden_tags
46
+ end
47
+
48
+ html do |form|
49
+ concat form.to_html(context: :start)
50
+ concat form.content
51
+ concat end_tag
52
+ end
53
+
54
+ def extra_hidden_tags
55
+ token_tag + utf8_enforcer_tag + method_tag
56
+ end
57
+
58
+ private
59
+
60
+ def token_tag
61
+ return '' unless options[:csrf_token].is_a? String
62
+
63
+ name = options[:csrf_token_name] || '_csrf_token'
64
+
65
+ Hidden.(value: options[:csrf_token], name: name).to_s
66
+ end
67
+
68
+ def utf8_enforcer_tag
69
+ return '' unless options[:enforce_utf8]
70
+
71
+ # Use raw HTML to ensure the value is written as an HTML entity; it
72
+ # needs to be the right character regardless of which encoding the
73
+ # browser infers.
74
+ %(<input name="utf8" type="hidden" value="✓"/>)
75
+ end
76
+
77
+ # because this mutates attributes, we have to call this before rendering the start_tag
78
+ def method_tag
79
+ method = attributes[:method]
80
+
81
+ case method
82
+ when /^get$/ # must be case-insensitive, but can't use downcase as might be nil
83
+ attributes[:method] = 'get'
84
+ ''
85
+ when /^post$/, '', nil
86
+ attributes[:method] = 'post'
87
+ ''
88
+ else
89
+ attributes[:method] = 'post'
90
+ Hidden.(value: method, name: '_method').to_s
91
+ end
92
+ end
93
+ end
94
+
95
+ class ErrorNotification < Formular::Element
96
+ tag :div
97
+ add_option_keys :message
98
+
99
+ html do |element|
100
+ if element.builder_errors?
101
+ concat start_tag
102
+ concat element.error_message
103
+ concat end_tag
104
+ else
105
+ ''
106
+ end
107
+ end
108
+
109
+ def error_message
110
+ options[:message] || 'Please review the problems below:'
111
+ end
112
+
113
+ def builder_errors?
114
+ return false if builder.nil?
115
+ !builder.errors.empty?
116
+ end
117
+ end
118
+
119
+ class Error < P
120
+ include Formular::Element::Modules::Error
121
+ add_option_keys :attribute_name
122
+ set_default :content, :error_text
123
+ end # class Error
124
+
125
+ class Textarea < Control
126
+ include Formular::Element::Modules::Container
127
+ tag :textarea
128
+ add_option_keys :value
129
+
130
+ def content
131
+ options[:value] || super
132
+ end
133
+ end # class Textarea
134
+
135
+ class Label < Container
136
+ tag :label
137
+ add_option_keys :labeled_control, :attribute_name
138
+ set_default :for, :labeled_control_id
139
+
140
+ # as per MDN A label element can have both a 'for' attribute and a contained control element,
141
+ # as long as the for attribute points to the contained control element.
142
+ def labeled_control_id
143
+ return options[:labeled_control].attributes[:id] if options[:labeled_control]
144
+ return builder.path(options[:attribute_name]).to_encoded_id if options[:attribute_name] && builder
145
+ end
146
+ end # class Label
147
+
148
+ class Submit < Formular::Element
149
+ tag :input
150
+
151
+ set_default :type, 'submit'
152
+
153
+ html { closed_start_tag }
154
+ end # class Submit
155
+
156
+ class Button < Formular::Element::Container
157
+ tag :button
158
+ add_option_keys :value
159
+
160
+ def content
161
+ options[:value] || super
162
+ end
163
+ end # class Button
164
+
165
+ class Input < Control
166
+ tag :input
167
+ set_default :type, 'text'
168
+ html { closed_start_tag }
169
+ end # class Input
170
+
171
+ class Select < Control
172
+ include Formular::Element::Modules::Collection
173
+ tag :select
174
+
175
+ add_option_keys :value
176
+
177
+ html do |input|
178
+ concat start_tag
179
+ concat input.option_tags
180
+ concat end_tag
181
+ end
182
+
183
+ # convert the collection array into option tags also supports option groups
184
+ # when the array is nested
185
+ # example 1:
186
+ # [[1,"True"], [0,"False"]] =>
187
+ # <option value="1">true</option><option value="0">false</option>
188
+ # example 2:
189
+ # [
190
+ # ["Genders", [["m", "Male"], ["f", "Female"]]],
191
+ # ["Booleans", [[1,"true"], [0,"false"]]]
192
+ # ] =>
193
+ # <optgroup label="Genders">
194
+ # <option value="m">Male</option>
195
+ # <option value="f">Female</option>
196
+ # </optgroup>
197
+ # <optgroup label="Booleans">
198
+ # <option value="1">true</option>
199
+ # <option value="0">false</option>
200
+ # </optgroup>
201
+ def option_tags
202
+ collection_to_options(options[:collection])
203
+ end
204
+
205
+ private
206
+
207
+ def collection_to_options(collection)
208
+ collection.map do |item|
209
+ if item.last.is_a?(Array)
210
+ opts = { label: item.first, content: collection_to_options(item.last) }
211
+
212
+ Formular::Element::OptGroup.new(opts).to_s
213
+ else
214
+ item_to_option(item)
215
+ end
216
+ end.join ''
217
+ end
218
+
219
+ def item_to_option(item)
220
+ opts = if item.is_a?(Array) && item.size > 2
221
+ item.pop
222
+ else
223
+ {}
224
+ end
225
+
226
+ opts[:value] = item.send(options[:value_method])
227
+ opts[:content] = item.send(options[:label_method])
228
+ opts[:selected] = 'selected' if opts[:value] == options[:value]
229
+
230
+ Formular::Element::Option.new(opts).to_s
231
+ end
232
+ end # class Select
233
+
234
+ class Checkbox < Control
235
+ tag :input
236
+
237
+ add_option_keys :unchecked_value, :include_hidden, :multiple
238
+
239
+ set_default :type, 'checkbox'
240
+ set_default :unchecked_value, :default_unchecked_value
241
+ set_default :value, '1' # instead of reader value
242
+ set_default :include_hidden, true
243
+
244
+ include Formular::Element::Modules::Checkable
245
+
246
+ html do |element|
247
+ if element.collection?
248
+ element.to_html(context: :with_collection)
249
+ else
250
+ concat element.hidden_tag
251
+ concat closed_start_tag
252
+ end
253
+ end
254
+
255
+ html(:with_collection) do |element|
256
+ concat element.to_html(context: :collection)
257
+ concat element.hidden_tag
258
+ end
259
+
260
+ def hidden_tag
261
+ return '' unless options[:include_hidden]
262
+
263
+ Hidden.(value: options[:unchecked_value], name: attributes[:name]).to_s
264
+ end
265
+
266
+ private
267
+
268
+ def default_unchecked_value
269
+ collection? ? '' : '0'
270
+ end
271
+
272
+ # only append the [] to name if part of a collection, or the multiple option is set
273
+ def form_encoded_name
274
+ return unless path
275
+
276
+ options[:multiple] || options[:collection] ? super + '[]' : super
277
+ end
278
+
279
+ def collection_base_options
280
+ super.merge(include_hidden: false)
281
+ end
282
+ end # class Checkbox
283
+
284
+ class Radio < Control
285
+ tag :input
286
+
287
+ set_default :type, 'radio'
288
+ set_default :value, nil # instead of reader value
289
+
290
+ include Formular::Element::Modules::Checkable
291
+
292
+ html { closed_start_tag }
293
+ end # class Radio
294
+ end # class Element
295
+ end # module Formular