-
Notifications
You must be signed in to change notification settings - Fork 35
Rethink Active Form's structure #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4dae810
5fd52ae
147d2b0
9c3c964
01e4e2e
fad8bcf
452a9c4
b558987
70fd90a
abae194
0383647
39a1b94
2436d72
bc3a48e
484b707
6d04e61
91df85c
2b61e96
d30297f
abcc2d2
8111d6b
fce1494
dd1eacf
33d8304
d8efa75
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could this be There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They're not collecting the same errors.
|
||
end | ||
end | ||
|
||
def collect_errors_from(model) | ||
model.errors.each do |attribute, error| | ||
errors.add(attribute, error) | ||
end | ||
end | ||
end | ||
end |
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 |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this be done at class-level and There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 |
||
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 |
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍