form_obj 0.4.0 → 0.5.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.
data/README.md CHANGED
@@ -12,7 +12,8 @@ Ruby: 2.2.8+
12
12
  ActiveSupport: 3.2+
13
13
  ActiveModel: 3.2+
14
14
 
15
- The gem is tested against all ruby versions and all versions of its dependencies.
15
+ The gem is tested against all ruby versions and all versions of its dependencies
16
+ except ActiveSupport and ActiveModel version 4.0.x because they requires Minitest 4 which is not compatible with Minitest 5.
16
17
 
17
18
  ## Installation
18
19
 
@@ -32,9 +33,9 @@ Or install it yourself as:
32
33
 
33
34
  ## Usage
34
35
 
35
- **WARNING!!!** The gem is still under development. Expect braking changes.<br/>
36
+ **WARNING!!!** The gem is still under development. Expect braking changes in `FormObj::ModelMapper` module.<br/>
36
37
 
37
- Class `FormObj::Struct` allows to describe complicated data structure.
38
+ Class `FormObj::Struct` allows to describe complicated data structure, to update it with `update_attributes` method and to get its hash representation with `to_hash` method.
38
39
 
39
40
  Class `FormObj::Form` inherits from `FormObj::Struct` and adds form builder compatibility and includes ActiveModel validations.
40
41
 
@@ -48,58 +49,139 @@ model attributes name) and
48
49
 
49
50
  ### Table of Contents
50
51
 
51
- 1. [Definition](#1-definition)
52
- 1. [Nested Form Objects](11-nested-form-objects)
53
- 2. [Array of Form Objects](12-array-of-form-objects)
54
- 2. [Update Attributes](2-update-attributes)
55
- 1. [Nested Form Objects](21-nested-form-objects)
56
- 2. [Array of Form Objects](22-array-of-form-objects)
57
- 3. [Serialize to Hash](3-serialize-to-hash)
58
- 1. [Nested Form Objects](31-nested-form-objects)
59
- 2. [Array of Form Objects](32-array-of-form-objects)
60
- 4. [Map Form Object to Models](4-map-form-objects-to-models)
61
- 1. [Multiple Models Example](41-multiple-models-example)
62
- 2. [Skip Attribute Mapping](42-skip-attribute-mapping)
63
- 1. [Map Nested Form Object Attribute to Parent Level Model Attribute](421-map-nested-form-object-attribute-to-parent-level-model-attribute)
64
- 3. [Map Nested Form Object to A Hash Model](43-map-nested-form-object-to-a-hash-model)
65
- 5. [Load Form Object from Models](5-load-form-object-from-models)
66
- 6. [Save Form Object to Models](6-save-form-object-to-models)
67
- 1. [Array of Form Objects and Models](61-array-of-form-objects-and-models)
68
- 7. [Serialize Form Object to Model Hash](7-serialize-form-object-to-model-hash)
69
- 8. [Validation and Coercion](8-validation-and-coercion)
70
- 9. [Copy Model Validation Errors into Form Object](9-copy-model-validation-errors-into-form-object)
71
- 10. [Rails Example](10-rails-example)
72
- 11. [Reference Guide](11-reference-guide-attribute-parameters)
73
-
74
- ### 1. Definition
75
-
76
- Inherit your class from `FormObj::Form` and define its attributes.
77
-
78
- ```ruby
79
- class SimpleForm < FormObj::Form
52
+ 1. [`FormObj::Struct`](#1-formobjstruct)
53
+ 1. [Nesting `FormObj::Struct`](#11-nesting-formobjstruct)
54
+ 2. [Array of `FormObj::Struct`](#12-array-of-formobjstruct)
55
+ 3. [Serialize `FormObj::Struct` to Hash](#13-serialize-formobjstruct-to-hash)
56
+ 2. [`FormObj::Form`](#2-formobjform)
57
+ 1. [`FormObj::Form` Validation](#21-formobjform-validation)
58
+ 2. [`FormObj::Form` Persistence](#22-formobjform-persistence)
59
+ 3. [Delete from Array of `FormObj::Form` via `update_attributes` method](#23-delete-from-array-of-formobjform-via-update_attributes-method)
60
+ 4. [Using `FormObj::Form` in Form Builder](#24-using-formobjform-in-form-builder)
61
+ 3. [`FormObj::ModelMapper`](#3-formobjmodelmapper)
62
+ 1. [`load_from_model` - Initialize Form Object from Model](#31-load_from_model---initialize-form-object-from-model)
63
+ 2. [`load_from_models` - Initialize Form Object from Few Models](#32-load_from_models---initialize-form-object-from-few-models)
64
+ 3. [Do Not Map Certain Attribute](#33-do-not-map-certain-attribute)
65
+ 4. [Map Nested Form Objects](#34-map-nested-form-objects)
66
+ 5. [Map Nested Form Objects to A Hash Model](#35-map-nested-form-object-to-a-hash-model)
67
+ 6. [Custom Implementation of Loading of Array of Models](#36-custom-implementation-of-loading-of-array-of-models)
68
+ 7. [Sync Form Object to Models](#37-sync-form-object-to-models)
69
+ 1. [Array of Form Objects and Models](#371-array-of-form-objects-and-models)
70
+ 8. [Serialize Form Object to Model Hash](#38-serialize-form-object-to-model-hash)
71
+ 9. [Copy Model Validation Errors into Form Object](#39-copy-model-validation-errors-into-form-object)
72
+ 4. [Rails Example](#4-rails-example)
73
+ 5. [Reference Guide](#5-reference-guide-attribute-parameters)
74
+
75
+ ### 1. `FormObj::Struct`
76
+
77
+ Inherit your class from `FormObj::Struct` and define its attributes.
78
+
79
+ ```ruby
80
+ class Team < FormObj::Struct
80
81
  attribute :name
81
82
  attribute :year
82
83
  end
83
84
  ```
84
85
 
85
- Use it in a form builder.
86
+ Read and write attribute values using dot-notation.
86
87
 
87
- ```erb
88
- <%= form_for(@simple_form) do |f| %>
89
- <%= f.label :name %>
90
- <%= f.text_field :name %>
88
+ ```ruby
89
+ team = Team.new # => #<Team name: nil, year: nil>
90
+ team.name = 'Ferrari' # => "Ferrari"
91
+ team.year = 1950 # => 1950
91
92
 
92
- <%= f.label :year %>
93
- <%= f.text_field :year %>
94
- <% end %>
93
+ team.name # => "Ferrari"
94
+ team.year # => 1950
95
95
  ```
96
96
 
97
- #### 1.1. Nested Form Objects
97
+ Initialize attributes in constructor.
98
+
99
+ ```ruby
100
+ team = Team.new(
101
+ name: 'Ferrari',
102
+ year: 1950
103
+ ) # => #<Team name: "Ferrari", year: 1950>
104
+ team.name # => "Ferrari"
105
+ team.year # => 1950
106
+ ```
98
107
 
99
- Use blocks to define nested forms.
108
+ Update attributes using `update_attributes` method.
100
109
 
101
110
  ```ruby
102
- class NestedForm < FormObj::Form
111
+ team.update_attributes(
112
+ name: 'McLaren',
113
+ year: 1966
114
+ ) # => #<Team name: "McLaren", year: 1966>
115
+ team.name # => "McLaren"
116
+ team.year # => 1966
117
+ ```
118
+
119
+ In both cases (initialization or `update_attributes`) hash is transformed to `HashWithIndifferentAccess` before applying its values
120
+ so it doesn't matter whether keys are symbols or strings.
121
+
122
+ ```ruby
123
+ team.update_attributes(
124
+ 'name' => 'Ferrari',
125
+ 'year' => 1950
126
+ ) # => #<Team name: "Ferrari", year: 1950>
127
+ ```
128
+
129
+ Attribute value stays unchanged if hash doesn't have corresponding key.
130
+
131
+ ```ruby
132
+ team = Team.new(name: 'Ferrari') # => #<Team name: "Ferrari", year: nil>
133
+ team.update_attributes(year: 1950) # => #<Team name: "Ferrari", year: 1950>
134
+ ```
135
+
136
+ Exception `UnknownAttributeError` is raised if there is key that doesn't correspond to any attribute.
137
+
138
+ ```ruby
139
+ Team.new(name: 'Ferrari', a: 1) # => FormObj::UnknownAttributeError: a
140
+ Team.new.update_attributes(a: 1) # => FormObj::UnknownAttributeError: a
141
+ ```
142
+
143
+ Use parameter `raise_if_not_found: false` in order to avoid exception and silently skip unknown key in the hash.
144
+
145
+ ```ruby
146
+ team = Team.new({
147
+ name: 'Ferrari',
148
+ a: 1
149
+ }, raise_if_not_found: false) # => #<Team name: "Ferrari", year: nil>
150
+
151
+ team.update_attributes({
152
+ name: 'McLaren',
153
+ a: 1
154
+ }, raise_if_not_found: false) # => #<Team name: "McLaren", year: nil>
155
+ ```
156
+
157
+ Define default attribute value using `default` parameter.
158
+ Use `Proc` to calculate default value dynamically.
159
+ `Proc` is calculated only once at the moment of first access to attribute.
160
+ `Proc` receives two arguments:
161
+ - `struct_class` - class (!!! not an instance) where attribute is defined
162
+ - `attribute` - internal representation of attribute
163
+
164
+ ```ruby
165
+ class Team < FormObj::Struct
166
+ attribute :name, default: 'Ferrari'
167
+ attribute :year, default: ->(struct_class, attribute) { struct_class.default_year(attribute) }
168
+
169
+ def self.default_year(attribute)
170
+ "#{attribute.name} = 1950"
171
+ end
172
+ end
173
+
174
+ team = Team.new # => #<Team name: "Ferrari", year: "year = 1950">
175
+ team.name # => "Ferrari"
176
+ team.year # => "year = 1950"
177
+ ```
178
+
179
+ #### 1.1. Nesting `FormObj::Struct`
180
+
181
+ Use blocks to define nested structs.
182
+
183
+ ```ruby
184
+ class Team < FormObj::Struct
103
185
  attribute :name
104
186
  attribute :year
105
187
  attribute :car do
@@ -113,62 +195,163 @@ class NestedForm < FormObj::Form
113
195
  end
114
196
  ```
115
197
 
116
- Or explicitly define nested form class.
198
+ Or explicitly define nested struct classes.
117
199
 
118
200
  ```ruby
119
- class EngineForm < FormObj::Form
201
+ class Engine < FormObj::Struct
120
202
  attribute :power
121
203
  attribute :volume
122
204
  end
123
- class CarForm < FormObj::Form
205
+ class Car < FormObj::Struct
124
206
  attribute :code
125
207
  attribute :driver
126
- attribute :engine, class: EngineForm
208
+ attribute :engine, class: Engine
127
209
  end
128
- class NestedForm < FormObj::Form
210
+ class Team < FormObj::Struct
129
211
  attribute :name
130
212
  attribute :year
131
- attribute :car, class: CarForm
213
+ attribute :car, class: Car
132
214
  end
133
215
  ```
134
216
 
135
- Use nested forms in form builder.
217
+ Read and write attribute values using dot-notation.
136
218
 
137
- ```erb
138
- <%= form_for(@nested_form) do |f| %>
139
- <%= f.label :name %>
140
- <%= f.text_field :name %>
219
+ ```ruby
220
+ team = Team.new # => #<Team name: nil, year: nil, car: #< code: nil, driver: nil, engine: #< power: nil, volume: nil>>>
221
+ team.name = 'Ferrari' # => "Ferrari"
222
+ team.year = 1950 # => 1950
223
+ team.car.code = '340 F1' # => "340 F1"
224
+ team.car.driver = 'Ascari' # => "Ascari"
225
+ team.car.engine.power = 335 # => 335
226
+ team.car.engine.volume = 4.1 # => 4.1
227
+
228
+ team.name # => "Ferrari"
229
+ team.year # => 1950
230
+ team.car.code # => "340 F1"
231
+ team.car.driver # => "Ascari"
232
+ team.car.engine.power # => 335
233
+ team.car.engine.volume # => 4.1
234
+ ```
141
235
 
142
- <%= f.label :year %>
143
- <%= f.text_field :year %>
236
+ Initialize nested struct using nested hash.
144
237
 
145
- <%= f.fields_for(:car) do |fc| %>
146
- <%= fc.label :code %>
147
- <%= fc.text_field :code %>
238
+ ```ruby
239
+ team = Team.new(
240
+ name: 'Ferrari',
241
+ year: 1950,
242
+ car: {
243
+ code: '340 F1',
244
+ driver: 'Ascari',
245
+ engine: {
246
+ power: 335,
247
+ volume: 4.1,
248
+ }
249
+ }
250
+ ) # => #<Team name: "Ferrari", year: 1950, car: #< code: "340 F1", driver: "Ascari", engine: #< power: 335, volume: 4.1>>>
251
+
252
+ team.name # => "Ferrari"
253
+ team.year # => 1950
254
+ team.car.code # => "340 F1"
255
+ team.car.driver # => "Ascari"
256
+ team.car.engine.power # => 335
257
+ team.car.engine.volume # => 4.1
258
+ ```
148
259
 
149
- <%= fc.label :driver %>
150
- <%= fc.text_field :driver %>
260
+ Update nested struct using nested hash.
151
261
 
152
- <%= fc.field_for(:engine) do |fce| %>
153
- <%= fce.label :power %>
154
- <%= fce.text_field :power %>
262
+ ```ruby
263
+ team.update_attributes(
264
+ name: 'McLaren',
265
+ year: 1966,
266
+ car: {
267
+ code: 'M2B',
268
+ driver: 'Bruce McLaren',
269
+ engine: {
270
+ power: 300,
271
+ volume: 3.0
272
+ }
273
+ }
274
+ ) # => #<Team name: "McLaren", year: 1966, car: #< code: "M2B", driver: "Bruce McLaren", engine: #< power: 300, volume: 3.0>>>
275
+
276
+ team.name # => "McLaren"
277
+ team.year # => 1966
278
+ team.car.code # => "M2B"
279
+ team.car.driver # => "Bruce McLaren"
280
+ team.car.engine.power # => 300
281
+ team.car.engine.volume # => 3.0
282
+ ```
155
283
 
156
- <%= fce.label :volume %>
157
- <%= fce.text_field :volume %>
158
- <% end %>
159
- <% end %>
160
- <% end %>
284
+ Use hash to define default value of nested struct defined with block.
285
+
286
+ ```ruby
287
+ class Team < FormObj::Struct
288
+ attribute :car, default: { code: '340 F1', driver: 'Ascari' } do
289
+ attribute :code
290
+ attribute :driver
291
+ end
292
+ end
293
+
294
+ team = Team.new # => #<Team car: #< code: "340 F1", driver: "Ascari">>
295
+ team.car.code # => "340 F1"
296
+ team.car.driver # => "Ascari"
297
+ ```
298
+
299
+ Use hash or struct instance to define default value of nested struct defined with class.
300
+
301
+ ```ruby
302
+ class Car < FormObj::Struct
303
+ attribute :code
304
+ attribute :driver
305
+ end
306
+
307
+ class Team < FormObj::Struct
308
+ attribute :car, class: Car, default: Car.new(code: '340 F1', driver: 'Ascari')
309
+ end
310
+
311
+ team = Team.new # => #<Team car: #<Car code: "340 F1", driver: "Ascari">>
312
+ team.car.code # => "340 F1"
313
+ team.car.driver # => "Ascari"
314
+ ```
315
+
316
+ The struct instance class should correspond to nested attribute class!
317
+
318
+ ```ruby
319
+ class Team < FormObj::Struct
320
+ attribute :car, class: Car, default: 36
321
+ end
322
+
323
+ Team.new # => FormObj::WrongDefaultValueClass: FormObj::WrongDefaultValueClass
161
324
  ```
162
325
 
163
- #### 1.2. Array of Form Objects
326
+ #### 1.2. Array of `FormObj::Struct`
164
327
 
165
- Specify attribute parameter `array: true` in order to define an array of form objects
328
+ Use parameter `array: true` in order to define an array of nested structs.
329
+ Define `primary_key` so that `update_attribute` method be able to distinguish
330
+ whether to update existing array element or create a new one.
331
+ By default attribute `id` is considered to be a primary key.
166
332
 
167
333
  ```ruby
168
- class ArrayForm < FormObj::Form
334
+ class Team < FormObj::Struct
169
335
  attribute :name
170
336
  attribute :year
171
337
  attribute :cars, array: true do
338
+ attribute :code, primary_key: true # <- primary key is specified on attribute level
339
+ attribute :driver
340
+ attribute :engine do
341
+ attribute :power
342
+ attribute :volume
343
+ end
344
+ end
345
+ end
346
+ ```
347
+
348
+ or
349
+
350
+ ```ruby
351
+ class Team < FormObj::Struct
352
+ attribute :name
353
+ attribute :year
354
+ attribute :cars, array: true, primary_key: :code do # <- primary key is specified on struct level
172
355
  attribute :code
173
356
  attribute :driver
174
357
  attribute :engine do
@@ -182,130 +365,294 @@ end
182
365
  or
183
366
 
184
367
  ```ruby
185
- class EngineForm < FormObj::Form
368
+ class Engine < FormObj::Struct
186
369
  attribute :power
187
370
  attribute :volume
188
371
  end
189
- class CarForm < FormObj::Form
190
- attribute :code
372
+ class Car < FormObj::Struct
373
+ attribute :code, primary_key: true # <- primary key is specified on attribute level
191
374
  attribute :driver
192
- attribute :engine, class: EngineForm
375
+ attribute :engine, class: Engine
193
376
  end
194
- class ArrayForm < FormObj::Form
377
+ class Team < FormObj::Struct
195
378
  attribute :name
196
379
  attribute :year
197
- attribute :cars, array: true, class: CarForm
380
+ attribute :cars, array: true, class: Car
198
381
  end
199
382
  ```
200
383
 
201
- Add new elements in the array by using method :create.
384
+ or
202
385
 
203
386
  ```ruby
204
- @array_form = ArrayForm.new
205
- @array_form.cars.size # => 0
206
- @array_form.cars.create
207
- @array_form.cars.size # => 1
387
+ class Engine < FormObj::Struct
388
+ attribute :power
389
+ attribute :volume
390
+ end
391
+ class Car < FormObj::Struct
392
+ attribute :code
393
+ attribute :driver
394
+ attribute :engine, class: Engine
395
+ end
396
+ class Team < FormObj::Struct
397
+ attribute :name
398
+ attribute :year
399
+ attribute :cars, array: true, class: Car, primary_key: :code # <- primary key is specified on struct level
400
+ end
208
401
  ```
209
402
 
210
- Use array of nested forms in the form builder.
403
+ Read and write attribute values using dot-notation.
404
+ Add new elements in the array using method `create`.
211
405
 
212
- ```erb
213
- <%= form_for(@array_form) do |f| %>
214
- <%= f.label :name %>
215
- <%= f.text_field :name %>
406
+ ```ruby
407
+ team = Team.new # => #<Team name: nil, year: nil, cars: []>
408
+ team.name = 'Ferrari' # => "Ferrari"
409
+ team.year = 1950 # => 1950
410
+
411
+ team.cars.size # => 0
412
+ car1 = team.cars.create # => #< code: nil, driver: nil, engine: #< power: nil, volume: nil>>
413
+ team.cars.size # => 1
414
+ car1.code = '340 F1' # => "340 F1"
415
+ car1.driver = 'Ascari' # => "Ascari"
416
+ car1.engine.power = 335 # => 335
417
+ car1.engine.volume = 4.1 # => 4.1
418
+
419
+ car2 = team.cars.create # => #< code: nil, driver: nil, engine: #< power: nil, volume: nil>>
420
+ team.cars.size # => 2
421
+ car2.code = '275 F1' # => "275 F1"
422
+ car2.driver = 'Villoresi' # => "Villoresi"
423
+ car2.engine.power = 330 # => 330
424
+ car2.engine.volume = 3.3 # => 3.3
425
+
426
+ team.name # => "Ferrari"
427
+ team.year # => 1950
428
+
429
+ team.cars[0].code # => "340 F1"
430
+ team.cars[0].driver # => "Ascari"
431
+ team.cars[0].engine.power # => 335
432
+ team.cars[0].engine.volume # => 4.1
433
+
434
+ team.cars[1].code # => "275 F1"
435
+ team.cars[1].driver # => "Villoresi"
436
+ team.cars[1].engine.power # => 330
437
+ team.cars[1].engine.volume # => 3.3
438
+ ```
216
439
 
217
- <%= f.label :year %>
218
- <%= f.text_field :year %>
440
+ Initialize attributes using hash with array of hashes.
219
441
 
220
- <% f.cars.each do |car| %>
221
- <%= f.fields_for(:cars, car, index: '') do |fc| %>
222
- <%= fc.label :code %>
223
- <%= fc.text_field :code %>
442
+ ```ruby
443
+ team = Team.new(
444
+ name: 'Ferrari',
445
+ year: 1950,
446
+ cars: [
447
+ {
448
+ code: '340 F1',
449
+ driver: 'Ascari',
450
+ engine: {
451
+ power: 335,
452
+ volume: 4.1,
453
+ }
454
+ }, {
455
+ code: '275 F1',
456
+ driver: 'Villoresi',
457
+ engine: {
458
+ power: 330,
459
+ volume: 3.3,
460
+ }
461
+ }
462
+ ],
463
+ ) # => #<Team name: "Ferrari", year: 1950, cars: [#< code: "340 F1", driver: "Ascari", engine: #< power: 335, volume: 4.1>>, #< code: "275 F1", driver: "Villoresi", engine: #< power: 330, volume: 3.3>>]>
464
+
465
+ team.name # => "Ferrari"
466
+ team.year # => 1950
467
+
468
+ team.cars[0].code # => "340 F1"
469
+ team.cars[0].driver # => "Ascari"
470
+ team.cars[0].engine.power # => 335
471
+ team.cars[0].engine.volume # => 4.1
472
+
473
+ team.cars[1].code # => "275 F1"
474
+ team.cars[1].driver # => "Villoresi"
475
+ team.cars[1].engine.power # => 330
476
+ team.cars[1].engine.volume # => 3.3
477
+ ```
224
478
 
225
- <%= fc.label :driver %>
226
- <%= fc.text_field :driver %>
479
+ Update attributes using hash with array of hashes.
227
480
 
228
- <%= fc.field_for(:engine) do |fce| %>
229
- <%= fce.label :power %>
230
- <%= fce.text_field :power %>
481
+ ```ruby
482
+ team.update_attributes(
483
+ name: 'McLaren',
484
+ year: 1966,
485
+ cars: [
486
+ {
487
+ code: '275 F1',
488
+ driver: 'Bruce McLaren',
489
+ engine: {
490
+ volume: 3.0
491
+ }
492
+ }, {
493
+ code: 'M7A',
494
+ driver: 'Denis Hulme',
495
+ engine: {
496
+ power: 415,
497
+ }
498
+ }
499
+ ],
500
+ ) # => #<Team name: "McLaren", year: 1966, cars: [#< code: "M2B", driver: "Bruce McLaren", engine: #< power: nil, volume: 3.0>>, #< code: "M7A", driver: "Denis Hulme", engine: #< power: 415, volume: nil>>]>
501
+
502
+ team.name # => "McLaren"
503
+ team.year # => 1966
504
+
505
+ team.cars[0].code # => "275 F1"
506
+ team.cars[0].driver # => "Bruce McLaren"
507
+ team.cars[0].engine.power # => 330 - this value was not updated in :update_attributes method
508
+ team.cars[0].engine.volume # => 3.0
509
+
510
+ team.cars[1].code # => "M7A"
511
+ team.cars[1].driver # => "Denis Hulme"
512
+ team.cars[1].engine.power # => 415
513
+ team.cars[1].engine.volume # => nil - this value is nil because this car was created in :updated_attributes method
514
+ ```
231
515
 
232
- <%= fce.label :volume %>
233
- <%= fce.text_field :volume %>
234
- <% end %>
235
- <% end %>
236
- <% end %>
237
- <% end %>
516
+ Use `primary_key` method on class to get primary key attribute name.
517
+ Use `primary_key` and `primary_key=` method on instance to get and set primary key attribute value.
518
+
519
+ ```ruby
520
+ Team.primary_key # => :id - By default primary key is :id even if there is no such attribute
521
+ Car.primary_key # => :code
522
+ team.cars.first.primary_key # => "275 F1"
523
+ team.cars.last.primary_key # => "M7A"
238
524
  ```
239
525
 
240
- ### 2. Update Attributes
526
+ `update_attributes` compares present elements in the array with new elements in hash by using primary key.
527
+ By default `update_attributes`:
528
+ - calls attribute setter under hood to update attribute value of present elements,
529
+ - calls `FormObj::Struct` constructor to create all new elements (that exists in the hash but absent in the present array),
530
+ - calls `delete_if` to delete all removed elements (that exists in the present array but absent in the hash).
531
+
532
+ Default behaviour could be easily redefined by overwriting corresponding methods.
533
+
534
+ ```ruby
535
+ class MyStruct < FormObj::Struct
536
+ class Array < FormObj::Struct::Array
537
+ private
241
538
 
242
- Update form object attributes with the parameter hash received from the browser.
243
- Method `update_attributes(new_attrs_hash, options)` returns self so one can chain calls.
539
+ def create_item(hash, raise_if_not_found:)
540
+ puts "Create new element from #{hash}"
541
+ super
542
+ end
244
543
 
245
- `options` hash can have `:raise_if_not_found` key which has `true` value by default.
246
- If `new_attrs_hash` has key that does not correspond to any attributes
247
- and `raise_if_not_found` is `true` than `UnknownAttributeError` will be generated.
248
- `raise_if_not_found` equals to `false` prevents error generation
249
- and non existent attribute will be just ignored.
544
+ def delete_items(ids)
545
+ each do |item|
546
+ if ids.include? item.primary_key
547
+ item._destroy = true
548
+ puts "Mark item #{item.primary_key} for deletion"
549
+ end
550
+ end
551
+ end
552
+ end
553
+
554
+ def self.array_class
555
+ MyStruct::Array
556
+ end
557
+
558
+ def self.nested_class
559
+ MyStruct
560
+ end
561
+
562
+ private
563
+
564
+ def update_attribute(attribute, new_value)
565
+ puts "Update attribute :#{attribute.name} value from #{send(attribute.name)} to #{new_value}"
566
+ super
567
+ end
568
+ end
569
+
570
+ class Team < MyStruct
571
+ attribute :name
572
+ attribute :year
573
+ attribute :cars, array: true, primary_key: :code do
574
+ attribute :code
575
+ attribute :driver
576
+ attribute :engine do
577
+ attribute :power
578
+ attribute :volume
579
+ end
580
+ attr_accessor :_destroy
581
+ end
582
+ end
583
+
584
+ team = Team.new(name: 'Ferrari', cars: [{ code: '340 F1' }, { code: '275 F1' }])
585
+ # => Update attribute :name value from to Ferrari
586
+ # => Create new element from {"code"=>"340 F1"}
587
+ # => Update attribute :code value from to 340 F1
588
+ # => Create new element from {"code"=>"275 F1"}
589
+ # => Update attribute :code value from to 275 F1
590
+ # => => #<Team name: "Ferrari", year: nil, cars: [#< code: "340 F1", driver: nil, engine: #< power: nil, volume: nil>>, #< code: "275 F1", driver: nil, engine: #< power: nil, volume: nil>>]>
591
+
592
+ team.update_attributes(cars: [{ code: '275 F1' }])
593
+ # => Update attribute :code value from 275 F1 to 275 F1
594
+ # => Mark item 340 F1 for deletion
595
+ # => => #<Team name: "Ferrari", year: nil, cars: [#< code: "340 F1", driver: nil, engine: #< power: nil, volume: nil>>, #< code: "275 F1", driver: nil, engine: #< power: nil, volume: nil>>]>
596
+ ```
597
+
598
+ Use array of hashes to define default array of nested structs defined with block.
250
599
 
251
600
  ```ruby
252
- simple_form = SimpleForm.new
253
- simple_form.name = 'Ferrari'
254
- simple_form.year = 1950
255
- simple_form.update_attributes(
256
- name: 'McLaren',
257
- year: 1966
258
- )
259
- simple_form.name # => "McLaren"
260
- simple_form.year # => 1966
601
+ class Team < FormObj::Struct
602
+ attribute :cars, array: true, default: [{ code: '340 F1', driver: 'Ascari' }, { code: '275 F1', driver: 'Villoresi' }] do
603
+ attribute :code
604
+ attribute :driver
605
+ end
606
+ end
607
+
608
+ team = Team.new # => #<Team cars: [#< code: "340 F1", driver: "Ascari">, #< code: "275 F1", driver: "Villoresi">]>
609
+ team.cars.size # => 2
610
+ team.cars[0].code # => "340 F1"
611
+ team.cars[0].driver # => "Ascari"
612
+ team.cars[1].code # => "275 F1"
613
+ team.cars[1].driver # => "Villoresi"
261
614
  ```
262
615
 
263
- #### 2.1. Nested Form Objects
616
+ Use array of hashes or struct instances to define default array of nested structs defined with class.
264
617
 
265
618
  ```ruby
266
- nested_form = NestedForm.new
267
- nested_form.name = 'Ferrari'
268
- nested_form.year = 1950
269
- nested_form.car.code = '340 F1'
270
- nested_form.car.driver = 'Ascari'
271
- nested_form.car.engine.power = 335
272
- nested_form.car.engine.volume = 4.1
273
- nested_form.update_attributes(
274
- name: 'McLaren',
275
- year: 1966,
276
- car: {
277
- code: 'M2B',
278
- driver: 'Bruce McLaren',
279
- engine: {
280
- power: 300,
281
- volume: 3.0
282
- }
283
- }
284
- )
285
- nested_form.name # => "McLaren"
286
- nested_form.year # => 1966
287
- nested_form.car.code # => "M2B"
288
- nested_form.car.driver # => "Bruce McLaren"
289
- nested_form.car.engine.power # => 300
290
- nested_form.car.engine.volume # => 3.0
619
+ class Car < FormObj::Struct
620
+ attribute :code
621
+ attribute :driver
622
+ end
623
+
624
+ class Team < FormObj::Struct
625
+ attribute :cars, class: Car, array: true, default: [Car.new(code: '340 F1', driver: 'Ascari'), { code: '275 F1', driver: 'Villoresi' }]
626
+ end
627
+
628
+ team = Team.new # => #<Team cars: [#<Car code: "340 F1", driver: "Ascari">, #<Car code: "275 F1", driver: "Villoresi">]>
629
+ team.cars.size # => 2
630
+ team.cars[0].code # => "340 F1"
631
+ team.cars[0].driver # => "Ascari"
632
+ team.cars[1].code # => "275 F1"
633
+ team.cars[1].driver # => "Villoresi"
291
634
  ```
292
635
 
293
- #### 2.2. Array of Form Objects
636
+ The struct instance class should correspond to nested attribute class!
637
+
638
+ ```ruby
639
+ class Team < FormObj::Struct
640
+ attribute :cars, class: Car, array: true, default: [36]
641
+ end
642
+
643
+ Team.new # => FormObj::WrongDefaultValueClass: FormObj::WrongDefaultValueClass
644
+ ```
294
645
 
295
- Updating an array of form objects will compare the existing array with the new one.
296
- New array elements will be added, existing array elements will be updated, absent array elements will be deleted
297
- (deleting behavior is the subject of changes in future releases - only elements with flag `_destroy == true` will be deleted).
646
+ #### 1.3. Serialize `FormObj::Struct` to Hash
298
647
 
299
- In order to compare old and new array its elements have to be identified via the primary key.
300
- Primary key can be specified either on the attribute level or on the form level.
301
- If it is not specified the :id field is supposed to be the primary key.
648
+ Call `to_hash()` method in order to get a hash representation of `FormObj::Struct`
302
649
 
303
650
  ```ruby
304
- class ArrayForm < FormObj::Form
651
+ class Team < FormObj::Struct
305
652
  attribute :name
306
653
  attribute :year
307
654
  attribute :cars, array: true do
308
- attribute :code, primary_key: true # <- primary key is specified on attribute level
655
+ attribute :code, primary_key: true
309
656
  attribute :driver
310
657
  attribute :engine do
311
658
  attribute :power
@@ -313,14 +660,82 @@ class ArrayForm < FormObj::Form
313
660
  end
314
661
  end
315
662
  end
316
- ```
663
+
664
+ team = Team.new(
665
+ name: 'Ferrari',
666
+ year: 1950,
667
+ cars: [
668
+ {
669
+ code: '340 F1',
670
+ driver: 'Ascari',
671
+ engine: {
672
+ power: 335,
673
+ volume: 4.1,
674
+ }
675
+ }, {
676
+ code: '275 F1',
677
+ driver: 'Villoresi',
678
+ engine: {
679
+ power: 330,
680
+ volume: 3.3,
681
+ }
682
+ }
683
+ ],
684
+ ) # => #<Team name: "Ferrari", year: 1950, cars: [#< code: "340 F1", driver: "Ascari", engine: #< power: 335, volume: 4.1>>, #< code: "275 F1", driver: "Villoresi", engine: #< power: 330, volume: 3.3>>]>
685
+
686
+ team.to_hash # => {
687
+ # => :name => "Ferrari",
688
+ # => :year => 1950,
689
+ # => :cars => [{
690
+ # => :code => "340 F1",
691
+ # => :driver => "Ascari",
692
+ # => :engine => {
693
+ # => :power => 335,
694
+ # => :volume => 4.1
695
+ # => }
696
+ # => }, {
697
+ # => :code => "275 F1",
698
+ # => :driver => "Villoresi",
699
+ # => :engine => {
700
+ # => :power => 330,
701
+ # => :volume => 3.3
702
+ # => }
703
+ # => }]
704
+ # => }
705
+ ```
706
+
707
+ ### 2. `FormObj::Form`
708
+
709
+ `FormObj::Form` is inherited from `FormObj::Struct` and adds support for Rails compatible form builders and ActiveModel validations.
710
+
711
+ #### 2.1. `FormObj::Form` Validation
317
712
 
318
713
  ```ruby
319
- class ArrayForm < FormObj::Form
714
+ class Team < FormObj::Form
320
715
  attribute :name
321
716
  attribute :year
322
- attribute :cars, array: true, primary_key: :code do # <- primary key is specified on form level
323
- attribute :code
717
+
718
+ validates :name, length: { minimum: 10 }
719
+ end
720
+
721
+ team = Team.new(name: 'Ferrari') # => #<Team name: "Ferrari", year: nil>
722
+ team.valid? # => false
723
+ team.errors.messages # => {:name=>["is too short (minimum is 10 characters)"]}
724
+ ```
725
+
726
+ #### 2.2. `FormObj::Form` Persistence
727
+
728
+ In order to make `FormObj::Form` compatible with form builder it has to respond to `:persisted?` message.
729
+ It maintains persistence status. Initial form is not persisted.
730
+ It can be marked as persisted by assigning `persisted = true` which marks as persisted only form itself or
731
+ by calling `mark_as_persisted` method which marks as persisted the form itself and all nested forms and arrays.
732
+
733
+ ```ruby
734
+ class Team < FormObj::Form
735
+ attribute :name
736
+ attribute :year
737
+ attribute :cars, array: true do
738
+ attribute :code, primary_key: true
324
739
  attribute :driver
325
740
  attribute :engine do
326
741
  attribute :power
@@ -328,182 +743,295 @@ class ArrayForm < FormObj::Form
328
743
  end
329
744
  end
330
745
  end
331
- ```
332
746
 
333
- ```ruby
334
- array_form = ArrayForm.new
335
- array_form.name = 'Ferrari'
336
- array_form.year = 1950
747
+ team = Team.new(cars: [{code: 1}])
337
748
 
338
- car1 = array_form.cars.create
339
- car1.code = '340 F1'
340
- car1.driver = 'Ascari'
341
- car1.engine.power = 335
342
- car1.engine.volume = 4.1
749
+ team.persisted? # => false
750
+ team.cars[0].persisted? # => false
751
+ team.cars[0].engine.persisted? # => false
343
752
 
344
- car2 = array_form.cars.create
345
- car2.code = 'M2B'
346
- car2.driver = 'Villoresi'
347
- car2.engine.power = 300
348
- car2.engine.volume = 3.3
753
+ team.persisted = true
349
754
 
350
- array_form.update_attributes(
351
- name: 'McLaren',
352
- year: 1966,
353
- cars: [
354
- {
355
- code: 'M2B',
356
- driver: 'Bruce McLaren',
357
- engine: {
358
- volume: 3.0
359
- }
360
- }, {
361
- code: 'M7A',
362
- driver: 'Denis Hulme',
363
- engine: {
364
- power: 415,
365
- }
366
- }
367
- ],
368
- )
369
-
370
- array_form.name # => "McLaren"
371
- array_form.year # => 1966
755
+ team.persisted? # => false - because nested forms are not persisted
756
+ team.cars[0].persisted? # => false
757
+ team.cars[0].engine.persisted? # => false
758
+
759
+ team.cars[0].engine.persisted = true
760
+
761
+ team.persisted? # => false - because nested forms are not persisted
762
+ team.cars[0].persisted? # => false
763
+ team.cars[0].engine.persisted? # => true
764
+
765
+ team.mark_as_persisted
766
+
767
+ team.persisted? # => true
768
+ team.cars[0].persisted? # => true
769
+ team.cars[0].engine.persisted? # => true
770
+ ```
771
+
772
+ Change of attribute value (directly or by `update_attributes` call) will change persistence status to `false`.
773
+
774
+ ```ruby
775
+ team.name = 'Ferrari'
776
+ team.persisted? # => false
372
777
 
373
- array_form.cars[0].code # => "M2B"
374
- array_form.cars[0].driver # => "Bruce McLaren"
375
- array_form.cars[0].engine.power # => 300 - this value was not updated in update_attributes
376
- array_form.cars[0].engine.volume # => 3.0
778
+ team.mark_as_persisted
779
+ team.persisted? # => true
377
780
 
378
- array_form.cars[1].code # => "M7A"
379
- array_form.cars[1].driver # => "Denis Hulme"
380
- array_form.cars[1].engine.power # => 415
381
- array_form.cars[1].engine.volume # => nil - this value is nil because this car was created in updated_attributes
781
+ team.update_attributes(name: 'McLaren')
782
+ team.persisted? # => false
382
783
  ```
383
784
 
384
- ### 3. Serialize to Hash
785
+ #### 2.3. Delete from Array of `FormObj::Form` via `update_attributes` method
385
786
 
386
- Call `to_hash()` method in order to get a hash representation of the form object
787
+ `FormObj::Struct` `update_attributes` method by default deletes all array elements that are not present in the new hash.
387
788
 
388
789
  ```ruby
389
- simple_form.to_hash # => {
390
- # => :name => "McLaren",
391
- # => :year => 1966
392
- # => }
790
+ class Team < FormObj::Struct
791
+ attribute :cars, array: true, primary_key: :code do
792
+ attribute :code
793
+ attribute :driver
794
+ end
795
+ end
796
+
797
+ team = Team.new(cars: [{code: 1, driver: 'Ascari'}, {code: 2, driver: 'Villoresi'}])
798
+ team.update_attributes(cars: [{code: 1}])
799
+ team.cars # => [#< code: 1, driver: "Ascari">]
393
800
  ```
394
801
 
395
- #### 3.1. Nested Form Objects
802
+ In oppose to this `FormObj::Form` `update_attributes` method ignores elements that are absent in the hash but
803
+ marks for destruction those elements that has `_destroy: true` key in the hash.
396
804
 
397
805
  ```ruby
398
- nested_form.to_hash # => {
399
- # => :name => "McLaren",
400
- # => :year => 1966,
401
- # => :car => {
402
- # => :code => "340 F1",
403
- # => :driver => "Ascari",
404
- # => :engine => {
405
- # => :power => 335,
406
- # => :volume => 4.1
407
- # => }
408
- # => }
409
- # => }
806
+ class Team < FormObj::Form
807
+ attribute :cars, array: true, primary_key: :code do
808
+ attribute :code
809
+ attribute :driver
810
+ end
811
+ end
812
+
813
+ team = Team.new(cars: [{code: 1, driver: 'Ascari'}, {code: 2, driver: 'Villoresi'}])
814
+ team.update_attributes(cars: [{code: 2, driver: 'James Hunt'}])
815
+
816
+ team.cars[0].code # => 1
817
+ team.cars[0].driver # => 'Ascari'
818
+ team.cars[0].marked_for_destruction? # => false
819
+
820
+ team.cars[1].code # => 2
821
+ team.cars[1].driver # => 'James Hunt'
822
+ team.cars[1].marked_for_destruction? # => false
823
+
824
+ team.update_attributes(cars: [{code: 1, _destroy: true}])
825
+
826
+ team.cars[0].code # => 2
827
+ team.cars[0].driver # => 'James Hunt'
828
+ team.cars[0].marked_for_destruction? # => false
829
+
830
+ team.cars[1].code # => 1
831
+ team.cars[1].driver # => 'Ascari'
832
+ team.cars[1].marked_for_destruction? # => true
410
833
  ```
411
834
 
412
- #### 3.2. Array of Form Objects
835
+ Use `mark_for_destruction` in order to forcefully mark an array element for destruction.
413
836
 
414
837
  ```ruby
415
- array_form.to_hash # => {
416
- # => :name => "McLaren",
417
- # => :year => 1966,
418
- # => :cars => [{
419
- # => :code => "M2B",
420
- # => :driver => "Bruce McLaren",
421
- # => :engine => {
422
- # => :power => 300,
423
- # => :volume => 3.0
424
- # => }
425
- # => }, {
426
- # => :code => "M7A",
427
- # => :driver => "Denis Hulme",
428
- # => :engine => {
429
- # => :power => 415,
430
- # => : volume => nil
431
- # => }
432
- # => }]
433
- # => }
838
+ team.cars[0].marked_for_destruction? # => false
839
+ team.cars[0].mark_for_destruction
840
+ team.cars[0].marked_for_destruction? # => true
434
841
  ```
435
842
 
436
- ### 4. Map Form Object to/from Models
843
+ #### 2.4. Using `FormObj::Form` in Form Builder
437
844
 
438
- Include `DataMapper` mix-in and map form object attributes to one or more models by using `:model` and `:model_attribute` parameters.
439
- By default each form object attribute is mapped to the model attribute with the same name of the `:default` model.
845
+ ```ruby
846
+ class Team < FormObj::Form
847
+ attribute :name
848
+ attribute :year
849
+ attribute :cars, array: true, primary_key: :code do
850
+ attribute :code
851
+ attribute :driver
852
+ attribute :engine do
853
+ attribute :power
854
+ attribute :volume
855
+ end
856
+ end
857
+ end
858
+
859
+ @team = Team.new
860
+ ```
861
+
862
+ ```erb
863
+ <%= form_for(@team) do |f| %>
864
+ <%= f.label :name %>
865
+ <%= f.text_field :name %>
866
+
867
+ <%= f.label :year %>
868
+ <%= f.text_field :year %>
869
+
870
+ <% f.cars.each do |car| %>
871
+ <%= f.fields_for(:cars, car, index: '') do |fc| %>
872
+ <%= fc.label :code %>
873
+ <%= fc.text_field :code %>
874
+
875
+ <%= fc.label :driver %>
876
+ <%= fc.text_field :driver %>
877
+
878
+ <%= fc.field_for(:engine) do |fce| %>
879
+ <%= fce.label :power %>
880
+ <%= fce.text_field :power %>
881
+
882
+ <%= fce.label :volume %>
883
+ <%= fce.text_field :volume %>
884
+ <% end %>
885
+ <% end %>
886
+ <% end %>
887
+ <% end %>
888
+ ```
889
+
890
+ ### 3. `FormObj::ModelMapper`
891
+
892
+ Include `FormObj::ModelMapper` module and map form object attributes to one or more models by using `:model` and `:model_attribute` parameters.
893
+ Use dot notation to map model attribute to a nested model. Use colon to specify a "hash's attribute".
440
894
 
441
- Use dot notation to map model attribute to a nested model. Use colon to specify a "hash" attribute.
895
+ #### 3.1. `load_from_model` - Initialize Form Object from Model
896
+
897
+ Use `load_from_model(model)` method to initialize form object from the model.
442
898
 
443
899
  ```ruby
444
- class SingleForm < FormObj::Form
445
- include DataMapper
900
+ class Team < FormObj::Form
901
+ include FormObj::ModelMapper
446
902
 
447
903
  attribute :name, model_attribute: :team_name
448
904
  attribute :year
449
905
  attribute :engine_power, model_attribute: 'car.:engine.power'
450
906
  end
907
+
908
+ car_model = { engine: Struct.new(:power).new(335) }
909
+ team_model = Struct.new(:team_name, :year, :car).new('Ferrari', 1950, car_model)
910
+
911
+ team = Team.new.load_from_model(team_model)
912
+ team.to_hash # => {
913
+ # => :name => "Ferrari"
914
+ # => :year => 1950
915
+ # => :engine_power => 335
916
+ # => }
451
917
  ```
452
918
 
453
- Suppose `single_form = SingleForm.new` and `model` to be an instance of a model.
919
+ So attributes are mapped as follows:
454
920
 
455
921
  | Form Object attribute | Model attribute |
456
922
  | --------------------- | --------------- |
457
- | `single_form.name` | `model.team_name` |
458
- | `single_form.year` | `model.year` |
459
- | `single_form.engine_power` | `model.car[:engine].power` |
923
+ | `team.name` | `team_model.team_name` |
924
+ | `team.year` | `team_model.year` |
925
+ | `team.engine_power` | `team_model.car[:engine].power` |
460
926
 
461
- #### 4.1. Multiple Models Example
927
+
928
+ #### 3.2. `load_from_models` - Initialize Form Object from Few Models
929
+
930
+ Use `load_from_models(models)` method to initialize form object from few models.
931
+ `models` parameter is a hash where keys are the name of models and values are models themselves.
932
+
933
+ By default each form object attribute is mapped to `:default` model.
934
+ Use parameter `:model` to map it to another model.
462
935
 
463
936
  ```ruby
464
- class MultiForm < FormObj::Form
465
- include ModelMapper
937
+ class Team < FormObj::Form
938
+ include FormObj::ModelMapper
466
939
 
467
940
  attribute :name, model_attribute: :team_name
468
941
  attribute :year
469
942
  attribute :engine_power, model: :car, model_attribute: ':engine.power'
470
943
  end
944
+
945
+ car_model = { engine: Struct.new(:power).new(335) }
946
+ team_model = Struct.new(:team_name, :year).new('Ferrari', 1950) # <- doesn't have car attribute !!!
947
+
948
+ team = Team.new.load_from_models(default: team_model, car: car_model)
949
+ team.to_hash # => {
950
+ # => :name => "Ferrari"
951
+ # => :year => 1950
952
+ # => :engine_power => 335
953
+ # => }
471
954
  ```
472
955
 
473
- Suppose `multi_form = MultiForm.new` and `default`, `car` to be instances of two models.
956
+ So attributes are mapped as follows:
474
957
 
475
958
  | Form Object attribute | Model attribute |
476
959
  | --------------------- | --------------- |
477
- | `multi_form.name` | `default.team_name` |
478
- | `multi_form.year` | `default.year` |
479
- | `multi_form.engine_power` | `car[:engine].power` |
960
+ | `team.name` | `team_model.team_name` |
961
+ | `team.year` | `team_model.year` |
962
+ | `team.engine_power` | `car_model[:engine].power` |
480
963
 
481
- #### 4.2. Skip Attribute Mapping
964
+ #### 3.3. Do Not Map Certain Attribute
482
965
 
483
- Use `model_attribute: false` in order to avoid attribute mapping to the model.
966
+ Use `model_attribute: false` in order to avoid mapping of this attribute.
484
967
 
485
968
  ```ruby
486
- class SimpleForm < FormObj::Form
969
+ class Team < FormObj::Form
487
970
  include ModelMapper
488
971
 
489
- attribute :name, model_attribute: :team_name
972
+ attribute :name, model_attribute: :team_name
490
973
  attribute :year
491
974
  attribute :engine_power, model_attribute: false
492
975
  end
976
+
977
+ team_model = Struct.new(:team_name, :year, :engine_power).new('Ferrari', 1950, 335)
978
+
979
+ team = Team.new.load_from_model(team_model)
980
+ team.to_hash # => {
981
+ # => :name => "Ferrari"
982
+ # => :year => 1950
983
+ # => :engine_power => nil
984
+ # => }
493
985
  ```
494
986
 
495
- Suppose `form = SimpleForm.new` and `model` to be an instance of a model.
987
+ So attributes are mapped as follows:
496
988
 
497
989
  | Form Object attribute | Model attribute |
498
990
  | --------------------- | --------------- |
499
- | `form.name` | `model.team_name` |
500
- | `form.year` | `model.year` |
991
+ | `form.name` | `team_model.team_name` |
992
+ | `form.year` | `team_model.year` |
501
993
  | `form.engine_power` | - |
502
994
 
503
- ##### 4.2.1. Map Nested Form Object Attribute to Parent Level Model Attribute
995
+ #### 3.4. Map Nested Form Objects
996
+
997
+ Nested forms are mapped by default to corresponding nested models.
998
+
999
+ ```ruby
1000
+ class Team < FormObj::Form
1001
+ include ModelMapper
1002
+
1003
+ attribute :name, model_attribute: :team_name
1004
+ attribute :year
1005
+ attribute :car do
1006
+ attribute :code
1007
+ attribute :driver
1008
+ end
1009
+ end
504
1010
 
505
- TODO: replace `model_attribute` by `model`
506
- Use `model_attribute: false` for nested form object in order to map its attributes to the parent level of the model.
1011
+ car_model = Struct.new(:code, :driver).new('340 F1', 'Ascari')
1012
+ team_model = Struct.new(:team_name, :year, :car).new('Ferrari', 1950, car_model)
1013
+
1014
+ team = Team.new.load_from_model(team_model)
1015
+ team.to_hash # => {
1016
+ # => :name => "Ferrari",
1017
+ # => :year => 1950,
1018
+ # => :car => {
1019
+ # => :code => "340 F1",
1020
+ # => :driver => "Ascari"
1021
+ # => }
1022
+ # => }
1023
+ ```
1024
+
1025
+ So attributes are mapped as follows:
1026
+
1027
+ | Form Object attribute | Model attribute |
1028
+ | --------------------- | --------------- |
1029
+ | `team.name` | `team_model.team_name` |
1030
+ | `team.year` | `team_model.year` |
1031
+ | `team.car.code` | `team_model.car.code` |
1032
+ | `team.car.driver` | `team_model.car.driver` |
1033
+
1034
+ Use `model_nesting: false` parameter to map nested form object to map parent level model.
507
1035
 
508
1036
  ```ruby
509
1037
  class NestedForm < FormObj::Form
@@ -511,34 +1039,40 @@ class NestedForm < FormObj::Form
511
1039
 
512
1040
  attribute :name, model_attribute: :team_name
513
1041
  attribute :year
514
- attribute :car, model_attribute: false do # nesting only in form object but not in a model
1042
+ attribute :car, model_nesting: false do # nesting only in form object but not in a model
515
1043
  attribute :code
516
1044
  attribute :driver
517
- attribute :engine do
518
- attribute :power
519
- attribute :volume
520
- end
521
1045
  end
522
1046
  end
1047
+
1048
+ team_model = Struct.new(:team_name, :year, :code, :driver).new('Ferrari', 1950, '340 F1', 'Ascari')
1049
+
1050
+ team = Team.new.load_from_model(team_model)
1051
+ team.to_hash # => {
1052
+ # => :name => "Ferrari",
1053
+ # => :year => 1950,
1054
+ # => :car => {
1055
+ # => :code => "340 F1",
1056
+ # => :driver => "Ascari"
1057
+ # => }
1058
+ # => }
523
1059
  ```
524
1060
 
525
- Suppose `form = NestedForm.new` and `model` to be an instance of a model.
1061
+ So attributes are mapped as follows:
526
1062
 
527
1063
  | Form Object attribute | Model attribute |
528
1064
  | --------------------- | --------------- |
529
- | `form.name` | `model.team_name` |
530
- | `form.year` | `model.year` |
531
- | `form.car.code` | `model.code` |
532
- | `form.car.driver` | `model.driver` |
533
- | `form.car.engine.power` | `model.engine.power` |
534
- | `form.car.engine.volume` | `model.engine.volume` |
1065
+ | `team.name` | `team_model.team_name` |
1066
+ | `team.year` | `team_model.year` |
1067
+ | `team.car.code` | `team_model.code` |
1068
+ | `team.car.driver` | `team_model.driver` |
535
1069
 
536
- #### 4.3. Map Nested Form Object to A Hash Model
1070
+ #### 3.5. Map Nested Form Object to A Hash Model
537
1071
 
538
1072
  Use `model_hash: true` in order to map a nested form object to a hash as a model.
539
1073
 
540
1074
  ```ruby
541
- class NestedForm < FormObj::Form
1075
+ class Team < FormObj::Form
542
1076
  include ModelMapper
543
1077
 
544
1078
  attribute :name, model_attribute: :team_name
@@ -546,60 +1080,191 @@ class NestedForm < FormObj::Form
546
1080
  attribute :car, model_hash: true do # nesting only in form object but not in a model
547
1081
  attribute :code
548
1082
  attribute :driver
549
- attribute :engine do
550
- attribute :power
551
- attribute :volume
552
- end
553
1083
  end
554
1084
  end
1085
+
1086
+ car_model = { code: '340 F1', driver: 'Ascari' }
1087
+ team_model = Struct.new(:team_name, :year, :car).new('Ferrari', 1950, car_model)
1088
+
1089
+ team = Team.new.load_from_model(team_model)
1090
+ team.to_hash # => {
1091
+ # => :name => "Ferrari",
1092
+ # => :year => 1950,
1093
+ # => :car => {
1094
+ # => :code => "340 F1",
1095
+ # => :driver => "Ascari"
1096
+ # => }
1097
+ # => }
555
1098
  ```
556
1099
 
557
- Suppose `form = NestedForm.new` and `model` to be an instance of a model.
1100
+ So attributes are mapped as follows:
558
1101
 
559
1102
  | Form Object attribute | Model attribute |
560
1103
  | --------------------- | --------------- |
561
- | `form.name` | `model.team_name` |
562
- | `form.year` | `model.year` |
563
- | `form.car.code` | `model.car[:code]` |
564
- | `form.car.driver` | `model.car[:driver]` |
565
- | `form.car.engine.power` | `model.car[:engine].power` |
566
- | `form.car.engine.volume` | `model.car[:engine].volume` |
1104
+ | `team.name` | `team_model.team_name` |
1105
+ | `team.year` | `team_model.year` |
1106
+ | `team.car.code` | `team_model.car[:code]` |
1107
+ | `team.car.driver` | `team_model.car[:driver]` |
567
1108
 
568
- ### 5. Load Form Object from Models
1109
+ #### 3.6. Custom Implementation of Loading of Array of Models
569
1110
 
570
- Use `load_from_models(models)` to load form object attributes from mapped models.
571
- Method returns self so one can chain calls.
1111
+ By default `load_from_model(s)` methods loads all models from arrays.
572
1112
 
573
1113
  ```ruby
574
- class MultiForm < FormObj::Form
1114
+ class Team < FormObj::Form
575
1115
  include ModelMapper
576
-
1116
+
577
1117
  attribute :name, model_attribute: :team_name
578
1118
  attribute :year
579
- attribute :engine_power, model: :car, model_attribute: ':engine.power'
1119
+ attribute :cars, array: true do
1120
+ attribute :code
1121
+ attribute :driver
1122
+ end
1123
+ attribute :colours, array: true do
1124
+ attribute :name
1125
+ attribute :rgb
1126
+ end
580
1127
  end
581
1128
 
582
- default_model = Struct.new(:team_name, :year).new('Ferrari', 1950)
583
- car_model = { engine: Struct.new(:power).new(335) }
1129
+ CarModel = Struct.new(:code, :driver)
1130
+ ColourModel = Struct.new(:name, :rgb)
1131
+
1132
+ cars_model = [CarModel.new('340 F1', 'Ascari'), CarModel.new('275 F1', 'Villoresi')]
1133
+ colours_model = [ColourModel.new(:red, 0xFF0000), ColourModel.new(:white, 0xFFFFFF)]
1134
+ team_model = Struct.new(:team_name, :year, :cars, :colours).new('Ferrari', 1950, cars_model, colours_model)
1135
+
1136
+ team = Team.new.load_from_model(team_model)
1137
+ team.to_hash # => {
1138
+ # => :name => "Ferrari",
1139
+ # => :year => 1950,
1140
+ # => :cars => [{
1141
+ # => :code => "340 F1",
1142
+ # => :driver => "Ascari"
1143
+ # => }, {
1144
+ # => :code => "275 F1",
1145
+ # => :driver => "Villoresi"
1146
+ # => }],
1147
+ # => :colours => [{
1148
+ # => :name => :red,
1149
+ # => :rgb => 0xFF0000
1150
+ # => }, {
1151
+ # => :name => :white,
1152
+ # => :rgb => 0xFFFFFF
1153
+ # => }]
1154
+ # => }
1155
+ ```
584
1156
 
585
- multi_form = MultiForm.new.load_from_models(default: default_model, car: car_model)
586
- multi_form.to_hash # => {
587
- # => :name => "Ferrari"
588
- # => :year => 1950
589
- # => :engine_power => 335
590
- # => }
591
- ```
1157
+ `FormObj::ModelMapper::Array` class implements method (where `*args` are additional params passed to `load_from_model(s)` methods)
1158
+
1159
+ ```ruby
1160
+ def iterate_through_models_to_load_them(models, *args, &block)
1161
+ models.each { |model| block.call(model) }
1162
+ end
1163
+ ```
592
1164
 
593
- Use `load_from_models(default: model)` or `load_from_model(model)` to load from single model.
1165
+ This method should iterate through all models that has to be loaded and call a block for each of them.
1166
+ In the example above it will receive `cars_model` as the value of `models` parameter.
1167
+ Overwrite this method in order to implement your own logic.
594
1168
 
595
- ### 6. Sync Form Object to Models
1169
+ ```ruby
1170
+ class ArrayLoadLimit < FormObj::ModelMapper::Array
1171
+ private
1172
+
1173
+ def iterate_through_models_to_load_them(models, params = {}, &block)
1174
+ models = models.slice(params[:offset] || 0, params[:limit] || 999999999) if model_attribute.names.last == :cars
1175
+ super(models, &block)
1176
+ end
1177
+ end
1178
+
1179
+ class LoadLimitForm < FormObj::Form
1180
+ include FormObj::ModelMapper
1181
+
1182
+ def self.array_class
1183
+ ArrayLoadLimit
1184
+ end
1185
+ end
1186
+
1187
+ class Team < LoadLimitForm
1188
+ attribute :name, model_attribute: :team_name
1189
+ attribute :year
1190
+ attribute :cars, array: true do
1191
+ attribute :code
1192
+ attribute :driver
1193
+ end
1194
+ attribute :colours, array: true do
1195
+ attribute :name
1196
+ attribute :rgb
1197
+ end
1198
+ end
1199
+
1200
+ CarModel = Struct.new(:code, :driver)
1201
+ ColourModel = Struct.new(:name, :rgb)
1202
+
1203
+ cars_model = [CarModel.new('340 F1', 'Ascari'), CarModel.new('275 F1', 'Villoresi')]
1204
+ colours_model = [ColourModel.new(:red, 0xFF0000), ColourModel.new(:white, 0xFFFFFF)]
1205
+ team_model = Struct.new(:team_name, :year, :cars, :colours).new('Ferrari', 1950, cars_model, colours_model)
1206
+
1207
+ team = Team.new.load_from_model(team_model, offset: 0, limit: 1)
1208
+ team.to_hash # => {
1209
+ # => :name => "Ferrari",
1210
+ # => :year => 1950,
1211
+ # => :cars => [{
1212
+ # => :code => "340 F1",
1213
+ # => :driver => "Ascari"
1214
+ # => }],
1215
+ # => :colours => [{
1216
+ # => :name => :red,
1217
+ # => :rgb => 0xFF0000
1218
+ # => }, {
1219
+ # => :name => :white,
1220
+ # => :rgb => 0xFFFFFF
1221
+ # => }]
1222
+ # => }
1223
+
1224
+ team = Team.new.load_from_model(team_model, offset: 1, limit: 1)
1225
+ team.to_hash # => {
1226
+ # => :name => "Ferrari",
1227
+ # => :year => 1950,
1228
+ # => :cars => [{
1229
+ # => :code => "275 F1",
1230
+ # => :driver => "Villoresi"
1231
+ # => }],
1232
+ # => :colours => [{
1233
+ # => :name => :red,
1234
+ # => :rgb => 0xFF0000
1235
+ # => }, {
1236
+ # => :name => :white,
1237
+ # => :rgb => 0xFFFFFF
1238
+ # => }]
1239
+ # => }
1240
+ ```
1241
+
1242
+ Note that our new implementation of `iterate_through_models_to_load_them` limits only cars but not colours.
1243
+ It identifies requested model attribute using `model_attribute.names` which returns
1244
+ an array of model attribute accessors (in our example `[:cars]`)
1245
+
1246
+ In case of `ActiveRecord` model `iterate_through_models_to_load_them` will receive an instance of `ActiveRecord::Relation` as `models` parameter.
1247
+ This allows to load in the memory only necessary association models.
1248
+
1249
+ ```ruby
1250
+ class ArrayLoadLimit < FormObj::ModelMapper::Array
1251
+ private
1252
+
1253
+ def iterate_through_models_to_load_them(models, params = {}, &block)
1254
+ models = models.offset(params[:offset] || 0).limit(params[:limit] || 999999999) if model_attribute.names.last == :cars
1255
+ super(models, &block)
1256
+ end
1257
+ end
1258
+ ```
1259
+
1260
+ #### 3.7. Sync Form Object to Models
596
1261
 
597
1262
  Use `sync_to_models(models)` to sync form object attributes to mapped models.
598
1263
  Method returns self so one can chain calls.
599
1264
 
600
1265
  ```ruby
601
1266
  class MultiForm < FormObj::Form
602
- include ModelMapper
1267
+ include FormObj::ModelMapper
603
1268
 
604
1269
  attribute :name, model_attribute: :team_name
605
1270
  attribute :year
@@ -627,7 +1292,7 @@ using `<attribute_name>=` accessors on the model(s).
627
1292
 
628
1293
  It is completely up to developer to do any additional validations on the model(s) and save it(them).
629
1294
 
630
- #### 6.1. Array of Form Objects and Models
1295
+ ##### 3.7.1. Array of Form Objects and Models
631
1296
 
632
1297
  Saving array of form objects to corresponding array of models requires the class of the model to be known by the form object
633
1298
  because it could create new instances of the model array elements.
@@ -636,7 +1301,7 @@ Form object will try to guess the name of the class from the name of the attribu
636
1301
 
637
1302
  ```ruby
638
1303
  class ArrayForm < FormObj::Form
639
- include ModelMapper
1304
+ include FormObj::ModelMapper
640
1305
 
641
1306
  attribute :name
642
1307
  attribute :year
@@ -652,7 +1317,7 @@ nested models the value of `:model_class` parameter should be an array of corres
652
1317
 
653
1318
  ```ruby
654
1319
  class ArrayForm < FormObj::Form
655
- include ModelMapper
1320
+ include FormObj::ModelMapper
656
1321
 
657
1322
  attribute :name
658
1323
  attribute :year
@@ -663,13 +1328,13 @@ class ArrayForm < FormObj::Form
663
1328
  end
664
1329
  ```
665
1330
 
666
- ### 7. Serialize Form Object to Model Hash
1331
+ #### 3.8. Serialize Form Object to Model Hash
667
1332
 
668
1333
  Use `to_model_hash(model = :default)` to get hash representation of the model that mapped to the form object.
669
1334
 
670
1335
  ```ruby
671
1336
  class MultiForm < FormObj::Form
672
- include ModelMapper
1337
+ include FormObj::ModelMapper
673
1338
 
674
1339
  attribute :name, model_attribute: :team_name
675
1340
  attribute :year
@@ -697,7 +1362,7 @@ If array of form objects mapped to the parent model (`model_attribute: false`) i
697
1362
 
698
1363
  ```ruby
699
1364
  class ArrayForm < FormObj::Form
700
- include ModelMapper
1365
+ include FormObj::ModelMapper
701
1366
 
702
1367
  attribute :name
703
1368
  attribute :year
@@ -733,38 +1398,7 @@ array_form.to_model_hash # => {
733
1398
  # => }
734
1399
  ```
735
1400
 
736
- ### 8. Validation and Coercion
737
-
738
- Form Object is just a Ruby class. By default it includes (could be changed in future releases):
739
-
740
- ```ruby
741
- extend ::ActiveModel::Naming
742
- extend ::ActiveModel::Translation
743
-
744
- include ::ActiveModel::Conversion
745
- include ::ActiveModel::Validations
746
- ```
747
-
748
- So add ActiveModel validations directly to Form Object class definition.
749
-
750
- ```ruby
751
- class MultiForm < FormObj::Form
752
- include ModelMapper
753
-
754
- attribute :name, model_attribute: :team_name
755
- attribute :year
756
- attribute :engine_power, model: :car, model_attribute: ':engine.power'
757
-
758
- validates :name, :year, presence: true
759
- end
760
- ```
761
-
762
- There is no coercion during assigning/updating form object attributes.
763
- Coercion can be done manually by redefining assigning methods `<attribute_name>=`
764
- or it will happen in the model when the form object will be saved to it.
765
- This is the standard way how coercion happens in Rails for example.
766
-
767
- ### 9. Copy Model Validation Errors into Form Object
1401
+ #### 3.9. Copy Model Validation Errors into Form Object
768
1402
 
769
1403
  Even though validation could and should happen in the form object it is possible to have (additional) validation(s) in the model(s).
770
1404
  In this case it is handy to copy model validation errors to form object in order to be able to present them to the user in a standard way.
@@ -781,7 +1415,7 @@ In case of single model:
781
1415
  single_form.copy_errors_from_model(model)
782
1416
  ```
783
1417
 
784
- ### 10. Rails Example
1418
+ ### 4. Rails Example
785
1419
 
786
1420
  ```ruby
787
1421
  # db/migrate/yyyymmddhhmiss_create_team.rb
@@ -825,7 +1459,7 @@ end
825
1459
  ```ruby
826
1460
  # app/form_objects/team_form.rb
827
1461
  class TeamForm < FormObj::Form
828
- include ModelMapper
1462
+ include FormObj::ModelMapper
829
1463
 
830
1464
  attribute :id
831
1465
  attribute :name, model_attribute: :team_name
@@ -922,18 +1556,20 @@ end
922
1556
  <% end %>
923
1557
  ```
924
1558
 
925
- ### 11. Reference Guide: `attribute` parameters
1559
+ ### 5. Reference Guide: `attribute` parameters
926
1560
 
927
1561
  | Parameter | Condition | Default value | Defined in | Description |
928
1562
  | --- |:---:|:---:|:---:| --- |
929
- | array | block* or `:class`** | `false` | `TreeStruct` | This attribute is an array of form objects. The structure of array element form object is described either in the block or in the separate class referenced by `:class` parameter |
930
- | class | - | - | `TreeStruct` | This attribute is either nested form object or array of form objects. The value of this parameter is the class of this form object or the name of the class. |
1563
+ | array | block* or `:class`** | `false` | `FormObj::Struct` | This attribute is an array of form objects. The structure of array element form object is described either in the block or in the separate class referenced by `:class` parameter |
1564
+ | class | - | - | `FormObj::Struct` | This attribute is either nested form object or array of form objects. The value of this parameter is the class of this form object or the name of the class. |
1565
+ | default | - | - | `FormObj::Struct` | Defines default value for the attribute. Nested structures default value can be defined either with Hash or with object. |
931
1566
  | model_hash | block* or `:class`** | `false` | `FormObj::ModelMapper` | This attribute is either nested form object or array of form objects. This form object is mapped to a model of the class `Hash` so all its attributes should be accessed by `[:<attribute_name>]` instead of `.<attribute_name>` |
932
1567
  | model | - | `:default` | `FormObj::ModelMapper` | The name of the model to which this attribute is mapped |
933
- | model_attribute | - | `<attribute_name>` | `FormObj::ModelMapper` | The name of the model attribute to which this form object attribute is mapped. Dot notation is used in order to map to nested model, ex. `"car.engine.power"`. Colon is used in front of the name if the model is hash, ex. `"car.:engine.power"` - means call to `#car` returns `Hash` so the model attribute should be accessed like `car[:engine].power`. `false` value means that attribute is not mapped. If attribute describes nested form object and has `model_attribute: false` the attributes of nested form will be called on the parent (upper level) model. If attribute describes array of form objects and has `model_attribute: false` the methods to access array elements (`:[]` etc.) will be called on the parent (upper level) model. |
1568
+ | model_attribute | - | `<attribute_name>` | `FormObj::ModelMapper` | The name of the model attribute to which this form object attribute is mapped. Dot notation is used in order to map to nested model, ex. `"car.engine.power"`. Colon is used in front of the name if the model is hash, ex. `"car.:engine.power"` - means call to `#car` returns `Hash` so the model attribute should be accessed like `car[:engine].power`. `false` value means that attribute is not mapped. |
934
1569
  | model_class | block* or `:class`** or dot notation for `:model_attribute`*** | `<attribute_name>.classify` | `FormObj::ModelMapper` | The class (or the name of the class) of the mapped model. |
935
- | primary_key | no block* and no `:class`** | `false` | `FormObj::Form` | This attribute is the primary key of the form object. The mapped model attribute is considered to be a primary key for the corresponding model. |
936
- | primary_key | block* or `:class`** | - | `FormObj::Form` | This attribute is either nested form object or array of form objects. The value of this parameter is the name of the primary key attribute of this form object. |
1570
+ | model_nesting | block* or `:class`** | `true` | `FornObj::ModelMapper` | If attribute describes nested form object and has `model_nesting: false` the attributes of nested form will be called on the parent (upper level) model. If attribute describes array of form objects and has `model_nesting: false` the methods to access array elements (`:[]` etc.) will be called on the parent (upper level) model. |
1571
+ | primary_key | no block* and no `:class`** | `false` | `FormObj::Struct` | This attribute is the primary key of the form object. The mapped model attribute is considered to be a primary key for the corresponding model. |
1572
+ | primary_key | block* or `:class`** | - | `FormObj::Struct` | This attribute is either nested form object or array of form objects. The value of this parameter is the name of the primary key attribute of this form object. |
937
1573
  \* block - means that there is block definition for the attribute
938
1574
 
939
1575
  \** `:class` - means that this attribute has `:class` parameter specified