form_objects 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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