sexy_form 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE +21 -0
- data/README.md +275 -0
- data/Rakefile +15 -0
- data/lib/sexy_form.rb +84 -0
- data/lib/sexy_form/builder.rb +306 -0
- data/lib/sexy_form/themes.rb +22 -0
- data/lib/sexy_form/themes/base_theme.rb +39 -0
- data/lib/sexy_form/themes/bootstrap_2_horizontal.rb +83 -0
- data/lib/sexy_form/themes/bootstrap_2_inline.rb +73 -0
- data/lib/sexy_form/themes/bootstrap_2_vertical.rb +80 -0
- data/lib/sexy_form/themes/bootstrap_3_horizontal.rb +95 -0
- data/lib/sexy_form/themes/bootstrap_3_inline.rb +71 -0
- data/lib/sexy_form/themes/bootstrap_3_vertical.rb +70 -0
- data/lib/sexy_form/themes/bootstrap_4_horizontal.rb +95 -0
- data/lib/sexy_form/themes/bootstrap_4_inline.rb +80 -0
- data/lib/sexy_form/themes/bootstrap_4_vertical.rb +79 -0
- data/lib/sexy_form/themes/bulma_horizontal.rb +81 -0
- data/lib/sexy_form/themes/bulma_vertical.rb +73 -0
- data/lib/sexy_form/themes/default.rb +55 -0
- data/lib/sexy_form/themes/foundation.rb +67 -0
- data/lib/sexy_form/themes/materialize.rb +65 -0
- data/lib/sexy_form/themes/milligram.rb +62 -0
- data/lib/sexy_form/themes/semantic_ui_inline.rb +63 -0
- data/lib/sexy_form/themes/semantic_ui_vertical.rb +63 -0
- data/lib/sexy_form/version.rb +3 -0
- data/spec/custom_assertions.rb +21 -0
- data/spec/sexy_form/builder_spec.rb +104 -0
- data/spec/sexy_form/themes/base_theme_spec.rb +16 -0
- data/spec/sexy_form/themes/bootstrap_2_horizontal_spec.rb +114 -0
- data/spec/sexy_form/themes/bootstrap_2_inline_spec.rb +108 -0
- data/spec/sexy_form/themes/bootstrap_2_vertical_spec.rb +111 -0
- data/spec/sexy_form/themes/bootstrap_3_horizontal_spec.rb +116 -0
- data/spec/sexy_form/themes/bootstrap_3_inline_spec.rb +104 -0
- data/spec/sexy_form/themes/bootstrap_3_vertical_spec.rb +122 -0
- data/spec/sexy_form/themes/bootstrap_4_horizontal_spec.rb +124 -0
- data/spec/sexy_form/themes/bootstrap_4_inline_spec.rb +116 -0
- data/spec/sexy_form/themes/bootstrap_4_vertical_spec.rb +114 -0
- data/spec/sexy_form/themes/bulma_horizontal_spec.rb +126 -0
- data/spec/sexy_form/themes/bulma_vertical_spec.rb +114 -0
- data/spec/sexy_form/themes/default_spec.rb +102 -0
- data/spec/sexy_form/themes/foundation_spec.rb +103 -0
- data/spec/sexy_form/themes/materialize_spec.rb +103 -0
- data/spec/sexy_form/themes/milligram_spec.rb +120 -0
- data/spec/sexy_form/themes/semantic_ui_inline_spec.rb +105 -0
- data/spec/sexy_form/themes/semantic_ui_vertical_spec.rb +105 -0
- data/spec/sexy_form/themes/theme_spec_helper.rb +0 -0
- data/spec/sexy_form/themes_spec.rb +52 -0
- data/spec/sexy_form_spec.rb +54 -0
- data/spec/spec_helper.rb +16 -0
- metadata +160 -0
checksums.yaml
ADDED
@@ -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
|
data/CHANGELOG.md
ADDED
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.
|
data/README.md
ADDED
@@ -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)
|
data/Rakefile
ADDED
@@ -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
|
data/lib/sexy_form.rb
ADDED
@@ -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
|