simple_form 1.3.0 → 1.3.1
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.
- data/.gitignore +2 -0
- data/.gitmodules +3 -0
- data/CHANGELOG.rdoc +85 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +82 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +16 -2
- data/Rakefile +27 -0
- data/lib/generators/simple_form/templates/_form.html.haml +2 -2
- data/lib/simple_form/action_view_extensions/builder.rb +30 -5
- data/lib/simple_form/action_view_extensions/form_helper.rb +3 -4
- data/lib/simple_form/components/labels.rb +1 -1
- data/lib/simple_form/form_builder.rb +18 -13
- data/lib/simple_form/inputs/base.rb +13 -3
- data/lib/simple_form/inputs/collection_input.rb +5 -0
- data/lib/simple_form/inputs/date_time_input.rb +4 -0
- data/lib/simple_form/inputs/priority_input.rb +5 -1
- data/lib/simple_form/map_type.rb +5 -2
- data/lib/simple_form/version.rb +1 -1
- data/simple_form.gemspec +22 -0
- data/test/components/label_test.rb +10 -0
- data/test/form_builder_test.rb +49 -0
- data/test/inputs_test.rb +84 -0
- data/test/support/misc_helpers.rb +14 -0
- data/test/support/models.rb +2 -1
- metadata +20 -27
data/.gitignore
ADDED
data/.gitmodules
ADDED
data/CHANGELOG.rdoc
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
* enhancements
|
|
2
|
+
* Add :autofocus HTML5 attribute support (by github.com/jpzwarte)
|
|
3
|
+
* Add possibility to specify custom builder and inherit mappings (by github.com/rejeep)
|
|
4
|
+
* Make custom mappings work with all attributes types (by github.com/rafaelfranca)
|
|
5
|
+
* Add support for procs/lambdas in text/value methods for collection_select
|
|
6
|
+
|
|
7
|
+
* deprecation
|
|
8
|
+
* removed the deprecated :remote_form_for
|
|
9
|
+
|
|
10
|
+
* bug fix
|
|
11
|
+
* Only add the "required" HTML 5 attribute for valid inputs, disable in selects (not allowed)
|
|
12
|
+
* Fix error when using hints without an attribute (by github.com/butsjoh and github.com/rafaelfranca)
|
|
13
|
+
* Fix messy html output for hint, error and label components (by github.com/butsjoh and github.com/rafaelfranca)
|
|
14
|
+
* Allow direct setting of for attribute on label (by github.com/Bertg)
|
|
15
|
+
|
|
16
|
+
== 1.3.0
|
|
17
|
+
|
|
18
|
+
* enhancements
|
|
19
|
+
* Allow collection input to accept a collection of symbols
|
|
20
|
+
* Add default css class to button
|
|
21
|
+
* Allow forms for objects that don't respond to the "errors" method
|
|
22
|
+
* collection_check_boxes and collection_radio now wrap the input in the label
|
|
23
|
+
* Automatic add min/max values for numeric attributes based on validations and step for integers - HTML5 (by github.com/dasch)
|
|
24
|
+
* Add :placeholder option for string inputs, allowing customization through I18n - HTML5 (by github.com/jonathan)
|
|
25
|
+
* Add :search and :tel input types, with :tel mapping automatically from attributes matching "phone" - HTML5
|
|
26
|
+
* Add :required html attribute for required inputs - HTML5
|
|
27
|
+
* Add optional :components option to input to control component rendering (by github.com/khoan)
|
|
28
|
+
* Add SimpleForm.translate as an easy way to turn off SimpleForm internal translations
|
|
29
|
+
* Add :disabled option for all inputs (by github.com/fabiob)
|
|
30
|
+
* Add collection wrapper tag and item wrapper tag to wrap elements in collection helpers - radio / check boxes
|
|
31
|
+
* Add SimpleForm.input_mappings to allow configuring custom mappings for inputs (by github.com/TMaYaD)
|
|
32
|
+
|
|
33
|
+
* bug fix
|
|
34
|
+
* Search for validations on both association and attribute
|
|
35
|
+
* Use controller.action_name to lookup action only when available, to fix issue with Rspec views tests (by github.com/rafaelfranca)
|
|
36
|
+
|
|
37
|
+
== 1.2.2
|
|
38
|
+
|
|
39
|
+
* enhancements
|
|
40
|
+
* Compatibility with Rails 3 RC
|
|
41
|
+
|
|
42
|
+
== 1.2.1
|
|
43
|
+
|
|
44
|
+
* enhancements
|
|
45
|
+
* Added haml generator support (by github.com/grimen)
|
|
46
|
+
* Added error_notification message to form builder
|
|
47
|
+
* Added required by default as configuration option
|
|
48
|
+
* Added label_input as component, allowing boolean to change its order (input appearing first than label)
|
|
49
|
+
* Added error_method to tidy up how errors are exhibited
|
|
50
|
+
* Added error class on wrappers (by github.com/jduff)
|
|
51
|
+
* Changed numeric types to have type=number for HTML5
|
|
52
|
+
|
|
53
|
+
== 1.2.0
|
|
54
|
+
|
|
55
|
+
* deprecation
|
|
56
|
+
* Changed simple_form_install generator to simple_form:install
|
|
57
|
+
|
|
58
|
+
* enhancements
|
|
59
|
+
* Added support to presence validation to check if attribute is required or not (by github.com/gcirne)
|
|
60
|
+
* Added .input as class to wrapper tag
|
|
61
|
+
* Added config options for hint and error tags (by github.com/tjogin)
|
|
62
|
+
|
|
63
|
+
== 1.1.3
|
|
64
|
+
|
|
65
|
+
* deprecation
|
|
66
|
+
* removed :conditions, :order, :joins and :include support in f.association
|
|
67
|
+
|
|
68
|
+
== 1.1.2
|
|
69
|
+
|
|
70
|
+
* bug fix
|
|
71
|
+
* Ensure type is set to "text" and not "string"
|
|
72
|
+
|
|
73
|
+
== 1.1.1
|
|
74
|
+
|
|
75
|
+
* bug fix
|
|
76
|
+
* Fix some escaping issues
|
|
77
|
+
|
|
78
|
+
== 1.1.0
|
|
79
|
+
|
|
80
|
+
* enhancements
|
|
81
|
+
* Rails 3 support with generators, templates and HTML 5
|
|
82
|
+
|
|
83
|
+
== 1.0
|
|
84
|
+
|
|
85
|
+
* First release
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
GEM
|
|
2
|
+
remote: http://rubygems.org/
|
|
3
|
+
specs:
|
|
4
|
+
abstract (1.0.0)
|
|
5
|
+
actionmailer (3.0.3)
|
|
6
|
+
actionpack (= 3.0.3)
|
|
7
|
+
mail (~> 2.2.9)
|
|
8
|
+
actionpack (3.0.3)
|
|
9
|
+
activemodel (= 3.0.3)
|
|
10
|
+
activesupport (= 3.0.3)
|
|
11
|
+
builder (~> 2.1.2)
|
|
12
|
+
erubis (~> 2.6.6)
|
|
13
|
+
i18n (~> 0.4)
|
|
14
|
+
rack (~> 1.2.1)
|
|
15
|
+
rack-mount (~> 0.6.13)
|
|
16
|
+
rack-test (~> 0.5.6)
|
|
17
|
+
tzinfo (~> 0.3.23)
|
|
18
|
+
activemodel (3.0.3)
|
|
19
|
+
activesupport (= 3.0.3)
|
|
20
|
+
builder (~> 2.1.2)
|
|
21
|
+
i18n (~> 0.4)
|
|
22
|
+
activerecord (3.0.3)
|
|
23
|
+
activemodel (= 3.0.3)
|
|
24
|
+
activesupport (= 3.0.3)
|
|
25
|
+
arel (~> 2.0.2)
|
|
26
|
+
tzinfo (~> 0.3.23)
|
|
27
|
+
activeresource (3.0.3)
|
|
28
|
+
activemodel (= 3.0.3)
|
|
29
|
+
activesupport (= 3.0.3)
|
|
30
|
+
activesupport (3.0.3)
|
|
31
|
+
arel (2.0.6)
|
|
32
|
+
builder (2.1.2)
|
|
33
|
+
columnize (0.3.2)
|
|
34
|
+
erubis (2.6.6)
|
|
35
|
+
abstract (>= 1.0.0)
|
|
36
|
+
i18n (0.5.0)
|
|
37
|
+
linecache (0.43)
|
|
38
|
+
mail (2.2.12)
|
|
39
|
+
activesupport (>= 2.3.6)
|
|
40
|
+
i18n (>= 0.4.0)
|
|
41
|
+
mime-types (~> 1.16)
|
|
42
|
+
treetop (~> 1.4.8)
|
|
43
|
+
mime-types (1.16)
|
|
44
|
+
mocha (0.9.10)
|
|
45
|
+
rake
|
|
46
|
+
polyglot (0.3.1)
|
|
47
|
+
rack (1.2.1)
|
|
48
|
+
rack-mount (0.6.13)
|
|
49
|
+
rack (>= 1.0.0)
|
|
50
|
+
rack-test (0.5.6)
|
|
51
|
+
rack (>= 1.0)
|
|
52
|
+
rails (3.0.3)
|
|
53
|
+
actionmailer (= 3.0.3)
|
|
54
|
+
actionpack (= 3.0.3)
|
|
55
|
+
activerecord (= 3.0.3)
|
|
56
|
+
activeresource (= 3.0.3)
|
|
57
|
+
activesupport (= 3.0.3)
|
|
58
|
+
bundler (~> 1.0)
|
|
59
|
+
railties (= 3.0.3)
|
|
60
|
+
railties (3.0.3)
|
|
61
|
+
actionpack (= 3.0.3)
|
|
62
|
+
activesupport (= 3.0.3)
|
|
63
|
+
rake (>= 0.8.7)
|
|
64
|
+
thor (~> 0.14.4)
|
|
65
|
+
rake (0.8.7)
|
|
66
|
+
ruby-debug (0.10.4)
|
|
67
|
+
columnize (>= 0.1)
|
|
68
|
+
ruby-debug-base (~> 0.10.4.0)
|
|
69
|
+
ruby-debug-base (0.10.4)
|
|
70
|
+
linecache (>= 0.3)
|
|
71
|
+
thor (0.14.6)
|
|
72
|
+
treetop (1.4.9)
|
|
73
|
+
polyglot (>= 0.3.1)
|
|
74
|
+
tzinfo (0.3.23)
|
|
75
|
+
|
|
76
|
+
PLATFORMS
|
|
77
|
+
ruby
|
|
78
|
+
|
|
79
|
+
DEPENDENCIES
|
|
80
|
+
mocha
|
|
81
|
+
rails (~> 3.0.0)
|
|
82
|
+
ruby-debug
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2010 PlataformaTec http://blog.plataformatec.com.br/
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
CHANGED
|
@@ -370,9 +370,23 @@ SimpleForm has several configuration values. You can read and change them in the
|
|
|
370
370
|
|
|
371
371
|
rails generate simple_form:install
|
|
372
372
|
|
|
373
|
-
==
|
|
373
|
+
== Custom form builder
|
|
374
374
|
|
|
375
|
-
|
|
375
|
+
You can create a custom form builder that uses SimpleForm.
|
|
376
|
+
|
|
377
|
+
Create a helper method that calls simple_form_for with a custom builder:
|
|
378
|
+
|
|
379
|
+
def custom_form_for(object, *args, &block)
|
|
380
|
+
simple_form_for(object, *(args << { :builder => CustomFormBuilder }), &block)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
Create a form builder class that inherits from SimpleForm::FormBuilder.
|
|
384
|
+
|
|
385
|
+
class CustomFormBuilder < SimpleForm::FormBuilder
|
|
386
|
+
def input(attribute_name, *args, &block)
|
|
387
|
+
super(attribute_name, *(args << { :input_html => { :class => 'custom' } }), &block)
|
|
388
|
+
end
|
|
389
|
+
end
|
|
376
390
|
|
|
377
391
|
== Maintainers
|
|
378
392
|
|
data/Rakefile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
|
|
3
|
+
require 'bundler'
|
|
4
|
+
Bundler::GemHelper.install_tasks
|
|
5
|
+
|
|
6
|
+
require 'rake/testtask'
|
|
7
|
+
require 'rake/rdoctask'
|
|
8
|
+
|
|
9
|
+
desc 'Default: run unit tests.'
|
|
10
|
+
task :default => :test
|
|
11
|
+
|
|
12
|
+
desc 'Test the simple_form plugin.'
|
|
13
|
+
Rake::TestTask.new(:test) do |t|
|
|
14
|
+
t.libs << 'lib'
|
|
15
|
+
t.libs << 'test'
|
|
16
|
+
t.pattern = 'test/**/*_test.rb'
|
|
17
|
+
t.verbose = true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
desc 'Generate documentation for the simple_form plugin.'
|
|
21
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
|
22
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
23
|
+
rdoc.title = 'SimpleForm'
|
|
24
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
|
25
|
+
rdoc.rdoc_files.include('README.rdoc')
|
|
26
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
27
|
+
end
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
prohibited this <%= singular_name %> from being saved:
|
|
7
7
|
|
|
8
8
|
%ul
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
- @<%= singular_name %>.errors.full_messages.each do |msg|
|
|
10
|
+
%li= msg
|
|
11
11
|
|
|
12
12
|
.inputs
|
|
13
13
|
<%- attributes.each do |attribute| -%>
|
|
@@ -7,7 +7,9 @@ module SimpleForm
|
|
|
7
7
|
# Create a collection of radio inputs for the attribute. Basically this
|
|
8
8
|
# helper will create a radio input associated with a label for each
|
|
9
9
|
# text/value option in the collection, using value_method and text_method
|
|
10
|
-
# to convert these text/value.
|
|
10
|
+
# to convert these text/value. You can give a symbol or a proc to both
|
|
11
|
+
# value_method and text_method, that will be evaluated for each item in
|
|
12
|
+
# the collection.
|
|
11
13
|
#
|
|
12
14
|
# == Examples
|
|
13
15
|
#
|
|
@@ -42,9 +44,11 @@ module SimpleForm
|
|
|
42
44
|
end
|
|
43
45
|
end
|
|
44
46
|
|
|
45
|
-
# Creates a collection of check boxes for each item in the collection,
|
|
46
|
-
# with a clickable label. Use value_method and text_method to
|
|
47
|
-
# the collection for use as text/value in check boxes.
|
|
47
|
+
# Creates a collection of check boxes for each item in the collection,
|
|
48
|
+
# associated with a clickable label. Use value_method and text_method to
|
|
49
|
+
# convert items in the collection for use as text/value in check boxes.
|
|
50
|
+
# You can give a symbol or a proc to both value_method and text_method,
|
|
51
|
+
# that will be evaluated for each item in the collection.
|
|
48
52
|
#
|
|
49
53
|
# == Examples
|
|
50
54
|
#
|
|
@@ -154,4 +158,25 @@ module SimpleForm
|
|
|
154
158
|
end
|
|
155
159
|
end
|
|
156
160
|
|
|
157
|
-
ActionView::Helpers::FormBuilder
|
|
161
|
+
class ActionView::Helpers::FormBuilder
|
|
162
|
+
include SimpleForm::ActionViewExtensions::Builder
|
|
163
|
+
|
|
164
|
+
# Override default Rails collection_select helper to handle lambdas/procs in
|
|
165
|
+
# text and value methods, so it works the same way as collection_radio and
|
|
166
|
+
# collection_check_boxes in SimpleForm. If none of text/value methods is a
|
|
167
|
+
# callable object, then it just delegates back to original collection select.
|
|
168
|
+
#
|
|
169
|
+
alias :original_collection_select :collection_select
|
|
170
|
+
def collection_select(attribute, collection, value_method, text_method, options={}, html_options={})
|
|
171
|
+
if value_method.respond_to?(:call) || text_method.respond_to?(:call)
|
|
172
|
+
collection = collection.map do |item|
|
|
173
|
+
value = value_for_collection(item, value_method)
|
|
174
|
+
text = value_for_collection(item, text_method)
|
|
175
|
+
[value, text]
|
|
176
|
+
end
|
|
177
|
+
value_method, text_method = :first, :last
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
original_collection_select(attribute, collection, value_method, text_method, options, html_options)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
module SimpleForm
|
|
2
2
|
module ActionViewExtensions
|
|
3
|
-
# This
|
|
4
|
-
# fields_for and remote_form_for.
|
|
3
|
+
# This module creates simple form wrappers around default form_for and fields_for.
|
|
5
4
|
#
|
|
6
5
|
# Example:
|
|
7
6
|
#
|
|
@@ -30,11 +29,11 @@ module SimpleForm
|
|
|
30
29
|
result
|
|
31
30
|
end
|
|
32
31
|
|
|
33
|
-
[:form_for, :fields_for
|
|
32
|
+
[:form_for, :fields_for].each do |helper|
|
|
34
33
|
class_eval <<-METHOD, __FILE__, __LINE__
|
|
35
34
|
def simple_#{helper}(record_or_name_or_array, *args, &block)
|
|
36
35
|
options = args.extract_options!
|
|
37
|
-
options[:builder]
|
|
36
|
+
options[:builder] ||= SimpleForm::FormBuilder
|
|
38
37
|
css_class = case record_or_name_or_array
|
|
39
38
|
when String, Symbol then record_or_name_or_array.to_s
|
|
40
39
|
when Array then dom_class(record_or_name_or_array.last)
|
|
@@ -37,7 +37,7 @@ module SimpleForm
|
|
|
37
37
|
|
|
38
38
|
def label_html_options
|
|
39
39
|
label_options = html_options_for(:label, [input_type, required_class])
|
|
40
|
-
label_options[:for] = options[:input_html][:id] if options.key?(:input_html)
|
|
40
|
+
label_options[:for] = options[:input_html][:id] if options.key?(:input_html) && options[:input_html].key?(:id)
|
|
41
41
|
label_options
|
|
42
42
|
end
|
|
43
43
|
|
|
@@ -175,7 +175,7 @@ module SimpleForm
|
|
|
175
175
|
# f.error :name, :id => "cool_error"
|
|
176
176
|
#
|
|
177
177
|
def error(attribute_name, options={})
|
|
178
|
-
options[:error_html] = options
|
|
178
|
+
options[:error_html] = options.dup
|
|
179
179
|
column = find_attribute_column(attribute_name)
|
|
180
180
|
input_type = default_input_type(attribute_name, column, options)
|
|
181
181
|
SimpleForm::Inputs::Base.new(self, attribute_name, column, input_type, options).error
|
|
@@ -192,7 +192,7 @@ module SimpleForm
|
|
|
192
192
|
# f.hint "Don't forget to accept this"
|
|
193
193
|
#
|
|
194
194
|
def hint(attribute_name, options={})
|
|
195
|
-
options[:hint_html] = options
|
|
195
|
+
options[:hint_html] = options.dup
|
|
196
196
|
if attribute_name.is_a?(String)
|
|
197
197
|
options[:hint] = attribute_name
|
|
198
198
|
attribute_name, column, input_type = nil, nil, nil
|
|
@@ -219,8 +219,8 @@ module SimpleForm
|
|
|
219
219
|
def label(attribute_name, *args)
|
|
220
220
|
return super if args.first.is_a?(String)
|
|
221
221
|
options = args.extract_options!
|
|
222
|
+
options[:label_html] = options.dup
|
|
222
223
|
options[:label] = options.delete(:label)
|
|
223
|
-
options[:label_html] = options
|
|
224
224
|
options[:required] = options.delete(:required)
|
|
225
225
|
column = find_attribute_column(attribute_name)
|
|
226
226
|
input_type = default_input_type(attribute_name, column, options)
|
|
@@ -250,9 +250,9 @@ module SimpleForm
|
|
|
250
250
|
def default_input_type(attribute_name, column, options) #:nodoc:
|
|
251
251
|
return options[:as].to_sym if options[:as]
|
|
252
252
|
return :select if options[:collection]
|
|
253
|
+
custom_type = find_custom_type(attribute_name.to_s) and return custom_type
|
|
253
254
|
|
|
254
255
|
input_type = column.try(:type)
|
|
255
|
-
|
|
256
256
|
case input_type
|
|
257
257
|
when :timestamp
|
|
258
258
|
:datetime
|
|
@@ -264,10 +264,6 @@ module SimpleForm
|
|
|
264
264
|
when /email/ then :email
|
|
265
265
|
when /phone/ then :tel
|
|
266
266
|
when /url/ then :url
|
|
267
|
-
else
|
|
268
|
-
SimpleForm.input_mappings.find { |match, type|
|
|
269
|
-
attribute_name.to_s =~ match
|
|
270
|
-
}.try(:last) if SimpleForm.input_mappings
|
|
271
267
|
end
|
|
272
268
|
|
|
273
269
|
match || input_type || file_method?(attribute_name) || :string
|
|
@@ -276,21 +272,30 @@ module SimpleForm
|
|
|
276
272
|
end
|
|
277
273
|
end
|
|
278
274
|
|
|
275
|
+
def find_custom_type(attribute_name)
|
|
276
|
+
SimpleForm.input_mappings.find { |match, type|
|
|
277
|
+
attribute_name =~ match
|
|
278
|
+
}.try(:last) if SimpleForm.input_mappings
|
|
279
|
+
end
|
|
280
|
+
|
|
279
281
|
# Checks if attribute is a file_method.
|
|
280
282
|
def file_method?(attribute_name) #:nodoc:
|
|
281
283
|
file = @object.send(attribute_name) if @object.respond_to?(attribute_name)
|
|
282
284
|
:file if file && SimpleForm.file_methods.any? { |m| file.respond_to?(m) }
|
|
283
285
|
end
|
|
284
286
|
|
|
285
|
-
# Finds the database column for the given attribute
|
|
287
|
+
# Finds the database column for the given attribute.
|
|
286
288
|
def find_attribute_column(attribute_name) #:nodoc:
|
|
287
|
-
|
|
289
|
+
if @object.respond_to?(:column_for_attribute)
|
|
290
|
+
@object.column_for_attribute(attribute_name)
|
|
291
|
+
end
|
|
288
292
|
end
|
|
289
293
|
|
|
290
|
-
# Find reflection related to association
|
|
294
|
+
# Find reflection related to association.
|
|
291
295
|
def find_association_reflection(association) #:nodoc:
|
|
292
|
-
|
|
296
|
+
if @object.class.respond_to?(:reflect_on_association)
|
|
297
|
+
@object.class.reflect_on_association(association)
|
|
298
|
+
end
|
|
293
299
|
end
|
|
294
|
-
|
|
295
300
|
end
|
|
296
301
|
end
|
|
@@ -28,8 +28,9 @@ module SimpleForm
|
|
|
28
28
|
@reflection = options.delete(:reflection)
|
|
29
29
|
@options = options
|
|
30
30
|
@input_html_options = html_options_for(:input, input_html_classes).tap do |o|
|
|
31
|
-
o[:required]
|
|
32
|
-
o[:disabled]
|
|
31
|
+
o[:required] = true if has_required?
|
|
32
|
+
o[:disabled] = true if disabled?
|
|
33
|
+
o[:autofocus] = true if has_autofocus?
|
|
33
34
|
end
|
|
34
35
|
end
|
|
35
36
|
|
|
@@ -71,8 +72,17 @@ module SimpleForm
|
|
|
71
72
|
end
|
|
72
73
|
end
|
|
73
74
|
|
|
75
|
+
# Whether this input is valid for HTML 5 required attribute.
|
|
76
|
+
def has_required?
|
|
77
|
+
attribute_required?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def has_autofocus?
|
|
81
|
+
options[:autofocus]
|
|
82
|
+
end
|
|
83
|
+
|
|
74
84
|
def has_validators?
|
|
75
|
-
object.class.respond_to?(:validators_on)
|
|
85
|
+
attribute_name && object.class.respond_to?(:validators_on)
|
|
76
86
|
end
|
|
77
87
|
|
|
78
88
|
def attribute_validators
|
|
@@ -31,6 +31,11 @@ module SimpleForm
|
|
|
31
31
|
@collection ||= (options.delete(:collection) || self.class.boolean_collection).to_a
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# Select components does not allow the required html tag.
|
|
35
|
+
def has_required?
|
|
36
|
+
super && input_type != :select
|
|
37
|
+
end
|
|
38
|
+
|
|
34
39
|
# Check if :include_blank must be included by default.
|
|
35
40
|
def skip_include_blank?
|
|
36
41
|
(options.keys & [:prompt, :include_blank, :default, :selected]).any? ||
|
data/lib/simple_form/map_type.rb
CHANGED
data/lib/simple_form/version.rb
CHANGED
data/simple_form.gemspec
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
|
3
|
+
require "simple_form/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |s|
|
|
6
|
+
s.name = "simple_form"
|
|
7
|
+
s.version = SimpleForm::VERSION.dup
|
|
8
|
+
s.platform = Gem::Platform::RUBY
|
|
9
|
+
s.summary = "Forms made easy!"
|
|
10
|
+
s.email = "contact@plataformatec.com.br"
|
|
11
|
+
s.homepage = "http://github.com/plataformatec/simple_form"
|
|
12
|
+
s.description = "Forms made easy!"
|
|
13
|
+
s.authors = ['José Valim', 'Carlos Antônio']
|
|
14
|
+
|
|
15
|
+
s.files = `git ls-files`.split("\n")
|
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
17
|
+
s.test_files -= Dir["test/support/country_select/**/*"]
|
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
|
19
|
+
s.require_paths = ["lib"]
|
|
20
|
+
|
|
21
|
+
s.rubyforge_project = "simple_form"
|
|
22
|
+
end
|
|
@@ -170,6 +170,16 @@ class LabelTest < ActionView::TestCase
|
|
|
170
170
|
assert_select 'label[for=my_new_id]'
|
|
171
171
|
end
|
|
172
172
|
|
|
173
|
+
test 'label should allow overwriting of for attribute' do
|
|
174
|
+
with_label_for @user, :name, :string, :label_html => { :for => 'my_new_id' }
|
|
175
|
+
assert_select 'label[for=my_new_id]'
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
test 'label should allow overwriting of for attribute with input_html not containing id' do
|
|
179
|
+
with_label_for @user, :name, :string, :label_html => { :for => 'my_new_id' }, :input_html => {:class => 'foo'}
|
|
180
|
+
assert_select 'label[for=my_new_id]'
|
|
181
|
+
end
|
|
182
|
+
|
|
173
183
|
test 'label should use default input id when it was not overridden' do
|
|
174
184
|
with_label_for @user, :name, :string, :input_html => { :class => 'my_new_id' }
|
|
175
185
|
assert_select 'label[for=user_name]'
|
data/test/form_builder_test.rb
CHANGED
|
@@ -9,6 +9,12 @@ class FormBuilderTest < ActionView::TestCase
|
|
|
9
9
|
end
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
def with_custom_form_for(object, *args, &block)
|
|
13
|
+
with_concat_custom_form_for(object) do |f|
|
|
14
|
+
f.input(*args, &block)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
12
18
|
def with_button_for(object, *args)
|
|
13
19
|
with_concat_form_for(object) do |f|
|
|
14
20
|
f.button(*args)
|
|
@@ -70,6 +76,14 @@ class FormBuilderTest < ActionView::TestCase
|
|
|
70
76
|
end
|
|
71
77
|
end
|
|
72
78
|
|
|
79
|
+
test 'builder should allow adding custom input mappings for integer input types' do
|
|
80
|
+
swap SimpleForm, :input_mappings => { /lock_version/ => :hidden } do
|
|
81
|
+
with_form_for @user, :lock_version
|
|
82
|
+
assert_no_select 'form input#user_lock_version.integer'
|
|
83
|
+
assert_select 'form input#user_lock_version.hidden'
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
73
87
|
test 'builder uses the first matching custom input map when more than one match' do
|
|
74
88
|
swap SimpleForm, :input_mappings => { /count$/ => :integer, /^post_/ => :password } do
|
|
75
89
|
with_form_for @user, :post_count
|
|
@@ -78,6 +92,14 @@ class FormBuilderTest < ActionView::TestCase
|
|
|
78
92
|
end
|
|
79
93
|
end
|
|
80
94
|
|
|
95
|
+
test 'builder uses the custom map only for matched attributes' do
|
|
96
|
+
swap SimpleForm, :input_mappings => { /lock_version/ => :hidden } do
|
|
97
|
+
with_form_for @user, :post_count
|
|
98
|
+
assert_no_select 'form input#user_post_count.hidden'
|
|
99
|
+
assert_select 'form input#user_post_count.string'
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
81
103
|
# INPUT TYPES
|
|
82
104
|
test 'builder should generate text fields for string columns' do
|
|
83
105
|
with_form_for @user, :name
|
|
@@ -371,6 +393,11 @@ class FormBuilderTest < ActionView::TestCase
|
|
|
371
393
|
assert_select 'span.error', "can't be blank"
|
|
372
394
|
end
|
|
373
395
|
|
|
396
|
+
test 'builder should generate an error tag with a clean HTML' do
|
|
397
|
+
with_error_for @user, :name
|
|
398
|
+
assert_no_select 'span.error[error_html]'
|
|
399
|
+
end
|
|
400
|
+
|
|
374
401
|
test 'builder should allow passing options to error tag' do
|
|
375
402
|
with_error_for @user, :name, :id => 'name_error'
|
|
376
403
|
assert_select 'span.error#name_error', "can't be blank"
|
|
@@ -384,11 +411,22 @@ class FormBuilderTest < ActionView::TestCase
|
|
|
384
411
|
end
|
|
385
412
|
end
|
|
386
413
|
|
|
414
|
+
test 'builder should generate a hint component tag for the given text for a model with ActiveModel::Validations' do
|
|
415
|
+
with_hint_for @validating_user, 'Hello World!'
|
|
416
|
+
assert_select 'span.hint', 'Hello World!'
|
|
417
|
+
end
|
|
418
|
+
|
|
387
419
|
test 'builder should generate a hint component tag for the given text' do
|
|
388
420
|
with_hint_for @user, 'Hello World!'
|
|
389
421
|
assert_select 'span.hint', 'Hello World!'
|
|
390
422
|
end
|
|
391
423
|
|
|
424
|
+
test 'builder should generate a hint componet tag with a clean HTML' do
|
|
425
|
+
with_hint_for @validating_user, 'Hello World!'
|
|
426
|
+
assert_no_select 'span.hint[hint]'
|
|
427
|
+
assert_no_select 'span.hint[hint_html]'
|
|
428
|
+
end
|
|
429
|
+
|
|
392
430
|
test 'builder should allow passing options to hint tag' do
|
|
393
431
|
with_hint_for @user, :name, :hint => 'Hello World!', :id => 'name_hint'
|
|
394
432
|
assert_select 'span.hint#name_hint', 'Hello World!'
|
|
@@ -400,6 +438,11 @@ class FormBuilderTest < ActionView::TestCase
|
|
|
400
438
|
assert_select 'label.string[for=user_name]', /Name/
|
|
401
439
|
end
|
|
402
440
|
|
|
441
|
+
test 'builder should generate a label componet tag with a clean HTML' do
|
|
442
|
+
with_label_for @user, :name
|
|
443
|
+
assert_no_select 'label.string[label_html]'
|
|
444
|
+
end
|
|
445
|
+
|
|
403
446
|
test 'builder should add a required class to label if the attribute is required' do
|
|
404
447
|
with_label_for @validating_user, :name
|
|
405
448
|
assert_select 'label.string.required[for=validating_user_name]', /Name/
|
|
@@ -562,4 +605,10 @@ class FormBuilderTest < ActionView::TestCase
|
|
|
562
605
|
assert_select 'form ul', :count => 1
|
|
563
606
|
assert_select 'form ul li', :count => 3
|
|
564
607
|
end
|
|
608
|
+
|
|
609
|
+
# CUSTOM FORM BUILDER
|
|
610
|
+
test 'custom builder should inherit mappings' do
|
|
611
|
+
with_custom_form_for @user, :email
|
|
612
|
+
assert_select 'form input[type=email]#user_email.custom'
|
|
613
|
+
end
|
|
565
614
|
end
|
data/test/inputs_test.rb
CHANGED
|
@@ -62,6 +62,41 @@ class InputTest < ActionView::TestCase
|
|
|
62
62
|
assert_select 'select.datetime:not([disabled])'
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
test 'input should generate autofocus attribute based on the autofocus option' do
|
|
66
|
+
with_input_for @user, :name, :string, :autofocus => true
|
|
67
|
+
assert_select 'input.string[autofocus]'
|
|
68
|
+
with_input_for @user, :description, :text, :autofocus => true
|
|
69
|
+
assert_select 'textarea.text[autofocus]'
|
|
70
|
+
with_input_for @user, :age, :integer, :autofocus => true
|
|
71
|
+
assert_select 'input.integer[autofocus]'
|
|
72
|
+
with_input_for @user, :born_at, :date, :autofocus => true
|
|
73
|
+
assert_select 'select.date[autofocus]'
|
|
74
|
+
with_input_for @user, :created_at, :datetime, :autofocus => true
|
|
75
|
+
assert_select 'select.datetime[autofocus]'
|
|
76
|
+
|
|
77
|
+
with_input_for @user, :name, :string, :autofocus => false
|
|
78
|
+
assert_select 'input.string:not([autofocus])'
|
|
79
|
+
with_input_for @user, :description, :text, :autofocus => false
|
|
80
|
+
assert_select 'textarea.text:not([autofocus])'
|
|
81
|
+
with_input_for @user, :age, :integer, :autofocus => false
|
|
82
|
+
assert_select 'input.integer:not([autofocus])'
|
|
83
|
+
with_input_for @user, :born_at, :date, :autofocus => false
|
|
84
|
+
assert_select 'select.date:not([autofocus])'
|
|
85
|
+
with_input_for @user, :created_at, :datetime, :autofocus => false
|
|
86
|
+
assert_select 'select.datetime:not([autofocus])'
|
|
87
|
+
|
|
88
|
+
with_input_for @user, :name, :string
|
|
89
|
+
assert_select 'input.string:not([autofocus])'
|
|
90
|
+
with_input_for @user, :description, :text
|
|
91
|
+
assert_select 'textarea.text:not([autofocus])'
|
|
92
|
+
with_input_for @user, :age, :integer
|
|
93
|
+
assert_select 'input.integer:not([autofocus])'
|
|
94
|
+
with_input_for @user, :born_at, :date
|
|
95
|
+
assert_select 'select.date:not([autofocus])'
|
|
96
|
+
with_input_for @user, :created_at, :datetime
|
|
97
|
+
assert_select 'select.datetime:not([autofocus])'
|
|
98
|
+
end
|
|
99
|
+
|
|
65
100
|
test 'input should render components according to an optional :components option' do
|
|
66
101
|
with_input_for @user, :name, :string, :components => [:input, :label]
|
|
67
102
|
assert_select 'input + label'
|
|
@@ -321,6 +356,12 @@ class InputTest < ActionView::TestCase
|
|
|
321
356
|
assert_no_select 'select option[value=]', /^$/
|
|
322
357
|
end
|
|
323
358
|
|
|
359
|
+
test 'priority input should not generate invalid required html attribute' do
|
|
360
|
+
with_input_for @user, :country, :country
|
|
361
|
+
assert_select 'select.required'
|
|
362
|
+
assert_no_select 'select[required]'
|
|
363
|
+
end
|
|
364
|
+
|
|
324
365
|
# DateTime input
|
|
325
366
|
test 'input should generate a datetime select by default for datetime attributes' do
|
|
326
367
|
with_input_for @user, :created_at, :datetime
|
|
@@ -395,6 +436,12 @@ class InputTest < ActionView::TestCase
|
|
|
395
436
|
assert_select 'label[for=project_created_at_4i]'
|
|
396
437
|
end
|
|
397
438
|
|
|
439
|
+
test 'date time input should not generate invalid required html attribute' do
|
|
440
|
+
with_input_for @user, :delivery_time, :time, :required => true
|
|
441
|
+
assert_select 'select.required'
|
|
442
|
+
assert_no_select 'select[required]'
|
|
443
|
+
end
|
|
444
|
+
|
|
398
445
|
# CollectionInput
|
|
399
446
|
test 'input should generate boolean radio buttons by default for radio types' do
|
|
400
447
|
with_input_for @user, :active, :radio
|
|
@@ -559,6 +606,31 @@ class InputTest < ActionView::TestCase
|
|
|
559
606
|
assert_select 'label.collection_radio', 'CARLOS'
|
|
560
607
|
end
|
|
561
608
|
|
|
609
|
+
test 'input should allow overriding label and value method using a lambda for collection selects' do
|
|
610
|
+
with_input_for @user, :name, :select,
|
|
611
|
+
:collection => ['Jose' , 'Carlos'],
|
|
612
|
+
:label_method => lambda { |i| i.upcase },
|
|
613
|
+
:value_method => lambda { |i| i.downcase }
|
|
614
|
+
assert_select 'select option[value=jose]', "JOSE"
|
|
615
|
+
assert_select 'select option[value=carlos]', "CARLOS"
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
test 'input should allow overriding only label but not value method using a lambda for collection select' do
|
|
619
|
+
with_input_for @user, :name, :select,
|
|
620
|
+
:collection => ['Jose' , 'Carlos'],
|
|
621
|
+
:label_method => lambda { |i| i.upcase }
|
|
622
|
+
assert_select 'select option[value=Jose]', "JOSE"
|
|
623
|
+
assert_select 'select option[value=Carlos]', "CARLOS"
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
test 'input should allow overriding only value but not label method using a lambda for collection select' do
|
|
627
|
+
with_input_for @user, :name, :select,
|
|
628
|
+
:collection => ['Jose' , 'Carlos'],
|
|
629
|
+
:value_method => lambda { |i| i.downcase }
|
|
630
|
+
assert_select 'select option[value=jose]', "Jose"
|
|
631
|
+
assert_select 'select option[value=carlos]', "Carlos"
|
|
632
|
+
end
|
|
633
|
+
|
|
562
634
|
test 'input should allow symbols for collections' do
|
|
563
635
|
with_input_for @user, :name, :select, :collection => [:jose, :carlos]
|
|
564
636
|
assert_select 'select.select#user_name'
|
|
@@ -566,6 +638,18 @@ class InputTest < ActionView::TestCase
|
|
|
566
638
|
assert_select 'select option[value=carlos]', 'carlos'
|
|
567
639
|
end
|
|
568
640
|
|
|
641
|
+
test 'collection input with radio type should generate required html attribute' do
|
|
642
|
+
with_input_for @user, :name, :radio, :collection => ['Jose' , 'Carlos']
|
|
643
|
+
assert_select 'input[type=radio].required'
|
|
644
|
+
assert_select 'input[type=radio][required]'
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
test 'collection input with select type should not generate invalid required html attribute' do
|
|
648
|
+
with_input_for @user, :name, :select, :collection => ['Jose' , 'Carlos']
|
|
649
|
+
assert_select 'select.required'
|
|
650
|
+
assert_no_select 'select[required]'
|
|
651
|
+
end
|
|
652
|
+
|
|
569
653
|
# With no object
|
|
570
654
|
test 'input should be generated properly when object is not present' do
|
|
571
655
|
with_input_for :project, :name, :string
|
|
@@ -28,4 +28,18 @@ module MiscHelpers
|
|
|
28
28
|
def with_concat_form_for(object, &block)
|
|
29
29
|
concat simple_form_for(object, &block)
|
|
30
30
|
end
|
|
31
|
+
|
|
32
|
+
def with_concat_custom_form_for(object, &block)
|
|
33
|
+
concat custom_form_for(object, &block)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def custom_form_for(object, *args, &block)
|
|
37
|
+
simple_form_for(object, *(args << { :builder => CustomFormBuilder }), &block)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class CustomFormBuilder < SimpleForm::FormBuilder
|
|
42
|
+
def input(attribute_name, *args, &block)
|
|
43
|
+
super(attribute_name, *(args << { :input_html => { :class => 'custom' } }), &block)
|
|
44
|
+
end
|
|
31
45
|
end
|
data/test/support/models.rb
CHANGED
|
@@ -40,7 +40,7 @@ class User
|
|
|
40
40
|
|
|
41
41
|
attr_accessor :id, :name, :company, :company_id, :time_zone, :active, :description, :created_at, :updated_at,
|
|
42
42
|
:credit_limit, :age, :password, :delivery_time, :born_at, :special_company_id, :country, :url, :tag_ids,
|
|
43
|
-
:avatar, :email, :status, :residence_country, :phone_number, :post_count
|
|
43
|
+
:avatar, :email, :status, :residence_country, :phone_number, :post_count, :lock_version
|
|
44
44
|
|
|
45
45
|
def initialize(options={})
|
|
46
46
|
options.each do |key, value|
|
|
@@ -70,6 +70,7 @@ class User
|
|
|
70
70
|
when :delivery_time then :time
|
|
71
71
|
when :created_at then :datetime
|
|
72
72
|
when :updated_at then :timestamp
|
|
73
|
+
when :lock_version then :integer
|
|
73
74
|
end
|
|
74
75
|
Column.new(attribute, column_type, limit)
|
|
75
76
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: simple_form
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
hash:
|
|
5
|
-
prerelease:
|
|
4
|
+
hash: 25
|
|
5
|
+
prerelease:
|
|
6
6
|
segments:
|
|
7
7
|
- 1
|
|
8
8
|
- 3
|
|
9
|
-
-
|
|
10
|
-
version: 1.3.
|
|
9
|
+
- 1
|
|
10
|
+
version: 1.3.1
|
|
11
11
|
platform: ruby
|
|
12
12
|
authors:
|
|
13
13
|
- "Jos\xC3\xA9 Valim"
|
|
@@ -16,34 +16,27 @@ autorequire:
|
|
|
16
16
|
bindir: bin
|
|
17
17
|
cert_chain: []
|
|
18
18
|
|
|
19
|
-
date:
|
|
19
|
+
date: 2011-02-06 00:00:00 -02:00
|
|
20
20
|
default_executable:
|
|
21
|
-
dependencies:
|
|
22
|
-
|
|
23
|
-
prerelease: false
|
|
24
|
-
type: :runtime
|
|
25
|
-
name: rails
|
|
26
|
-
version_requirements: &id001 !ruby/object:Gem::Requirement
|
|
27
|
-
none: false
|
|
28
|
-
requirements:
|
|
29
|
-
- - ~>
|
|
30
|
-
- !ruby/object:Gem::Version
|
|
31
|
-
hash: 7
|
|
32
|
-
segments:
|
|
33
|
-
- 3
|
|
34
|
-
- 0
|
|
35
|
-
- 0
|
|
36
|
-
version: 3.0.0
|
|
37
|
-
requirement: *id001
|
|
21
|
+
dependencies: []
|
|
22
|
+
|
|
38
23
|
description: Forms made easy!
|
|
39
24
|
email: contact@plataformatec.com.br
|
|
40
25
|
executables: []
|
|
41
26
|
|
|
42
27
|
extensions: []
|
|
43
28
|
|
|
44
|
-
extra_rdoc_files:
|
|
45
|
-
|
|
29
|
+
extra_rdoc_files: []
|
|
30
|
+
|
|
46
31
|
files:
|
|
32
|
+
- .gitignore
|
|
33
|
+
- .gitmodules
|
|
34
|
+
- CHANGELOG.rdoc
|
|
35
|
+
- Gemfile
|
|
36
|
+
- Gemfile.lock
|
|
37
|
+
- MIT-LICENSE
|
|
38
|
+
- README.rdoc
|
|
39
|
+
- Rakefile
|
|
47
40
|
- init.rb
|
|
48
41
|
- lib/generators/simple_form/USAGE
|
|
49
42
|
- lib/generators/simple_form/install_generator.rb
|
|
@@ -78,7 +71,7 @@ files:
|
|
|
78
71
|
- lib/simple_form/inputs/string_input.rb
|
|
79
72
|
- lib/simple_form/map_type.rb
|
|
80
73
|
- lib/simple_form/version.rb
|
|
81
|
-
-
|
|
74
|
+
- simple_form.gemspec
|
|
82
75
|
- test/action_view_extensions/builder_test.rb
|
|
83
76
|
- test/action_view_extensions/form_helper_test.rb
|
|
84
77
|
- test/components/error_test.rb
|
|
@@ -123,8 +116,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
123
116
|
version: "0"
|
|
124
117
|
requirements: []
|
|
125
118
|
|
|
126
|
-
rubyforge_project:
|
|
127
|
-
rubygems_version: 1.
|
|
119
|
+
rubyforge_project: simple_form
|
|
120
|
+
rubygems_version: 1.5.0
|
|
128
121
|
signing_key:
|
|
129
122
|
specification_version: 3
|
|
130
123
|
summary: Forms made easy!
|