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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b6aada85f9b13d5b28592d880d3cfebd1f5ef8e4
4
- data.tar.gz: 9d15cc5152cb460307365fe91dadb63c8ca72370
3
+ metadata.gz: 3d7dd5ed97fb50b463cd137f4e37cd54a6b5039f
4
+ data.tar.gz: cf2be44d209f36980cb248e89498060ae50374a8
5
5
  SHA512:
6
- metadata.gz: ab8c256758e5eebecf4e3f2fd2f93ab85c763fed8924e61629cd3e7765a12527f911c22341d484bacdacc0347de62804f06c8ff20ecc3a52aac85b0e6f539d4e
7
- data.tar.gz: 43ee6c65289f1194bb92076c517a28fcdd10733517c8dc0d4e3dd702f625d8bbe629659d047f3b887b4e25154c64880db85a3b29e619e517a8dd751a09b02022
6
+ metadata.gz: c967c12d103f110b2932b03e06b4c8e708e1623abc970270c0dc4fdd2f33ccab5936ecccc440e0b8b4922cdac099258eb37b735bd068cef5727c7b62ea2108f1
7
+ data.tar.gz: b26ae23736314ecfa179e4c380fa4d8e91ea7a09cd18ccd6fae5c9b9c64778225698b040e7bfbe41f67027cfcb90d2176b6f6bc1e0299373c441cc7eb1857756
data/CHANGES.md ADDED
@@ -0,0 +1,10 @@
1
+ Changelog
2
+ =========
3
+
4
+ 2.00
5
+ ----
6
+ * New expose syntax to support prefix:, suffix:, and as: options
7
+
8
+ 1.00
9
+ ----
10
+ * First public release
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
- class PreferencesController
42
- def show
43
- @customer = Customer.find(params[:id])
44
- end
45
-
46
- def update
47
- @customer = Customer.find(params[:id])
48
- @customer.update!(params[:customer].permit(:name, :email, :phone_number)
49
- redirect_to preferences_path(@customer)
50
- rescue ActiveRecord::RecordInvalid
51
- render :show
52
- end
53
- end
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
- class PreferencesController
58
- def show
59
- @customer = PreferencesForm.new(Customer.find(params[:id]))
60
- end
61
-
62
- def update
63
- @customer = PreferencesForm.new(Customer.find(params[:id]))
64
- @customer.update!(params[:customer])
65
- rescue ActiveRecord::RecordInvalid
66
- render :show
67
- end
68
- end
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
- class PreferencesForm < OnForm::Form
73
- expose :customer => %i(name email phone_number)
76
+ ```ruby
77
+ class PreferencesForm < OnForm::Form
78
+ expose %i(name email phone_number), on: :customer
74
79
 
75
- def initialize(customer)
76
- @customer = customer
77
- end
78
- end
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 made up of multiple models pass more than one key to `expose`, or call it multiple times if you prefer. They'll automatically be saved in the same order you declared them.
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
- class HouseListingForm < OnForm::Form
91
- expose :house => %i(street_number street_name city),
92
- :vendor => %i(name phone_number)
93
-
94
- def initialize(house)
95
- @house = house
96
- @vendor = house.vendor
97
- end
98
- end
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 keys are the name of the methods on the form object which return the records, not the class names. In this example, vendor might actually be an instance of our `Customer` model from the earlier examples.
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 names passed to `expose`. `expose` will automatically add an `attr_reader` for each key it's given, meaning you only need to set the instance variables.
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
- class HouseListingForm < OnForm::Form
111
- delegate :vendor, :to => :house
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
- expose :house => %i(street_number street_name city),
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
- def initialize(house)
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
- 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 should be idempotent.
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
- class AddEmergencyContactForm < OnForm::Form
130
- expose :customer => %i(next_of_kin_name next_of_kin_phone_number)
161
+ ```ruby
162
+ class AddEmergencyContactForm < OnForm::Form
163
+ expose %i(next_of_kin_name next_of_kin_phone_number), on: :customer
131
164
 
132
- validates_presence_of :next_of_kin_name, :next_of_kin_phone_number
165
+ validates_presence_of :next_of_kin_name, :next_of_kin_phone_number
133
166
 
134
- def initialize(customer)
135
- @customer = customer
136
- end
137
- end
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
- class NewBranchForm < OnForm::Form
146
- expose :branch => %w(bank_id branch_number branch_name)
179
+ ```ruby
180
+ class NewBranchForm < OnForm::Form
181
+ expose %w(bank_id branch_number branch_name), on: :branch
147
182
 
148
- before_save :lock_bank
183
+ before_save :lock_bank
149
184
 
150
- protected
151
- def lock_bank
152
- branch.bank.lock!
153
- end
154
- end
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
- class AdminHouseListingForm < HouseListingForm
177
- expose :house => %i(listing_approved)
178
- end
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
- module AccountFormComponent
185
- def self.included(form)
186
- form.expose :customer => %i(email phone_number)
187
- end
188
- end
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
- class NewAccountForm < OnForm::Form
191
- include AccountFormComponent
229
+ class NewAccountForm < OnForm::Form
230
+ include AccountFormComponent
192
231
 
193
- expose :customer => %i(name)
232
+ expose %i(name), on: :customer
194
233
 
195
- def initialize(customer)
196
- @customer = customer
197
- end
198
- end
234
+ def initialize(customer)
235
+ @customer = customer
236
+ end
237
+ end
199
238
 
200
- class EditAccountForm < OnForm::Form
201
- include AccountFormComponent
239
+ class EditAccountForm < OnForm::Form
240
+ include AccountFormComponent
202
241
 
203
- delegate :name, to: :customer
242
+ delegate :name, to: :customer
204
243
 
205
- def initialize(customer)
206
- @customer = customer
207
- end
208
- end
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
- RAILS_VERSION=5.0.0.1 bundle update
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
- bundle exec rake
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
- * For version 2, 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.)
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
@@ -20,7 +20,7 @@ module OnForm
20
20
  end
21
21
 
22
22
  def attribute_names
23
- self.class.exposed_attributes.values.reduce(:+).collect(&:to_s)
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 backing_model(backing_model_name)
49
+ def backing_model_instance(backing_model_name)
50
50
  send(backing_model_name)
51
51
  end
52
52
 
53
- def backing_models
54
- self.class.exposed_attributes.keys.collect { |backing_model_name| backing_model(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 backing_object_for_attribute(attribute_name)
58
- self.class.exposed_attributes.each do |backing_model_name, attribute_names|
59
- return backing_model(backing_model_name) if attribute_names.include?(attribute_name.to_sym)
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
@@ -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, exposed_attributes_on_backing_model|
14
- backing_model(backing_model_name).errors.each do |backing_attribute, attribute_errors|
15
- if backing_attribute == :base || exposed_attributes_on_backing_model.include?(backing_attribute)
16
- Array(attribute_errors).each { |error| errors[backing_attribute] << error }
17
- end
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].concat(v) }
17
+ exposed_attributes.each { |k, v| child.exposed_attributes[k].merge!(v) }
18
18
  end
19
19
  end
20
20
 
21
- def self.expose(backing_models_and_attribute_names)
22
- backing_models_and_attribute_names.each do |backing_model_name, attribute_names|
23
- backing_model_name = backing_model_name.to_sym
24
- expose_backing_model(backing_model_name)
25
- attribute_names.each do |attribute_name|
26
- expose_attribute(backing_model_name, attribute_name)
27
- end
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, attribute_name)
38
- exposed_attributes[backing_model_name] << attribute_name.to_sym
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
- [attribute_name, "#{attribute_name}_before_type_cast", "#{attribute_name}?"].each do |attribute_method|
41
- define_method(attribute_method) { backing_model(backing_model_name).send(attribute_method) }
42
- end
43
- ["#{attribute_name}="].each do |attribute_method|
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(backing_object_for_attribute(name), name, values_with_empty_parameters).read_value)
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?)
@@ -5,7 +5,7 @@ module OnForm
5
5
  end
6
6
 
7
7
  def transaction(&block)
8
- with_transactions(backing_models, &block)
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
- backing_models.each { |backing_model| backing_model.save! }
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
- backing_models.collect { |backing_model| backing_model.valid? }
75
+ backing_model_instances.collect { |backing_model| backing_model.valid? }
76
76
  collect_errors
77
77
  end
78
78
  end
@@ -1,3 +1,3 @@
1
1
  module OnForm
2
- VERSION = "1.0.0"
2
+ VERSION = "2.0.0"
3
3
  end
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: 1.0.0
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-17 00:00:00.000000000 Z
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