formation 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/Gemfile +8 -0
- data/Rakefile +22 -0
- data/demo.rb +179 -0
- data/formation.gemspec +21 -0
- data/lib/formation.rb +24 -0
- data/lib/formation/element.rb +9 -0
- data/lib/formation/field.rb +32 -0
- data/lib/formation/fieldset.rb +14 -0
- data/lib/formation/form.rb +146 -0
- data/lib/formation/printer.rb +52 -0
- data/lib/formation/type.rb +19 -0
- data/lib/formation/types/checkbox.rb +15 -0
- data/lib/formation/types/password.rb +15 -0
- data/lib/formation/types/text.rb +15 -0
- data/lib/formation/types/text_area.rb +15 -0
- data/lib/formation/util.rb +11 -0
- data/lib/formation/version.rb +5 -0
- data/readme.md +61 -0
- data/test/field_spec.rb +17 -0
- data/test/form_spec.rb +86 -0
- data/test/forms/post_form.rb +16 -0
- data/test/forms/simple_form.rb +33 -0
- data/test/test_helper.rb +9 -0
- data/test/type_spec.rb +15 -0
- data/test/types/checkbox_spec.rb +56 -0
- data/test/types/password_spec.rb +42 -0
- data/test/types/text_area_spec.rb +42 -0
- data/test/types/text_spec.rb +42 -0
- data/test/util_spec.rb +21 -0
- metadata +107 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
require 'rake/testtask'
|
5
|
+
Rake::TestTask.new do |t|
|
6
|
+
t.test_files = FileList['test/**/*_{test,spec}.rb']
|
7
|
+
# t.warning = true
|
8
|
+
end
|
9
|
+
|
10
|
+
begin
|
11
|
+
require 'rcov/rcovtask'
|
12
|
+
Rcov::RcovTask.new do |t|
|
13
|
+
t.test_files = FileList['test/**/*_{test,spec}.rb']
|
14
|
+
t.verbose = true
|
15
|
+
t.rcov_opts << %w{ --exclude .rvm }
|
16
|
+
end
|
17
|
+
rescue LoadError
|
18
|
+
desc 'Analyze code coverage with tests'
|
19
|
+
task :rcov do
|
20
|
+
puts "Install rcov"
|
21
|
+
end
|
22
|
+
end
|
data/demo.rb
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'sinatra'
|
3
|
+
require 'datamapper'
|
4
|
+
require File.expand_path('../lib/formation', __FILE__)
|
5
|
+
|
6
|
+
class Post
|
7
|
+
|
8
|
+
include DataMapper::Resource
|
9
|
+
|
10
|
+
property :id, Serial
|
11
|
+
property :title, String
|
12
|
+
property :body, Text
|
13
|
+
property :created_at, DateTime
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
class PostForm
|
18
|
+
|
19
|
+
include Formation::Form
|
20
|
+
|
21
|
+
resource :post do
|
22
|
+
fieldset :legend => 'New Post' do
|
23
|
+
field :title, :label => 'Title', :required => true
|
24
|
+
field :body, :label => 'Body', :type => :textarea, :required => true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(post)
|
29
|
+
@post = post
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
class RegistrationForm
|
35
|
+
|
36
|
+
include Formation::Form
|
37
|
+
|
38
|
+
fieldset :account do
|
39
|
+
field :login_name, :required => true
|
40
|
+
field :password, :required => true
|
41
|
+
end
|
42
|
+
|
43
|
+
fieldset :address do
|
44
|
+
field :address
|
45
|
+
field :city
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
DataMapper.setup :default, 'sqlite::memory:'
|
51
|
+
DataMapper.auto_migrate!
|
52
|
+
|
53
|
+
get '/styles.css' do
|
54
|
+
<<-CSS
|
55
|
+
*, html {
|
56
|
+
font-family: Verdana, Arial, Helvetica, sans-serif;
|
57
|
+
}
|
58
|
+
body, form, ul, li, p, h2, h3, h4, h5 {
|
59
|
+
margin: 0;
|
60
|
+
padding: 0;
|
61
|
+
}
|
62
|
+
body {
|
63
|
+
background-color: #606061;
|
64
|
+
color: #ffffff;
|
65
|
+
}
|
66
|
+
img {
|
67
|
+
border: none;
|
68
|
+
}
|
69
|
+
p {
|
70
|
+
font-size: 1em;
|
71
|
+
margin: 0 0 1em 0;
|
72
|
+
}
|
73
|
+
h2 {
|
74
|
+
font-size: 14px;
|
75
|
+
margin: 0 0 12px;
|
76
|
+
}
|
77
|
+
|
78
|
+
form {
|
79
|
+
margin: 20px auto;
|
80
|
+
width: 610px;
|
81
|
+
}
|
82
|
+
form fieldset {
|
83
|
+
margin: 0 0 20px;
|
84
|
+
padding: 20px;
|
85
|
+
-webkit-border-radius: 5px;
|
86
|
+
-moz-border-radius: 5px;
|
87
|
+
border-radius: 5px;
|
88
|
+
}
|
89
|
+
form ol {
|
90
|
+
list-style-type: none;
|
91
|
+
padding: 0;
|
92
|
+
margin: 0;
|
93
|
+
}
|
94
|
+
form li {
|
95
|
+
margin: 0 0 12px;
|
96
|
+
position: relative;
|
97
|
+
}
|
98
|
+
form label {
|
99
|
+
width: 150px;
|
100
|
+
display: inline-block;
|
101
|
+
vertical-align: top;
|
102
|
+
}
|
103
|
+
form input, form textarea, form select {
|
104
|
+
background: #fff;
|
105
|
+
display: inline-block;
|
106
|
+
width: 371px;
|
107
|
+
border: 1px solid #fff;
|
108
|
+
padding: 3px 26px 3px 3px;
|
109
|
+
-moz-transition: background-color 1s ease;
|
110
|
+
-webkit-transition: background-color 1s ease;
|
111
|
+
-o-transition: background-color 1s ease;
|
112
|
+
transition: background-color 1s ease;
|
113
|
+
-webkit-border-radius: 5px;
|
114
|
+
-moz-border-radius: 5px;
|
115
|
+
border-radius: 5px;
|
116
|
+
}
|
117
|
+
CSS
|
118
|
+
end
|
119
|
+
|
120
|
+
get '/register' do
|
121
|
+
@form = RegistrationForm.new
|
122
|
+
erb :register
|
123
|
+
end
|
124
|
+
|
125
|
+
post '/register' do
|
126
|
+
@form = RegistrationForm.new
|
127
|
+
@form.submit params
|
128
|
+
if @form.valid?
|
129
|
+
erb :thanks
|
130
|
+
else
|
131
|
+
erb :register
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
get '/posts/new' do
|
136
|
+
@form = PostForm.new(Post.new)
|
137
|
+
erb :new_post
|
138
|
+
end
|
139
|
+
|
140
|
+
post '/posts' do
|
141
|
+
@form = PostForm.new(Post.new)
|
142
|
+
@form.submit params
|
143
|
+
if @form.valid?
|
144
|
+
@form.post.save
|
145
|
+
raise @form.inspect
|
146
|
+
redirect "/posts/#{@form.post.id}"
|
147
|
+
else
|
148
|
+
erb :new_post
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
__END__
|
153
|
+
|
154
|
+
@@ layout
|
155
|
+
<html>
|
156
|
+
<head>
|
157
|
+
<link rel="stylesheet" type="text/css" href="/styles.css" media="screen" />
|
158
|
+
</head>
|
159
|
+
<body>
|
160
|
+
<%= yield %>
|
161
|
+
</body>
|
162
|
+
</html>
|
163
|
+
|
164
|
+
@@ register
|
165
|
+
<h1>Register</h1>
|
166
|
+
<form action="/register" method="post">
|
167
|
+
<%= Formation::Printer.new(@form).print %>
|
168
|
+
<input type="submit" />
|
169
|
+
</form>
|
170
|
+
|
171
|
+
@@ thanks
|
172
|
+
<h1>Thanks!</h1>
|
173
|
+
|
174
|
+
@@ new_post
|
175
|
+
<h1>New Post</h1>
|
176
|
+
<form action="/posts" method="post">
|
177
|
+
<%= Formation::Printer.new(@form).print %>
|
178
|
+
<input type="submit" />
|
179
|
+
</form>
|
data/formation.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "formation/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "formation"
|
7
|
+
s.version = Formation::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ['Chris Gibson']
|
10
|
+
s.email = ['chrislgibson@gmail.com']
|
11
|
+
s.homepage = "http://github.com/chrisgibson/formation"
|
12
|
+
s.summary = %q{first-class ruby forms}
|
13
|
+
s.description = %q{first-class ruby forms}
|
14
|
+
|
15
|
+
# s.rubyforge_project = "formation"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
end
|
data/lib/formation.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
module Formation
|
2
|
+
|
3
|
+
module Support
|
4
|
+
|
5
|
+
end
|
6
|
+
|
7
|
+
module Types
|
8
|
+
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
require File.expand_path('../formation/util', __FILE__)
|
14
|
+
require File.expand_path('../formation/element', __FILE__)
|
15
|
+
require File.expand_path('../formation/field', __FILE__)
|
16
|
+
require File.expand_path('../formation/fieldset', __FILE__)
|
17
|
+
require File.expand_path('../formation/form', __FILE__)
|
18
|
+
require File.expand_path('../formation/type', __FILE__)
|
19
|
+
require File.expand_path('../formation/types/checkbox', __FILE__)
|
20
|
+
require File.expand_path('../formation/types/password', __FILE__)
|
21
|
+
require File.expand_path('../formation/types/text', __FILE__)
|
22
|
+
require File.expand_path('../formation/types/text_area', __FILE__)
|
23
|
+
|
24
|
+
require File.expand_path('../formation/printer', __FILE__)
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Formation::Field < Formation::Element
|
2
|
+
|
3
|
+
attr_reader :label, :fieldset, :type
|
4
|
+
attr_accessor :value
|
5
|
+
|
6
|
+
def initialize(name, options = {})
|
7
|
+
@name = name
|
8
|
+
if @fieldset = options.delete(:fieldset)
|
9
|
+
@fieldset.fields << self
|
10
|
+
end
|
11
|
+
@type = options.delete(:type) || :text
|
12
|
+
@type = Formation::Type.create(self, @type)
|
13
|
+
@label = options.delete(:label) || Formation::Util.titleize(@name)
|
14
|
+
@required = options.delete(:required) || false
|
15
|
+
|
16
|
+
# Attach any left-over entries as accessors
|
17
|
+
options.each do |key, value|
|
18
|
+
metaclass = class << self; self; end
|
19
|
+
metaclass.send :attr_accessor, key.to_sym
|
20
|
+
send "#{key}=", value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def field?
|
25
|
+
true
|
26
|
+
end
|
27
|
+
|
28
|
+
def required?
|
29
|
+
@required
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
module Formation::Form
|
2
|
+
|
3
|
+
module ClassMethods
|
4
|
+
|
5
|
+
def elements
|
6
|
+
@elements ||= []
|
7
|
+
end
|
8
|
+
|
9
|
+
def field(name, options = {})
|
10
|
+
@_current_resource ||= nil
|
11
|
+
@_current_fieldset ||= nil
|
12
|
+
if @_current_resource
|
13
|
+
name = "#{@_current_resource}[#{name}]"
|
14
|
+
else
|
15
|
+
name = name.to_s
|
16
|
+
end
|
17
|
+
fields[name] = Formation::Field.new(name, { :fieldset => @_current_fieldset }.merge(options))
|
18
|
+
elements << fields[name] unless @_current_fieldset
|
19
|
+
end
|
20
|
+
|
21
|
+
def fields
|
22
|
+
@fields ||= {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def fieldset(name, options = {})
|
26
|
+
if name.is_a? Hash
|
27
|
+
options = name
|
28
|
+
name = Formation::Util.underscore(options[:legend])
|
29
|
+
end
|
30
|
+
@_current_fieldset = (fieldsets[name] ||= Formation::Fieldset.new(name, options))
|
31
|
+
elements << @_current_fieldset
|
32
|
+
yield
|
33
|
+
@_current_fieldset = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def fieldsets
|
37
|
+
@fieldsets ||= {}
|
38
|
+
end
|
39
|
+
|
40
|
+
def resource(resource)
|
41
|
+
resources << resource
|
42
|
+
class_eval <<-RUBY
|
43
|
+
attr_accessor :#{resource}
|
44
|
+
RUBY
|
45
|
+
if block_given?
|
46
|
+
@_current_resource = resource
|
47
|
+
yield
|
48
|
+
@_current_resource = nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def resources
|
53
|
+
@resources ||= []
|
54
|
+
end
|
55
|
+
|
56
|
+
def validates_with_method(method_name)
|
57
|
+
method_name = method_name.to_sym
|
58
|
+
validators << method_name
|
59
|
+
end
|
60
|
+
|
61
|
+
def validators
|
62
|
+
@validators ||= []
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.included(base)
|
68
|
+
base.extend(ClassMethods)
|
69
|
+
end
|
70
|
+
|
71
|
+
def elements
|
72
|
+
self.class.elements
|
73
|
+
end
|
74
|
+
|
75
|
+
def errors
|
76
|
+
@errors ||= []
|
77
|
+
end
|
78
|
+
|
79
|
+
def fields
|
80
|
+
fields = elements.map do |element|
|
81
|
+
element.field? ? element : element.fields
|
82
|
+
end
|
83
|
+
fields.flatten
|
84
|
+
end
|
85
|
+
|
86
|
+
def submit(params)
|
87
|
+
@submitted = true
|
88
|
+
errors.clear
|
89
|
+
fields.each do |field|
|
90
|
+
field.value = extract_value_from_params(field.name, params)
|
91
|
+
if field.required? && (field.value.nil? || field.value.empty?)
|
92
|
+
errors << "#{field.label} is required"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
validators.each do |validator|
|
96
|
+
send "#{validator}"
|
97
|
+
end
|
98
|
+
self
|
99
|
+
end
|
100
|
+
|
101
|
+
def valid?
|
102
|
+
!errors.any?
|
103
|
+
end
|
104
|
+
|
105
|
+
def validators
|
106
|
+
self.class.validators
|
107
|
+
end
|
108
|
+
|
109
|
+
def value_for(name)
|
110
|
+
@values[name.to_s]
|
111
|
+
end
|
112
|
+
|
113
|
+
def values
|
114
|
+
@values ||= {}
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def extract_value_from_params(name, params)
|
120
|
+
key = %r{([^\[\]=&]+)}.match(name).captures.first
|
121
|
+
value = if self.class.resources.include? key.to_sym
|
122
|
+
captures = %r{\[([^\[\]=&]+)\]}.match(name).captures
|
123
|
+
return nil unless captures.any?
|
124
|
+
resource = params.fetch(key, nil)
|
125
|
+
return nil unless resource
|
126
|
+
actual_value = resource.fetch(captures.first, nil)
|
127
|
+
return nil unless actual_value
|
128
|
+
send(key).send("#{captures.first}=", actual_value)
|
129
|
+
else
|
130
|
+
if params[key]
|
131
|
+
branch = params[key].dup
|
132
|
+
match = %r{\[([^\[\]=&]+)\]}.match(name)
|
133
|
+
if match
|
134
|
+
match.captures.each do |capture|
|
135
|
+
branch = branch[capture]
|
136
|
+
end
|
137
|
+
end
|
138
|
+
branch
|
139
|
+
else
|
140
|
+
nil
|
141
|
+
end
|
142
|
+
end
|
143
|
+
values[name] = value
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
class Formation::Printer
|
2
|
+
|
3
|
+
attr_reader :form
|
4
|
+
|
5
|
+
def initialize(form)
|
6
|
+
@form = form
|
7
|
+
end
|
8
|
+
|
9
|
+
def print
|
10
|
+
html = ''
|
11
|
+
html << print_errors(form.errors)
|
12
|
+
form.elements.each do |element|
|
13
|
+
if element.field?
|
14
|
+
html << print_field(element)
|
15
|
+
else
|
16
|
+
html << print_fieldset(element)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
html
|
20
|
+
end
|
21
|
+
|
22
|
+
def print_error(error)
|
23
|
+
<<-HTML
|
24
|
+
<p style="color: red">#{error}</p>
|
25
|
+
HTML
|
26
|
+
end
|
27
|
+
|
28
|
+
def print_errors(errors)
|
29
|
+
errors.map { |error| print_error error }.join("\n")
|
30
|
+
end
|
31
|
+
|
32
|
+
def print_field(field)
|
33
|
+
<<-HTML
|
34
|
+
<li>
|
35
|
+
<label>#{field.label}</label>
|
36
|
+
#{field.type.to_html}
|
37
|
+
</li>
|
38
|
+
HTML
|
39
|
+
end
|
40
|
+
|
41
|
+
def print_fieldset(fieldset)
|
42
|
+
<<-HTML
|
43
|
+
<fieldset>
|
44
|
+
<legend>#{fieldset.legend}</legend>
|
45
|
+
<ol>
|
46
|
+
#{fieldset.fields.map { |f| print_field f }.join("\n")}
|
47
|
+
</ol>
|
48
|
+
</fieldset>
|
49
|
+
HTML
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class Formation::Type
|
2
|
+
|
3
|
+
def self.create(field, type)
|
4
|
+
klass = case type.to_sym
|
5
|
+
when :checkbox
|
6
|
+
Formation::Types::Checkbox
|
7
|
+
when :password
|
8
|
+
Formation::Types::Password
|
9
|
+
when :text
|
10
|
+
Formation::Types::Text
|
11
|
+
when :textarea
|
12
|
+
Formation::Types::TextArea
|
13
|
+
else
|
14
|
+
raise "Invalid or unsupported field type: #{type}"
|
15
|
+
end
|
16
|
+
klass.new(field)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Formation::Types::Checkbox
|
2
|
+
|
3
|
+
attr_reader :field
|
4
|
+
|
5
|
+
def initialize(field)
|
6
|
+
@field = field
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_html
|
10
|
+
<<-HTML.strip
|
11
|
+
<input type="checkbox" name="#{field.name}" value="1" checked="#{field.value ? 'checked' : ''}" />
|
12
|
+
HTML
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
data/readme.md
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
Formation
|
2
|
+
=========
|
3
|
+
|
4
|
+
`formation` is a ruby library for building, validating, and extending forms.
|
5
|
+
|
6
|
+
Simple:
|
7
|
+
|
8
|
+
class RegistrationForm
|
9
|
+
|
10
|
+
include Formation::Form
|
11
|
+
|
12
|
+
fieldset :account do
|
13
|
+
field :login_name, :type => :text, :required => true
|
14
|
+
field :password, :type => :text, :required => true
|
15
|
+
end
|
16
|
+
|
17
|
+
fieldset :address do
|
18
|
+
field :address, :type => :text
|
19
|
+
field :city, :type => :text
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
Model:
|
25
|
+
|
26
|
+
class Post
|
27
|
+
|
28
|
+
include DataMapper::Resource
|
29
|
+
|
30
|
+
property :id, Serial
|
31
|
+
property :title, String
|
32
|
+
property :body, Text
|
33
|
+
property :created_at, DateTime
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
class PostForm
|
38
|
+
|
39
|
+
include Formation::Form
|
40
|
+
|
41
|
+
resource :post do
|
42
|
+
fieldset :legend => 'New Post' do
|
43
|
+
field :title, :type => :text, :required => true
|
44
|
+
field :body, :type => :textarea, :required => true
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def initialize(post)
|
49
|
+
@post = post
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
### Validating
|
55
|
+
...
|
56
|
+
|
57
|
+
### Printing
|
58
|
+
...
|
59
|
+
|
60
|
+
### Extending
|
61
|
+
...
|
data/test/field_spec.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Formation::Field do
|
4
|
+
|
5
|
+
describe '.label' do
|
6
|
+
|
7
|
+
it 'should use the given label, if specified' do
|
8
|
+
Formation::Field.new('first_name', :label => 'Name').label.must_equal 'Name'
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'should use the titleized name, if not specified' do
|
12
|
+
Formation::Field.new('first_name').label.must_equal 'First Name'
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
data/test/form_spec.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Formation::Form do
|
4
|
+
|
5
|
+
describe '#fields' do
|
6
|
+
|
7
|
+
it 'should have the correct name' do
|
8
|
+
SimpleForm.fields['first_name'].name.must_equal 'first_name'
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'should have the label of the fieldset it belongs to (if any)' do
|
12
|
+
SimpleForm.fields['address'].fieldset.legend.must_equal 'Address'
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should pass-through custom attributes' do
|
16
|
+
SimpleForm.fields['username'].custom.must_equal 'test'
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#fieldsets' do
|
22
|
+
|
23
|
+
it 'should contain the fields belonging to it' do
|
24
|
+
SimpleForm.fieldsets['Address'].fields.map(&:name).must_equal %w{ address city zip_code }
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
describe 'an instance' do
|
30
|
+
|
31
|
+
before do
|
32
|
+
@simple_form = SimpleForm.new(:first_name => 'Chris')
|
33
|
+
@post = OpenStruct.new(:title => 'Test')
|
34
|
+
@model_form = PostForm.new(@post)
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '.elements' do
|
38
|
+
|
39
|
+
it 'should return the elements in the correct order' do
|
40
|
+
@simple_form.elements.map(&:name).must_equal %w{ first_name Address login_details }
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
describe '.submit' do
|
46
|
+
|
47
|
+
it 'should accept empty params' do
|
48
|
+
@simple_form.submit({})
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should extract values of fields' do
|
52
|
+
@simple_form.submit({'first_name' => 'Christopher' })
|
53
|
+
@simple_form.values['first_name'].must_equal 'Christopher'
|
54
|
+
|
55
|
+
@model_form.submit({'post' => { 'title' => 'Testing' }, 'author' => { 'name' => 'Chris'}})
|
56
|
+
@model_form.post.title.must_equal 'Testing'
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should be valid if all required values were provided' do
|
60
|
+
@simple_form.submit({'first_name' => 'Christopher', 'zip_code' => '12345' })
|
61
|
+
@simple_form.valid?.must_equal true
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should not be valid if all required values were not provided' do
|
65
|
+
@simple_form.submit({})
|
66
|
+
@simple_form.valid?.must_equal false
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'should have an error for each missing required value' do
|
70
|
+
@simple_form.submit({})
|
71
|
+
@simple_form.errors.must_equal ['First Name is required', 'Zip Code is invalid']
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
describe '.values' do
|
77
|
+
|
78
|
+
it 'should extract values from resource' do
|
79
|
+
@simple_form.values['first_name'].must_equal 'Chris'
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class SimpleForm
|
2
|
+
include Formation::Form
|
3
|
+
|
4
|
+
field 'first_name', :required => true
|
5
|
+
|
6
|
+
fieldset 'Address' do
|
7
|
+
field 'address', :type => :text
|
8
|
+
field 'city', :type => :text
|
9
|
+
field 'zip_code', :type => :text
|
10
|
+
end
|
11
|
+
|
12
|
+
fieldset :legend => 'Login Details' do
|
13
|
+
field 'username', :custom => 'test'
|
14
|
+
end
|
15
|
+
|
16
|
+
validates_with_method :validate_zip_code
|
17
|
+
|
18
|
+
def initialize(options = {})
|
19
|
+
options.each do |key, value|
|
20
|
+
values[key.to_s] = value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def validate_zip_code
|
25
|
+
if value_for(:zip_code) =~ /^\d{5}$/
|
26
|
+
true
|
27
|
+
else
|
28
|
+
errors << 'Zip Code is invalid'
|
29
|
+
false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require 'minitest/pride'
|
5
|
+
require 'ostruct'
|
6
|
+
|
7
|
+
require File.expand_path('../../lib/formation', __FILE__)
|
8
|
+
require File.expand_path('../forms/simple_form', __FILE__)
|
9
|
+
require File.expand_path('../forms/post_form', __FILE__)
|
data/test/type_spec.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Formation::Type do
|
4
|
+
|
5
|
+
describe '#create' do
|
6
|
+
|
7
|
+
it 'should raise an exception if the type is unsupported' do
|
8
|
+
lambda {
|
9
|
+
Formation::Type.create(nil, :foo)
|
10
|
+
}.must_raise RuntimeError
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require File.expand_path('../../test_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Formation::Types::Checkbox do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@field = Formation::Field.new('remember_me', :type => :checkbox)
|
7
|
+
@type = @field.type
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '.to_html' do
|
11
|
+
|
12
|
+
describe 'with no value' do
|
13
|
+
|
14
|
+
before do
|
15
|
+
@field.value = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should render correctly' do
|
19
|
+
@type.to_html.must_equal <<-HTML.strip
|
20
|
+
<input type="checkbox" name="remember_me" value="1" checked="" />
|
21
|
+
HTML
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
describe 'with a false value' do
|
27
|
+
|
28
|
+
before do
|
29
|
+
@field.value = false
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should render correctly' do
|
33
|
+
@type.to_html.must_equal <<-HTML.strip
|
34
|
+
<input type="checkbox" name="remember_me" value="1" checked="" />
|
35
|
+
HTML
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
describe 'with a true value' do
|
41
|
+
|
42
|
+
before do
|
43
|
+
@field.value = true
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should render correctly' do
|
47
|
+
@type.to_html.must_equal <<-HTML.strip
|
48
|
+
<input type="checkbox" name="remember_me" value="1" checked="checked" />
|
49
|
+
HTML
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require File.expand_path('../../test_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Formation::Types::Password do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@field = Formation::Field.new('password', :type => :password)
|
7
|
+
@type = @field.type
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '.to_html' do
|
11
|
+
|
12
|
+
describe 'with no value' do
|
13
|
+
|
14
|
+
before do
|
15
|
+
@field.value = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should render correctly' do
|
19
|
+
@type.to_html.must_equal <<-HTML.strip
|
20
|
+
<input type="password" name="password" />
|
21
|
+
HTML
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
describe 'with a value' do
|
27
|
+
|
28
|
+
before do
|
29
|
+
@field.value = 'testing'
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should render correctly' do
|
33
|
+
@type.to_html.must_equal <<-HTML.strip
|
34
|
+
<input type="password" name="password" />
|
35
|
+
HTML
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require File.expand_path('../../test_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Formation::Types::TextArea do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@field = Formation::Field.new('body', :type => :textarea)
|
7
|
+
@type = @field.type
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '.to_html' do
|
11
|
+
|
12
|
+
describe 'with no value' do
|
13
|
+
|
14
|
+
before do
|
15
|
+
@field.value = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should render correctly' do
|
19
|
+
@type.to_html.must_equal <<-HTML.strip
|
20
|
+
<textarea name="body"></textarea>
|
21
|
+
HTML
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
describe 'with a value' do
|
27
|
+
|
28
|
+
before do
|
29
|
+
@field.value = 'Blah, blah, blah'
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should render correctly' do
|
33
|
+
@type.to_html.must_equal <<-HTML.strip
|
34
|
+
<textarea name="body">Blah, blah, blah</textarea>
|
35
|
+
HTML
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require File.expand_path('../../test_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Formation::Types::Text do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@field = Formation::Field.new('username', :type => :text)
|
7
|
+
@type = @field.type
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '.to_html' do
|
11
|
+
|
12
|
+
describe 'with no value' do
|
13
|
+
|
14
|
+
before do
|
15
|
+
@field.value = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should render correctly' do
|
19
|
+
@type.to_html.must_equal <<-HTML.strip
|
20
|
+
<input type="text" name="username" value="" />
|
21
|
+
HTML
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
describe 'with a value' do
|
27
|
+
|
28
|
+
before do
|
29
|
+
@field.value = 'chris'
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should render correctly' do
|
33
|
+
@type.to_html.must_equal <<-HTML.strip
|
34
|
+
<input type="text" name="username" value="chris" />
|
35
|
+
HTML
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
data/test/util_spec.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Formation::Util do
|
4
|
+
|
5
|
+
describe '.titleize' do
|
6
|
+
|
7
|
+
it 'should handle underscores and case' do
|
8
|
+
Formation::Util.titleize('first_name').must_equal 'First Name'
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '.underscore' do
|
14
|
+
|
15
|
+
it 'should handle spaces and case' do
|
16
|
+
Formation::Util.underscore('First Name').must_equal 'first_name'
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: formation
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Chris Gibson
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-03-23 00:00:00 -05:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: first-class ruby forms
|
23
|
+
email:
|
24
|
+
- chrislgibson@gmail.com
|
25
|
+
executables: []
|
26
|
+
|
27
|
+
extensions: []
|
28
|
+
|
29
|
+
extra_rdoc_files: []
|
30
|
+
|
31
|
+
files:
|
32
|
+
- .gitignore
|
33
|
+
- Gemfile
|
34
|
+
- Rakefile
|
35
|
+
- demo.rb
|
36
|
+
- formation.gemspec
|
37
|
+
- lib/formation.rb
|
38
|
+
- lib/formation/element.rb
|
39
|
+
- lib/formation/field.rb
|
40
|
+
- lib/formation/fieldset.rb
|
41
|
+
- lib/formation/form.rb
|
42
|
+
- lib/formation/printer.rb
|
43
|
+
- lib/formation/type.rb
|
44
|
+
- lib/formation/types/checkbox.rb
|
45
|
+
- lib/formation/types/password.rb
|
46
|
+
- lib/formation/types/text.rb
|
47
|
+
- lib/formation/types/text_area.rb
|
48
|
+
- lib/formation/util.rb
|
49
|
+
- lib/formation/version.rb
|
50
|
+
- readme.md
|
51
|
+
- test/field_spec.rb
|
52
|
+
- test/form_spec.rb
|
53
|
+
- test/forms/post_form.rb
|
54
|
+
- test/forms/simple_form.rb
|
55
|
+
- test/test_helper.rb
|
56
|
+
- test/type_spec.rb
|
57
|
+
- test/types/checkbox_spec.rb
|
58
|
+
- test/types/password_spec.rb
|
59
|
+
- test/types/text_area_spec.rb
|
60
|
+
- test/types/text_spec.rb
|
61
|
+
- test/util_spec.rb
|
62
|
+
has_rdoc: true
|
63
|
+
homepage: http://github.com/chrisgibson/formation
|
64
|
+
licenses: []
|
65
|
+
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options: []
|
68
|
+
|
69
|
+
require_paths:
|
70
|
+
- lib
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
hash: 3
|
77
|
+
segments:
|
78
|
+
- 0
|
79
|
+
version: "0"
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
hash: 3
|
86
|
+
segments:
|
87
|
+
- 0
|
88
|
+
version: "0"
|
89
|
+
requirements: []
|
90
|
+
|
91
|
+
rubyforge_project:
|
92
|
+
rubygems_version: 1.5.3
|
93
|
+
signing_key:
|
94
|
+
specification_version: 3
|
95
|
+
summary: first-class ruby forms
|
96
|
+
test_files:
|
97
|
+
- test/field_spec.rb
|
98
|
+
- test/form_spec.rb
|
99
|
+
- test/forms/post_form.rb
|
100
|
+
- test/forms/simple_form.rb
|
101
|
+
- test/test_helper.rb
|
102
|
+
- test/type_spec.rb
|
103
|
+
- test/types/checkbox_spec.rb
|
104
|
+
- test/types/password_spec.rb
|
105
|
+
- test/types/text_area_spec.rb
|
106
|
+
- test/types/text_spec.rb
|
107
|
+
- test/util_spec.rb
|