on_form 1.0.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.md +10 -0
- data/README.md +133 -93
- data/lib/on_form/attributes.rb +7 -7
- data/lib/on_form/errors.rb +11 -5
- data/lib/on_form/form.rb +15 -17
- data/lib/on_form/multiparameter_attributes.rb +1 -1
- data/lib/on_form/saving.rb +3 -3
- data/lib/on_form/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3d7dd5ed97fb50b463cd137f4e37cd54a6b5039f
|
4
|
+
data.tar.gz: cf2be44d209f36980cb248e89498060ae50374a8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c967c12d103f110b2932b03e06b4c8e708e1623abc970270c0dc4fdd2f33ccab5936ecccc440e0b8b4922cdac099258eb37b735bd068cef5727c7b62ea2108f1
|
7
|
+
data.tar.gz: b26ae23736314ecfa179e4c380fa4d8e91ea7a09cd18ccd6fae5c9b9c64778225698b040e7bfbe41f67027cfcb90d2176b6f6bc1e0299373c441cc7eb1857756
|
data/CHANGES.md
ADDED
data/README.md
CHANGED
@@ -38,87 +38,119 @@ This version of OnForm depends on both the `activemodel` and `activerecord` gems
|
|
38
38
|
|
39
39
|
Let's say you have a big fat legacy model called `Customer`, and you have a preferences controller:
|
40
40
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
41
|
+
```ruby
|
42
|
+
class PreferencesController
|
43
|
+
def show
|
44
|
+
@customer = Customer.find(params[:id])
|
45
|
+
end
|
46
|
+
|
47
|
+
def update
|
48
|
+
@customer = Customer.find(params[:id])
|
49
|
+
@customer.update!(params[:customer].permit(:name, :email, :phone_number)
|
50
|
+
redirect_to preferences_path(@customer)
|
51
|
+
rescue ActiveRecord::RecordInvalid
|
52
|
+
render :show
|
53
|
+
end
|
54
|
+
end
|
55
|
+
```
|
54
56
|
|
55
57
|
Let's wrap the customer object in a form object. Ideally we'd call this `@customer_form`, but you may not feel you have time to go and update all your view code, so in this example we'll keep calling it `@customer`.
|
56
58
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
59
|
+
```ruby
|
60
|
+
class PreferencesController
|
61
|
+
def show
|
62
|
+
@customer = PreferencesForm.new(Customer.find(params[:id]))
|
63
|
+
end
|
64
|
+
|
65
|
+
def update
|
66
|
+
@customer = PreferencesForm.new(Customer.find(params[:id]))
|
67
|
+
@customer.update!(params[:customer])
|
68
|
+
rescue ActiveRecord::RecordInvalid
|
69
|
+
render :show
|
70
|
+
end
|
71
|
+
end
|
72
|
+
```
|
69
73
|
|
70
74
|
Now we need to make our form object. At this point we need to tell the form object which attributes on the model we want to expose. (In this example we have just one model and a couple of attributes, but you wouldn't bother using this library if this was all you had.)
|
71
75
|
|
72
|
-
|
73
|
-
|
76
|
+
```ruby
|
77
|
+
class PreferencesForm < OnForm::Form
|
78
|
+
expose %i(name email phone_number), on: :customer
|
74
79
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
80
|
+
def initialize(customer)
|
81
|
+
@customer = customer
|
82
|
+
end
|
83
|
+
end
|
84
|
+
```
|
79
85
|
|
80
86
|
The form object responds to the usual persistance methods like `email`, `email=`, `save`, `save!`, `update`, and `update!`.
|
81
87
|
|
82
|
-
It will automatically write those exposed attributes back onto the models, and *it exposes any validation errors from those fields on the form object itself* - you don't have to copy them back manually or move your field validation code over to get started. It'll also expose any errors on base on the models whose attributes you exposed.
|
88
|
+
It will automatically write those exposed attributes back onto the models, and *it exposes any validation errors from those fields on the form object itself* - you don't have to copy them back manually or move your field validation code over to get started. It'll also expose any errors on base on the models whose attributes you exposed. See the Validations section below for more.
|
83
89
|
|
84
90
|
### A multi-model form
|
85
91
|
|
86
|
-
You aren't limited to having one primary model - if your form is
|
92
|
+
You aren't limited to having one primary model - if your form is backed by multiple models just call `expose` for each one. They'll automatically be saved in the same order you declared them.
|
87
93
|
|
88
94
|
In this example, the new models we're exposing are associated with the first one, so we don't need to pass them in to the constructor.
|
89
95
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
96
|
+
```ruby
|
97
|
+
class HouseListingForm < OnForm::Form
|
98
|
+
expose %i(street_number street_name city), on: :house
|
99
|
+
expose %i(name phone_number), on: :vendor
|
100
|
+
|
101
|
+
def initialize(house)
|
102
|
+
@house = house
|
103
|
+
@vendor = house.vendor
|
104
|
+
end
|
105
|
+
end
|
106
|
+
```
|
99
107
|
|
100
108
|
Transactions will automatically be started so that _all_ database updates will be rolled back if _any_ record fails to save (for example, due to a validation error).
|
101
109
|
|
102
|
-
Note that the
|
110
|
+
Note that the `on:` kwarg gives the name of the method on the form object which returns the record - nothing to do with class names. In this example, vendor might actually be an instance of our `Customer` model from the earlier examples.
|
103
111
|
|
104
112
|
### Model accessor methods
|
105
113
|
|
106
|
-
In the previous example, the constructor set `@house` and `@vendor` because these variables correspond to the
|
114
|
+
In the previous example, the constructor set `@house` and `@vendor` because these variables correspond to the name passed to `expose` in the `on` option. `expose` will automatically add an `attr_reader` for this name, meaning you only need to set the instance variables.
|
107
115
|
|
108
116
|
But if you prefer, you can define a method with the same name yourself, for example using delegation. `expose` won't run `attr_reader` if you've already defined the method, and there's no requirement to set an instance variable.
|
109
117
|
|
110
|
-
|
111
|
-
|
118
|
+
```ruby
|
119
|
+
class HouseListingForm < OnForm::Form
|
120
|
+
delegate :vendor, :to => :house
|
121
|
+
|
122
|
+
expose %i(street_number street_name city), on: :house
|
123
|
+
expose %i(name phone_number), on: :vendor
|
124
|
+
|
125
|
+
def initialize(house)
|
126
|
+
@house = house
|
127
|
+
end
|
128
|
+
end
|
129
|
+
```
|
130
|
+
|
131
|
+
You can also define your own method over the top of the `attr_reader`. Just remember it will be called more than once, so it must be idempotent.
|
132
|
+
|
133
|
+
### Renaming attributes
|
112
134
|
|
113
|
-
|
114
|
-
:vendor => %i(name phone_number)
|
135
|
+
By default the attribute names exposed on the form object are the same as the attributes on the backing models. Sometimes this leads to unclear meanings, and sometimes you'll have duplicate attribute names in a multi-model form.
|
115
136
|
|
116
|
-
|
117
|
-
@house = house
|
118
|
-
end
|
119
|
-
end
|
137
|
+
To address this you can use the `prefix` and/or `suffix` options to `expose`.
|
120
138
|
|
121
|
-
|
139
|
+
```ruby
|
140
|
+
class AccountHolderForm < OnForm::Form
|
141
|
+
expose %i(name date_of_birth), on: :customer, prefix: "account_holder_"
|
142
|
+
expose %i(email), on: :customer, suffix: "_for_billing"
|
143
|
+
expose %i(phone_number), on: :customer, as: "mobile_number"
|
144
|
+
|
145
|
+
def initialize(customer)
|
146
|
+
@customer = customer
|
147
|
+
end
|
148
|
+
end
|
149
|
+
```
|
150
|
+
|
151
|
+
This is especially useful if you like to use helpers like `error_messages_on` which will "humanize" the attribute names and use them in the human-readable page.
|
152
|
+
|
153
|
+
Try to use this only when it makes the attribute names more meaningful. In particular, automatically renaming all of your attributes with a prefix matching the backing model is considered a bad habit because it leads to unnecessary coupling between the views and the current backing data model schema.
|
122
154
|
|
123
155
|
### Validations
|
124
156
|
|
@@ -126,15 +158,17 @@ Validations on the underlying models not only get used, but their validation err
|
|
126
158
|
|
127
159
|
But you can also declare validations on the form object itself, which is useful when you have business rules applicable to this form that aren't intrinsic to the domain model.
|
128
160
|
|
129
|
-
|
130
|
-
|
161
|
+
```ruby
|
162
|
+
class AddEmergencyContactForm < OnForm::Form
|
163
|
+
expose %i(next_of_kin_name next_of_kin_phone_number), on: :customer
|
131
164
|
|
132
|
-
|
165
|
+
validates_presence_of :next_of_kin_name, :next_of_kin_phone_number
|
133
166
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
167
|
+
def initialize(customer)
|
168
|
+
@customer = customer
|
169
|
+
end
|
170
|
+
end
|
171
|
+
```
|
138
172
|
|
139
173
|
Note that when you call `save!`, `update!`, or `update_attributes!` on the form object, validation errors from records will still raise `ActiveRecord::RecordInvalid`, but validation errors from validations defined on the form itself will raise `ActiveModel::ValidationError`. You will usually want to rescue both.
|
140
174
|
|
@@ -142,16 +176,18 @@ Note that when you call `save!`, `update!`, or `update_attributes!` on the form
|
|
142
176
|
|
143
177
|
You can also use the `before_validation`, `before_save`, `after_save`, and `around_save` validations. Like ActiveRecord, these will run inside the database transaction when you're calling one of the save or update methods, which is especially useful if you need to take locks on parent records.
|
144
178
|
|
145
|
-
|
146
|
-
|
179
|
+
```ruby
|
180
|
+
class NewBranchForm < OnForm::Form
|
181
|
+
expose %w(bank_id branch_number branch_name), on: :branch
|
147
182
|
|
148
|
-
|
183
|
+
before_save :lock_bank
|
149
184
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
185
|
+
protected
|
186
|
+
def lock_bank
|
187
|
+
branch.bank.lock!
|
188
|
+
end
|
189
|
+
end
|
190
|
+
```
|
155
191
|
|
156
192
|
Note that model save calls are nested inside the form save calls, which means that although form validation takes place before form save starts, model validation takes place after form saving begins.
|
157
193
|
|
@@ -173,39 +209,43 @@ Note that model save calls are nested inside the form save calls, which means th
|
|
173
209
|
|
174
210
|
You can descend form classes from other form classes and expose additional models or additional attributes on existing models.
|
175
211
|
|
176
|
-
|
177
|
-
|
178
|
-
|
212
|
+
```ruby
|
213
|
+
class AdminHouseListingForm < HouseListingForm
|
214
|
+
expose %i(listing_approved), on: :house
|
215
|
+
end
|
216
|
+
```
|
179
217
|
|
180
218
|
This works well for some use cases, but can quickly become cumbersome if you have a lot of partial form reuse, and it may not be obvious to other developers that the parent form is also used to derive the other forms. Consider breaking your form parts into reuseable modules, and defining each form separately.
|
181
219
|
|
182
220
|
You can use standard Ruby hooks for this:
|
183
221
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
222
|
+
```ruby
|
223
|
+
module AccountFormComponent
|
224
|
+
def self.included(form)
|
225
|
+
form.expose %i(email phone_number), on: :customer
|
226
|
+
end
|
227
|
+
end
|
189
228
|
|
190
|
-
|
191
|
-
|
229
|
+
class NewAccountForm < OnForm::Form
|
230
|
+
include AccountFormComponent
|
192
231
|
|
193
|
-
|
232
|
+
expose %i(name), on: :customer
|
194
233
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
234
|
+
def initialize(customer)
|
235
|
+
@customer = customer
|
236
|
+
end
|
237
|
+
end
|
199
238
|
|
200
|
-
|
201
|
-
|
239
|
+
class EditAccountForm < OnForm::Form
|
240
|
+
include AccountFormComponent
|
202
241
|
|
203
|
-
|
242
|
+
delegate :name, to: :customer
|
204
243
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
244
|
+
def initialize(customer)
|
245
|
+
@customer = customer
|
246
|
+
end
|
247
|
+
end
|
248
|
+
```
|
209
249
|
|
210
250
|
In this example the initialize method could actually be moved to the module as well, but that makes it harder to compose forms from multiple modules.
|
211
251
|
|
@@ -215,11 +255,11 @@ If you prefer, you can use the Rails `included` block syntax in the module inste
|
|
215
255
|
|
216
256
|
After checking out the repo, pick the rails version you'd like to run tests against, and run:
|
217
257
|
|
218
|
-
|
258
|
+
RAILS_VERSION=5.0.0.1 bundle update
|
219
259
|
|
220
260
|
You should then be able to run the test suite:
|
221
261
|
|
222
|
-
|
262
|
+
bundle exec rake
|
223
263
|
|
224
264
|
## Contributing
|
225
265
|
|
@@ -227,7 +267,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/powers
|
|
227
267
|
|
228
268
|
## Roadmap
|
229
269
|
|
230
|
-
*
|
270
|
+
* Currently, the author is looking into support for declaring attributes on the form. (You can use plain old Ruby object `attr_accessor` for untyped attributes in the meantime.)
|
231
271
|
* After that we'll need to tackle the other use cases for ActiveRecord nested attributes, such as one-to-many associations and auto-building/deleting associated records.
|
232
272
|
|
233
273
|
## License
|
data/lib/on_form/attributes.rb
CHANGED
@@ -20,7 +20,7 @@ module OnForm
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def attribute_names
|
23
|
-
self.class.exposed_attributes.values.
|
23
|
+
self.class.exposed_attributes.values.flat_map(&:keys).collect(&:to_s)
|
24
24
|
end
|
25
25
|
|
26
26
|
def attributes
|
@@ -46,17 +46,17 @@ module OnForm
|
|
46
46
|
end
|
47
47
|
|
48
48
|
private
|
49
|
-
def
|
49
|
+
def backing_model_instance(backing_model_name)
|
50
50
|
send(backing_model_name)
|
51
51
|
end
|
52
52
|
|
53
|
-
def
|
54
|
-
self.class.exposed_attributes.keys.collect { |backing_model_name|
|
53
|
+
def backing_model_instances
|
54
|
+
self.class.exposed_attributes.keys.collect { |backing_model_name| backing_model_instance(backing_model_name) }
|
55
55
|
end
|
56
56
|
|
57
|
-
def
|
58
|
-
self.class.exposed_attributes.each do |backing_model_name,
|
59
|
-
return
|
57
|
+
def backing_model_for_attribute(exposed_name)
|
58
|
+
self.class.exposed_attributes.each do |backing_model_name, attribute_mappings|
|
59
|
+
return backing_model_instance(backing_model_name) if attribute_mappings[exposed_name.to_sym]
|
60
60
|
end
|
61
61
|
nil
|
62
62
|
end
|
data/lib/on_form/errors.rb
CHANGED
@@ -10,13 +10,19 @@ module OnForm
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def collect_errors
|
13
|
-
self.class.exposed_attributes.each do |backing_model_name,
|
14
|
-
backing_model(backing_model_name)
|
15
|
-
|
16
|
-
|
17
|
-
|
13
|
+
self.class.exposed_attributes.each do |backing_model_name, attribute_mappings|
|
14
|
+
backing_model = backing_model_instance(backing_model_name)
|
15
|
+
|
16
|
+
collect_errors_on(backing_model, :base, :base)
|
17
|
+
|
18
|
+
attribute_mappings.each do |exposed_name, backing_name|
|
19
|
+
collect_errors_on(backing_model, exposed_name, backing_name)
|
18
20
|
end
|
19
21
|
end
|
20
22
|
end
|
23
|
+
|
24
|
+
def collect_errors_on(backing_model, exposed_name, backing_name)
|
25
|
+
Array(backing_model.errors[backing_name]).each { |error| errors[exposed_name] << error }
|
26
|
+
end
|
21
27
|
end
|
22
28
|
end
|
data/lib/on_form/form.rb
CHANGED
@@ -9,22 +9,22 @@ module OnForm
|
|
9
9
|
include Saving
|
10
10
|
|
11
11
|
def self.exposed_attributes
|
12
|
-
@exposed_attributes ||= Hash.new { |h, k| h[k] =
|
12
|
+
@exposed_attributes ||= Hash.new { |h, k| h[k] = {} }
|
13
13
|
end
|
14
14
|
|
15
15
|
class << self
|
16
16
|
def inherited(child)
|
17
|
-
exposed_attributes.each { |k, v| child.exposed_attributes[k].
|
17
|
+
exposed_attributes.each { |k, v| child.exposed_attributes[k].merge!(v) }
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
-
def self.expose(
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
21
|
+
def self.expose(backing_attribute_names, on:, prefix: nil, suffix: nil, as: nil)
|
22
|
+
raise ArgumentError, "can't expose multiple attributes as the same form attribute!" if as && backing_attribute_names.size != 1
|
23
|
+
on = on.to_sym
|
24
|
+
expose_backing_model(on)
|
25
|
+
backing_attribute_names.each do |backing_name|
|
26
|
+
exposed_name = as || "#{prefix}#{backing_name}#{suffix}"
|
27
|
+
expose_attribute(on, exposed_name, backing_name)
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
@@ -34,15 +34,13 @@ module OnForm
|
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
37
|
-
def self.expose_attribute(backing_model_name,
|
38
|
-
exposed_attributes[backing_model_name]
|
37
|
+
def self.expose_attribute(backing_model_name, exposed_name, backing_name)
|
38
|
+
exposed_attributes[backing_model_name][exposed_name.to_sym] = backing_name.to_sym
|
39
39
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
define_method(attribute_method) { |arg| backing_model(backing_model_name).send(attribute_method, arg) }
|
45
|
-
end
|
40
|
+
define_method(exposed_name) { backing_model_instance(backing_model_name).send(backing_name) }
|
41
|
+
define_method("#{exposed_name}_before_type_cast") { backing_model_instance(backing_model_name).send("#{backing_name}_before_type_cast") }
|
42
|
+
define_method("#{exposed_name}?") { backing_model_instance(backing_model_name).send("#{backing_name}?") }
|
43
|
+
define_method("#{exposed_name}=") { |arg| backing_model_instance(backing_model_name).send("#{backing_name}=", arg) }
|
46
44
|
end
|
47
45
|
end
|
48
46
|
end
|
@@ -22,7 +22,7 @@ module OnForm
|
|
22
22
|
if defined?(ActiveRecord::AttributeAssignment::MultiparameterAttribute)
|
23
23
|
# ActiveRecord 4.2 and below: you must use MultiparameterAttribute to construct the attribute value.
|
24
24
|
# we therefore have to look up which model the attribute actually lives on.
|
25
|
-
send("#{name}=", ActiveRecord::AttributeAssignment::MultiparameterAttribute.new(
|
25
|
+
send("#{name}=", ActiveRecord::AttributeAssignment::MultiparameterAttribute.new(backing_model_for_attribute(name), name, values_with_empty_parameters).read_value)
|
26
26
|
else
|
27
27
|
# ActiveRecord 5.0+: you can assign the indexed hash to the column and it will construct the value for you.
|
28
28
|
if values_with_empty_parameters.each_value.all?(&:nil?)
|
data/lib/on_form/saving.rb
CHANGED
@@ -5,7 +5,7 @@ module OnForm
|
|
5
5
|
end
|
6
6
|
|
7
7
|
def transaction(&block)
|
8
|
-
with_transactions(
|
8
|
+
with_transactions(backing_model_instances, &block)
|
9
9
|
end
|
10
10
|
|
11
11
|
def invalid?
|
@@ -21,7 +21,7 @@ module OnForm
|
|
21
21
|
end
|
22
22
|
run_callbacks :save do
|
23
23
|
begin
|
24
|
-
|
24
|
+
backing_model_instances.each { |backing_model| backing_model.save! }
|
25
25
|
rescue ActiveRecord::RecordInvalid, ActiveModel::ValidationError
|
26
26
|
collect_errors
|
27
27
|
raise
|
@@ -72,7 +72,7 @@ module OnForm
|
|
72
72
|
end
|
73
73
|
|
74
74
|
def run_backing_model_validations
|
75
|
-
|
75
|
+
backing_model_instances.collect { |backing_model| backing_model.valid? }
|
76
76
|
collect_errors
|
77
77
|
end
|
78
78
|
end
|
data/lib/on_form/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: on_form
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Will Bryant
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-09-
|
11
|
+
date: 2016-09-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -89,6 +89,7 @@ extensions: []
|
|
89
89
|
extra_rdoc_files: []
|
90
90
|
files:
|
91
91
|
- ".gitignore"
|
92
|
+
- CHANGES.md
|
92
93
|
- Gemfile
|
93
94
|
- LICENSE.txt
|
94
95
|
- README.md
|