form_obj 0.1.0 → 0.2.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 +4 -4
- data/Gemfile.lock +7 -3
- data/README.md +870 -5
- data/form_obj.gemspec +3 -2
- data/lib/form_obj/array.rb +9 -69
- data/lib/form_obj/attribute.rb +13 -19
- data/lib/form_obj/attributes.rb +13 -0
- data/lib/form_obj/form.rb +92 -0
- data/lib/form_obj/mappable/array.rb +66 -0
- data/lib/form_obj/mappable/attribute.rb +35 -0
- data/lib/form_obj/mappable/model_attribute/item.rb +77 -0
- data/lib/form_obj/mappable/model_attribute.rb +93 -0
- data/lib/form_obj/mappable/model_primary_key.rb +17 -0
- data/lib/form_obj/mappable.rb +124 -0
- data/lib/form_obj/version.rb +2 -2
- data/lib/form_obj.rb +5 -431
- metadata +27 -5
data/README.md
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
# FormObj
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
TODO: Delete this and the text above, and describe your gem
|
3
|
+
Form Object allows to describe complicated data structure (nesting, arrays) and use it with Rails-cmpatible form builders.
|
4
|
+
Form Object can serialize and deserialize itself to/from model and hash.
|
6
5
|
|
7
6
|
## Installation
|
8
7
|
|
@@ -22,7 +21,873 @@ Or install it yourself as:
|
|
22
21
|
|
23
22
|
## Usage
|
24
23
|
|
25
|
-
|
24
|
+
**WARNING!!!** The gem is still under development. Expecting braking changes.<br/>
|
25
|
+
|
26
|
+
Form Object `FormObj::Form` is inherited from `TreeStruct` (https://github.com/akoltun/tree_struct).
|
27
|
+
So on top of all `TreeStruct` functionality `FormObj::Obj` adds `update_attributes` method for mass update of attributes
|
28
|
+
(similar to ActuveRecord) and syntax sugar to easily use ActiveModel::Validations and ActiveModel::Errors with `TreeStruct`.
|
29
|
+
|
30
|
+
`Mappable` module included in `FormObj::Form` oblect allows to map form object to a model,
|
31
|
+
load attributes from and attributes to it, represent form object as model hash (similar to `to_hash` method but
|
32
|
+
includes only attributes mapped to the model and with model attributes names) and copy errors from the model(s)
|
33
|
+
into a from object.
|
34
|
+
|
35
|
+
### Table of Contents
|
36
|
+
|
37
|
+
1. [Definition](#1-definition)
|
38
|
+
1. [Nested Form Objects](11-nested-form-objects)
|
39
|
+
2. [Array of Form Objects](12-array-of-form-objects)
|
40
|
+
2. [Update Attributes](2-update-attributes)
|
41
|
+
1. [Nested Form Objects](21-nested-form-objects)
|
42
|
+
2. [Array of Form Objects](22-array-of-form-objects)
|
43
|
+
3. [Serialize to Hash](3-serialize-to-hash)
|
44
|
+
1. [Nested Form Objects](31-nested-form-objects)
|
45
|
+
2. [Array of Form Objects](32-array-of-form-objects)
|
46
|
+
4. [Map Form Object to Models](4-map-form-objects-to-models)
|
47
|
+
1. [Multiple Models Example](41-multiple-models-example)
|
48
|
+
2. [Skip Attribute Mapping](42-skip-attribute-mapping)
|
49
|
+
1. [Map Nested Form Object Attribute to Parent Level Model Attribute](421-map-nested-form-object-attribute-to-parent-level-model-attribute)
|
50
|
+
3. [Map Nested Form Object to A Hash Model](43-map-nested-form-object-to-a-hash-model)
|
51
|
+
5. [Load Form Object from Models](5-load-form-object-from-models)
|
52
|
+
6. [Save Form Object to Models](6-save-form-object-to-models)
|
53
|
+
1. [Array of Form Objects and Models](61-array-of-form-objects-and-models)
|
54
|
+
7. [Serialize Form Object to Model Hash](7-serialize-form-object-to-model-hash)
|
55
|
+
8. [Validation and Coercion](8-validation-and-coercion)
|
56
|
+
9. [Copy Model Validation Errors into Form Object](9-copy-model-validation-errors-into-form-object)
|
57
|
+
10. [Rails Example](10-rails-example)
|
58
|
+
11. [Reference Guide](11-reference-guide-attribute-parameters)
|
59
|
+
|
60
|
+
### 1. Definition
|
61
|
+
|
62
|
+
Inherit your class from `FormObj::Form` and define its attributes.
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
class SimpleForm < FormObj::Form
|
66
|
+
attribute :name
|
67
|
+
attribute :year
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
Use it in form builder.
|
72
|
+
|
73
|
+
```erb
|
74
|
+
<%= form_for(@simple_form) do |f| %>
|
75
|
+
<%= f.label :name %>
|
76
|
+
<%= f.text_field :name %>
|
77
|
+
|
78
|
+
<%= f.label :year %>
|
79
|
+
<%= f.text_field :year %>
|
80
|
+
<% end %>
|
81
|
+
```
|
82
|
+
|
83
|
+
#### 1.1. Nested Form Objects
|
84
|
+
|
85
|
+
Use blocks to define nested forms.
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
class NestedForm < FormObj::Form
|
89
|
+
attribute :name
|
90
|
+
attribute :year
|
91
|
+
attribute :car do
|
92
|
+
attribute :model
|
93
|
+
attribute :driver
|
94
|
+
attribute :engine do
|
95
|
+
attribute :power
|
96
|
+
attribute :volume
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
```
|
101
|
+
|
102
|
+
Or explicitly define nested form class.
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
class EngineForm < FormObj::Form
|
106
|
+
attribute :power
|
107
|
+
attribute :volume
|
108
|
+
end
|
109
|
+
class CarForm < FormObj::Form
|
110
|
+
attribute :model
|
111
|
+
attribute :driver
|
112
|
+
attribute :engine, class: EngineForm
|
113
|
+
end
|
114
|
+
class NestedForm < FormObj::Form
|
115
|
+
attribute :name
|
116
|
+
attribute :year
|
117
|
+
attribute :car, class: CarForm
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
Use nested forms in form builder.
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
<%= form_for(@nested_form) do |f| %>
|
125
|
+
<%= f.label :name %>
|
126
|
+
<%= f.text_field :name %>
|
127
|
+
|
128
|
+
<%= f.label :year %>
|
129
|
+
<%= f.text_field :year %>
|
130
|
+
|
131
|
+
<%= f.fields_for(:car) do |fc| %>
|
132
|
+
<%= fc.label :model %>
|
133
|
+
<%= fc.text_field :model %>
|
134
|
+
|
135
|
+
<%= fc.label :driver %>
|
136
|
+
<%= fc.text_field :driver %>
|
137
|
+
|
138
|
+
<%= fc.field_for(:engine) do |fce| %>
|
139
|
+
<%= fce.label :power %>
|
140
|
+
<%= fce.text_field :power %>
|
141
|
+
|
142
|
+
<%= fce.label :volume %>
|
143
|
+
<%= fce.text_field :volume %>
|
144
|
+
<% end %>
|
145
|
+
<% end %>
|
146
|
+
<% end %>
|
147
|
+
```
|
148
|
+
|
149
|
+
#### 1.2. Array of Form Objects
|
150
|
+
|
151
|
+
Specify attribute parameter `array: true` in order to define an array of form objects
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
class ArrayForm < FormObj::Form
|
155
|
+
attribute :name
|
156
|
+
attribute :year
|
157
|
+
attribute :cars, array: true do
|
158
|
+
attribute :model
|
159
|
+
attribute :driver
|
160
|
+
attribute :engine do
|
161
|
+
attribute :power
|
162
|
+
attribute :volume
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
```
|
167
|
+
|
168
|
+
or
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
class EngineForm < FormObj::Form
|
172
|
+
attribute :power
|
173
|
+
attribute :volume
|
174
|
+
end
|
175
|
+
class CarForm < FormObj::Form
|
176
|
+
attribute :model
|
177
|
+
attribute :driver
|
178
|
+
attribute :engine, class: EngineForm
|
179
|
+
end
|
180
|
+
class ArrayForm < FormObj::Form
|
181
|
+
attribute :name
|
182
|
+
attribute :year
|
183
|
+
attribute :cars, array: true, class: CarForm
|
184
|
+
end
|
185
|
+
```
|
186
|
+
|
187
|
+
Add new elements in the array by using method :create on which adds a new it.
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
array_form = ArrayForm.new
|
191
|
+
array_form.size # => 0
|
192
|
+
array_form.cars.create
|
193
|
+
array_form.size # => 1
|
194
|
+
```
|
195
|
+
|
196
|
+
### 2. Update Attributes
|
197
|
+
|
198
|
+
Update form object attributes with the parameter hash received from the browser.
|
199
|
+
Method `update_attributes(new_attrs_hash)` returns self so one can chain calls.
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
simple_form = SimpleForm.new
|
203
|
+
simple_form.name = 'Ferrari'
|
204
|
+
simple_form.year = 1950
|
205
|
+
simple_form.update_attributes(
|
206
|
+
name: 'McLaren',
|
207
|
+
year: 1966
|
208
|
+
)
|
209
|
+
simple_form.name # => "McLaren"
|
210
|
+
simple_form.year # => 1966
|
211
|
+
```
|
212
|
+
|
213
|
+
#### 2.1. Nested Form Objects
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
nested_form = NestedForm.new
|
217
|
+
nested_form.name = 'Ferrari'
|
218
|
+
nested_form.year = 1950
|
219
|
+
nested_form.car.model = '340 F1'
|
220
|
+
nested_form.car.driver = 'Ascari'
|
221
|
+
nested_form.car.engine.power = 335
|
222
|
+
nested_form.car.engine.volume = 4.1
|
223
|
+
nested_form.update_attributes(
|
224
|
+
name: 'McLaren',
|
225
|
+
year: 1966,
|
226
|
+
car: {
|
227
|
+
model: 'M2B',
|
228
|
+
driver: 'Bruce McLaren',
|
229
|
+
engine: {
|
230
|
+
power: 300,
|
231
|
+
volume: 3.0
|
232
|
+
}
|
233
|
+
}
|
234
|
+
)
|
235
|
+
nested_form.name # => "McLaren"
|
236
|
+
nested_form.year # => 1966
|
237
|
+
nested_form.car.model # => "M2B"
|
238
|
+
nested_form.car.driver # => "Bruce McLaren"
|
239
|
+
nested_form.car.engine.power # => 300
|
240
|
+
nested_form.car.engine.volume # => 3.0
|
241
|
+
```
|
242
|
+
|
243
|
+
#### 2.2. Array of Form Objects
|
244
|
+
|
245
|
+
Updating array of form objects will compare the existing array and the new one.
|
246
|
+
New array elements will be added, existing array elements will be updated, absent array elements will be deleted
|
247
|
+
(deleting behaviour is the subject of changes in future releases - only elements with flag `_destroy == true` will be deleted).
|
248
|
+
|
249
|
+
In order to compare old and new array its elements have to be identified via the primary key.
|
250
|
+
Primary key can be specified either on the attribute level or on the form level.
|
251
|
+
If it is not specified the :id field is supposed to be a primary key.
|
252
|
+
|
253
|
+
```ruby
|
254
|
+
class ArrayForm < FormObj::Form
|
255
|
+
attribute :name
|
256
|
+
attribute :year
|
257
|
+
attribute :cars, array: true do
|
258
|
+
attribute :model, primary_key: true # <- primary key is specified on attribute level
|
259
|
+
attribute :driver
|
260
|
+
attribute :engine do
|
261
|
+
attribute :power
|
262
|
+
attribute :volume
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
```
|
267
|
+
|
268
|
+
```ruby
|
269
|
+
class ArrayForm < FormObj::Form
|
270
|
+
attribute :name
|
271
|
+
attribute :year
|
272
|
+
attribute :cars, array: true, primary_key: :model do # <- primary key is specified on form level
|
273
|
+
attribute :model
|
274
|
+
attribute :driver
|
275
|
+
attribute :engine do
|
276
|
+
attribute :power
|
277
|
+
attribute :volume
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
```
|
282
|
+
|
283
|
+
```ruby
|
284
|
+
array_form = ArrayForm.new
|
285
|
+
array_form.name = 'Ferrari'
|
286
|
+
array_form.year = 1950
|
287
|
+
|
288
|
+
car1 = array_form.cars.create
|
289
|
+
car1.model = '340 F1'
|
290
|
+
car1.driver = 'Ascari'
|
291
|
+
car1.engine.power = 335
|
292
|
+
car1.engine.volume = 4.1
|
293
|
+
|
294
|
+
car2 = array_form.cars.create
|
295
|
+
car2.model = 'M2B'
|
296
|
+
car2.driver = 'Villoresi'
|
297
|
+
car2.engine.power = 300
|
298
|
+
car2.engine.volume = 3.3
|
299
|
+
|
300
|
+
array_form.update_attributes(
|
301
|
+
name: 'McLaren',
|
302
|
+
year: 1966,
|
303
|
+
cars: [
|
304
|
+
{
|
305
|
+
model: 'M2B',
|
306
|
+
driver: 'Bruce McLaren',
|
307
|
+
engine: {
|
308
|
+
volume: 3.0
|
309
|
+
}
|
310
|
+
}, {
|
311
|
+
model: 'M7A',
|
312
|
+
driver: 'Denis Hulme',
|
313
|
+
engine: {
|
314
|
+
power: 415,
|
315
|
+
}
|
316
|
+
}
|
317
|
+
],
|
318
|
+
)
|
319
|
+
|
320
|
+
array_form.name # => "McLaren"
|
321
|
+
array_form.year # => 1966
|
322
|
+
|
323
|
+
array_form.cars[0].model # => "M2B"
|
324
|
+
array_form.cars[0].driver # => "Bruce McLaren"
|
325
|
+
array_form.cars[0].engine.power # => 300 - this value was not updated in update_attributes
|
326
|
+
array_form.cars[0].engine.volume # => 3.0
|
327
|
+
|
328
|
+
array_form.cars[1].model # => "M7A"
|
329
|
+
array_form.cars[1].driver # => "Denis Hulme"
|
330
|
+
array_form.cars[1].engine.power # => 415
|
331
|
+
array_form.cars[1].engine.volume # => nil - this value is nil because this car was created in updated_attributes
|
332
|
+
```
|
333
|
+
|
334
|
+
### 3. Serialize to Hash
|
335
|
+
|
336
|
+
Call `to_hash()` method in order to get hash representation of the form object
|
337
|
+
|
338
|
+
```ruby
|
339
|
+
simple_form.to_hash # => {
|
340
|
+
# => :name => "McLaren",
|
341
|
+
# => :year => 1966
|
342
|
+
# => }
|
343
|
+
```
|
344
|
+
|
345
|
+
#### 3.1. Nested Form Objects
|
346
|
+
|
347
|
+
```ruby
|
348
|
+
nested_form.to_hash # => {
|
349
|
+
# => :name => "McLaren",
|
350
|
+
# => :year => 1966,
|
351
|
+
# => :car => {
|
352
|
+
# => :model => "340 F1",
|
353
|
+
# => :driver => "Ascari",
|
354
|
+
# => :engine => {
|
355
|
+
# => :power => 335,
|
356
|
+
# => :volume => 4.1
|
357
|
+
# => }
|
358
|
+
# => }
|
359
|
+
# => }
|
360
|
+
```
|
361
|
+
|
362
|
+
#### 3.2. Array of Form Objects
|
363
|
+
|
364
|
+
```ruby
|
365
|
+
array_form.to_hash # => {
|
366
|
+
# => :name => "McLaren",
|
367
|
+
# => :year => 1966,
|
368
|
+
# => :cars => [{
|
369
|
+
# => :model => "M2B",
|
370
|
+
# => :driver => "Bruce McLaren",
|
371
|
+
# => :engine => {
|
372
|
+
# => :power => 300,
|
373
|
+
# => :volume => 3.0
|
374
|
+
# => }
|
375
|
+
# => }, {
|
376
|
+
# => :model => "M7A",
|
377
|
+
# => :driver => "Denis Hulme",
|
378
|
+
# => :engine => {
|
379
|
+
# => :power => 415,
|
380
|
+
# => : volume => nil
|
381
|
+
# => }
|
382
|
+
# => }]
|
383
|
+
# => }
|
384
|
+
```
|
385
|
+
|
386
|
+
### 4. Map Form Object to Models
|
387
|
+
|
388
|
+
Include `Mappable` mix-in and map form object attributes to one or few models by using `:model` and `:model_attribute` parameters.
|
389
|
+
By default each form object attribute is mapped to the model attribute with the same name of the `:default` model.
|
390
|
+
|
391
|
+
Use dot notation to map model attribute to nested model. Use colon to specify "hash" attribute.
|
392
|
+
|
393
|
+
```ruby
|
394
|
+
class SingleForm < FormObj::Form
|
395
|
+
include Mappable
|
396
|
+
|
397
|
+
attribute :name, model_attribute: :team_name
|
398
|
+
attribute :year
|
399
|
+
attribute :engine_power, model_attribute: 'car.:engine.power'
|
400
|
+
end
|
401
|
+
```
|
402
|
+
|
403
|
+
Suppose `single_form = SingleForm.new` and `model` to be an instance of a model.
|
404
|
+
|
405
|
+
| Form Object attribute | Model attribute |
|
406
|
+
| --------------------- | --------------- |
|
407
|
+
| `single_form.name` | `model.team_name` |
|
408
|
+
| `single_form.year` | `model.year` |
|
409
|
+
| `single_form.engine_power` | `model.car[:engine].power` |
|
410
|
+
|
411
|
+
#### 4.1. Multiple Models Example
|
412
|
+
|
413
|
+
```ruby
|
414
|
+
class MultiForm < FormObj::Form
|
415
|
+
include Mappable
|
416
|
+
|
417
|
+
attribute :name, model_attribute: :team_name
|
418
|
+
attribute :year
|
419
|
+
attribute :engine_power, model: :car, model_attribute: ':engine.power'
|
420
|
+
end
|
421
|
+
```
|
422
|
+
|
423
|
+
Suppose `multi_form = MultiForm.new` and `default`, `car` to be instances of two models.
|
424
|
+
|
425
|
+
| Form Object attribute | Model attribute |
|
426
|
+
| --------------------- | --------------- |
|
427
|
+
| `multi_form.name` | `default.team_name` |
|
428
|
+
| `multi_form.year` | `default.year` |
|
429
|
+
| `multi_form.engine_power` | `car[:engine].power` |
|
430
|
+
|
431
|
+
#### 4.2. Skip Attribute Mapping
|
432
|
+
|
433
|
+
Use `model_attribute: false` in order to avoid attribute mapping to the model.
|
434
|
+
|
435
|
+
```ruby
|
436
|
+
class SimpleForm < FormObj::Form
|
437
|
+
include Mappable
|
438
|
+
|
439
|
+
attribute :name, model_attribute: :team_name
|
440
|
+
attribute :year
|
441
|
+
attribute :engine_power, model_attribute: false
|
442
|
+
end
|
443
|
+
```
|
444
|
+
|
445
|
+
Suppose `form = SimpleForm.new` and `model` to be an instance of a model.
|
446
|
+
|
447
|
+
| Form Object attribute | Model attribute |
|
448
|
+
| --------------------- | --------------- |
|
449
|
+
| `form.name` | `model.team_name` |
|
450
|
+
| `form.year` | `model.year` |
|
451
|
+
| `form.engine_power` | - |
|
452
|
+
|
453
|
+
##### 4.2.1. Map Nested Form Object Attribute to Parent Level Model Attribute
|
454
|
+
|
455
|
+
Use `model_attribute: false` for nested form object in order to map its attributes to the parent level of the model.
|
456
|
+
|
457
|
+
```ruby
|
458
|
+
class NestedForm < FormObj::Form
|
459
|
+
include Mappable
|
460
|
+
|
461
|
+
attribute :name, model_attribute: :team_name
|
462
|
+
attribute :year
|
463
|
+
attribute :car, model_attribute: false do # nesting only in form object but not in a model
|
464
|
+
attribute :model
|
465
|
+
attribute :driver
|
466
|
+
attribute :engine do
|
467
|
+
attribute :power
|
468
|
+
attribute :volume
|
469
|
+
end
|
470
|
+
end
|
471
|
+
end
|
472
|
+
```
|
473
|
+
|
474
|
+
Suppose `form = NestedForm.new` and `model` to be an instance of a model.
|
475
|
+
|
476
|
+
| Form Object attribute | Model attribute |
|
477
|
+
| --------------------- | --------------- |
|
478
|
+
| `form.name` | `model.team_name` |
|
479
|
+
| `form.year` | `model.year` |
|
480
|
+
| `form.car.model` | `model.model` |
|
481
|
+
| `form.car.driver` | `model.driver` |
|
482
|
+
| `form.car.engine.power` | `model.engine.power` |
|
483
|
+
| `form.car.engine.volume` | `model.engine.volume` |
|
484
|
+
|
485
|
+
#### 4.3. Map Nested Form Object to A Hash Model
|
486
|
+
|
487
|
+
Use `hash: true` in order to map a nested form object to a hash as a model.
|
488
|
+
|
489
|
+
```ruby
|
490
|
+
class NestedForm < FormObj::Form
|
491
|
+
include Mappable
|
492
|
+
|
493
|
+
attribute :name, model_attribute: :team_name
|
494
|
+
attribute :year
|
495
|
+
attribute :car, hash: true do # nesting only in form object but not in a model
|
496
|
+
attribute :model
|
497
|
+
attribute :driver
|
498
|
+
attribute :engine do
|
499
|
+
attribute :power
|
500
|
+
attribute :volume
|
501
|
+
end
|
502
|
+
end
|
503
|
+
end
|
504
|
+
```
|
505
|
+
|
506
|
+
Suppose `form = NestedForm.new` and `model` to be an instance of a model.
|
507
|
+
|
508
|
+
| Form Object attribute | Model attribute |
|
509
|
+
| --------------------- | --------------- |
|
510
|
+
| `form.name` | `model.team_name` |
|
511
|
+
| `form.year` | `model.year` |
|
512
|
+
| `form.car.model` | `model.car[:model]` |
|
513
|
+
| `form.car.driver` | `model.car[:driver]` |
|
514
|
+
| `form.car.engine.power` | `model.car[:engine].power` |
|
515
|
+
| `form.car.engine.volume` | `model.car[:engine].volume` |
|
516
|
+
|
517
|
+
### 5. Load Form Object from Models
|
518
|
+
|
519
|
+
Use `load_from_models(models)` to load form object attributes from mapped models.
|
520
|
+
Method returns self so one can chain calls.
|
521
|
+
|
522
|
+
```ruby
|
523
|
+
class MultiForm < FormObj::Form
|
524
|
+
include Mappable
|
525
|
+
|
526
|
+
attribute :name, model_attribute: :team_name
|
527
|
+
attribute :year
|
528
|
+
attribute :engine_power, model: :car, model_attribute: ':engine.power'
|
529
|
+
end
|
530
|
+
|
531
|
+
default_model = Struct.new(:team_name, :year).new('Ferrari', 1950)
|
532
|
+
car_model = { engine: Struct.new(:power).new(335) }
|
533
|
+
|
534
|
+
multi_form = MultiForm.new.load_from_models(default: default_model, car: car_model)
|
535
|
+
multi_form.to_hash # => {
|
536
|
+
# => :name => "Ferrari"
|
537
|
+
# => :year => 1950
|
538
|
+
# => :engine_power => 335
|
539
|
+
# => }
|
540
|
+
```
|
541
|
+
|
542
|
+
Use `load_from_models(default: model)` or `load_from_model(model)` to load from single model.
|
543
|
+
|
544
|
+
### 6. Save Form Object to Models
|
545
|
+
|
546
|
+
Use `save_to_models(models)` to save form object attributes to mapped models.
|
547
|
+
Method returns self so one can chain calls.
|
548
|
+
|
549
|
+
```ruby
|
550
|
+
class MultiForm < FormObj::Form
|
551
|
+
include Mappable
|
552
|
+
|
553
|
+
attribute :name, model_attribute: :team_name
|
554
|
+
attribute :year
|
555
|
+
attribute :engine_power, model: :car, model_attribute: ':engine.power'
|
556
|
+
end
|
557
|
+
|
558
|
+
default_model = Struct.new(:team_name, :year).new('Ferrari', 1950)
|
559
|
+
car_model = { engine: Struct.new(:power).new(335) }
|
560
|
+
|
561
|
+
multi_form = MultiForm.new
|
562
|
+
multi_form.update_attributes(name: 'McLaren', year: 1966, engine_power: 415)
|
563
|
+
multi_form.save_to_models(default: default_model, car: car_model)
|
564
|
+
|
565
|
+
default_model.name # => "McLaren"
|
566
|
+
default_model.year # => 1966
|
567
|
+
car_model[:engine].power # => 415
|
568
|
+
```
|
569
|
+
|
570
|
+
Use `save_to_models(default: model)` or `save_to_model(model)` to save to single model.
|
571
|
+
|
572
|
+
Neither `save_to_models` nor `save_to_model` calls `save` method on the model(s).
|
573
|
+
Also they don't call `valid?` method on the model(s).
|
574
|
+
Instead they just assign form object attributes values to mapped model attributes
|
575
|
+
using `<attribute_name>=` accessors on the model(s).
|
576
|
+
|
577
|
+
It is completely up to developer to do any additional validations on the model(s) and save it(them).
|
578
|
+
|
579
|
+
#### 6.1. Array of Form Objects and Models
|
580
|
+
|
581
|
+
Saving array of form objects to corresponding array of models requires the class of the model to be known by the form object
|
582
|
+
because it could create new instances of the model array elements.
|
583
|
+
Use `:model_class` parameter to specify it.
|
584
|
+
Form object will try to guess the name of the class from the name of the attribute if this parameter is absent.
|
585
|
+
|
586
|
+
```ruby
|
587
|
+
class ArrayForm < FormObj::Form
|
588
|
+
include Mappable
|
589
|
+
|
590
|
+
attribute :name
|
591
|
+
attribute :year
|
592
|
+
attribute :cars, array: true, model_class: Car do
|
593
|
+
attribute :model, primary_key: true # <- primary key is specified on attribute level
|
594
|
+
attribute :driver
|
595
|
+
end
|
596
|
+
end
|
597
|
+
```
|
598
|
+
|
599
|
+
If corresponding `:model_attribute` parameter uses dot notations to reference
|
600
|
+
nested models the value of `:model_class` parameter should be an array of corresponding model classes.
|
601
|
+
|
602
|
+
```ruby
|
603
|
+
class ArrayForm < FormObj::Form
|
604
|
+
include Mappable
|
605
|
+
|
606
|
+
attribute :name
|
607
|
+
attribute :year
|
608
|
+
attribute :cars, array: true, model_attribute: 'equipment.cars', model_class: [Equipment, Car] do
|
609
|
+
attribute :model, primary_key: true # <- primary key is specified on attribute level
|
610
|
+
attribute :driver
|
611
|
+
end
|
612
|
+
end
|
613
|
+
```
|
614
|
+
|
615
|
+
### 7. Serialize Form Object to Model Hash
|
616
|
+
|
617
|
+
Use `to_model_hash(model = :default)` to get hash representation of the model that mapped to the form object.
|
618
|
+
|
619
|
+
```ruby
|
620
|
+
class MultiForm < FormObj::Form
|
621
|
+
include Mappable
|
622
|
+
|
623
|
+
attribute :name, model_attribute: :team_name
|
624
|
+
attribute :year
|
625
|
+
attribute :engine_power, model: :car, model_attribute: ':engine.power'
|
626
|
+
end
|
627
|
+
|
628
|
+
multi_form = MultiForm.new
|
629
|
+
multi_form.update_attributes(name: 'McLaren', year: 1966, engine_power: 415)
|
630
|
+
|
631
|
+
multi_form.to_model_hash # => { :team_name => "McLaren", :year => 1966 }
|
632
|
+
multi_form.to_model_hash(:default) # => { :team_name => "McLaren", :year => 1966 }
|
633
|
+
multi_form.to_model_hash(:car) # => { :engine => { :power => 415 } }
|
634
|
+
```
|
635
|
+
|
636
|
+
Use `to_models_hash()` to get hash representation of all models that mapped to the form object.
|
637
|
+
|
638
|
+
```ruby
|
639
|
+
multi_form.to_models_hash # => {
|
640
|
+
# => default: { :team_name => "McLaren", :year => 1966 }
|
641
|
+
# => car: { :engine => { :power => 415 } }
|
642
|
+
# => }
|
643
|
+
```
|
644
|
+
|
645
|
+
If array of form objects mapped to the parent model (`model_attribute: false`) it is serialized to `:self` key.
|
646
|
+
|
647
|
+
```ruby
|
648
|
+
class ArrayForm < FormObj::Form
|
649
|
+
include Mappable
|
650
|
+
|
651
|
+
attribute :name
|
652
|
+
attribute :year
|
653
|
+
attribute :cars, array: true, model_attribute: false do
|
654
|
+
attribute :model, primary_key: true
|
655
|
+
attribute :driver
|
656
|
+
end
|
657
|
+
end
|
658
|
+
|
659
|
+
array_form = ArrayForm.new
|
660
|
+
array_form.update_attributes(
|
661
|
+
name: 'McLaren',
|
662
|
+
year: 1966,
|
663
|
+
cars: [{
|
664
|
+
model: 'M2B',
|
665
|
+
driver: 'Bruce McLaren'
|
666
|
+
}, {
|
667
|
+
model: 'M7A',
|
668
|
+
driver: 'Denis Hulme'
|
669
|
+
}]
|
670
|
+
)
|
671
|
+
|
672
|
+
array_form.to_model_hash # => {
|
673
|
+
# => :team_name => "McLaren",
|
674
|
+
# => :year => 1966,
|
675
|
+
# => :self => {
|
676
|
+
# => :model => "M2B",
|
677
|
+
# => :driver => "Bruce McLaren"
|
678
|
+
# => }, {
|
679
|
+
# => :model => "M7A",
|
680
|
+
# => :driver => "Denis Hulme"
|
681
|
+
# => }
|
682
|
+
# => }
|
683
|
+
```
|
684
|
+
|
685
|
+
### 8. Validation and Coercion
|
686
|
+
|
687
|
+
Form Object is just a Ruby class. By default it includes (could be changed in future releases):
|
688
|
+
|
689
|
+
```ruby
|
690
|
+
extend ::ActiveModel::Naming
|
691
|
+
extend ::ActiveModel::Translation
|
692
|
+
|
693
|
+
include ::ActiveModel::Conversion
|
694
|
+
include ::ActiveModel::Validations
|
695
|
+
```
|
696
|
+
|
697
|
+
So add ActiveModel validations directly to Form Object class definition.
|
698
|
+
|
699
|
+
```ruby
|
700
|
+
class MultiForm < FormObj::Form
|
701
|
+
include Mappable
|
702
|
+
|
703
|
+
attribute :name, model_attribute: :team_name
|
704
|
+
attribute :year
|
705
|
+
attribute :engine_power, model: :car, model_attribute: ':engine.power'
|
706
|
+
|
707
|
+
validates :name, :year, presence: true
|
708
|
+
end
|
709
|
+
```
|
710
|
+
|
711
|
+
There is no coercion during assigning/updating form object attributes.
|
712
|
+
Coercion can be done manually by redefining assigning methods `<attribute_name>=`
|
713
|
+
or it will happen in the model when the form object will be saved to it.
|
714
|
+
This is the standard way how coercion happens in Rails for example.
|
715
|
+
|
716
|
+
### 9. Copy Model Validation Errors into Form Object
|
717
|
+
|
718
|
+
Even though validation could and should happen in the form object it is possible to have (additional) validation(s) in the model(s).
|
719
|
+
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.
|
720
|
+
|
721
|
+
Use `copy_errors_from_models(models)` or `copy_errors_from_model(model)` in order to do it.
|
722
|
+
Methods return self so one can chain calls.
|
723
|
+
|
724
|
+
```ruby
|
725
|
+
multi_form.copy_errors_from_models(default: default_model, car: car_model)
|
726
|
+
```
|
727
|
+
|
728
|
+
In case of single model:
|
729
|
+
```ruby
|
730
|
+
single_form.copy_errors_from_model(model)
|
731
|
+
```
|
732
|
+
|
733
|
+
### 10. Rails Example
|
734
|
+
|
735
|
+
```ruby
|
736
|
+
# db/migrate/yyyymmddhhmiss_create_team.rb
|
737
|
+
class CreateTeam < ActiveRecord::Migration
|
738
|
+
def change
|
739
|
+
create_table :teams do |t|
|
740
|
+
t.string :team_name
|
741
|
+
t.integer :year
|
742
|
+
end
|
743
|
+
end
|
744
|
+
end
|
745
|
+
```
|
746
|
+
```ruby
|
747
|
+
# app/models/team.rb
|
748
|
+
class Team < ApplicationRecord
|
749
|
+
has_many :cars, autosave: true
|
750
|
+
|
751
|
+
validates :year, numericality: { greater_than_or_equal_to: 1950 }
|
752
|
+
end
|
753
|
+
```
|
754
|
+
```ruby
|
755
|
+
# db/migrate/yyyymmddhhmiss_create_car.rb
|
756
|
+
class CreateCar < ActiveRecord::Migration
|
757
|
+
def change
|
758
|
+
create_table :cars do |t|
|
759
|
+
t.references :team
|
760
|
+
t.string :model
|
761
|
+
t.text :engine
|
762
|
+
end
|
763
|
+
end
|
764
|
+
end
|
765
|
+
```
|
766
|
+
```ruby
|
767
|
+
# app/models/car.rb
|
768
|
+
class Car < ApplicationRecord
|
769
|
+
belongs_to :team
|
770
|
+
|
771
|
+
serialize :engine, Hash
|
772
|
+
end
|
773
|
+
```
|
774
|
+
```ruby
|
775
|
+
# app/form_objects/team_form.rb
|
776
|
+
class TeamForm < FormObj::Form
|
777
|
+
include Mappable
|
778
|
+
|
779
|
+
attribute :id
|
780
|
+
attribute :name, model_attribute: :team_name
|
781
|
+
attribute :year
|
782
|
+
attribute :cars, array: true do
|
783
|
+
attribute :id
|
784
|
+
attribute :model
|
785
|
+
attribute :engine_power, model_attribute: 'engine.:power'
|
786
|
+
|
787
|
+
validates :model, presence: true
|
788
|
+
end
|
789
|
+
|
790
|
+
validates :name, :year, presence: true
|
791
|
+
end
|
792
|
+
```
|
793
|
+
```ruby
|
794
|
+
# app/controllers/teams_controller.rb
|
795
|
+
class TeamsController < ApplicationController
|
796
|
+
def show
|
797
|
+
@team = TeamForm.new.load_from_model(Team.find(params[:id]))
|
798
|
+
end
|
799
|
+
|
800
|
+
def new
|
801
|
+
@team = TeamForm.new
|
802
|
+
end
|
803
|
+
|
804
|
+
def edit
|
805
|
+
@team = TeamForm.new.load_from_model(Team.find(params[:id]))
|
806
|
+
end
|
807
|
+
|
808
|
+
def create
|
809
|
+
@team = TeamForm.new.update_attributes(params[:team])
|
810
|
+
|
811
|
+
if @team.valid?
|
812
|
+
@team.save_to_model(model = Team.new)
|
813
|
+
if model.save
|
814
|
+
return redirect_to team_path(model), notice: 'Team has been created'
|
815
|
+
else
|
816
|
+
@team.copy_errors_from_model(model)
|
817
|
+
end
|
818
|
+
end
|
819
|
+
|
820
|
+
render :new
|
821
|
+
end
|
822
|
+
|
823
|
+
def update
|
824
|
+
@team = TeamForm.new.load_from_model(model = Team.find(params[:id]))
|
825
|
+
@team.update_attributes(params[:team])
|
826
|
+
|
827
|
+
if @team.valid?
|
828
|
+
@team.save_to_model(model)
|
829
|
+
if model.save
|
830
|
+
return redirect_to team_path(model), notice: 'Team has been updated'
|
831
|
+
else
|
832
|
+
@team.copy_errors_from_model(model)
|
833
|
+
end
|
834
|
+
end
|
835
|
+
|
836
|
+
render :edit
|
837
|
+
end
|
838
|
+
end
|
839
|
+
```
|
840
|
+
```erb
|
841
|
+
# app/views/teams/show.erb.erb
|
842
|
+
<p>Name: <%= @team.name %></p>
|
843
|
+
<p>Year: <%= @team.year %></p>
|
844
|
+
<p>Cars:</p>
|
845
|
+
<ul>
|
846
|
+
<% @team.cars.each do |car| %>
|
847
|
+
<li><%= car.model %> (<%= car.engine[:power] %> hp)</li>
|
848
|
+
<% end %>
|
849
|
+
</ul>
|
850
|
+
```
|
851
|
+
```erb
|
852
|
+
# app/views/teams/new.erb.erb
|
853
|
+
<%= nested_form_for @team do |f| %>
|
854
|
+
<%= f.text_field :name %>
|
855
|
+
<%= f.text_field :year %>
|
856
|
+
|
857
|
+
<%= f.link_to_add 'Add a Car', :cars %>
|
858
|
+
<% end %>
|
859
|
+
```
|
860
|
+
```erb
|
861
|
+
# app/views/teams/edit.erb.erb
|
862
|
+
<%= nested_form_for @team do |f| %>
|
863
|
+
<%= f.text_field :name %>
|
864
|
+
<%= f.text_field :year %>
|
865
|
+
|
866
|
+
<%= f.fields_for :cars do |cf| %>
|
867
|
+
<%= cf.text_field :model %>
|
868
|
+
<%= cf.link_to_remove 'Remove the Car' %>
|
869
|
+
<% end %>
|
870
|
+
<%= f.link_to_add 'Add a Car', :cars %>
|
871
|
+
<% end %>
|
872
|
+
```
|
873
|
+
|
874
|
+
### 11. Reference Guide: `attribute` parameters
|
875
|
+
|
876
|
+
| Parameter | Condition | Default value | Defined in | Description |
|
877
|
+
| --- |:---:|:---:|:---:| --- |
|
878
|
+
| 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 |
|
879
|
+
| 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. |
|
880
|
+
| hash | block* or `:class`** | `false` | `FormObj::Mappable` | 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>` |
|
881
|
+
| model | - | `:default` | `FormObj::Mappable` | The name of the model to which this attribute is mapped |
|
882
|
+
| model_attribute | - | `<attribute_name>` | `FormObj::Mappable` | 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. |
|
883
|
+
| model_class | block* or `:class`** or dot notation for `:model_attribute`*** | `<attribute_name>.classify` | `FormObj::Mappable` | The class (or the name of the class) of the mapped model. |
|
884
|
+
| 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. |
|
885
|
+
| 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. |
|
886
|
+
\* block - means that there is block definition for the attribute
|
887
|
+
|
888
|
+
\** `:class` - means that this attribute has `:class` parameter specified
|
889
|
+
|
890
|
+
\*** dot notation for `:model_attribute` - means that this attribute is mapped to nested model attribute (using dot notation)
|
26
891
|
|
27
892
|
## Development
|
28
893
|
|
@@ -32,7 +897,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
32
897
|
|
33
898
|
## Contributing
|
34
899
|
|
35
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
900
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/akoltun/form_obj.
|
36
901
|
|
37
902
|
## License
|
38
903
|
|