sexy_form 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE +21 -0
  4. data/README.md +275 -0
  5. data/Rakefile +15 -0
  6. data/lib/sexy_form.rb +84 -0
  7. data/lib/sexy_form/builder.rb +306 -0
  8. data/lib/sexy_form/themes.rb +22 -0
  9. data/lib/sexy_form/themes/base_theme.rb +39 -0
  10. data/lib/sexy_form/themes/bootstrap_2_horizontal.rb +83 -0
  11. data/lib/sexy_form/themes/bootstrap_2_inline.rb +73 -0
  12. data/lib/sexy_form/themes/bootstrap_2_vertical.rb +80 -0
  13. data/lib/sexy_form/themes/bootstrap_3_horizontal.rb +95 -0
  14. data/lib/sexy_form/themes/bootstrap_3_inline.rb +71 -0
  15. data/lib/sexy_form/themes/bootstrap_3_vertical.rb +70 -0
  16. data/lib/sexy_form/themes/bootstrap_4_horizontal.rb +95 -0
  17. data/lib/sexy_form/themes/bootstrap_4_inline.rb +80 -0
  18. data/lib/sexy_form/themes/bootstrap_4_vertical.rb +79 -0
  19. data/lib/sexy_form/themes/bulma_horizontal.rb +81 -0
  20. data/lib/sexy_form/themes/bulma_vertical.rb +73 -0
  21. data/lib/sexy_form/themes/default.rb +55 -0
  22. data/lib/sexy_form/themes/foundation.rb +67 -0
  23. data/lib/sexy_form/themes/materialize.rb +65 -0
  24. data/lib/sexy_form/themes/milligram.rb +62 -0
  25. data/lib/sexy_form/themes/semantic_ui_inline.rb +63 -0
  26. data/lib/sexy_form/themes/semantic_ui_vertical.rb +63 -0
  27. data/lib/sexy_form/version.rb +3 -0
  28. data/spec/custom_assertions.rb +21 -0
  29. data/spec/sexy_form/builder_spec.rb +104 -0
  30. data/spec/sexy_form/themes/base_theme_spec.rb +16 -0
  31. data/spec/sexy_form/themes/bootstrap_2_horizontal_spec.rb +114 -0
  32. data/spec/sexy_form/themes/bootstrap_2_inline_spec.rb +108 -0
  33. data/spec/sexy_form/themes/bootstrap_2_vertical_spec.rb +111 -0
  34. data/spec/sexy_form/themes/bootstrap_3_horizontal_spec.rb +116 -0
  35. data/spec/sexy_form/themes/bootstrap_3_inline_spec.rb +104 -0
  36. data/spec/sexy_form/themes/bootstrap_3_vertical_spec.rb +122 -0
  37. data/spec/sexy_form/themes/bootstrap_4_horizontal_spec.rb +124 -0
  38. data/spec/sexy_form/themes/bootstrap_4_inline_spec.rb +116 -0
  39. data/spec/sexy_form/themes/bootstrap_4_vertical_spec.rb +114 -0
  40. data/spec/sexy_form/themes/bulma_horizontal_spec.rb +126 -0
  41. data/spec/sexy_form/themes/bulma_vertical_spec.rb +114 -0
  42. data/spec/sexy_form/themes/default_spec.rb +102 -0
  43. data/spec/sexy_form/themes/foundation_spec.rb +103 -0
  44. data/spec/sexy_form/themes/materialize_spec.rb +103 -0
  45. data/spec/sexy_form/themes/milligram_spec.rb +120 -0
  46. data/spec/sexy_form/themes/semantic_ui_inline_spec.rb +105 -0
  47. data/spec/sexy_form/themes/semantic_ui_vertical_spec.rb +105 -0
  48. data/spec/sexy_form/themes/theme_spec_helper.rb +0 -0
  49. data/spec/sexy_form/themes_spec.rb +52 -0
  50. data/spec/sexy_form_spec.rb +54 -0
  51. data/spec/spec_helper.rb +16 -0
  52. metadata +160 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7f0af203c8de766b37f80dc7035d48fa38a3538d
4
+ data.tar.gz: 599ce829b7d97dc9c3ba0b5ec8273e3c63654b02
5
+ SHA512:
6
+ metadata.gz: 98e3842d0986c28c923f5b9f368338443b341053ce5ef3554bb2a59c97fd91a423a68be6718cf9d9239bb1e748bfe0b3e3261b737c2be546302bd1010268c1fb
7
+ data.tar.gz: 0baf4af1e5e99584a887949a375c505061736cf88406e2014402874fa9ff6ae4eb7cb31c28a954c0cd824a6f6c3485dfac15cf4746e91037ebdbb4bd45b5deaa
@@ -0,0 +1,5 @@
1
+ CHANGELOG
2
+ ---------
3
+
4
+ - **1.0.0** - February 15, 2019
5
+ - Gem Initial Release
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Weston Ganger
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,275 @@
1
+ # Sexy Form.rb
2
+
3
+ <a href="https://badge.fury.io/rb/sexy_form.rb" target="_blank"><img height="21" style='border:0px;height:21px;' border='0' src="https://badge.fury.io/rb/sexy_form.rb.svg" alt="Gem Version"></a>
4
+ <a href='https://travis-ci.org/westonganger/sexy_form.rb' target='_blank'><img height='21' style='border:0px;height:21px;' src='https://travis-ci.org/westonganger/sexy_form.rb.svg?branch=master' border='0' alt='Build Status'></a>
5
+ <a href='https://rubygems.org/gems/sexy_form.rb' target='_blank'><img height='21' style='border:0px;height:21px;' src='https://ruby-gem-downloads-badge.herokuapp.com/sexy_form.rb?label=rubygems&type=total&total_label=downloads&color=brightgreen' border='0' alt='RubyGems Downloads' /></a>
6
+ <a href='https://ko-fi.com/A5071NK' target='_blank'><img height='22' style='border:0px;height:22px;' src='https://az743702.vo.msecnd.net/cdn/kofi1.png?v=a' border='0' alt='Buy Me a Coffee'></a>
7
+
8
+
9
+ Dead simple HTML form builder for Ruby with built-in support for many popular UI libraries such as Bootstrap. Pairs nicely with any Ruby web framework such as Rails
10
+
11
+ # Features
12
+
13
+ - Easily generate HTML markup for forms, labels, inputs, help text and errors
14
+ - Integrates with many UI libraries such as Bootstrap
15
+ - Custom theme support
16
+
17
+ # Supported UI Libraries
18
+
19
+ Out of the box Form Builder can generate HTML markup for the following UI libraries:
20
+
21
+ - Bootstrap 4
22
+ * `theme: :bootstrap_4_vertical`
23
+ * `theme: :bootstrap_4_inline`
24
+ * `theme: :bootstrap_4_horizontal` or `theme: SexyForm::Themes::Bootstrap4Horizontal.new(column_classes: ["col-sm-3","col-sm-9"])`
25
+ - Bootstrap 3
26
+ * `theme: :bootstrap_3_vertical`
27
+ * `theme: :bootstrap_3_inline`
28
+ * `theme: :bootstrap_3_horizontal` or `theme: SexyForm::Themes::Bootstrap3Horizontal.new(column_classes: ["col-sm-3","col-sm-9"])`
29
+ - Bootstrap 2
30
+ * `theme: :bootstrap_2_vertical`
31
+ * `theme: :bootstrap_2_inline`
32
+ * `theme: :bootstrap_2_horizontal`
33
+ - Bulma
34
+ * `theme: :bulma_vertical`
35
+ * `theme: :bulma_horizontal`
36
+ - Foundation
37
+ * `theme: :foundation`
38
+ - Materialize
39
+ * `theme: :materialize`
40
+ - Milligram
41
+ * `theme: :milligram`
42
+ - Semantic UI
43
+ * `theme: :semantic_ui_vertical`
44
+ * `theme: :semantic_ui_inline`
45
+ - None (Default)
46
+ * `theme: :default`
47
+ * `theme: nil`
48
+ * or simply do not provide a `:theme` argument
49
+
50
+ If you dont see your favourite UI library here feel free to create a PR to add it. I recommend creating an issue to discuss it first.
51
+
52
+ # Installation
53
+
54
+ ```ruby
55
+ gem "sexy_form"
56
+ ```
57
+
58
+ # Usage
59
+
60
+ The following field types are supported:
61
+
62
+ - `:checkbox`
63
+ - `:file`
64
+ - `:hidden`
65
+ - `:password`
66
+ - `:radio`
67
+ - `:select`
68
+ - `:text`
69
+ - `:textarea`
70
+
71
+ ## SexyForm in View Templates (Example in Slim)
72
+
73
+ ```ruby
74
+ = SexyForm.form(theme: :bootstrap_4_vertical, action: "/products", method: :post, form_html: {style: "margin-top: 20px;", "data-foo" => "bar"}) do |f|
75
+ .row.main-examples
76
+ .col-sm-6
77
+ ### -- Field Options
78
+ ### type : (Required)
79
+ ### name : (Optional)
80
+ ### label = true : (Optional) String or Bool
81
+ ### help_text : (Optional)
82
+
83
+ ### value : (Optional)
84
+ ### -- Note: The `input_html["value"]` option will take precedence over the :value option (except for `type: :textarea/:select`)
85
+
86
+ ### errors : (Optional) String or Array of Strings
87
+ ### -- Note: Using an Array generates a list of help text elements. If you have an Array of errors and you only want a single help text element, then join your errors array to a single String
88
+
89
+ ### -- For the following Hash options, String keys will take precedence over any Symbol keys
90
+ ### input_html : (Optional) Hash ### contains attributes to be added to the input/field
91
+ ### label_html : (Optional) Hash ### contains attributes to be added to the label
92
+ ### wrapper_html : (Optional) Hash ### contains attributes to be added to the outer wrapper for the label and input
93
+ ### help_text_html : (Optional) Hash ### contains attributes to be added to the help text container
94
+ ### error_html : (Optional) Hash ### contains attributes to be added to the error container(s)
95
+
96
+ = f.field name: "product[name]", label: "Name", type: :text, errors: product_errors["name"]
97
+
98
+ = f.field name: "product[description]", label: "Description", type: :textarea, input_html: {class: "foobar"}, wrapper_html: {style: "margin-top: 10px"}, label_html: {style: "color: red;"}
99
+
100
+ = f.field name: "product[file]", type: :file, help_text: "Must be a PDF", help_text_html: {style: "color: blue;"}
101
+
102
+ .col-sm-6
103
+ = f.field name: "product[available]", type: :checkbox, label: "In Stock?"
104
+
105
+ = f.field name: "product[class]", type: :radio, label: false
106
+
107
+ = f.field name: "product[secret]", type: :hidden, value: "foobar"
108
+
109
+ .row.select-example
110
+ ### -- Additional Options for `type: :select`
111
+ ### collection: {
112
+ ### options : (Required) Array, Nested Array or String. Note: The non-Array String type is for passing in a pre-built html options string
113
+ ### selected : (Optional) String or Array of Strings
114
+ ### disabled : (Optional) String or Array of Strings
115
+ ### include_blank : (Optional) String or Bool
116
+ ### }
117
+ ### -- Note: String keys will take precedence over any Symbol keys
118
+
119
+ ### -- When passing a Nested Array to collection[:options] the Option pairs are defined as: [required_value, optional_label]
120
+ - opts = [["A", "Type A"], ["B" "Type B"], ["C", "Type C"], "Other"]
121
+
122
+ = f.field name: "product[type]", label: "Type", type: :select, collection: {options: opts, selected: ["B"], disabled: ["C"]}
123
+ ```
124
+
125
+ ## SexyForm in Plain Ruby Code
126
+
127
+ When using the `SexyForm.form` method in plain Ruby code, the `<<` syntax is required to add the generated field HTML to the form HTML string
128
+
129
+ ```ruby
130
+ form_html_str = SexyForm.form(theme: :bootstrap_4_vertical, action: "/products", method: :post, form_html: {style: "margin-top: 20px;", "data-foo" => "bar"}) do |f|
131
+ f << f.field(name: "name", type: :text, label: "Name")
132
+ f << f.field(name: "sku", type: :text, label: "SKU")
133
+ f << %Q(<strong>Hello World</strong>"
134
+ end
135
+ ```
136
+
137
+ ## SexyForm without a Form
138
+
139
+ ```ruby
140
+ - f = SexyForm::Builder.new(theme: :bootstrap_4_vertical)
141
+
142
+ = f.field name: "name", type: :text, label: "Name"
143
+ = f.field name: "sku", type: :text, label: "SKU"
144
+ ```
145
+
146
+ ## Error Handling
147
+
148
+ The form builder is capable of handling error messages too. If the `:errors` argument is provided it will generate the appropriate error help text element(s) next to the field.
149
+
150
+ ```ruby
151
+ = SexyForm.form(theme: :bootstrap_4_vertical) do |f|
152
+ = f.field name: "name", type: :text, label: "Name", errors: "cannot be blank"
153
+ = f.field name: "sku", type: :text, label: "SKU", errors: ["must be unique", "incorrect SKU format")
154
+ ```
155
+
156
+ ## Custom Themes
157
+
158
+ SexyForm allows you to create custom themes very easily.
159
+
160
+ Example Usage:
161
+
162
+ ```ruby
163
+ SexyForm.form(theme: :custom)
164
+ ```
165
+
166
+ Example Theme Class:
167
+
168
+ ```ruby
169
+ # config/initializers/sexy_form.rb
170
+
171
+ module SexyForm
172
+ class Themes
173
+ class Custom < BaseTheme
174
+
175
+ ### (Optional) If your theme name doesnt perfectly match the underscored of the theme class name
176
+ def self.theme_name
177
+ "custom"
178
+ end
179
+
180
+ ### (Optional) If your theme requires additional variables similar to `Bootstrap3Horizontal.new(columns: ["col-sm-3", "col-sm-9"])`
181
+ def initialize
182
+ ### For an example see `lib/sexy_form/themes/bootstrap_3_horizontal.rb`
183
+ end
184
+
185
+ def wrap_field(field_type: , html_field: , html_label: nil, html_help_text: nil, html_errors: nil, wrapper_html_attributes: {})
186
+ s = ""
187
+
188
+ wrapper_html_attributes["class"] = "form-group #{wrapper_html_attributes["class"]}".strip
189
+
190
+ ### `SexyForm.build_html_attr_string` is the one and only helper method for Themes
191
+ ### It converts any Hash to an HTML Attributes String
192
+ ### Example: {"class" => "foo", "data-role" => "ninja"} converts to "class=\"foo\" data-role=\"ninja\""
193
+ attr_str = SexyForm.build_html_attr_string(wrapper_html_attributes)
194
+
195
+ s << "#{attr_str.empty? ? "<div>" : (<div #{attr_str}>)}"
196
+
197
+ if ["checkbox", "radio"].include?(field_type) && html_label && (i = html_label.index(">"))
198
+ s << html_label.insert(i+1, "#{html_field} ")
199
+ else
200
+ s << "#{html_label}"
201
+ s << "#{html_field}"
202
+ end
203
+
204
+ s << "#{html_help_text}"
205
+
206
+ if html_errors
207
+ s << html_errors.join
208
+ end
209
+
210
+ s << "</div>"
211
+
212
+ s
213
+ end
214
+
215
+ def input_html_attributes(field_type: , has_errors: , html_attrs:)
216
+ html_attrs["class"] = "form-field other-class #{html_attrs["class"]}".strip
217
+ html_attrs["style"] = "color: blue; #{html_attrs["style"]}".strip
218
+
219
+ unless html_attrs.has_key?("data-foo")
220
+ html_attrs["data-foo"] = "bar"
221
+ end
222
+
223
+ html_attrs
224
+ end
225
+
226
+ def label_html_attributes(html_attrs: , field_type: , has_errors:)
227
+ html_attrs["class"] = "form-label other-class #{html_attrs["class"]}".strip
228
+ html_attrs["style"] = "color: red; #{html_attrs["style"]}".strip
229
+ html_attrs
230
+ end
231
+
232
+ def form_html_attributes(html_attrs:)
233
+ html_attrs["class"] = "form-inline #{html_attrs["class"]}"
234
+ html_attrs
235
+ end
236
+
237
+ def build_html_help_text(help_text: , html_attrs:)
238
+ html_attrs["class"] = "help-text #{html_attrs["class"]}".strip
239
+
240
+ s = ""
241
+ s << (html_attrs.empty? ? "<div>" : "<div #{build_html_attr_string(html_attrs)}>")
242
+ s << "#{help_text}"
243
+ s << "</div>"
244
+ s
245
+ end
246
+
247
+ def build_html_error(error: , html_attrs:)
248
+ html_attrs["class"] = "help-text error #{html_attrs["class"]}".strip
249
+ html_attrs["style"] = "color: red; #{html_attrs["style"]}".strip
250
+
251
+ s = ""
252
+ s << (html_attrs.empty? ? "<div>" : "<div #{build_html_attr_string(html_attrs)}>")
253
+ s << "#{error}"
254
+ s << "</div>"
255
+ s
256
+ end
257
+
258
+ end
259
+ end
260
+ end
261
+ ```
262
+
263
+ # Crystal Alternative
264
+
265
+ This library was originally written for Crystal language as [FormBuilder.cr](https://github.com/westonganger/form_builder.cr)
266
+
267
+ The pattern/implementation of FormBuilder.cr turned out so beautifully that I felt the desire to have the same syntax available in the Ruby language. Many Ruby developers also write Crystal and vice versa so this only made sense. What was awesome is that, the Crystal and Ruby syntax is so similar that converting Crystal code to Ruby was straight forward and quite simple.
268
+
269
+ # Credits
270
+
271
+ Created & Maintained by [Weston Ganger](https://westonganger.com) - [@westonganger](https://github.com/westonganger)
272
+
273
+ For any consulting or contract work please contact me via my company website: [Solid Foundation Web Development](https://solidfoundationwebdev.com)
274
+
275
+ [![Solid Foundation Web Development Logo](https://solidfoundationwebdev.com/logo-sm.png)](https://solidfoundationwebdev.com)
@@ -0,0 +1,15 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/lib/sexy_form/version')
2
+ require 'bundler/gem_tasks'
3
+
4
+ task :console do
5
+ require 'sexy_form'
6
+
7
+ require 'irb'
8
+ binding.irb
9
+ end
10
+
11
+ require 'rspec/core/rake_task'
12
+ RSpec::Core::RakeTask.new(:spec)
13
+
14
+ task default: :spec
15
+ task test: :spec
@@ -0,0 +1,84 @@
1
+ require "sexy_form/version"
2
+ require "sexy_form/themes"
3
+
4
+ ### Require all themes
5
+ require "sexy_form/themes/base_theme"
6
+ Dir[File.join(__dir__, "sexy_form/themes/*.rb")].each do |f|
7
+ require "sexy_form/themes/#{f.split("/").last}"
8
+ end
9
+
10
+ require "sexy_form/builder"
11
+
12
+ module SexyForm
13
+ def self.form(action: nil, method: "post", theme: nil, form_html: {})
14
+ self.verify_argument_type(arg_name: :form_html, value: form_html, expected_type: Hash)
15
+
16
+ action = action.to_s
17
+ method = method.to_s
18
+
19
+ builder = SexyForm::Builder.new(theme: theme)
20
+
21
+ themed_form_html = builder.theme.form_html_attributes(html_attrs: self.safe_string_hash(form_html))
22
+
23
+ themed_form_html["method"] = method.to_s == "get" ? "get" : "post"
24
+
25
+ if themed_form_html["multipart"] == true
26
+ themed_form_html.delete("multipart")
27
+ themed_form_html["enctype"] = "multipart/form-data"
28
+ end
29
+
30
+ str = ""
31
+
32
+ str << %Q(<form #{self.build_html_attr_string(themed_form_html)}>)
33
+
34
+ unless ["get", "post"].include?(method.to_s)
35
+ str << %Q(<input type="hidden" name="_method" value="#{method}")
36
+ end
37
+
38
+ if block_given?
39
+ yield builder
40
+ end
41
+
42
+ unless builder.html_string.empty?
43
+ str << builder.html_string
44
+ end
45
+
46
+ str << "</form>"
47
+
48
+ str
49
+ end
50
+
51
+ protected
52
+
53
+ def self.build_html_attr_string(hash)
54
+ hash.map{|k, v| "#{k}=\"#{v}\""}.join(" ")
55
+ end
56
+
57
+ def self.safe_string_hash(h)
58
+ new_h = {}
59
+ h.each do |k, v|
60
+ unless new_h.has_key?(k.to_s)
61
+ if k.is_a?(String)
62
+ new_h[k] = v
63
+ elsif !h.has_key?(k.to_s)
64
+ new_h[k.to_s] = v
65
+ end
66
+ end
67
+ end
68
+ new_h
69
+ end
70
+
71
+ def self.verify_argument_type(arg_name:, value:, expected_type:)
72
+ if expected_type.is_a?(Array)
73
+ invalid = expected_type.all?{|t| !value.is_a?(t) }
74
+ elsif !value.is_a?(expected_type)
75
+ invalid = true
76
+ end
77
+
78
+ if invalid
79
+ raise ArgumentError.new("Invalid type passed to argument :#{arg_name}")
80
+ end
81
+ end
82
+ ### END PROTECTED METHODS
83
+
84
+ end
@@ -0,0 +1,306 @@
1
+ module SexyForm
2
+ class Builder
3
+ FIELD_TYPES = ["checkbox", "file", "hidden", "password", "radio", "select", "text", "textarea"].freeze
4
+ INPUT_TYPES = ["checkbox", "file", "hidden", "password", "radio", "text"].freeze
5
+ COLLECTION_KEYS = ["options", "selected", "disabled", "include_blank"].freeze
6
+
7
+ def initialize(theme: nil)
8
+ @html = []
9
+
10
+ if theme
11
+ if theme.is_a?(SexyForm::Themes::BaseTheme)
12
+ @theme = theme
13
+ else
14
+ @theme = Themes.from_name(theme.to_s).new
15
+ end
16
+ else
17
+ @theme = SexyForm::Themes::Default.new
18
+ end
19
+ end
20
+
21
+ def theme
22
+ @theme
23
+ end
24
+
25
+ def <<(v)
26
+ v = v.to_s
27
+ @html.push(v)
28
+ v
29
+ end
30
+
31
+ ### This method should be considered private
32
+ def html_string
33
+ @html.join("")
34
+ end
35
+
36
+ def field(
37
+ type: ,
38
+ name: nil,
39
+ value: nil,
40
+ label: nil,
41
+ help_text: nil,
42
+ errors: nil,
43
+ input_html: {},
44
+ label_html: {},
45
+ help_text_html: {},
46
+ wrapper_html: {},
47
+ error_html: {},
48
+ collection: nil
49
+ )
50
+ type = type.to_s
51
+
52
+ unless FIELD_TYPES.include?(type)
53
+ raise ArgumentError.new("Invalid :type argument, valid field types are: #{FIELD_TYPES.join(", ")}`")
54
+ end
55
+
56
+ if collection && type != "select"
57
+ raise ArgumentError.new("Argument :collection is not supported for type: :#{type}")
58
+ end
59
+
60
+ if errors
61
+ SexyForm.verify_argument_type(arg_name: :errors, value: errors, expected_type: [Array, String])
62
+ end
63
+
64
+ SexyForm.verify_argument_type(arg_name: :input_html, value: input_html, expected_type: Hash)
65
+ SexyForm.verify_argument_type(arg_name: :label_html, value: label_html, expected_type: Hash)
66
+ SexyForm.verify_argument_type(arg_name: :wrapper_html, value: wrapper_html, expected_type: Hash)
67
+ SexyForm.verify_argument_type(arg_name: :help_text_html, value: help_text_html, expected_type: Hash)
68
+ SexyForm.verify_argument_type(arg_name: :error_html, value: error_html, expected_type: Hash)
69
+
70
+ if errors
71
+ if errors.is_a?(String)
72
+ errors = errors.empty? ? nil : [errors]
73
+ else
74
+ errors = errors.reject{|x| x.empty?}
75
+
76
+ if errors.empty?
77
+ errors = nil
78
+ end
79
+ end
80
+
81
+ if errors.nil?
82
+ html_errors = errors.map{|x|
83
+ @theme.build_html_error(error: x, field_type: type, html_attrs: SexyForm.safe_string_hash(errors))
84
+ }
85
+ end
86
+ end
87
+
88
+ themed_input_html = @theme.input_html_attributes(html_attrs: SexyForm.safe_string_hash(input_html), field_type: type, has_errors: (errors && !errors.empty?))
89
+
90
+ themed_label_html = @theme.label_html_attributes(html_attrs: SexyForm.safe_string_hash(label_html), field_type: type, has_errors: (errors && !errors.empty?))
91
+
92
+ if name
93
+ themed_input_html["name"] ||= name.to_s
94
+
95
+ unless themed_input_html.has_key?("id")
96
+ themed_input_html["id"] = css_safe(name)
97
+ end
98
+ end
99
+
100
+ if !themed_input_html.has_key?("value") && value && !value.to_s.empty? && INPUT_TYPES.include?(type)
101
+ themed_input_html["value"] = value.to_s
102
+ end
103
+
104
+ if themed_input_html.has_key?("id")
105
+ themed_label_html["for"] ||= themed_input_html["id"]
106
+ end
107
+
108
+ if ["checkbox", "radio"].include?(type)
109
+ ### Allow passing checked=true/false
110
+ if themed_input_html["checked"] == "true"
111
+ themed_input_html["checked"] = "checked"
112
+ elsif themed_input_html["checked"] == "false"
113
+ themed_input_html.delete("checked")
114
+ end
115
+ end
116
+
117
+ case type
118
+ when "checkbox"
119
+ html_field = input_field(type: type, attrs: themed_input_html)
120
+ when "file"
121
+ html_field = input_field(type: type, attrs: themed_input_html)
122
+ when "hidden"
123
+ html_field = input_field(type: type, attrs: themed_input_html)
124
+ when "password"
125
+ html_field = input_field(type: type, attrs: themed_input_html)
126
+ when "radio"
127
+ html_field = input_field(type: type, attrs: themed_input_html)
128
+ when "select"
129
+ if !collection
130
+ raise ArgumentError.new("Required argument `:collection` not provided")
131
+ end
132
+
133
+ SexyForm.verify_argument_type(arg_name: :collection, value: collection, expected_type: Hash)
134
+
135
+ safe_collection = SexyForm.safe_string_hash(collection)
136
+
137
+ if safe_collection.keys.any?{|x| !COLLECTION_KEYS.include?(x) }
138
+ raise ArgumentError.new("Invalid key passed to :collection argument. Supported keys are #{COLLECTION_KEYS.map{|x| ":#{x}"}.join(", ")}")
139
+ end
140
+
141
+ if !safe_collection.has_key?("options")
142
+ raise ArgumentError.new("Required argument `collection[:options]` not provided")
143
+ end
144
+
145
+ if safe_collection["options"].is_a?(Array)
146
+ collection_options = safe_collection["options"].map do |x|
147
+ if x.is_a?(Enumerable)
148
+ x.first(2).map{|y| y.respond_to?(:to_s) ? y.to_s : ""}
149
+ else
150
+ [(x.respond_to?(:to_s) ? x.to_s : "")]
151
+ end
152
+ end
153
+ elsif safe_collection["options"].is_a?(String)
154
+ collection_options = safe_collection["options"]
155
+ else
156
+ raise ArgumentError.new("Invalid type passed to argument `collection[:options]``")
157
+ end
158
+
159
+ if collection_options.is_a?(String)
160
+ ["selected", "disabled", "include_blank"].each do |k|
161
+ if safe_collection.has_key?(k)
162
+ raise ArgumentError.new("Argument `collection[:#{k}]` is not allowed when passing a pre-made HTML Options String to `collection[:options]`")
163
+ end
164
+ end
165
+ else
166
+ if safe_collection.has_key?("include_blank") && safe_collection["include_blank"] != false
167
+ collection_options.unshift([(safe_collection["include_blank"] == true ? "" : "#{safe_collection["include_blank"]}")])
168
+ end
169
+
170
+ if safe_collection.has_key?("selected")
171
+ if safe_collection["selected"].is_a?(Array)
172
+ collection_selected = safe_collection["selected"].map{|x| x.respond_to?(:to_s) ? x.to_s : ""}
173
+ else
174
+ collection_selected = [safe_collection["selected"].to_s]
175
+ end
176
+ end
177
+
178
+ if safe_collection.has_key?("disabled")
179
+ if safe_collection["disabled"].is_a?(Array)
180
+ collection_disabled = safe_collection["disabled"].map{|x| x.respond_to?(:to_s) ? x.to_s : ""}
181
+ else
182
+ collection_disabled = [safe_collection["disabled"].to_s]
183
+ end
184
+ end
185
+ end
186
+
187
+ if value && safe_collection["selected"]
188
+ raise ArgumentError.new("Cannot provide :value and :selected arguments together. The :selected argument is recommended for field `type: :select.`")
189
+ else
190
+ v = safe_collection["selected"] || value
191
+ if v
192
+ safe_collection["selected"] = v
193
+ end
194
+ end
195
+
196
+ html_field = select_field(options: collection_options, selected: collection_selected, disabled: collection_disabled, attrs: themed_input_html)
197
+ when "text"
198
+ html_field = input_field(type: type, attrs: themed_input_html)
199
+ when "textarea"
200
+ if themed_input_html.has_key?("size")
201
+ themed_input_html["cols"], themed_input_html["rows"] = themed_input_html.delete("size").to_s.split("x")
202
+ end
203
+
204
+ html_field = ""
205
+ html_field << (themed_input_html.empty? ? "<textarea>" : "<textarea #{SexyForm.build_html_attr_string(themed_input_html)}>")
206
+ html_field << "#{themed_input_html["value"]}"
207
+ html_field << "</textarea>"
208
+ end
209
+
210
+ if label != false
211
+ if label.is_a?(String)
212
+ label_text = label
213
+ elsif [nil, true].include?(label) && name
214
+ label_text = titleize(name)
215
+ end
216
+
217
+ if label_text
218
+ html_label = ""
219
+ html_label << (themed_label_html.empty? ? "<label>" : "<label #{SexyForm.build_html_attr_string(themed_label_html)}>")
220
+ html_label << label_text
221
+ html_label << "</label>"
222
+ end
223
+ end
224
+
225
+ if help_text
226
+ html_help_text = @theme.build_html_help_text(field_type: type, help_text: help_text, html_attrs: SexyForm.safe_string_hash(help_text_html))
227
+ end
228
+
229
+ @theme.wrap_field(
230
+ field_type: type,
231
+ html_field: html_field,
232
+ html_label: html_label,
233
+ html_help_text: html_help_text,
234
+ html_errors: html_errors,
235
+ wrapper_html_attributes: SexyForm.safe_string_hash(wrapper_html)
236
+ )
237
+ end
238
+
239
+ private
240
+
241
+ def input_field(type: , attrs: {})
242
+ unless INPUT_TYPES.include?(type.to_s)
243
+ raise ArgumentError.new("Invalid input :type, valid input types are `#{INPUT_TYPES.join(", ")}`")
244
+ end
245
+
246
+ attrs.delete("type")
247
+
248
+ boolean_attrs = []
249
+
250
+ ["disabled"].each do |opt|
251
+ if attrs[opt]
252
+ boolean_attrs.push(attrs[opt])
253
+ attrs.delete(opt)
254
+ end
255
+ end
256
+
257
+ str_attrs = attrs.map{|k, v| "#{k}=\"#{v}\""}
258
+
259
+ if !boolean_attrs.empty?
260
+ str_attrs.push(boolean_attrs.join(" "))
261
+ end
262
+
263
+ "<input type=\"#{type}\"#{" " unless str_attrs.empty?}#{str_attrs.join(" ")}>"
264
+ end
265
+
266
+ def select_field(options:, selected: nil, disabled: nil, attrs: {})
267
+ s = ""
268
+
269
+ s << (attrs.empty? ? "<select>" : "<select #{SexyForm.build_html_attr_string(attrs)}>")
270
+
271
+ if options.is_a?(String)
272
+ s << options
273
+ else
274
+ options.map do |option|
275
+ v = option[0].to_s
276
+
277
+ s << "<option value=\"#{v}\""
278
+
279
+ if selected
280
+ s << "#{" selected=\"selected\"" if selected.include?(v)}"
281
+ end
282
+
283
+ if disabled
284
+ s << "#{" disabled=\"disabled\"" if disabled.include?(v)}"
285
+ end
286
+
287
+ s << ">#{option[1] || v}</option>"
288
+ end
289
+ end
290
+
291
+ s << "</select>"
292
+
293
+ s
294
+ end
295
+
296
+ def css_safe(value)
297
+ values = value.to_s.strip.split(" ")
298
+ values.map{|v| v.gsub(/[^\w-]+/, " ").strip.gsub(/\s+/, "_")}.join(" ")
299
+ end
300
+
301
+ def titleize(value)
302
+ value.to_s.gsub(/\W|_/, " ").split(" ").map{|x| x.capitalize}.join(" ")
303
+ end
304
+
305
+ end
306
+ end