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 CHANGED
@@ -1,12 +1,22 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- form (0.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
@@ -1,6 +1,6 @@
1
1
  = form
2
2
 
3
- A simple form field for Ruby objects.
3
+ A simple form builder for Ruby objects.
4
4
 
5
5
  == Installation
6
6
 
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)
@@ -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
- # s.add_dependency "activesupport"
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
@@ -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,17 @@
1
+ module Form
2
+ module Component
3
+ class Input < Base
4
+ def attributes
5
+ options.merge({
6
+ value: value,
7
+ name: composed_name,
8
+ id: id_attribute
9
+ })
10
+ end
11
+
12
+ def to_html
13
+ Tag.new(:input, attributes).to_s
14
+ end
15
+ end
16
+ end
17
+ 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
@@ -0,0 +1,9 @@
1
+ module Form
2
+ module Component
3
+ autoload :Base, "form/component/base"
4
+ autoload :Button, "form/component/button"
5
+ autoload :Input, "form/component/input"
6
+ autoload :Label, "form/component/label"
7
+ autoload :TextArea, "form/component/text_area"
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ class Hash
2
+ # It returns a hash without the specified keys.
3
+ #
4
+ # {:a => 1, "a" => 2, :b => 3}.except(:a, :b)
5
+ # #=> {"a" => 2}
6
+ #
7
+ def except(*keys)
8
+ dup.delete_if {|key, value| keys.include?(key)}
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ en:
2
+ form:
3
+ required: Required
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(/&/, "&amp;").gsub(/\"/, "&quot;").gsub(/>/, "&gt;").gsub(/</, "&lt;")
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