formular 0.2.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.
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