Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4dae810
Remove whitespace.
kaspth Nov 6, 2014
5fd52ae
Use symbol#to_proc in update_form_models.
kaspth Nov 6, 2014
147d2b0
Remove main_class class accessor.
kaspth Nov 6, 2014
9c3c964
Rewrite association class method.
kaspth Nov 6, 2014
01e4e2e
Method renaming and using more idiomatic Ruby methods.
kaspth Nov 6, 2014
fad8bcf
Rename instance level forsm to nested_forms to reduce confusion.
kaspth Nov 6, 2014
452a9c4
Rename FormCollection to CollectionForm
kaspth Nov 6, 2014
b558987
Refactored CollectionForm for readability
kaspth Nov 6, 2014
70fd90a
All #new invocations called #instance_eval, do it automatically
kaspth Nov 6, 2014
abae194
Simplify model building.
kaspth Nov 6, 2014
0383647
Extract shared logic to AbstractForm.
kaspth Nov 6, 2014
39a1b94
Let Base inherit from AbstractForm and extract more logic.
kaspth Nov 6, 2014
2436d72
Extract association finding and attributes setting to AbstractForm.
kaspth Nov 6, 2014
bc3a48e
Refactor association to follow a common structure.
kaspth Nov 6, 2014
484b707
Simplify finding a form backing a model in a template.
kaspth Nov 6, 2014
6d04e61
Redefine save relationship
kaspth Nov 6, 2014
91df85c
Rely on AbstractForm including Validtions. Move forms to AbstractForm.
kaspth Nov 6, 2014
2b61e96
Redo FormDefinition relationship with other parts.
kaspth Nov 6, 2014
d30297f
Initial stab at using association classes instead of Form and FormCol…
kaspth Nov 9, 2014
abcc2d2
Fix syntax error in test file.
kaspth Nov 27, 2014
8111d6b
private model_name and let ModelAssociation find the class.
kaspth Nov 27, 2014
fce1494
Simplify main_association and mark it private.
kaspth Nov 27, 2014
dd1eacf
Make main_model a standard writer.
kaspth Nov 27, 2014
33d8304
Save automatically wraps a transaction.
kaspth Nov 27, 2014
d8efa75
Ease test compatibility.
kaspth Nov 27, 2014
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions lib/active_form.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
require 'active_form/base'
require 'active_form/form'
require 'active_form/form_collection'
require 'active_form/collection_form'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

require 'active_form/form_definition'
require 'active_form/too_many_records'
require 'active_form/view_helpers'

module ActiveForm
Expand Down
70 changes: 70 additions & 0 deletions lib/active_form/abstract_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
module ActiveForm
class AbstractForm
include ActiveModel::Validations

attr_reader :forms

def backing_form_for(association)
forms.find { |form| form.represents?(association) }
end

def represents?(association)
association_name.to_s == association.to_s
end

def valid?
super
model.valid?

collect_errors_from(model)
aggregate_form_errors

errors.empty?
end

def submit(params)
params.each do |key, value|
if nested_params?(value)
assign_association_attributes(key, value)
else
send("#{key}=", value)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could this be public_send?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably could. I'll try it out.

end
end
end

def models
self
end

private
def nested_params?(params)
params.is_a?(Hash)
end

def reject_form?(params)
params.all? { |k, v| k == '_destroy' || v.blank? }
end

def extract_association_name(association)
$1.to_sym if /\A(.+)_attributes\z/ =~ association
end

def assign_association_attributes(association, attributes)
name = extract_association_name(association)
backing_form_for(name).submit(attributes)
end

def aggregate_form_errors
forms.each do |form|
form.valid?
collect_errors_from(form)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this call needed here if errors are already aggregated as part of calling valid? at https://github.com/rails/activeform/pull/18/files#diff-f11f813344af58a51b99ef80ccdebb97R19

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're not collecting the same errors.

collect_errors_from(model) vs. collect_errors_from(form) where form is one of the sub forms of the form (yes, I don't like that either).

end
end

def collect_errors_from(model)
model.errors.each do |attribute, error|
errors.add(attribute, error)
end
end
end
end
28 changes: 28 additions & 0 deletions lib/active_form/associatable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module ActiveForm
concern :Associatable do
def association(name, options = {})
association = child_class_for(options).new(name, options)
association_scope.add_child(association, Proc.new)
define_association_accessors(name, association)
end

def association_scope
self
end

private
def child_class_for(options)
options.key?(:records) ? CollectionAssociation : ModelAssociation
end

def define_association_accessors(name, association)
class_eval do
define_method(name) { association }
define_method("#{name}=") { |i| association.instance = i }
define_method("#{name}_attributes=") do |attrs|
association.attributes = attrs
end
end
end
end
end
173 changes: 38 additions & 135 deletions lib/active_form/base.rb
Original file line number Diff line number Diff line change
@@ -1,163 +1,66 @@
require 'active_form/model_association'
require 'active_form/collection_association'
require 'active_form/associatable'

module ActiveForm
class Base
include ActiveModel::Model
extend ActiveModel::Callbacks

define_model_callbacks :save, only: [:after]
after_save :update_form_models
extend Associatable

delegate :persisted?, :to_model, :to_key, :to_param, :to_partial_path, to: :model
attr_reader :model, :forms

def initialize(model)
@model = model
@forms = []
populate_forms
end

def submit(params)
params.each do |key, value|
if nested_params?(value)
fill_association_with_attributes(key, value)
else
send("#{key}=", value)
end
# SignupForm.new(user: User.find(params[:user_id]))
def initialize(models = nil)
if models.respond_to?(:each)
assign_attributes(models)
else
main_association.instance = models
end
end

def get_model(assoc_name)
form = find_form_by_assoc_name(assoc_name)
form.get_model(assoc_name)
end

def save
if valid?
run_callbacks :save do
ActiveRecord::Base.transaction do
model.save
end
end
else
false
end
main_association.save if valid?
end

def valid?
super
model.valid?

collect_errors_from(model)
aggregate_form_errors

errors.empty?
def submit(params)
assign_attributes(params)
end

delegate :id, :persisted?, :to_model, :to_partial_path, to: :main_association

class << self
attr_accessor :main_class
attr_writer :main_model
delegate :reflect_on_association, to: :main_class
delegate :attributes, :association_scope, to: :main_association

def attributes(*names)
options = names.pop if names.last.is_a?(Hash)
attr_writer :main_model

if options && options[:required]
validates_presence_of *names
private
def main_association
@@main_association ||= ModelAssociation.new(main_model)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be done at class-level and main_association becoming cattr_reader?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you mean by class-level? It already is.

Earlier versions of this had it as a mattr_reader (same deal as what you're suggesting).

private # at instance level
  mattr_reader(:main_association) { ModelAssociation.new(main_model) }

The problem is that the block is run when Ruby evaluates the class definition at which point main_model isn't guaranteed to be set. Maybe we could fix it with some ifs strewn about, but that doesn't sit right with me either.

end

names.each do |attribute|
delegate attribute, "#{attribute}=", to: :model
def main_model
@main_model ||= name.sub(/Form$/, '')
end
end

def main_class
@main_class ||= main_model.to_s.camelize.constantize
end

def main_model
@main_model ||= name.sub(/Form$/, '').singularize
end

alias_method :attribute, :attributes

def association(name, options={}, &block)
macro = main_class.reflect_on_association(name).macro

case macro
when :has_one, :belongs_to
declare_form(name, &block)
when :has_many
declare_form_collection(name, options, &block)
end

define_method("#{name}_attributes=") {}
end

def declare_form_collection(name, options={}, &block)
forms << FormDefinition.new(name, block, options)
class_eval("def #{name}; @#{name}.models; end")
end

def declare_form(name, &block)
forms << FormDefinition.new(name, block)
attr_reader name
end

def forms
@forms ||= []
end
end

private

def update_form_models
forms.each do |form|
form.update_models
def respond_to_missing?(meth, include_private = false)
main_association.respond_to?(meth)
end
end

def populate_forms
self.class.forms.each do |definition|
definition.parent = model
nested_form = definition.to_form
forms << nested_form
name = definition.assoc_name
instance_variable_set("@#{name}", nested_form)
def method_missing(meth, *args, &block)
if main_association.respond_to?(meth)
main_association.send(meth, *args, &block)
else
super
end
end
end

def nested_params?(value)
value.is_a?(Hash)
end

ATTRIBUTES_KEY_REGEXP = /^(.+)_attributes$/

def find_association_name_in(key)
ATTRIBUTES_KEY_REGEXP.match(key)[1]
end

def fill_association_with_attributes(association, attributes)
assoc_name = find_association_name_in(association).to_sym
form = find_form_by_assoc_name(assoc_name)

form.submit(attributes)
end

def find_form_by_assoc_name(assoc_name)
forms.select { |form| form.represents?(assoc_name) }.first
end

def aggregate_form_errors
forms.each do |form|
form.valid?
collect_errors_from(form)
def main_association
@@main_association
end
end

def collect_errors_from(validatable_object)
validatable_object.errors.each do |attribute, error|
errors.add(attribute, error)
def assign_attributes(attributes)
attributes.each do |key, value|
self.public_send("#{key}=", value)
end if attributes
end
end
end

end
end
37 changes: 37 additions & 0 deletions lib/active_form/collection_association.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
require 'active_form/model_association'

module ActiveForm
class CollectionAssociation < ModelAssociation
def initialize(model_name, options)
super
@instances = []
end

def dynamic?
true # means we can add more instances in a form
end

def records
size
end

delegate :each, :size, :[], to: :@instances

# Map model attributes by key to association
# doghouse_attributes: { '1' => { name: 'McDiniis' }, '2' => { name: 'McDunuus' } }
def attributes=(model_attributes)
model_attributes.each do |model_id, attrs|
fetch_instance(model_id.to_i).attributes = attrs
end
end

def build_instance
model_class.new.tap { |i| @instances << i }
end

private
def fetch_instance(id)
@instances[id] || build_instance
end
end
end
Loading