reform 0.2.7 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,13 +3,15 @@ module Reform::Form::ActiveModel
3
3
  def self.included(base)
4
4
  base.class_eval do
5
5
  extend ClassMethods # ::model_name
6
+ features << FormBuilderMethods
6
7
  end
7
8
  end
8
9
 
9
10
  module ClassMethods
10
11
  def property(name, options={})
11
- super
12
- add_nested_attribute_compat(name) if options[:form] # TODO: fix that in Rails FB#1832 work.
12
+ super.tap do |definition|
13
+ add_nested_attribute_compat(name) if definition[:form] # TODO: fix that in Rails FB#1832 work.
14
+ end
13
15
  end
14
16
 
15
17
  private
@@ -20,7 +22,7 @@ module Reform::Form::ActiveModel
20
22
  end
21
23
 
22
24
  # Modify the incoming Rails params hash to be representable compliant.
23
- def validate(params)
25
+ def update!(params)
24
26
  # DISCUSS: #validate should actually expect the complete params hash and then pick the right key as it knows the form name.
25
27
  # however, this would cause confusion?
26
28
  mapper.new(self).nested_forms do |attr, model| # FIXME: make this simpler.
@@ -36,16 +38,17 @@ module Reform::Form::ActiveModel
36
38
  return unless params.has_key?(nested_name)
37
39
 
38
40
  value = params["#{attr.name}_attributes"]
39
- value = value.values if attr.options[:form_collection]
41
+ value = value.values if attr[:collection]
40
42
 
41
43
  params[attr.name] = value
42
44
  end
43
- end
45
+ end # FormBuilderMethods
44
46
 
45
47
 
46
48
  def self.included(base)
47
49
  base.class_eval do
48
50
  extend ClassMethods
51
+ features << ActiveModel
49
52
 
50
53
  delegate [:persisted?, :to_key, :to_param, :id] => :model
51
54
 
@@ -1,50 +1,43 @@
1
- class Reform::Form
2
- module ActiveRecord
3
- def self.included(base)
4
- base.class_eval do
5
- include Reform::Form::ActiveModel
6
- extend ClassMethods
7
- end
1
+ module Reform::Form::ActiveRecord
2
+ def self.included(base)
3
+ base.class_eval do
4
+ include Reform::Form::ActiveModel
5
+ extend ClassMethods
8
6
  end
7
+ end
9
8
 
10
- module ClassMethods
11
- def validates_uniqueness_of(attribute)
12
- validates_with UniquenessValidator, :attributes => [attribute]
13
- end
14
- def i18n_scope
15
- :activerecord
16
- end
9
+ module ClassMethods
10
+ def validates_uniqueness_of(attribute)
11
+ validates_with UniquenessValidator, :attributes => [attribute]
12
+ end
13
+ def i18n_scope
14
+ :activerecord
17
15
  end
16
+ end
18
17
 
19
- class UniquenessValidator < ::ActiveRecord::Validations::UniquenessValidator
20
- # when calling validates it should create the Vali instance already and set @klass there! # TODO: fix this in AM.
21
- def validate(form)
22
- property = attributes.first
18
+ class UniquenessValidator < ::ActiveRecord::Validations::UniquenessValidator
19
+ # when calling validates it should create the Vali instance already and set @klass there! # TODO: fix this in AM.
20
+ def validate(form)
21
+ property = attributes.first
23
22
 
24
- # here is the thing: why does AM::UniquenessValidator require a filled-out record to work properly? also, why do we need to set
25
- # the class? it would be way easier to pass #validate a hash of attributes and get back an errors hash.
26
- # the class for the finder could either be infered from the record or set in the validator instance itself in the call to ::validates.
27
- record = form.model_for_property(property)
28
- record.send("#{property}=", form.send(property))
23
+ # here is the thing: why does AM::UniquenessValidator require a filled-out record to work properly? also, why do we need to set
24
+ # the class? it would be way easier to pass #validate a hash of attributes and get back an errors hash.
25
+ # the class for the finder could either be infered from the record or set in the validator instance itself in the call to ::validates.
26
+ record = form.model_for_property(property)
27
+ record.send("#{property}=", form.send(property))
29
28
 
30
- @klass = record.class # this is usually done in the super-sucky #setup method.
31
- super(record).tap do |res|
32
- form.errors.add(property, record.errors.first.last) if record.errors.present?
33
- end
29
+ @klass = record.class # this is usually done in the super-sucky #setup method.
30
+ super(record).tap do |res|
31
+ form.errors.add(property, record.errors.first.last) if record.errors.present?
34
32
  end
35
33
  end
34
+ end
36
35
 
37
- def save(*)
38
- super.tap do
39
- model.save unless block_given? # DISCUSS: should we implement nested saving here?
40
- end
41
- end
42
36
 
43
- def model_for_property(name)
44
- return model unless is_a?(Reform::Form::Composition) # i am too lazy for proper inheritance. there should be a ActiveRecord::Composition that handles this.
37
+ def model_for_property(name)
38
+ return model unless is_a?(Reform::Form::Composition) # i am too lazy for proper inheritance. there should be a ActiveRecord::Composition that handles this.
45
39
 
46
- model_name = mapper.representable_attrs[name].options[:on]
47
- send(model_name)
48
- end
40
+ model_name = mapper.representable_attrs[name][:on]
41
+ send(model_name)
49
42
  end
50
43
  end
@@ -1,17 +1,16 @@
1
1
  require 'representable/decorator/coercion'
2
2
 
3
- class Reform::Form
4
- module Coercion
5
- def self.included(base)
6
- base.extend(ClassMethods)
7
- end
3
+ module Reform::Form::Coercion
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ base.features << self
7
+ end
8
8
 
9
- module ClassMethods
10
- def representer_class
11
- super.class_eval do
12
- include Representable::Decorator::Coercion unless self < Representable::Decorator::Coercion # DISCUSS: include it once. why do we have to check this?
13
- self
14
- end
9
+ module ClassMethods
10
+ def representer_class
11
+ super.class_eval do
12
+ include Representable::Decorator::Coercion unless self < Representable::Decorator::Coercion # DISCUSS: include it once. why do we have to check this?
13
+ self
15
14
  end
16
15
  end
17
16
  end
@@ -1,70 +1,60 @@
1
1
  require "reform/form/active_model"
2
2
 
3
- class Reform::Form
3
+ module Reform::Form::Composition
4
4
  # Automatically creates a Composition object for you when initializing the form.
5
- module Composition
6
- def self.included(base)
7
- base.class_eval do
8
- extend Reform::Form::ActiveModel::ClassMethods # ::model.
9
- extend ClassMethods
10
- end
5
+ def self.included(base)
6
+ base.class_eval do
7
+ extend Reform::Form::ActiveModel::ClassMethods # ::model.
8
+ extend ClassMethods
11
9
  end
10
+ end
12
11
 
13
- module ClassMethods
14
- #include Reform::Form::ActiveModel::ClassMethods # ::model.
12
+ module ClassMethods
13
+ #include Reform::Form::ActiveModel::ClassMethods # ::model.
15
14
 
16
- def model_class # DISCUSS: needed?
17
- rpr = representer_class
18
- @model_class ||= Class.new(Reform::Composition) do
19
- map_from rpr
20
- end
21
- end
15
+ def model_class # DISCUSS: needed?
16
+ @model_class ||= Reform::Composition.from(representer_class)
17
+ end
22
18
 
23
- def property(name, options={})
24
- super
25
- delegate options[:on] => :@model
19
+ def property(name, options={})
20
+ super.tap do |definition|
21
+ delegate options[:on] => :@model # form.band -> composition.band
26
22
  end
23
+ end
27
24
 
28
- # Same as ActiveModel::model but allows you to define the main model in the composition
29
- # using +:on+.
30
- #
31
- # class CoverSongForm < Reform::Form
32
- # model :song, on: :cover_song
33
- def model(main_model, options={})
34
- super
35
-
36
- composition_model = options[:on] || main_model
25
+ # Same as ActiveModel::model but allows you to define the main model in the composition
26
+ # using +:on+.
27
+ #
28
+ # class CoverSongForm < Reform::Form
29
+ # model :song, on: :cover_song
30
+ def model(main_model, options={})
31
+ super
37
32
 
38
- delegate composition_model => :model # #song => model.song
33
+ composition_model = options[:on] || main_model
39
34
 
40
- # FIXME: this should just delegate to :model as in FB, and the comp would take care of it internally.
41
- delegate [:persisted?, :to_key, :to_param] => composition_model # #to_key => song.to_key
35
+ delegate composition_model => :model # #song => model.song
42
36
 
43
- alias_method main_model, composition_model # #hit => model.song.
44
- end
45
- end
37
+ # FIXME: this should just delegate to :model as in FB, and the comp would take care of it internally.
38
+ delegate [:persisted?, :to_key, :to_param] => composition_model # #to_key => song.to_key
46
39
 
47
- def initialize(models)
48
- composition = self.class.model_class.new(models)
49
- super(composition)
50
- end
51
-
52
- def to_nested_hash
53
- model.nested_hash_for(to_hash) # use composition to compute nested hash.
40
+ alias_method main_model, composition_model # #hit => model.song.
54
41
  end
55
42
  end
56
43
 
44
+ def initialize(models)
45
+ composition = self.class.model_class.new(models)
46
+ super(composition)
47
+ end
57
48
 
58
- # TODO: remove me in 1.3.
59
- module DSL
60
- include Composition
49
+ def aliased_model # we don't need an Expose as we save the Composition instance in the constructor.
50
+ model
51
+ end
61
52
 
62
- def self.included(base)
63
- warn "[DEPRECATION] Reform::Form: `DSL` is deprecated. Please use `Composition` instead."
53
+ def to_nested_hash
54
+ model.nested_hash_for(to_hash) # use composition to compute nested hash.
55
+ end
64
56
 
65
- base.class_eval do
66
- extend Composition::ClassMethods
67
- end
68
- end
57
+ def to_hash(*args)
58
+ mapper.new(self).to_hash(*args)
69
59
  end
70
- end
60
+ end
@@ -1,5 +1,9 @@
1
- class Reform::Form
1
+ Reform::Form.class_eval do
2
2
  module MultiParameterAttributes
3
+ def self.included(base)
4
+ base.features << self
5
+ end
6
+
3
7
  class DateParamsFilter
4
8
  def call(params)
5
9
  date_attributes = {}
@@ -33,6 +37,7 @@ class Reform::Form
33
37
 
34
38
  def validate(params)
35
39
  # TODO: make it cleaner to hook into essential reform steps.
40
+ # TODO: test with nested.
36
41
  DateParamsFilter.new.call(params)
37
42
 
38
43
  super
@@ -0,0 +1,61 @@
1
+ Reform::Form.class_eval do
2
+ module Save
3
+ module RecursiveSave
4
+ def to_hash(*)
5
+ # process output from InputRepresenter {title: "Mint Car", hit: <Form>}
6
+ # and just call sync! on nested forms.
7
+ nested_forms do |attr|
8
+ attr.merge!(
9
+ :instance => lambda { |fragment, *| fragment },
10
+ :serialize => lambda { |object, args| object.save! unless args.binding[:save] === false },
11
+ )
12
+ end
13
+
14
+ super
15
+ end
16
+ end
17
+
18
+ def save
19
+ # DISCUSS: we should never hit @mapper here (which writes to the models) when a block is passed.
20
+ return yield self, to_nested_hash if block_given?
21
+
22
+ sync_models # recursion
23
+ save!
24
+ end
25
+
26
+ def save!
27
+ save_model
28
+ mapper.new(self).extend(RecursiveSave).to_hash # save! on all nested forms.
29
+ end
30
+
31
+ def save_model
32
+ model.save # TODO: implement nested (that should really be done by Twin/AR).
33
+ end
34
+
35
+
36
+ module NestedHash
37
+ def to_hash(*)
38
+ # Transform form data into a nested hash for #save.
39
+ nested_forms do |attr|
40
+ attr.merge!(
41
+ :instance => lambda { |fragment, *| fragment },
42
+ :serialize => lambda { |object, args| object.to_nested_hash },
43
+ )
44
+ end
45
+
46
+ representable_attrs.each do |attr|
47
+ attr.merge!(:as => attr[:private_name] || attr.name)
48
+ end
49
+
50
+ super
51
+ end
52
+ end
53
+
54
+ require "active_support/hash_with_indifferent_access" # DISCUSS: replace?
55
+ def to_nested_hash
56
+ map = mapper.new(self).extend(Save::NestedHash)
57
+
58
+ ActiveSupport::HashWithIndifferentAccess.new(map.to_hash)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,60 @@
1
+ Reform::Form.class_eval do
2
+ # #sync!
3
+ # 1. assign scalars to model (respecting virtual, excluded attributes)
4
+ # 2. call sync! on nested
5
+ module Sync
6
+ # Mechanics for writing input to model.
7
+ # Writes input to model.
8
+ module Writer
9
+ def from_hash(*)
10
+ # process output from InputRepresenter {title: "Mint Car", hit: <Form>}
11
+ # and just call sync! on nested forms.
12
+ nested_forms do |attr|
13
+ attr.merge!(
14
+ :instance => lambda { |fragment, *| fragment },
15
+ :deserialize => lambda { |object, *| object.sync! },
16
+ :setter => lambda { |*| } # don't write hit=<Form>.
17
+ )
18
+ end
19
+
20
+ super
21
+ end
22
+ end
23
+
24
+ # Transforms form input into what actually gets written to model.
25
+ # output: {title: "Mint Car", hit: <Form>}
26
+ module InputRepresenter
27
+ include Reform::Representer::WithOptions
28
+ # TODO: make dynamic.
29
+ include Reform::Form::EmptyAttributesOptions
30
+ include Reform::Form::ReadonlyAttributesOptions
31
+
32
+ def to_hash(*)
33
+ nested_forms do |attr|
34
+ attr.merge!(
35
+ :representable => false,
36
+ :prepare => lambda { |obj, *| obj }
37
+ )
38
+
39
+ end
40
+
41
+ super
42
+ end
43
+ end
44
+ end
45
+
46
+ ### TODO: add ToHash with :prepare => lambda { |form, args| form },
47
+
48
+ def sync_models
49
+ sync!
50
+ end
51
+ alias_method :sync, :sync_models
52
+
53
+ def sync! # semi-public.
54
+ input_representer = mapper.new(self).extend(Sync::InputRepresenter)
55
+
56
+ input = input_representer.to_hash
57
+
58
+ mapper.new(aliased_model).extend(Sync::Writer).from_hash(input)
59
+ end
60
+ end
@@ -0,0 +1,104 @@
1
+ # Mechanics for writing to forms in #validate.
2
+ module Reform::Form::Validate
3
+ module Update
4
+ def from_hash(*)
5
+ nested_forms do |attr|
6
+ attr.delete(:prepare)
7
+ attr.delete(:extend)
8
+
9
+ attr.merge!(
10
+ :collection => attr[:collection], # TODO: Def#merge! doesn't consider :collection if it's already set in attr YET.
11
+ :parse_strategy => :sync, # just use nested objects as they are.
12
+ :deserialize => lambda { |object, params, args| object.update!(params) },
13
+ )
14
+ end
15
+
16
+ super
17
+ end
18
+ end
19
+
20
+
21
+ module Populator
22
+ class PopulateIfEmpty
23
+ def initialize(*args)
24
+ @form, @fragment, args = args
25
+ @index = args.first
26
+ @args = args.last
27
+ end
28
+
29
+ def call
30
+ binding = @args.binding
31
+ form = binding.get
32
+
33
+ return if binding.array? and form and form[@index] # TODO: this should be handled by the Binding.
34
+ return if !binding.array? and form
35
+ # only get here when above form is nil.
36
+
37
+ if binding[:populate_if_empty].is_a?(Proc)
38
+ model = @form.instance_exec(@fragment, @args, &binding[:populate_if_empty]) # call user block.
39
+ else
40
+ model = binding[:populate_if_empty].new
41
+ end
42
+
43
+ form = binding[:form].new(model) # free service: wrap model with Form. this usually happens in #setup.
44
+
45
+ if binding.array?
46
+ @form.model.send("#{binding.getter}") << model # FIXME: i don't like this, but we have to add the model to the parent object to make associating work. i have to use #<< to stay compatible with AR's has_many API. DISCUSS: what happens when we get out-of-sync here?
47
+ @form.send("#{binding.getter}")[@index] = form
48
+ else
49
+ @form.model.send("#{binding.setter}", model) # FIXME: i don't like this, but we have to add the model to the parent object to make associating work.
50
+ @form.send("#{binding.setter}", form) # :setter is currently overwritten by :parse_strategy.
51
+ end
52
+ end
53
+ end # PopulateIfEmpty
54
+
55
+
56
+ def from_hash(params, *args)
57
+ populated_attrs = []
58
+
59
+ nested_forms do |attr|
60
+ next unless attr[:populate_if_empty]
61
+
62
+ attr.merge!(
63
+ # DISCUSS: it would be cool to move the lambda block to PopulateIfEmpty#call.
64
+ :populator => lambda do |fragment, *args|
65
+ PopulateIfEmpty.new(self, fragment, args).call
66
+ end
67
+ )
68
+ end
69
+
70
+
71
+ nested_forms do |attr|
72
+ next unless attr[:populator]
73
+
74
+ attr.merge!(
75
+ :parse_strategy => attr[:populator],
76
+ :representable => false
77
+ )
78
+ populated_attrs << attr.name.to_sym
79
+ end
80
+
81
+ super(params, {:include => populated_attrs})
82
+ end
83
+ end
84
+
85
+
86
+ def validate(params)
87
+ update!(params)
88
+
89
+ super()
90
+ end
91
+
92
+ def update!(params)
93
+ # puts "updating in #{self.class.name}"
94
+ populate!(params)
95
+
96
+ mapper.new(self).extend(Update).from_hash(params)
97
+ end
98
+
99
+ private
100
+ def populate!(params)
101
+ mapper.new(self).extend(Populator).from_hash(params)
102
+ end
103
+
104
+ end