form 0.0.0 → 0.0.1.alpha1
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/Gemfile.lock +15 -1
- data/README.rdoc +1 -1
- data/examples/hash.rb +27 -0
- data/examples/object.rb +19 -0
- data/form.gemspec +4 -1
- data/lib/form/builder.rb +130 -0
- data/lib/form/component/base.rb +69 -0
- data/lib/form/component/button.rb +26 -0
- data/lib/form/component/input.rb +17 -0
- data/lib/form/component/label.rb +25 -0
- data/lib/form/component/text_area.rb +23 -0
- data/lib/form/component.rb +9 -0
- data/lib/form/extensions/hash.rb +10 -0
- data/lib/form/locales/en.yml +3 -0
- data/lib/form/tag.rb +106 -0
- data/lib/form/version.rb +2 -2
- data/lib/form.rb +22 -0
- data/spec/form/builder_spec.rb +95 -0
- data/spec/form/component/base_spec.rb +80 -0
- data/spec/form/component/input_spec.rb +4 -0
- data/spec/form/component/label_spec.rb +56 -0
- data/spec/form/component/text_area_spec.rb +62 -0
- data/spec/form/extensions/hash_spec.rb +9 -0
- data/spec/form/tag_spec.rb +119 -0
- data/spec/form_spec.rb +27 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/support/have_tag_matcher.rb +121 -0
- data/spec/support/helpers.rb +9 -0
- data/spec/support/input_instantiation_shared.rb +28 -0
- data/spec/support/input_shared.rb +13 -0
- metadata +89 -14
data/Gemfile.lock
CHANGED
@@ -1,12 +1,22 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
form (0.0.
|
4
|
+
form (0.0.1.alpha1)
|
5
|
+
i18n
|
5
6
|
|
6
7
|
GEM
|
7
8
|
remote: http://rubygems.org/
|
8
9
|
specs:
|
10
|
+
awesome_print (1.0.2)
|
11
|
+
coderay (1.0.5)
|
9
12
|
diff-lcs (1.1.3)
|
13
|
+
i18n (0.6.0)
|
14
|
+
method_source (0.7.0)
|
15
|
+
nokogiri (1.5.0)
|
16
|
+
pry (0.9.8.2)
|
17
|
+
coderay (~> 1.0.5)
|
18
|
+
method_source (~> 0.7)
|
19
|
+
slop (>= 2.4.4, < 3)
|
10
20
|
rake (0.9.2.2)
|
11
21
|
rspec (2.8.0)
|
12
22
|
rspec-core (~> 2.8.0)
|
@@ -16,11 +26,15 @@ GEM
|
|
16
26
|
rspec-expectations (2.8.0)
|
17
27
|
diff-lcs (~> 1.1.2)
|
18
28
|
rspec-mocks (2.8.0)
|
29
|
+
slop (2.4.4)
|
19
30
|
|
20
31
|
PLATFORMS
|
21
32
|
ruby
|
22
33
|
|
23
34
|
DEPENDENCIES
|
35
|
+
awesome_print
|
24
36
|
form!
|
37
|
+
nokogiri (~> 1.5)
|
38
|
+
pry
|
25
39
|
rake (~> 0.9)
|
26
40
|
rspec (~> 2.8)
|
data/README.rdoc
CHANGED
data/examples/hash.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
$:.unshift File.expand_path("../../lib", __FILE__)
|
2
|
+
require "form"
|
3
|
+
|
4
|
+
params = {
|
5
|
+
:name => "John Doe",
|
6
|
+
:email => "john@example.org",
|
7
|
+
:bio => "Code-addicted.\nThat's all you need to know about me."
|
8
|
+
}
|
9
|
+
|
10
|
+
# The first argument indicates the datasource.
|
11
|
+
# In this case, we're using a simple hash.
|
12
|
+
# The second argument indicates the root name
|
13
|
+
# that will compose the input's name.
|
14
|
+
form = Form.new(params, :user)
|
15
|
+
|
16
|
+
# Just output an input[type=text].
|
17
|
+
puts form.text(:name)
|
18
|
+
|
19
|
+
# Same fashion, just output an input[type=email]
|
20
|
+
puts form.email(:email)
|
21
|
+
|
22
|
+
# More inputs.
|
23
|
+
puts form.textarea(:bio)
|
24
|
+
puts form.submit(:create)
|
25
|
+
|
26
|
+
# Output the label for name attribute.
|
27
|
+
puts form.label(:name)
|
data/examples/object.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
$:.unshift File.expand_path("../../lib", __FILE__)
|
2
|
+
require "form"
|
3
|
+
require "ostruct"
|
4
|
+
|
5
|
+
user = OpenStruct.new({
|
6
|
+
:name => "John Doe",
|
7
|
+
:email => "john@example.org"
|
8
|
+
})
|
9
|
+
|
10
|
+
# Similar to hash datasource, but using a
|
11
|
+
# regular object with attributes instead.
|
12
|
+
#
|
13
|
+
# Form just doesn't care about datasource's type.
|
14
|
+
# Just provide a object that responds to [] method
|
15
|
+
# or to the attribute you want.
|
16
|
+
form = Form.new(user, :user)
|
17
|
+
|
18
|
+
puts form.text :name
|
19
|
+
puts form.email :email
|
data/form.gemspec
CHANGED
@@ -17,7 +17,10 @@ Gem::Specification.new do |s|
|
|
17
17
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
18
|
s.require_paths = ["lib"]
|
19
19
|
|
20
|
-
|
20
|
+
s.add_dependency "i18n"
|
21
21
|
s.add_development_dependency "rake", "~> 0.9"
|
22
22
|
s.add_development_dependency "rspec", "~> 2.8"
|
23
|
+
s.add_development_dependency "nokogiri", "~> 1.5"
|
24
|
+
s.add_development_dependency "awesome_print"
|
25
|
+
s.add_development_dependency "pry"
|
23
26
|
end
|
data/lib/form/builder.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
module Form
|
2
|
+
class Builder
|
3
|
+
# The object that will be used to fulfill inputs.
|
4
|
+
#
|
5
|
+
attr_accessor :data
|
6
|
+
|
7
|
+
# The input's base name.
|
8
|
+
#
|
9
|
+
attr_accessor :base_name
|
10
|
+
|
11
|
+
# Initialize a new form builder.
|
12
|
+
#
|
13
|
+
def initialize(data = nil, base_name = nil)
|
14
|
+
@data = data
|
15
|
+
@base_name = base_name
|
16
|
+
end
|
17
|
+
|
18
|
+
# Render a <tt>input[type=text]</tt> input.
|
19
|
+
#
|
20
|
+
# form.text :name
|
21
|
+
#
|
22
|
+
def text(name, options = {})
|
23
|
+
text_input(name, "text", options)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Render a <tt>input[type=password]</tt> input.
|
27
|
+
#
|
28
|
+
# form.password :secret
|
29
|
+
#
|
30
|
+
def password(name, options = {})
|
31
|
+
text_input(name, "password", options)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Render a <tt>input[type=file]</tt> input.
|
35
|
+
#
|
36
|
+
# form.file :avatar
|
37
|
+
#
|
38
|
+
def file(name, options = {})
|
39
|
+
text_input(name, "file", options)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Render a <tt>input[type=hidden]</tt> input.
|
43
|
+
#
|
44
|
+
# form.hidden :honeypot
|
45
|
+
#
|
46
|
+
def hidden(name, options = {})
|
47
|
+
text_input(name, "hidden", options)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Render a <tt>input[type=number]</tt> input.
|
51
|
+
#
|
52
|
+
# form.number :quantity
|
53
|
+
#
|
54
|
+
def number(name, options = {})
|
55
|
+
text_input(name, "number", options)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Render a <tt>input[type=email]</tt> input.
|
59
|
+
#
|
60
|
+
# form.email :email
|
61
|
+
#
|
62
|
+
def email(name, options = {})
|
63
|
+
text_input(name, "email", options)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Render a <tt>input[type=url]</tt> input.
|
67
|
+
#
|
68
|
+
# form.url :blog
|
69
|
+
#
|
70
|
+
def url(name, options = {})
|
71
|
+
text_input(name, "url", options)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Render a <tt>input[type=search]</tt> input.
|
75
|
+
#
|
76
|
+
# form.search :query
|
77
|
+
#
|
78
|
+
def search(name, options = {})
|
79
|
+
text_input(name, "search", options)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Render a <tt>input[type=tel]</tt> input.
|
83
|
+
#
|
84
|
+
# form.phone :work
|
85
|
+
#
|
86
|
+
def phone(name, options = {})
|
87
|
+
text_input(name, "tel", options)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Render a <tt>input[type=submit]</tt> input.
|
91
|
+
#
|
92
|
+
# form.submit :new
|
93
|
+
#
|
94
|
+
def submit(label, options = {})
|
95
|
+
button_input(label, "submit", options)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Render a <tt>input[type=button]</tt> input.
|
99
|
+
#
|
100
|
+
# form.button :new
|
101
|
+
#
|
102
|
+
def button(label, options = {})
|
103
|
+
button_input(label, "button", options)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Render a +textarea+ field.
|
107
|
+
#
|
108
|
+
# form.text_area :content
|
109
|
+
#
|
110
|
+
def text_area(name, options = {})
|
111
|
+
Component::TextArea.new(self, name, options).to_html
|
112
|
+
end
|
113
|
+
alias_method :textarea, :text_area
|
114
|
+
|
115
|
+
#
|
116
|
+
#
|
117
|
+
def label(name, options = {})
|
118
|
+
Component::Label.new(self, name, options).to_html
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
def text_input(name, type, options)
|
123
|
+
Component::Input.new(self, name, options.merge(type: type)).to_html
|
124
|
+
end
|
125
|
+
|
126
|
+
def button_input(name, type, options)
|
127
|
+
Component::Button.new(self, name, options.merge(type: type)).to_html
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Form
|
2
|
+
module Component
|
3
|
+
class Base
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
def_delegators :form, :data
|
7
|
+
|
8
|
+
# The +Form::Builder+ instance. It will provide the base name
|
9
|
+
# and data object.
|
10
|
+
#
|
11
|
+
attr_accessor :form
|
12
|
+
|
13
|
+
# The input name. It will be used to compose
|
14
|
+
# the full name.
|
15
|
+
#
|
16
|
+
attr_accessor :name
|
17
|
+
|
18
|
+
# Hold the input options. It will be used as the input's
|
19
|
+
# attributes.
|
20
|
+
#
|
21
|
+
attr_accessor :options
|
22
|
+
|
23
|
+
# Initialize the component and set some variables.
|
24
|
+
#
|
25
|
+
def initialize(form, name, options = {})
|
26
|
+
@form = form
|
27
|
+
@name = name
|
28
|
+
@options = options
|
29
|
+
end
|
30
|
+
|
31
|
+
# Retrieve the value from the form data.
|
32
|
+
#
|
33
|
+
def value
|
34
|
+
if data.respond_to?(name)
|
35
|
+
data.public_send(name)
|
36
|
+
elsif data.respond_to?(:[])
|
37
|
+
data[name.to_sym] || data[name.to_s]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Return a composed name like <tt>user[profile][twitter]</tt>.
|
42
|
+
#
|
43
|
+
def composed_name
|
44
|
+
composed_name = [form.base_name, name].flatten.compact
|
45
|
+
"#{composed_name.shift}" + composed_name.map {|n| "[#{n}]"}.join("")
|
46
|
+
end
|
47
|
+
|
48
|
+
# Just pass all arguments to <tt>I18n.t</tt>.
|
49
|
+
#
|
50
|
+
def t(*args)
|
51
|
+
I18n.t(*args)
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
#
|
56
|
+
def humanize(name)
|
57
|
+
parts = name.to_s.split("_")
|
58
|
+
parts.first.gsub!(/\A(.)/) { $1.upcase }
|
59
|
+
parts.join(" ")
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
#
|
64
|
+
def id_attribute
|
65
|
+
[form.base_name, name].flatten.compact.join("-")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Form
|
2
|
+
module Component
|
3
|
+
class Button < Base
|
4
|
+
def attributes
|
5
|
+
{
|
6
|
+
:value => text,
|
7
|
+
:type => :submit
|
8
|
+
}.merge(options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def text
|
12
|
+
scopes = [
|
13
|
+
[:form, :buttons, form.base_name, name].flatten.compact.join(".").to_sym,
|
14
|
+
:"form.buttons.#{name}",
|
15
|
+
humanize(name)
|
16
|
+
].compact
|
17
|
+
|
18
|
+
options.fetch :text, t(scopes.shift, default: scopes)
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_html
|
22
|
+
Tag.new(:input, attributes).to_s
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Form
|
2
|
+
module Component
|
3
|
+
class Label < Base
|
4
|
+
def attributes
|
5
|
+
options.except(:text).tap do |attrs|
|
6
|
+
attrs[:for] ||= id_attribute
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def text
|
11
|
+
scopes = [
|
12
|
+
[:form, :labels, form.base_name, name].flatten.compact.join(".").to_sym,
|
13
|
+
:"form.labels.#{name}",
|
14
|
+
humanize(name)
|
15
|
+
].compact
|
16
|
+
|
17
|
+
options.fetch :text, t(scopes.shift, default: scopes)
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_html
|
21
|
+
Tag.new(:label, text, attributes).to_s
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Form
|
2
|
+
module Component
|
3
|
+
class TextArea < Base
|
4
|
+
def attributes
|
5
|
+
defaults.merge(options).merge({
|
6
|
+
:name => composed_name,
|
7
|
+
:id => id_attribute
|
8
|
+
})
|
9
|
+
end
|
10
|
+
|
11
|
+
def defaults
|
12
|
+
{
|
13
|
+
:cols => 50,
|
14
|
+
:rows => 5
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_html
|
19
|
+
Tag.new(:textarea, value, attributes).to_s
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/form/tag.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
module Form
|
2
|
+
class Tag
|
3
|
+
# List which tags don't need to be closed.
|
4
|
+
# Did you know that the technical name is "void elements"?
|
5
|
+
# http://www.w3.org/TR/html5/syntax.html#void-elements
|
6
|
+
#
|
7
|
+
VOID_ELEMENTS = [
|
8
|
+
:area, :base, :br, :col, :command, :embed,
|
9
|
+
:hr, :img, :input, :keygen, :link, :meta,
|
10
|
+
:param, :source, :track, :wbr
|
11
|
+
]
|
12
|
+
|
13
|
+
# The tag name.
|
14
|
+
#
|
15
|
+
attr_accessor :name
|
16
|
+
|
17
|
+
# The tag content. It will be ignored in open tags.
|
18
|
+
#
|
19
|
+
attr_accessor :content
|
20
|
+
|
21
|
+
# The tag attributes.
|
22
|
+
#
|
23
|
+
attr_accessor :attributes
|
24
|
+
|
25
|
+
# Escape the value.
|
26
|
+
#
|
27
|
+
def self.html_escape(string)
|
28
|
+
string.to_s.gsub(/&/, "&").gsub(/\"/, """).gsub(/>/, ">").gsub(/</, "<")
|
29
|
+
end
|
30
|
+
|
31
|
+
# Create a new tag.
|
32
|
+
#
|
33
|
+
# Form::Tag.new(:p, "Hello World!", class: "greeting").render
|
34
|
+
# #=> <p class="greeting">Hello World!</p>
|
35
|
+
#
|
36
|
+
def initialize(name, content = "", attributes = {}, &block)
|
37
|
+
if content.kind_of?(Hash)
|
38
|
+
attributes = content
|
39
|
+
content = ""
|
40
|
+
end
|
41
|
+
|
42
|
+
@name = name.to_sym
|
43
|
+
@content = content
|
44
|
+
@attributes = attributes
|
45
|
+
|
46
|
+
yield self if block_given?
|
47
|
+
end
|
48
|
+
|
49
|
+
# Detect if this tag omits the closing part.
|
50
|
+
#
|
51
|
+
def void?
|
52
|
+
VOID_ELEMENTS.include?(name)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Append the specified HTML to this tag.
|
56
|
+
#
|
57
|
+
def <<(element)
|
58
|
+
@content << element.to_s
|
59
|
+
end
|
60
|
+
|
61
|
+
# Create nested tags with ease.
|
62
|
+
#
|
63
|
+
def tag(name, content = "", attributes = {}, &block)
|
64
|
+
@content << self.class.new(name, content, attributes, &block).to_s
|
65
|
+
end
|
66
|
+
|
67
|
+
# Build the tag, concating all parts.
|
68
|
+
#
|
69
|
+
def to_s
|
70
|
+
open_tag << (void? ? "" : content.to_s) << close_tag
|
71
|
+
end
|
72
|
+
|
73
|
+
def html_escape(string)
|
74
|
+
self.class.html_escape(string)
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
#
|
79
|
+
#
|
80
|
+
def render_attributes
|
81
|
+
attributes.collect do |name, value|
|
82
|
+
next unless value && value != ""
|
83
|
+
bool?(value) ? " #{name}" : %[ #{name}="#{html_escape(value)}"]
|
84
|
+
end.join("")
|
85
|
+
end
|
86
|
+
|
87
|
+
# Check if value is boolean.
|
88
|
+
# LolRuby doesn't have a Boolean class or something.
|
89
|
+
#
|
90
|
+
def bool?(value)
|
91
|
+
value.kind_of?(TrueClass) || value.kind_of?(FalseClass)
|
92
|
+
end
|
93
|
+
|
94
|
+
# The opening part of the tag.
|
95
|
+
#
|
96
|
+
def open_tag
|
97
|
+
"<#{name}#{render_attributes}>"
|
98
|
+
end
|
99
|
+
|
100
|
+
# The closing part of the tag.
|
101
|
+
#
|
102
|
+
def close_tag
|
103
|
+
void? ? "" : "</#{name}>"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|