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 +4 -4
- data/README.md +61 -3
- data/lib/on_form.rb +1 -0
- data/lib/on_form/attributes.rb +2 -1
- data/lib/on_form/form.rb +24 -0
- data/lib/on_form/types.rb +52 -0
- 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: febeec1d904aefce31f0499e1a0259caff00003c
|
4
|
+
data.tar.gz: 0fa55ca7d0f8692479ae9ee574a1e2f03f2ed42c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
*
|
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
data/lib/on_form/attributes.rb
CHANGED
@@ -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
|
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: 2.0
|
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-
|
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
|