freeform 0.0.4 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -17
- data/Gemfile +1 -1
- data/Gemfile.lock +150 -0
- data/MIT-LICENSE +20 -0
- data/README.md +31 -43
- data/Rakefile +32 -1
- data/freeform.gemspec +22 -15
- data/lib/freeform/builder/builder_mixin.rb +111 -0
- data/lib/freeform/builder/builders.rb +32 -0
- data/lib/freeform/builder/engine.rb +14 -0
- data/lib/freeform/builder/view_helper.rb +56 -0
- data/lib/freeform/form/form_input_key.rb +1 -1
- data/lib/freeform/form/nested.rb +17 -55
- data/lib/freeform/form/property.rb +82 -42
- data/lib/freeform/form/validation.rb +38 -11
- data/lib/freeform/form.rb +24 -27
- data/lib/freeform/version.rb +1 -1
- data/lib/freeform.rb +4 -0
- data/lib/generators/freeform/install_generator.rb +17 -0
- data/lib/tasks/freeform_tasks.rake +4 -0
- data/spec/acceptance_spec.rb +261 -155
- data/spec/builder/builder_spec.rb +215 -0
- data/spec/builder/view_helper_spec.rb +97 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/images/.keep +0 -0
- data/spec/dummy/app/assets/javascripts/application.js +5 -0
- data/spec/dummy/app/assets/javascripts/jquery.js +9404 -0
- data/spec/dummy/app/assets/javascripts/jquery_events_test.js +19 -0
- data/spec/dummy/app/assets/javascripts/projects.js +2 -0
- data/spec/dummy/app/assets/javascripts/prototype.js +6082 -0
- data/spec/dummy/app/assets/javascripts/prototype_events_test.js +20 -0
- data/spec/dummy/app/assets/stylesheets/application.css +7 -0
- data/spec/dummy/app/assets/stylesheets/companies.css +4 -0
- data/spec/dummy/app/assets/stylesheets/projects.css +4 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/controllers/companies_controller.rb +5 -0
- data/spec/dummy/app/controllers/projects_controller.rb +11 -0
- data/spec/dummy/app/forms/milestone_form.rb +4 -0
- data/spec/dummy/app/forms/project_form.rb +11 -0
- data/spec/dummy/app/forms/task_form.rb +10 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.keep +0 -0
- data/spec/dummy/app/models/.keep +0 -0
- data/spec/dummy/app/models/company.rb +5 -0
- data/spec/dummy/app/models/milestone.rb +6 -0
- data/spec/dummy/app/models/project.rb +9 -0
- data/spec/dummy/app/models/project_task.rb +3 -0
- data/spec/dummy/app/models/task.rb +7 -0
- data/spec/dummy/app/views/companies/new.html.erb +16 -0
- data/spec/dummy/app/views/layouts/application.html.erb +19 -0
- data/spec/dummy/app/views/projects/new.html.erb +13 -0
- data/spec/dummy/app/views/projects/without_intermediate_inputs.html.erb +11 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/config/application.rb +35 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +29 -0
- data/spec/dummy/config/environments/production.rb +80 -0
- data/spec/dummy/config/environments/test.rb +36 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +13 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +5 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/migrate/20110710143903_initial_tables.rb +38 -0
- data/spec/dummy/db/schema.rb +43 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/lib/assets/.keep +0 -0
- data/spec/dummy/log/.keep +0 -0
- data/spec/dummy/log/development.log +0 -0
- data/spec/dummy/log/test.log +39736 -0
- data/spec/dummy/public/404.html +58 -0
- data/spec/dummy/public/422.html +58 -0
- data/spec/dummy/public/500.html +57 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/tmp/cache/assets/C54/850/sprockets%2Fd484f47085686f151320ff7248bb5105 +0 -0
- data/spec/dummy/tmp/cache/assets/C76/160/sprockets%2F49c594d143212b346d11f1ecf9358103 +0 -0
- data/spec/dummy/tmp/cache/assets/C99/4D0/sprockets%2F5e30a6b911437f1428dc32c3ae182123 +0 -0
- data/spec/dummy/tmp/cache/assets/C99/7D0/sprockets%2F79513e6956e0ee8624976e041fd5636d +0 -0
- data/spec/dummy/tmp/cache/assets/CAA/C90/sprockets%2F1e6c8ee1258009385ccf5b84015424b3 +0 -0
- data/spec/dummy/tmp/cache/assets/CB2/CB0/sprockets%2F11cc8d161d71a716dd36f16849d90870 +0 -0
- data/spec/dummy/tmp/cache/assets/CB7/7F0/sprockets%2Fac97b043470f6fcc925c352f16956643 +0 -0
- data/spec/dummy/tmp/cache/assets/CBA/680/sprockets%2F912d84e091e5f2190b1ab7926de19679 +0 -0
- data/spec/dummy/tmp/cache/assets/CC7/430/sprockets%2F48e99a3e16ba3e65749e009d82546f94 +0 -0
- data/spec/dummy/tmp/cache/assets/CD8/370/sprockets%2F357970feca3ac29060c1e3861e2c0953 +0 -0
- data/spec/dummy/tmp/cache/assets/CDB/8A0/sprockets%2Faed2a2575c376263c26e93b56b57051d +0 -0
- data/spec/dummy/tmp/cache/assets/CF1/E80/sprockets%2F7735295e1e3085f2e1dc29d59a45e8e6 +0 -0
- data/spec/dummy/tmp/cache/assets/D0F/E60/sprockets%2F1123d9a479fdf5c2b8415087e2a43ec6 +0 -0
- data/spec/dummy/tmp/cache/assets/D1F/A10/sprockets%2F60317e62cb324bfd9987e8da9636fd06 +0 -0
- data/spec/dummy/tmp/cache/assets/D25/E20/sprockets%2F29ba1a4a2b611731334f54abb9e5b5d1 +0 -0
- data/spec/dummy/tmp/cache/assets/D32/A10/sprockets%2F13fe41fee1fe35b49d145bcc06610705 +0 -0
- data/spec/dummy/tmp/cache/assets/D35/760/sprockets%2F49e26620c5c743ac4cdab7703f54db06 +0 -0
- data/spec/dummy/tmp/cache/assets/D49/870/sprockets%2F90896142645585acc8baf56ad57e3afb +0 -0
- data/spec/dummy/tmp/cache/assets/D4E/1B0/sprockets%2Ff7cbd26ba1d28d48de824f0e94586655 +0 -0
- data/spec/dummy/tmp/cache/assets/D5A/EA0/sprockets%2Fd771ace226fc8215a3572e0aa35bb0d6 +0 -0
- data/spec/dummy/tmp/cache/assets/D62/F00/sprockets%2F6ac03a007f26b1c18ed3d53498ad3eb8 +0 -0
- data/spec/dummy/tmp/cache/assets/D79/BC0/sprockets%2F85a1db977361cc5130fcefb4637ff67e +0 -0
- data/spec/dummy/tmp/cache/assets/D87/1F0/sprockets%2F61c9ceafb877fb740c67416ff6f782a9 +0 -0
- data/spec/dummy/tmp/cache/assets/DA8/900/sprockets%2Fde7a2b3987b89b9d9afb268ce4204a4a +0 -0
- data/spec/dummy/tmp/cache/assets/DAD/4F0/sprockets%2F765acd1bf68dc90f7d3e61da78b1e659 +0 -0
- data/spec/dummy/tmp/cache/assets/DDC/400/sprockets%2Fcffd775d018f68ce5dba1ee0d951a994 +0 -0
- data/spec/dummy/tmp/cache/assets/E03/3F0/sprockets%2F82b37ae9eccec44c1ef44cfdd07d8534 +0 -0
- data/spec/dummy/tmp/cache/assets/E04/890/sprockets%2F2f5173deea6c795b8fdde723bb4b63af +0 -0
- data/spec/dummy/tmp/cache/assets/E0A/CA0/sprockets%2F7fe72ac1c0db1a7e8e7f59cf25e8a39e +0 -0
- data/spec/dummy/tmp/cache/assets/E20/9B0/sprockets%2F2fa6cd86b1af7bda41f5b7921cdadf10 +0 -0
- data/spec/dummy/tmp/cache/assets/E3A/A60/sprockets%2F7c72c96cfc66454caf5fc8ca0fedd4f3 +0 -0
- data/spec/dummy/tmp/cache/assets/E54/400/sprockets%2Fd0cbc16cc37efcf9dc41f242b5dbbf81 +0 -0
- data/spec/dummy/tmp/cache/assets/EB7/AB0/sprockets%2F801d29a5debdbfbfb4eef14d70d9bcdb +0 -0
- data/spec/dummy/tmp/cache/assets/F48/5E0/sprockets%2F26cd6ffcffbebbe2fd6cd1a8f0c2debc +0 -0
- data/spec/form/nested_spec.rb +75 -61
- data/spec/form/property_spec.rb +167 -14
- data/spec/form/validation_spec.rb +46 -23
- data/spec/javascript_spec.rb +60 -0
- data/spec/persistence_spec.rb +608 -0
- data/spec/spec_helper.rb +18 -2
- data/vendor/assets/javascripts/jquery_freeform.js +120 -0
- data/vendor/assets/javascripts/jquery_nested_form.js +0 -0
- data/vendor/assets/javascripts/prototype_freeform.js +69 -0
- data/vendor/assets/javascripts/prototype_nested_form.js +0 -0
- metadata +341 -3
data/.gitignore
CHANGED
@@ -1,17 +1,8 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
coverage
|
10
|
-
doc/
|
11
|
-
lib/bundler/man
|
12
|
-
pkg
|
13
|
-
rdoc
|
14
|
-
spec/reports
|
15
|
-
test/tmp
|
16
|
-
test/version_tmp
|
17
|
-
tmp
|
1
|
+
.bundle/
|
2
|
+
log/*.log
|
3
|
+
pkg/
|
4
|
+
test/dummy/db/*.sqlite3
|
5
|
+
test/dummy/db/*.sqlite3-journal
|
6
|
+
test/dummy/log/*.log
|
7
|
+
test/dummy/tmp/*
|
8
|
+
test/dummy/.sass-cache
|
data/Gemfile
CHANGED
data/Gemfile.lock
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
freeform (0.0.4)
|
5
|
+
activemodel
|
6
|
+
formtastic
|
7
|
+
nested_form (~> 0.3.2)
|
8
|
+
rails (~> 3.2.0)
|
9
|
+
simple_form
|
10
|
+
|
11
|
+
GEM
|
12
|
+
remote: https://rubygems.org/
|
13
|
+
specs:
|
14
|
+
actionmailer (3.2.16)
|
15
|
+
actionpack (= 3.2.16)
|
16
|
+
mail (~> 2.5.4)
|
17
|
+
actionpack (3.2.16)
|
18
|
+
activemodel (= 3.2.16)
|
19
|
+
activesupport (= 3.2.16)
|
20
|
+
builder (~> 3.0.0)
|
21
|
+
erubis (~> 2.7.0)
|
22
|
+
journey (~> 1.0.4)
|
23
|
+
rack (~> 1.4.5)
|
24
|
+
rack-cache (~> 1.2)
|
25
|
+
rack-test (~> 0.6.1)
|
26
|
+
sprockets (~> 2.2.1)
|
27
|
+
activemodel (3.2.16)
|
28
|
+
activesupport (= 3.2.16)
|
29
|
+
builder (~> 3.0.0)
|
30
|
+
activerecord (3.2.16)
|
31
|
+
activemodel (= 3.2.16)
|
32
|
+
activesupport (= 3.2.16)
|
33
|
+
arel (~> 3.0.2)
|
34
|
+
tzinfo (~> 0.3.29)
|
35
|
+
activeresource (3.2.16)
|
36
|
+
activemodel (= 3.2.16)
|
37
|
+
activesupport (= 3.2.16)
|
38
|
+
activesupport (3.2.16)
|
39
|
+
i18n (~> 0.6, >= 0.6.4)
|
40
|
+
multi_json (~> 1.0)
|
41
|
+
arel (3.0.3)
|
42
|
+
builder (3.0.4)
|
43
|
+
capybara (1.1.4)
|
44
|
+
mime-types (>= 1.16)
|
45
|
+
nokogiri (>= 1.3.3)
|
46
|
+
rack (>= 1.0.0)
|
47
|
+
rack-test (>= 0.5.4)
|
48
|
+
selenium-webdriver (~> 2.0)
|
49
|
+
xpath (~> 0.1.4)
|
50
|
+
childprocess (0.3.9)
|
51
|
+
ffi (~> 1.0, >= 1.0.11)
|
52
|
+
diff-lcs (1.2.5)
|
53
|
+
erubis (2.7.0)
|
54
|
+
ffi (1.9.3)
|
55
|
+
formtastic (2.2.1)
|
56
|
+
actionpack (>= 3.0)
|
57
|
+
hike (1.2.3)
|
58
|
+
i18n (0.6.9)
|
59
|
+
journey (1.0.4)
|
60
|
+
json (1.8.1)
|
61
|
+
mail (2.5.4)
|
62
|
+
mime-types (~> 1.16)
|
63
|
+
treetop (~> 1.4.8)
|
64
|
+
metaclass (0.0.1)
|
65
|
+
mime-types (1.25.1)
|
66
|
+
mini_portile (0.5.2)
|
67
|
+
mocha (0.14.0)
|
68
|
+
metaclass (~> 0.0.1)
|
69
|
+
multi_json (1.8.2)
|
70
|
+
nested_form (0.3.2)
|
71
|
+
nokogiri (1.6.0)
|
72
|
+
mini_portile (~> 0.5.0)
|
73
|
+
polyglot (0.3.3)
|
74
|
+
rack (1.4.5)
|
75
|
+
rack-cache (1.2)
|
76
|
+
rack (>= 0.4)
|
77
|
+
rack-ssl (1.3.3)
|
78
|
+
rack
|
79
|
+
rack-test (0.6.2)
|
80
|
+
rack (>= 1.0)
|
81
|
+
rails (3.2.16)
|
82
|
+
actionmailer (= 3.2.16)
|
83
|
+
actionpack (= 3.2.16)
|
84
|
+
activerecord (= 3.2.16)
|
85
|
+
activeresource (= 3.2.16)
|
86
|
+
activesupport (= 3.2.16)
|
87
|
+
bundler (~> 1.0)
|
88
|
+
railties (= 3.2.16)
|
89
|
+
railties (3.2.16)
|
90
|
+
actionpack (= 3.2.16)
|
91
|
+
activesupport (= 3.2.16)
|
92
|
+
rack-ssl (~> 1.3.2)
|
93
|
+
rake (>= 0.8.7)
|
94
|
+
rdoc (~> 3.4)
|
95
|
+
thor (>= 0.14.6, < 2.0)
|
96
|
+
rake (10.1.0)
|
97
|
+
rdoc (3.12.2)
|
98
|
+
json (~> 1.4)
|
99
|
+
rspec (2.14.1)
|
100
|
+
rspec-core (~> 2.14.0)
|
101
|
+
rspec-expectations (~> 2.14.0)
|
102
|
+
rspec-mocks (~> 2.14.0)
|
103
|
+
rspec-core (2.14.7)
|
104
|
+
rspec-expectations (2.14.4)
|
105
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
106
|
+
rspec-mocks (2.14.4)
|
107
|
+
rspec-rails (2.14.0)
|
108
|
+
actionpack (>= 3.0)
|
109
|
+
activesupport (>= 3.0)
|
110
|
+
railties (>= 3.0)
|
111
|
+
rspec-core (~> 2.14.0)
|
112
|
+
rspec-expectations (~> 2.14.0)
|
113
|
+
rspec-mocks (~> 2.14.0)
|
114
|
+
rubyzip (1.1.0)
|
115
|
+
selenium-webdriver (2.38.0)
|
116
|
+
childprocess (>= 0.2.5)
|
117
|
+
multi_json (~> 1.0)
|
118
|
+
rubyzip (~> 1.0)
|
119
|
+
websocket (~> 1.0.4)
|
120
|
+
simple_form (2.1.1)
|
121
|
+
actionpack (~> 3.0)
|
122
|
+
activemodel (~> 3.0)
|
123
|
+
sprockets (2.2.2)
|
124
|
+
hike (~> 1.2)
|
125
|
+
multi_json (~> 1.0)
|
126
|
+
rack (~> 1.0)
|
127
|
+
tilt (~> 1.1, != 1.3.0)
|
128
|
+
sqlite3 (1.3.8)
|
129
|
+
thor (0.18.1)
|
130
|
+
tilt (1.4.1)
|
131
|
+
treetop (1.4.15)
|
132
|
+
polyglot
|
133
|
+
polyglot (>= 0.3.1)
|
134
|
+
tzinfo (0.3.38)
|
135
|
+
websocket (1.0.7)
|
136
|
+
xpath (0.1.4)
|
137
|
+
nokogiri (~> 1.3)
|
138
|
+
|
139
|
+
PLATFORMS
|
140
|
+
ruby
|
141
|
+
|
142
|
+
DEPENDENCIES
|
143
|
+
bundler (~> 1.3)
|
144
|
+
capybara (~> 1.1)
|
145
|
+
freeform!
|
146
|
+
mocha
|
147
|
+
rake
|
148
|
+
rspec
|
149
|
+
rspec-rails (~> 2.6)
|
150
|
+
sqlite3
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2013 YOURNAME
|
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.md
CHANGED
@@ -12,7 +12,7 @@ FreeForm is designed primarily with Rails in mind, but it should work on any Rub
|
|
12
12
|
|
13
13
|
Add this line to your application's Gemfile:
|
14
14
|
|
15
|
-
gem 'freeform', '>= 0.0
|
15
|
+
gem 'freeform', '>= 1.0.0'
|
16
16
|
|
17
17
|
And then execute:
|
18
18
|
|
@@ -24,7 +24,7 @@ Or install it yourself as:
|
|
24
24
|
|
25
25
|
## How It Works
|
26
26
|
|
27
|
-
FreeForm can 1-*n* models, exposing whatever attributes you wish from each model, and delegating those assignments back to the models themselves. This means that one form can be used just as easily to support parent/child models, multiple unrelated models, etc. Your database relationships can change, and your forms won't have to.
|
27
|
+
FreeForm creates model objects that can hold 1-*n* models, exposing whatever attributes you wish from each model, and delegating those assignments back to the models themselves. This means that one form can be used just as easily to support parent/child models, multiple unrelated models, etc. Your database relationships can change, and your forms won't have to.
|
28
28
|
|
29
29
|
**Example**
|
30
30
|
|
@@ -185,7 +185,9 @@ Personally, I use validations in both places. My domain models have their own v
|
|
185
185
|
|
186
186
|
## Nesting Forms
|
187
187
|
|
188
|
-
|
188
|
+
One of the benefits of forms objects is that you don't have to mess with models accepting nested attributes.
|
189
|
+
But sometimes, you need to be able to support a collection of unknown size (e.g. a user with many phone numbers).
|
190
|
+
Since FreeForm makes no assumptions about your domain models, we nest forms themselves.
|
189
191
|
|
190
192
|
```ruby
|
191
193
|
class UserForm < FreeForm::Form
|
@@ -193,51 +195,32 @@ class UserForm < FreeForm::Form
|
|
193
195
|
|
194
196
|
property :username, :on => :user
|
195
197
|
property :email, :on => :user
|
198
|
+
|
199
|
+
has_many :phone_numbers, :class => PhoneNumberForm, :default_initializer => :phone_initializer
|
196
200
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
property :area_code, :on => :phone
|
201
|
-
property :number, :on => :phone
|
201
|
+
def phone_initializer
|
202
|
+
{ :phone => user.phone_numbers.build }
|
202
203
|
end
|
203
204
|
end
|
205
|
+
|
206
|
+
class PhoneNumberForm < FreeForm::Form
|
207
|
+
form_models :phone
|
208
|
+
|
209
|
+
property :area_code, :on => :phone
|
210
|
+
property :number, :on => :phone
|
211
|
+
end
|
204
212
|
```
|
205
213
|
**Note:** The method `nested_form` is also aliased as `has_many` and `has_one`, if you prefer the expressiveness of that syntax. The functionality is the same in any case.
|
206
214
|
|
207
215
|
When using a nested form, the form starts with **no** nested forms pre-built. FreeForm provides a method called `build_#{nested_form_model}` (e.g. `build_phone_numbers`) that you can use to build a nested form. You must provide the initializer:
|
208
216
|
```ruby
|
209
217
|
form = UserForm.new(:user => User.new)
|
210
|
-
form.
|
211
|
-
# The singularized version is aliased as well.
|
212
|
-
form.build_phone_number(:phone => Phone.new)
|
213
|
-
```
|
214
|
-
|
215
|
-
**Working with nested_form gem**
|
216
|
-
In order to support the `nested_form` gem, FreeForm currently uses a bit of hackiness. You actually need to specify an option on your nested forms called `class_initializer`, that points to a class method to use to specify the default parameters. You can either provide the method hardcoded into the class, or set it externally. It accepts either a hardcoded hash, or a Proc.
|
217
|
-
```ruby
|
218
|
-
class UserForm < FreeForm::Form
|
219
|
-
form_models :user
|
220
|
-
|
221
|
-
property :username, :on => :user
|
222
|
-
property :email, :on => :user
|
223
|
-
|
224
|
-
nested_form :phone_numbers, :class_initializer => :phone_initializer do
|
225
|
-
form_models :phone
|
226
|
-
|
227
|
-
property :area_code, :on => :phone
|
228
|
-
property :number, :on => :phone
|
229
|
-
end
|
230
|
-
end
|
231
|
-
|
232
|
-
UserForm.phone_initializer = lambda { { :phone => Phone.new } }
|
233
|
-
form = UserForm.new( :user => User.new )
|
234
|
-
form.build_phone_number # Uses Phone.new to initialize the nested form.
|
218
|
+
form.build_phone_number # Now the nested form is created
|
235
219
|
```
|
236
|
-
I apologize for the ugliness - In the future, I plan to roll FreeForm's own version of the `nested_form` javascript functionality.
|
237
220
|
|
238
|
-
|
239
|
-
|
240
|
-
|
221
|
+
**Ryan Bates' nested_form gem functionality**
|
222
|
+
FreeForm has a built in implementation of the functionality from Ryan Bates' *nested_form* gem.
|
223
|
+
When building a form, just use the method `freeform_for...`
|
241
224
|
|
242
225
|
** Example **
|
243
226
|
```ruby
|
@@ -250,14 +233,20 @@ class UserForm < FreeForm::Form
|
|
250
233
|
property :username, :on => :user
|
251
234
|
property :email, :on => :user
|
252
235
|
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
property :number, :on => :phone
|
236
|
+
has_many :phone_numbers, :class => PhoneNumberForm, :default_initializer => :phone_initializer
|
237
|
+
|
238
|
+
def phone_initializer
|
239
|
+
{ :phone => user.phone_numbers.build }
|
258
240
|
end
|
259
241
|
end
|
260
242
|
|
243
|
+
class PhoneNumberForm < FreeForm::Form
|
244
|
+
form_models :phone
|
245
|
+
|
246
|
+
property :area_code, :on => :phone
|
247
|
+
property :number, :on => :phone
|
248
|
+
end
|
249
|
+
|
261
250
|
form = UserForm.new(:user => current_user)
|
262
251
|
```
|
263
252
|
|
@@ -265,7 +254,6 @@ Will the current_user's phone numbers automatically appear as nested forms? **No
|
|
265
254
|
If you want them there, put them there, like this:
|
266
255
|
|
267
256
|
```ruby
|
268
|
-
UserForm.phone_initializer = lambda { { :phone => current_user.phone_numbers.build } }
|
269
257
|
current_user.phone_numbers.each do |phone_number|
|
270
258
|
form.build_phone_number(:phone => phone_number)
|
271
259
|
end
|
data/Rakefile
CHANGED
@@ -1 +1,32 @@
|
|
1
|
-
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Freeform'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
Bundler::GemHelper.install_tasks
|
21
|
+
|
22
|
+
require 'rake/testtask'
|
23
|
+
|
24
|
+
Rake::TestTask.new(:test) do |t|
|
25
|
+
t.libs << 'lib'
|
26
|
+
t.libs << 'test'
|
27
|
+
t.pattern = 'test/**/*_test.rb'
|
28
|
+
t.verbose = false
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
task default: :test
|
data/freeform.gemspec
CHANGED
@@ -4,24 +4,31 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'freeform/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name
|
8
|
-
spec.version
|
9
|
-
spec.authors
|
10
|
-
spec.email
|
11
|
-
spec.description
|
12
|
-
spec.summary
|
13
|
-
spec.homepage
|
14
|
-
spec.license
|
7
|
+
spec.name = "freeform"
|
8
|
+
spec.version = Freeform::VERSION
|
9
|
+
spec.authors = ["brycesenz"]
|
10
|
+
spec.email = ["bryce.senz@gmail.com"]
|
11
|
+
spec.description = %q{Decouple your forms from your domain models}
|
12
|
+
spec.summary = %q{FreeForm lets you compose forms however you like, explicitly guiding how each field maps back to your domain models. No more accepting nested attributes!}
|
13
|
+
spec.homepage = "https://github.com/brycesenz/freeform"
|
14
|
+
spec.license = "MIT"
|
15
15
|
|
16
|
-
spec.files
|
17
|
-
spec.executables
|
18
|
-
spec.test_files
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
-
spec.add_dependency
|
21
|
+
spec.add_dependency "activemodel"
|
22
|
+
spec.add_dependency "simple_form"
|
23
|
+
spec.add_dependency "formtastic"
|
24
|
+
spec.add_dependency "nested_form", "~> 0.3.2"
|
25
|
+
spec.add_dependency "rails", "~> 3.2.0"
|
22
26
|
spec.add_development_dependency "bundler", "~> 1.3"
|
23
27
|
spec.add_development_dependency "rake"
|
24
|
-
|
25
|
-
# Customer Development Dependencies
|
28
|
+
spec.add_development_dependency "sqlite3"
|
26
29
|
spec.add_development_dependency "rspec"
|
27
|
-
|
30
|
+
spec.add_development_dependency "rspec-rails", "~> 2.6"
|
31
|
+
spec.add_development_dependency "mocha"
|
32
|
+
spec.add_development_dependency "capybara", "~> 1.1"
|
33
|
+
end
|
34
|
+
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module FreeForm
|
2
|
+
module BuilderMixin
|
3
|
+
# Adds a link to insert a new associated records. The first argument is the name of the link, the second is the name of the association.
|
4
|
+
#
|
5
|
+
# f.link_to_add("Add Task", :tasks)
|
6
|
+
#
|
7
|
+
# You can pass HTML options in a hash at the end and a block for the content.
|
8
|
+
#
|
9
|
+
# <%= f.link_to_add(:tasks, :class => "add_task", :href => new_task_path) do %>
|
10
|
+
# Add Task
|
11
|
+
# <% end %>
|
12
|
+
#
|
13
|
+
# You can also pass <tt>model_object</tt> option with an object for use in
|
14
|
+
# the blueprint, e.g.:
|
15
|
+
#
|
16
|
+
# <%= f.link_to_add(:tasks, :model_object => Task.new(:name => 'Task')) %>
|
17
|
+
#
|
18
|
+
# See the README for more details on where to call this method.
|
19
|
+
def link_to_add(*args, &block)
|
20
|
+
options = args.extract_options!.symbolize_keys
|
21
|
+
association = args.pop
|
22
|
+
|
23
|
+
unless object.respond_to?("#{association}_attributes=")
|
24
|
+
raise ArgumentError, "Invalid association. Make sure that a nested form for #{association.to_s} exists."
|
25
|
+
end
|
26
|
+
|
27
|
+
#FIXME: I need to use the build method for child models
|
28
|
+
model_object = options.delete(:model_object) do
|
29
|
+
object.send("build_#{association.to_s.singularize}")
|
30
|
+
end
|
31
|
+
|
32
|
+
options[:class] = [options[:class], "add_nested_form"].compact.join(" ")
|
33
|
+
options["data-association"] = association
|
34
|
+
options["data-blueprint-id"] = fields_blueprint_id = fields_blueprint_id_for(association)
|
35
|
+
args << (options.delete(:href) || "javascript:void(0)")
|
36
|
+
args << options
|
37
|
+
|
38
|
+
@fields ||= {}
|
39
|
+
@template.after_freeform(fields_blueprint_id) do
|
40
|
+
blueprint = {:id => fields_blueprint_id, :style => 'display: none'}
|
41
|
+
block, options = @fields[fields_blueprint_id].values_at(:block, :options)
|
42
|
+
options[:child_index] = "new_#{association}"
|
43
|
+
blueprint[:"data-blueprint"] = fields_for(association, model_object, options, &block).to_str
|
44
|
+
@template.content_tag(:div, nil, blueprint)
|
45
|
+
end
|
46
|
+
@template.link_to(*args, &block)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Adds a link to remove the associated record. The first argment is the name of the link.
|
50
|
+
#
|
51
|
+
# f.link_to_remove("Remove Task")
|
52
|
+
#
|
53
|
+
# You can pass HTML options in a hash at the end and a block for the content.
|
54
|
+
#
|
55
|
+
# <%= f.link_to_remove(:class => "remove_task", :href => "#") do %>
|
56
|
+
# Remove Task
|
57
|
+
# <% end %>
|
58
|
+
#
|
59
|
+
# See the README for more details on where to call this method.
|
60
|
+
def link_to_remove(*args, &block)
|
61
|
+
options = args.extract_options!.symbolize_keys
|
62
|
+
options[:class] = [options[:class], "remove_nested_form"].compact.join(" ")
|
63
|
+
|
64
|
+
# Extracting "milestones" from "...[milestones_attributes][...]"
|
65
|
+
md = object_name.to_s.match(/(\w+)_attributes\](?:\[[\w\d]+\])?$/)
|
66
|
+
association = md && md[1]
|
67
|
+
options["data-association"] = association
|
68
|
+
|
69
|
+
args << (options.delete(:href) || "javascript:void(0)")
|
70
|
+
args << options
|
71
|
+
hidden_field(:_destroy, :value => 0) << @template.link_to(*args, &block)
|
72
|
+
end
|
73
|
+
|
74
|
+
def fields_for_with_nested_attributes(association_name, *args)
|
75
|
+
# TODO Test this better
|
76
|
+
block = args.pop || Proc.new { |fields| @template.render(:partial => "#{association_name.to_s.singularize}_fields", :locals => {:f => fields}) }
|
77
|
+
|
78
|
+
options = args.dup.extract_options!
|
79
|
+
|
80
|
+
# Rails 3.0.x
|
81
|
+
if options.empty? && args[0].kind_of?(Array)
|
82
|
+
options = args[0].dup.extract_options!
|
83
|
+
end
|
84
|
+
|
85
|
+
@fields ||= {}
|
86
|
+
@fields[fields_blueprint_id_for(association_name)] = { :block => block, :options => options }
|
87
|
+
super(association_name, *(args << block))
|
88
|
+
end
|
89
|
+
|
90
|
+
def fields_for_nested_model(name, object, options, block)
|
91
|
+
classes = 'fields'
|
92
|
+
classes << ' marked_for_destruction' if object.respond_to?(:marked_for_destruction?) && object.marked_for_destruction?
|
93
|
+
|
94
|
+
perform_wrap = options.fetch(:nested_wrapper, true)
|
95
|
+
perform_wrap &&= options[:wrapper] != false # wrap even if nil
|
96
|
+
|
97
|
+
if perform_wrap
|
98
|
+
@template.content_tag(:div, super, :class => classes)
|
99
|
+
else
|
100
|
+
super
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
def fields_blueprint_id_for(association)
|
106
|
+
assocs = object_name.to_s.scan(/(\w+)_attributes/).map(&:first)
|
107
|
+
assocs << association
|
108
|
+
assocs.join('_') + '_fields_blueprint'
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'action_view'
|
2
|
+
require 'freeform/builder/builder_mixin'
|
3
|
+
|
4
|
+
module FreeForm
|
5
|
+
class Builder < ::ActionView::Helpers::FormBuilder
|
6
|
+
include ::FreeForm::BuilderMixin
|
7
|
+
end
|
8
|
+
|
9
|
+
begin
|
10
|
+
require 'simple_form'
|
11
|
+
class SimpleBuilder < ::SimpleForm::FormBuilder
|
12
|
+
include ::FreeForm::BuilderMixin
|
13
|
+
end
|
14
|
+
rescue LoadError
|
15
|
+
end
|
16
|
+
|
17
|
+
begin
|
18
|
+
require 'formtastic'
|
19
|
+
class FormtasticBuilder < (defined?(::Formtastic::FormBuilder) ? Formtastic::FormBuilder : ::Formtastic::SemanticFormBuilder)
|
20
|
+
include ::FreeForm::BuilderMixin
|
21
|
+
end
|
22
|
+
rescue LoadError
|
23
|
+
end
|
24
|
+
|
25
|
+
begin
|
26
|
+
require 'formtastic-bootstrap'
|
27
|
+
class FormtasticBootstrapBuilder < ::FormtasticBootstrap::FormBuilder
|
28
|
+
include ::FreeForm::BuilderMixin
|
29
|
+
end
|
30
|
+
rescue LoadError
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rails'
|
2
|
+
|
3
|
+
module FreeForm
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
initializer 'freeform' do |app|
|
6
|
+
ActiveSupport.on_load(:action_view) do
|
7
|
+
require "freeform/builder/view_helper"
|
8
|
+
class ActionView::Base
|
9
|
+
include FreeForm::ViewHelper
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module FreeForm
|
2
|
+
module ViewHelper
|
3
|
+
def freeform_for(*args, &block)
|
4
|
+
options = args.extract_options!.reverse_merge(:builder => FreeForm::Builder)
|
5
|
+
form_for(*(args << options)) do |f|
|
6
|
+
capture(f, &block).to_s << after_freeform_callbacks
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
if defined?(FreeForm::SimpleBuilder)
|
11
|
+
def simple_freeform_for(*args, &block)
|
12
|
+
options = args.extract_options!.reverse_merge(:builder => FreeForm::SimpleBuilder)
|
13
|
+
simple_form_for(*(args << options)) do |f|
|
14
|
+
capture(f, &block).to_s << after_freeform_callbacks
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
if defined?(FreeForm::FormtasticBuilder)
|
20
|
+
def semantic_freeform_for(*args, &block)
|
21
|
+
options = args.extract_options!.reverse_merge(:builder => FreeForm::FormtasticBuilder)
|
22
|
+
semantic_form_for(*(args << options)) do |f|
|
23
|
+
capture(f, &block).to_s << after_freeform_callbacks
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
if defined?(FreeForm::FormtasticBootstrapBuilder)
|
29
|
+
def semantic_bootstrap_freeform_for(*args, &block)
|
30
|
+
options = args.extract_options!.reverse_merge(:builder => FreeForm::FormtasticBootstrapBuilder)
|
31
|
+
semantic_form_for(*(args << options)) do |f|
|
32
|
+
capture(f, &block).to_s << after_freeform_callbacks
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def after_freeform(association, &block)
|
38
|
+
@associations ||= []
|
39
|
+
@after_freeform_callbacks ||= []
|
40
|
+
unless @associations.include?(association)
|
41
|
+
@associations << association
|
42
|
+
@after_freeform_callbacks << block
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
def after_freeform_callbacks
|
48
|
+
@after_freeform_callbacks ||= []
|
49
|
+
fields = []
|
50
|
+
while callback = @after_freeform_callbacks.shift
|
51
|
+
fields << callback.call
|
52
|
+
end
|
53
|
+
fields.join(" ").html_safe
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -7,7 +7,7 @@ module FreeForm
|
|
7
7
|
module ClassMethods
|
8
8
|
def form_input_key(name)
|
9
9
|
constant = name.to_s.camelize
|
10
|
-
Object.const_set(constant,
|
10
|
+
Object.const_set(constant, Module.new) unless const_defined?(constant)
|
11
11
|
define_singleton_method(:model_name) do
|
12
12
|
ActiveModel::Name.new(constant.constantize)
|
13
13
|
end
|