on_form 2.0.1 → 2.1.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: 820b56db21bed598a2c4c4fbef8aa5c33cabc693
4
- data.tar.gz: 7cd239b39707c8fefa12ba3acec8c2cbe3d733b4
3
+ metadata.gz: febeec1d904aefce31f0499e1a0259caff00003c
4
+ data.tar.gz: 0fa55ca7d0f8692479ae9ee574a1e2f03f2ed42c
5
5
  SHA512:
6
- metadata.gz: 897e50e32ef49cf93fea57d5487c750b8c3ed8e65d8610dce0ffb657f6171697b329bfc7125175883223e529bb857442fc5381d34c4ad08f8568721d0194ad5d
7
- data.tar.gz: eefe006c8c937acad840b960076233592eed058c4265ca5aaa8e86aa0176a047c4b57d3b962b9aa0d9368431d687a1fbec8a67a322afce90cd273f44787b23e9
6
+ metadata.gz: c3a32337ff68ecc0037ab9fb431b279f6897fda0f2a82c90a657add23ca0eeac43c1ce1bf517e2768fa2caa471399551fa32b5a25cc4478912632011ea679dcc
7
+ data.tar.gz: bb78f1d4a027fee037958b23e232c9c381efa7c9d426018e9b6723e7060a1042c2d14b0876302c75e555226c236e795a2bc49dac33709f45136b78b12dc0b90a
data/README.md CHANGED
@@ -134,7 +134,7 @@ You can also define your own method over the top of the `attr_reader`. Just rem
134
134
 
135
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.
136
136
 
137
- To address this you can use the `prefix` and/or `suffix` options to `expose`.
137
+ To address this you can use the `prefix` and/or `suffix` options to `expose`, or if you need to change the name completely, the `as` option.
138
138
 
139
139
  ```ruby
140
140
  class AccountHolderForm < OnForm::Form
@@ -205,6 +205,65 @@ Note that model save calls are nested inside the form save calls, which means th
205
205
  form around_save ends
206
206
  form after_save
207
207
 
208
+ ### Adding artifical attributes
209
+
210
+ In addition to mapping attributes between models and the form, you can introduce new attributes which are not directly persisted anywhere. You can use any of the "standard" (non-database-specific) ActiveRecord types, and you can add `default`, `scale`, and `precision` options.
211
+
212
+ ```ruby
213
+ class ChangeEmailForm < OnForm::Form
214
+ expose %i(email), on: :customer, as: :new_email
215
+ attribute :email_confirmation, :string, :default => "(please confirm)"
216
+
217
+ validate :email_confirmation_matches
218
+
219
+ def initialize(customer)
220
+ @customer = customer
221
+ end
222
+
223
+ def email_confirmation_matches
224
+ errors[:email_confirmation] << "does not match" unless email_confirmation == new_email
225
+ end
226
+ end
227
+ ```
228
+
229
+ ### Model-less forms
230
+
231
+ Taking this one step further, you can define forms which have _no_ exposed model attributes. *Be aware that forms that expose no models do not automatically start a database transaction, because they don't know which database connection to use.*
232
+
233
+ To actually perform a data change in response to the form submission, you can add a `before_save` or `after_save` callback and from there call your existing model code or service objects. It's best to keep the code in the form object to just the bits specific to the form - try not to put your business logic in your form objects!
234
+
235
+ ```ruby
236
+ class ChangePasswordForm < OnForm::Form
237
+ attribute :current_password, :string
238
+ attribute :password, :string
239
+ attribute :password_confirmation, :string
240
+
241
+ validate :current_password_correct
242
+ validate :password_confirmation_matches
243
+ before_save :set_new_password
244
+
245
+ def initialize(customer)
246
+ @customer = customer
247
+ end
248
+
249
+ def current_password_correct
250
+ unless @customer.password_correct?(current_password)
251
+ errors[:current_password] << "is incorrect"
252
+ end
253
+ end
254
+
255
+ def password_confirmation_matches
256
+ unless password_confirmation == password
257
+ errors[:password_confirmation] << "doesn't match"
258
+ end
259
+ end
260
+
261
+ def set_new_password
262
+ @customer.change_password!(password)
263
+ end
264
+ end
265
+ ```
266
+
208
267
  ### Reusing and extending forms
209
268
 
210
269
  You can descend form classes from other form classes and expose additional models or additional attributes on existing models.
@@ -267,8 +326,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/powers
267
326
 
268
327
  ## Roadmap
269
328
 
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.)
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.
329
+ * The author is currently assessing other use cases for ActiveRecord nested attributes, such as one-to-many associations and auto-building/deleting associated records. Feedback welcome.
272
330
 
273
331
  ## License
274
332
 
data/lib/on_form.rb CHANGED
@@ -5,4 +5,5 @@ require "on_form/rails_compat"
5
5
  require "on_form/attributes"
6
6
  require "on_form/errors"
7
7
  require "on_form/saving"
8
+ require "on_form/types"
8
9
  require "on_form/form"
@@ -20,7 +20,8 @@ module OnForm
20
20
  end
21
21
 
22
22
  def attribute_names
23
- self.class.exposed_attributes.values.flat_map(&:keys).collect(&:to_s)
23
+ self.class.exposed_attributes.values.flat_map(&:keys).collect(&:to_s) +
24
+ self.class.introduced_attribute_types.keys.collect(&:to_s)
24
25
  end
25
26
 
26
27
  def attributes
data/lib/on_form/form.rb CHANGED
@@ -12,9 +12,14 @@ module OnForm
12
12
  @exposed_attributes ||= Hash.new { |h, k| h[k] = {} }
13
13
  end
14
14
 
15
+ def self.introduced_attribute_types
16
+ @introduced_attribute_types ||= {}
17
+ end
18
+
15
19
  class << self
16
20
  def inherited(child)
17
21
  exposed_attributes.each { |k, v| child.exposed_attributes[k].merge!(v) }
22
+ child.introduced_attribute_types.merge!(introduced_attribute_types)
18
23
  end
19
24
  end
20
25
 
@@ -44,5 +49,24 @@ module OnForm
44
49
  define_method("#{exposed_name}_was") { backing_model_instance(backing_model_name).send("#{backing_name}_was") }
45
50
  define_method("#{exposed_name}=") { |arg| backing_model_instance(backing_model_name).send("#{backing_name}=", arg) }
46
51
  end
52
+
53
+ def self.attribute(name, type, options = {})
54
+ name = name.to_sym
55
+ introduced_attribute_types[name] = Types.lookup(type, options)
56
+ define_method(name) { introduced_attribute_values.fetch(name) { type = self.class.introduced_attribute_types[name]; type.cast(introduced_attribute_values_before_type_cast.fetch(name) { type.default }) } }
57
+ define_method("#{name}_before_type_cast") { introduced_attribute_values_before_type_cast[name] }
58
+ define_method("#{name}_changed?") { send(name) != send("#{name}_was") }
59
+ define_method("#{name}_was") { type = self.class.introduced_attribute_types[name]; type.cast(type.default) }
60
+ define_method("#{name}=") { |arg| introduced_attribute_values.delete(name); introduced_attribute_values_before_type_cast[name] = arg }
61
+ end
62
+
63
+ protected
64
+ def introduced_attribute_values
65
+ @introduced_attribute_values ||= {}
66
+ end
67
+
68
+ def introduced_attribute_values_before_type_cast
69
+ @introduced_attribute_values_before_type_cast ||= {}
70
+ end
47
71
  end
48
72
  end
@@ -0,0 +1,52 @@
1
+ module OnForm
2
+ module Types
3
+ class Type
4
+ attr_reader :default
5
+
6
+ def initialize(type, default)
7
+ @type = type
8
+ @default = default
9
+ end
10
+ end
11
+
12
+ if ActiveRecord::Type.methods.include?(:lookup)
13
+ def self.lookup(type, options)
14
+ default = options.delete(:default)
15
+ Type.new(ActiveRecord::Type.lookup(type, options), default)
16
+ end
17
+
18
+ class Type
19
+ def cast(arg)
20
+ @type.cast(arg)
21
+ end
22
+ end
23
+ else
24
+ # for rails 4.2 and below, the type map lives on individual database adapters, but we may
25
+ # not have any models, so here we fall back to the map defined by the abstract adapter class.
26
+ def self.lookup(type, options)
27
+ default = options.delete(:default)
28
+ if precision = options.delete(:precision)
29
+ if scale = options.delete(:scale)
30
+ type = "#{type}(#{precision},#{scale})"
31
+ else
32
+ type = "#{type}(#{precision})"
33
+ end
34
+ elsif options[:scale]
35
+ raise ArgumentError, "Can't apply scale without precision on Rails 4.2. The precision is used when converting Float values."
36
+ end
37
+ raise ArgumentError, "Unknown type option: #{options}" unless options.empty?
38
+ Type.new(_adapter.type_map.lookup(type), default)
39
+ end
40
+
41
+ def self._adapter
42
+ @_adapter ||= ActiveRecord::ConnectionAdapters::AbstractAdapter.new(nil)
43
+ end
44
+
45
+ class Type
46
+ def cast(arg)
47
+ @type.type_cast_from_user(arg)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,3 +1,3 @@
1
1
  module OnForm
2
- VERSION = "2.0.1"
2
+ VERSION = "2.1.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: 2.0.1
4
+ version: 2.1.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-27 00:00:00.000000000 Z
11
+ date: 2016-09-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -102,6 +102,7 @@ files:
102
102
  - lib/on_form/form.rb
103
103
  - lib/on_form/rails_compat.rb
104
104
  - lib/on_form/saving.rb
105
+ - lib/on_form/types.rb
105
106
  - lib/on_form/version.rb
106
107
  - on_form.gemspec
107
108
  homepage: https://github.com/powershop/on_form