Skip to content

Commit 6f8d7c1

Browse files
committed
Initial stab at using association classes instead of Form and FormCollection.
1 parent 636b9f6 commit 6f8d7c1

File tree

5 files changed

+207
-69
lines changed

5 files changed

+207
-69
lines changed

lib/active_form/associatable.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
module ActiveForm
2+
concern :Associatable do
3+
def association(name, options = {})
4+
association = child_class_for(options).new(name, options)
5+
association_scope.add_child(association, Proc.new)
6+
define_association_accessors(name, association)
7+
end
8+
9+
def association_scope
10+
self
11+
end
12+
13+
private
14+
def child_class_for(options)
15+
options.key?(:records) ? CollectionAssociation : ModelAssociation
16+
end
17+
18+
def define_association_accessors(name, association)
19+
class_eval do
20+
define_method(name) { association }
21+
define_method("#{name}=") { |i| association.instance = i }
22+
define_method("#{name}_attributes=") do |attrs|
23+
association.attributes = attrs
24+
end
25+
end
26+
end
27+
end
28+
end

lib/active_form/base.rb

Lines changed: 48 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,71 @@
1-
require 'active_form/abstract_form'
1+
require 'active_form/model_association'
2+
require 'active_form/collection_association'
3+
require 'active_form/associatable'
24

35
module ActiveForm
4-
class Base < AbstractForm
5-
include ActiveModel::Model
6-
7-
delegate :persisted?, :to_model, :to_key, :to_param, :to_partial_path, to: :model
8-
attr_reader :model
9-
10-
def initialize(model)
11-
@model = model
12-
13-
@forms = []
14-
populate_forms
6+
class Base
7+
extend Associatable
8+
9+
# SignupForm.new(user: User.find(params[:user_id]))
10+
def initialize(models = nil)
11+
if models.respond_to?(:each)
12+
assign_attributes(models)
13+
else
14+
main_association.instance = models
15+
end
1516
end
1617

1718
def save
1819
return false unless valid?
1920

20-
if ActiveRecord::Base.transaction { model.save }
21-
forms.each(&:reset)
21+
ActiveRecord::Base.transaction do
22+
main_association.save
2223
end
2324
end
2425

25-
class << self
26-
attr_accessor :main_model
27-
delegate :reflect_on_association, to: :model_class
28-
29-
def attributes(*names)
30-
options = names.pop if names.last.is_a?(Hash)
31-
32-
if options && options[:required]
33-
validates_presence_of *names
34-
end
35-
36-
names.each do |attribute|
37-
delegate attribute, "#{attribute}=", to: :model
38-
end
39-
end
26+
def submit(params)
27+
assign_attributes(params)
28+
end
4029

41-
alias_method :attribute, :attributes
30+
mattr_accessor :main_model, instance_writer: false
4231

43-
def association(name, options = {}, &block)
44-
define_method(name) { instance_variable_get("@#{name}").models }
45-
define_method("#{name}_attributes=") {}
32+
delegate :id, :persisted?, :to_model, :to_partial_path, to: :main_association
4633

47-
forms << [name, options, block]
34+
class << self
35+
delegate :attributes, :association_scope, to: :main_association
36+
37+
def main_association
38+
@@main_association ||= \
39+
if main_model
40+
ModelAssociation.new(main_model)
41+
else
42+
raise ArgumentError, "you need to set the main_model for this form," \
43+
" like self.main_model = :article"
44+
end
4845
end
46+
end
4947

50-
def forms
51-
@forms ||= []
48+
private
49+
def respond_to_missing?(meth, include_private = false)
50+
main_association.respond_to?(meth)
5251
end
5352

54-
private
55-
def model_class
56-
@model_class ||= main_model.to_s.camelize.constantize
53+
def method_missing(meth, *args, &block)
54+
if main_association.respond_to?(meth)
55+
main_association.send(meth, *args, &block)
56+
else
57+
super
5758
end
58-
end
59+
end
5960

60-
private
61+
def main_association
62+
@@main_association
63+
end
6164

62-
def populate_forms
63-
self.class.forms.each do |(name, options, block)|
64-
FormDefinition.new(name, block, options).build_for(model).tap do |form|
65-
forms << form
66-
instance_variable_set("@#{name}", form)
67-
end
65+
def assign_attributes(attributes)
66+
attributes.each do |key, value|
67+
self.public_send("#{key}=", value)
68+
end if attributes
6869
end
69-
end
7070
end
7171
end
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
require 'active_form/model_association'
2+
3+
module ActiveForm
4+
class CollectionAssociation < ModelAssociation
5+
def initialize(model_name, options)
6+
super
7+
@instances = []
8+
end
9+
10+
def dynamic?
11+
true # means we can add more instances in a form
12+
end
13+
14+
delegate :each, :size, :[], to: :@instances
15+
16+
# Map model attributes by key to association
17+
# doghouse_attributes: { '1' => { name: 'McDiniis' }, '2' => { name: 'McDunuus' } }
18+
def attributes=(model_attributes)
19+
model_attributes.each do |model_id, attrs|
20+
fetch_instance(model_id.to_i).attributes = attrs
21+
end
22+
end
23+
24+
def build_instance
25+
model_class.new.tap { |i| @instances << i }
26+
end
27+
28+
private
29+
def fetch_instance(id)
30+
@instances[id] || build_instance
31+
end
32+
end
33+
end
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
require 'active_support/core_ext/module/delegation'
2+
require 'active_form/associatable'
3+
4+
module ActiveForm
5+
class ModelAssociation
6+
include Associatable
7+
include ActiveModel::Model
8+
9+
attr_accessor :parent_association
10+
11+
def initialize(model_name, options = {})
12+
@model_name = model_name
13+
@options = options
14+
end
15+
16+
def add_child(child, block = nil)
17+
children << child
18+
child.parent_association = self
19+
child.instance_eval(&block) if block
20+
end
21+
22+
# Test compatibility method
23+
def forms
24+
@children
25+
end
26+
27+
def instance=(instance)
28+
@model = instance if instance.is_a?(model_class)
29+
end
30+
31+
def attributes(*attribute_names)
32+
options = attribute_names.extract_options!
33+
34+
delegate_accessors_to_model attribute_names, options[:prefix]
35+
36+
if options && options[:required]
37+
validates_presence_of(*attribute_names)
38+
end
39+
end
40+
alias :attribute :attributes
41+
42+
def attributes=(attributes)
43+
attributes.each do |name, value|
44+
self.public_send("#{name}=", value)
45+
end if attributes
46+
end
47+
48+
def model
49+
@model ||= model_class.new
50+
end
51+
52+
delegate :persisted?, to: :model
53+
54+
def dynamic?
55+
false
56+
end
57+
58+
def save
59+
aggregate(&:save)
60+
end
61+
62+
def valid?
63+
aggregate(&:valid?)
64+
end
65+
66+
delegate :reflect_on_association, to: :model_class
67+
68+
private
69+
def children
70+
@children ||= []
71+
end
72+
73+
def reflection
74+
parent_association.reflect_on_association(@name)
75+
end
76+
77+
def model_class
78+
@model_class ||= @model_name.to_s.singularize.camelize.constantize
79+
end
80+
81+
def delegate_accessors_to_model(names, prefix = false)
82+
names.each do |attr|
83+
self.class.delegate attr, "#{attr}=", to: :model, prefix: prefix
84+
end
85+
end
86+
87+
def aggregate
88+
yield model
89+
children.each { |c| yield c }
90+
end
91+
end
92+
end

test/forms/conference_form_test.rb

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,14 @@ def setup
1212
end
1313

1414
test "contains getter for presentations sub-form" do
15-
assert_respond_to @form.speaker, :presentations
16-
17-
presentations_form = @form.speaker.forms.first
18-
assert_instance_of ActiveForm::CollectionForm, presentations_form
19-
end
20-
21-
test "#represents? returns true if the argument matches the Form's association name, false otherwise" do
22-
presentations_form = @form.speaker.forms.first
23-
24-
assert presentations_form.represents?("presentations")
25-
assert_not presentations_form.represents?("presentation")
15+
assert @form.speaker.presentations
2616
end
2717

2818
test "main form provides getter method for collection objects" do
2919
assert_respond_to @form.speaker, :presentations
3020

31-
presentations = @form.speaker.presentations
32-
33-
presentations.each do |form|
34-
assert_instance_of ActiveForm::Form, form
35-
assert_instance_of Presentation, form.model
21+
@form.speaker.presentations.each do |model|
22+
assert_instance_of Presentation, model
3623
end
3724
end
3825

@@ -47,12 +34,10 @@ def setup
4734
test "presentations sub-form initializes the number of records specified" do
4835
presentations_form = @form.speaker.forms.first
4936

50-
assert_respond_to presentations_form, :models
51-
assert_equal 2, presentations_form.models.size
37+
assert_equal 2, presentations_form.size
5238

53-
presentations_form.each do |form|
54-
assert_instance_of ActiveForm::Form, form
55-
assert_instance_of Presentation, form.model
39+
presentations_form.each do |model|
40+
assert_instance_of Presentation, form
5641

5742
assert_respond_to form, :topic
5843
assert_respond_to form, :topic=

0 commit comments

Comments
 (0)