form_objects 1.0.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 29632d193346200cb575dc61d4693a60ce990177
4
+ data.tar.gz: 08422a044d13f719e346d2efd03dc88816f7a78a
5
+ SHA512:
6
+ metadata.gz: be332e69fa86a7066cdfa3e0c3ebfc4826e5eab0821ea77a4a6c267774c582446a766a9389f396826314ef980bd944a5396bb5e16ad74f69f2b529fcb4b6e45f
7
+ data.tar.gz: 90f906ac406cb2b4bf720dae71790a6b8bc5ad1fa251bb13e9404f7fac6ee824efc5ee46bc49898a9ea3287e4b9445fc73433ad1efe1483ea83758ffd6b4791e
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/.travis.yml ADDED
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ before_install: gem install bundler
3
+ rvm:
4
+ - 1.9.3
5
+ - 2.1.0
6
+ cache: bundler
7
+ script:
8
+ - RAILS_ENV=test bundle exec rspec
9
+ addons:
10
+ code_climate:
11
+ repo_token: e835e063eadc1a6c9a4d69f1721ea34fc25063cb9dff277855d73f969882cd3f
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in form_objects.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'rspec', '~> 2.14'
8
+ gem 'rspec-mocks', '~> 2.14'
9
+ gem "codeclimate-test-reporter", require: nil
10
+ gem 'mutant'
11
+ gem 'mutant-rspec'
12
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Przemek Lusar
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,524 @@
1
+ # FormObjects
2
+
3
+ [![Code Climate](https://codeclimate.com/github/lluzak/form_objects.png)](https://codeclimate.com/github/lluzak/form_objects)
4
+ [![Build Status](https://travis-ci.org/lluzak/form_objects.png?branch=master)](https://travis-ci.org/lluzak/form_objects)
5
+
6
+ FormObjects gives you a easy way of building complex and nested form objects.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'form_objects'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install form_objects
21
+
22
+ ## Usage
23
+
24
+ In this micro-library you will not find any magic. Explicit is better than implicit. Simple is better than complex.
25
+
26
+ ### Standard form
27
+
28
+ At the beginning of the life of your application most of the objects is exactly the same as the form. User include `first_name` and `last_name`.
29
+ Only `first_name` is required.
30
+
31
+ ```ruby
32
+ class User
33
+ validates :first_name, :presence => true
34
+ end
35
+ ```
36
+
37
+ ```ruby
38
+ # controller
39
+
40
+ def new
41
+ @user = User.new
42
+ end
43
+ ```
44
+
45
+ ```erb
46
+ <%= form_for @user do |f| %>
47
+ <%= f.label :first_name %>:
48
+ <%= f.text_field :first_name %><br />
49
+
50
+ <%= f.label :last_name %>:
51
+ <%= f.text_field :last_name %><br />
52
+
53
+ <%= f.submit %>
54
+ <% end %>
55
+ ```
56
+
57
+ ## Form with FormObjects
58
+
59
+ How the same can be achieved using `FormObjects`?
60
+
61
+ ```ruby
62
+ class UserForm < FormObjects::Base
63
+ field :first_name, String
64
+ field :last_name, String
65
+
66
+ validates :first_name, presence: true
67
+ end
68
+ ```
69
+
70
+ Out new `UserForm` class does not know nothing about user. Because there is no connection to database.
71
+ That is why you need to explicitly defined each field. First argument is name of attribute and second argument is type
72
+ of this attribute. `#field` method is just alias for `attribute` method from [virtus](https://github.com/solnic/virtus#using-virtus-with-classes).
73
+
74
+ On `FormObjects` you can use the same validations like in `ActiveRecord::Base` object. So here there is no change.
75
+
76
+
77
+ ```ruby
78
+ # controller
79
+
80
+ def new
81
+ @user_form = UserForm.new
82
+ end
83
+ ```
84
+
85
+ ```erb
86
+ <%= form_for @user_form do |f| %>
87
+ <%= f.label :first_name %>:
88
+ <%= f.text_field :first_name %><br />
89
+
90
+ <%= f.label :last_name %>:
91
+ <%= f.text_field :last_name %><br />
92
+
93
+ <%= f.submit %>
94
+ <% end %>
95
+ ```
96
+
97
+ ## How to save FormObject do database?
98
+
99
+ Ok, now we can just save user to our storage. Do you you think about `@user_form.save`?
100
+
101
+ ![](http://dc472.4shared.com/img/G-w_8x6P/s3/13754405010/Nooo.gif)
102
+
103
+ Keep your objects simple. Form object is responsible for maintaining and validating data. Things like storing these data leave other objects. So what now?
104
+ You can create `UserCreator`.
105
+
106
+ ```ruby
107
+ class UserCreator
108
+ def initialize(attributes)
109
+ @attributes = attributes
110
+ end
111
+
112
+ def create
113
+ User.create(@attributes)
114
+ end
115
+ end
116
+ ```
117
+
118
+ ## Namespace for attributes
119
+
120
+ Rails form generator will generate form with attributes scoped in `user_form`. So when you submit your form `params` will look like this:
121
+
122
+ ```ruby
123
+ {
124
+ :user_form => {
125
+ :first_name => "First name",
126
+ :last_name => "Last name"
127
+ }
128
+ }
129
+ ```
130
+
131
+ You can change it by adding `FormObjects::Naming` to your form class definition.
132
+
133
+ ```ruby
134
+ class UserForm < FormObjects::Base
135
+ include FormObjects::Naming
136
+
137
+ field :first_name, String
138
+ field :last_name, String
139
+
140
+ validates :first_name, presence: true
141
+ end
142
+ ```
143
+
144
+ `FormObjects::Naming` will generate `.model_name` method. This method will return `ActiveModel::Name` object who will pretend that the model does not include `Form` in the name.
145
+ You can of course define your own `.model_name` method.
146
+
147
+ ```ruby
148
+ class UserForm < FormObjects::Base
149
+ field :first_name, String
150
+ field :last_name, String
151
+
152
+ validates :first_name, presence: true
153
+
154
+ def self.model_name
155
+ ActiveModel::Name.new(self, nil, "User")
156
+ end
157
+ end
158
+ ```
159
+
160
+ After this change params will look like this:
161
+
162
+ ```ruby
163
+ {
164
+ :user => {
165
+ :first_name => "First name",
166
+ :last_name => "Last name"
167
+ }
168
+ }
169
+ ```
170
+
171
+ So we can implement `create` controller action.
172
+
173
+
174
+ ```ruby
175
+ # controller
176
+
177
+ def create
178
+ @user_form = UserForm.new(params[:user])
179
+
180
+ if @user_form.valid?
181
+ UserCreator.new(@user_form.serialized_attributes).create
182
+ else
183
+ render :new
184
+ end
185
+ end
186
+ ```
187
+
188
+ ## Additional attribute
189
+
190
+ Let's do something standard. Add term and condition checkbox. In rails way you will add `term` attribute to your `User` model, didn't you?
191
+ Do not you think it's a little weird? I think so. Let's do this in `UserForm`.
192
+
193
+ ```ruby
194
+ class UserForm < FormObjects::Base
195
+ include FormObjects::Naming
196
+
197
+ field :first_name, String
198
+ field :last_name, String
199
+ field :terms, Boolean
200
+
201
+ validates :first_name, presence: true
202
+ validates :terms, acceptance: true
203
+ end
204
+ ```
205
+
206
+ But there is a problem with `terms` validation.
207
+
208
+ ```ruby
209
+ UserForm.new(:terms => "1")
210
+ # => #<UserForm:0x00000004bbd2e0 @first_name=nil, @last_name=nil, @terms=true>
211
+ ```
212
+
213
+ Virtus library will transform `terms` value into boolean. But by default `acceptance` will look for `"1"` value.
214
+
215
+ ```ruby
216
+ form = UserForm.new(:terms => "1")
217
+ # => #<UserForm:0x00000004be2400 @first_name=nil, @last_name=nil, @terms=true>
218
+ form.valid?
219
+ # => false
220
+ form.errors.full_messages
221
+ # => ["First name can't be blank", "Terms must be accepted"]
222
+ ```
223
+
224
+ Solution? You can change `terms` field into `String` type. But this is strange. I recommended clarify validation.
225
+
226
+ ```ruby
227
+ class UserForm < FormObjects::Base
228
+ include FormObjects::Naming
229
+
230
+ field :first_name, String
231
+ field :last_name, String
232
+ field :terms, Boolean
233
+
234
+ validates :first_name, presence: true
235
+ validates :terms, acceptance: { accept: true }
236
+ end
237
+ ```
238
+
239
+ Now everything should works just fine. No magic.
240
+
241
+ ```ruby
242
+ form = UserForm.new(:terms => "1")
243
+ # => #<UserForm:0x00000004de7f20 @terms=true, @first_name=nil, @last_name=nil>
244
+ form.valid?
245
+ # => false
246
+ form.errors.full_messages
247
+ # => ["First name can't be blank"]
248
+ # No terms errors
249
+ ```
250
+
251
+ ## Form in form (nested_form)
252
+
253
+ Let add another form to our `UserForm`. User during registration should give the address. Lets create `LocationForm`.
254
+
255
+ ```ruby
256
+ class LocationForm < FormObjects::Form
257
+ field :address, String
258
+
259
+ validates :address, presence: true
260
+ end
261
+ ```
262
+
263
+ Instead of `field` method we need to use `nested_form`.
264
+
265
+ ```ruby
266
+ class UserForm < FormObjects::Base
267
+ include FormObjects::Naming
268
+
269
+ field :first_name, String
270
+ field :last_name, String
271
+ field :terms, Boolean
272
+
273
+ nested_form :address, LocationForm
274
+
275
+ validates :first_name, presence: true
276
+ validates :terms, acceptance: { accept: true }
277
+ end
278
+ ```
279
+
280
+ I will switch now to `simple_form`. But you can use original `form_for` form rails.
281
+
282
+ ```ruby
283
+ <%= simple_form_for @user_form, :url => homes_path do |f| %>
284
+ <%= f.input :first_name %>
285
+ <%= f.input :last_name %>
286
+ <%= f.input :terms, :as => :boolean %>
287
+
288
+ <%= f.simple_fields_for :address do |a| %>
289
+ <%= a.input :address %>
290
+ <% end %>
291
+
292
+ <%= f.button :submit %>
293
+ <% end %>
294
+ ```
295
+
296
+ You will notice one problem. That `address` field is not rendered. The reason is that `LocationForm` is not initialized.
297
+ You can use Virtus `default` attribute to accomplish this.
298
+
299
+ ```ruby
300
+ class UserForm < FormObjects::Base
301
+ include FormObjects::Naming
302
+
303
+ field :first_name, String
304
+ field :last_name, String
305
+ field :terms, Boolean
306
+
307
+ nested_form :address, LocationForm, default: proc { LocationForm.new }
308
+
309
+ validates :first_name, presence: true
310
+ validates :terms, acceptance: { accept: true }
311
+ end
312
+ ```
313
+
314
+ After this change location form should be rendered. When you submit this form params will looks like:
315
+
316
+ ```ruby
317
+ {
318
+ :user => {
319
+ :first_name => "FirstName",
320
+ :last_name => "LastName",
321
+ :terms => "1",
322
+ :address_attributes => {
323
+ :address => "Street"
324
+ }
325
+ }
326
+ }
327
+ ```
328
+
329
+ When you pass these `params` to form object you can use `serialized_attriubtes` method. It will return developer-friendly hash with values.
330
+
331
+ ```ruby
332
+ UserForm.new(params).serialized_attributes
333
+ # => {:first_name=>"FirstName", :last_name=>"LastName", :terms=>true, :address=>{:address=>"Street"}}
334
+ ```
335
+
336
+ You can use this `Hash` inside your classes, services etc.
337
+
338
+ ## Many forms in form
339
+
340
+ What we should do when we need more than 1 address? We can use `Array` from Virtus.
341
+
342
+ ```ruby
343
+ class UserForm < FormObjects::Base
344
+ include FormObjects::Naming
345
+
346
+ field :first_name, String
347
+ field :last_name, String
348
+ field :terms, Boolean
349
+
350
+ nested_form :addresses, Array[LocationForm]
351
+
352
+ validates :first_name, presence: true
353
+ validates :terms, acceptance: { accept: true }
354
+ end
355
+ ```
356
+
357
+ I changed `address` to `addresses` and instead of simple `LocationForm` we will use `Array[LocationForm]`. But once again problem with default values.
358
+ You can use `default` attribute from Virtus.
359
+
360
+ ```ruby
361
+ Array.new(2, LocationForm.new)
362
+ # => [#<LocationForm:0x00000004ffe0e8 @address=nil>, #<LocationForm:0x00000004ffe0e8 @address=nil>]
363
+ ```
364
+
365
+ So we can apply this to our form.
366
+
367
+ ```ruby
368
+ class UserForm < FormObjects::Base
369
+ include FormObjects::Naming
370
+
371
+ NUMBER_OF_LOCATION_FORMS = 2
372
+
373
+ field :first_name, String
374
+ field :last_name, String
375
+ field :terms, Boolean
376
+
377
+ nested_form :addresses, Array[LocationForm], default: proc { Array.new(NUMBER_OF_LOCATION_FORMS, LocationForm.new) }
378
+
379
+ validates :first_name, presence: true
380
+ validates :terms, acceptance: { accept: true }
381
+ end
382
+ ```
383
+
384
+ After this your form will be renderer. But almost for sure you will get exception:
385
+
386
+ ```
387
+ undefined method `0=' for #<LocationForm:0x007fdbc002bb80>
388
+ ```
389
+
390
+ Now our params looks like this:
391
+
392
+ ```ruby
393
+ {
394
+ :user =>{
395
+ :first_name => "FirstName",
396
+ :last_name" => "LastName",
397
+ :terms => "1",
398
+ :addresses_attributes => {
399
+ "0" => {:address=>"Street1"},
400
+ "1" => {"address=>"Street2"}
401
+ }
402
+ }
403
+ }
404
+ ```
405
+
406
+ From now we need to use `FormObjects::ParamsConverter`. Because Virtus models will not accept rails magic.
407
+
408
+ ```ruby
409
+ FormObjects::ParamsConverter.new(params).params
410
+
411
+ {
412
+ :user => {
413
+ :first_name => "FirstName",
414
+ :last_name => "LastName",
415
+ :terms => "1",
416
+ :addresses_attributes=> [
417
+ {:address => "Street1"},
418
+ {:address => "Street2"}
419
+ ]
420
+ }
421
+ }
422
+ ```
423
+
424
+ `FormObjects::ParamsConverter` convert `Hash` created by rails to friendly Array. You can use this Hash to initialize your form.
425
+
426
+ ```ruby
427
+ UserForm.new(converted_params[:user])
428
+
429
+ private
430
+
431
+ def converted_params
432
+ FormObjects::ParamsConverter.new(params).params
433
+ end
434
+ ```
435
+
436
+ ## Summary
437
+
438
+ * FormObjects use Virtus for Property API
439
+ * Nested forms objects are validate together with parent form, errors are being push to parent.
440
+ * ``` #serialized_attributes ``` method returns attributes hash
441
+
442
+ ```ruby
443
+ class AddressForm < FormObjects::Base
444
+ field :street, String
445
+ field :city, String
446
+
447
+ validates :street, presence: true
448
+ end
449
+
450
+ class PersonalInfoForm < FormObjects::Base
451
+ field :first_name, String
452
+ field :last_name, String
453
+
454
+ validates :first_name, presence: true
455
+ end
456
+
457
+ class UserForm < FormObjects::Base
458
+ field :email, String
459
+
460
+ nested_form :addresses, Array[AddressForm]
461
+ nested_form :personal_info, PersonalInfoForm
462
+ end
463
+
464
+ service = UserUpdater.new
465
+ form = UserForm.new
466
+
467
+ form.update({
468
+ email: 'john.doe@example.com',
469
+ personal_info_attributes: {first_name: 'John'},
470
+ addresses_attributes: [{street: 'Golden Street'}]
471
+ })
472
+
473
+ if form.valid?
474
+ service.update(form.serialized_attributes)
475
+ end
476
+ ```
477
+
478
+ # Params conversion
479
+
480
+ ## Array parameters
481
+
482
+ When you use HTTP there is no ensure that parameters that you receive will be ordered. That why rails wrap Arrays inside Hash.
483
+
484
+ ```ruby
485
+ ["one", "two", "three"] => {"0" => "one", "1" => "two", "2" => "three"}
486
+ ```
487
+
488
+ But form object expects that nested params will be kind of Array
489
+
490
+ ```ruby
491
+ class UserForm < FormObjects::Base
492
+ nested_form :addresses, Array[AddressForm]
493
+ end
494
+
495
+ UserForm.new(:addresses_attributes => [{:name => "Name"}]) # good
496
+ # instead of
497
+ UserForm.new(:addresses_attributes => {"0" => {:name => "Name"}}) # bad
498
+ ```
499
+
500
+ To avoid these problems you can use `FormObjects::ParamsConverter`.
501
+
502
+ ```ruby
503
+ params = { "event_attributes" => {"0" => "one", "1" => "two", "2" => "three"} }
504
+ converter = FormObjects::ParamsConverter.new(params)
505
+ converter.params #=> { "event_attributes" => ["one", "two", "three"] }
506
+ ```
507
+
508
+ ## Date parameters
509
+
510
+ Multi-parameter dates can be easily converted to friendly form.
511
+
512
+ ```ruby
513
+ params = { "event" => { "date(1i)" => "2014", "date(2i)" => "12", "date(3i)" => "16", "date(4i)" => "12", "date(5i)" => "30", "date(6i)" => "45" } }
514
+ converter = FormObjects::ParamsConverter.new(params)
515
+ converter.params #=> { "event" => { "date" => "2014.12.16 12:30:45" } }
516
+ ```
517
+
518
+ ## Contributing
519
+
520
+ 1. Fork it
521
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
522
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
523
+ 4. Push to the branch (`git push origin my-new-feature`)
524
+ 5. Create new Pull Request