Class: FormObject::Base

Inherits:
Object
  • Object
show all
Includes:
ActiveModel::AttributeAssignment, ActiveModel::Model, MultiModelWizard::DynamicValidation
Defined in:
lib/form_object/base.rb

Constant Summary collapse

ATTRIBUTES =

These are the default atrributes for all form objects

%i[
  current_step
  new_form
]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from MultiModelWizard::DynamicValidation

#valid_attribute?, #validate_attribute_with_message

Constructor Details

#initializeBase

Returns a new instance of Base.



48
49
50
51
52
53
54
# File 'lib/form_object/base.rb', line 48

def initialize
  @models = []
  @dynamic_models = []
  @multiple_instance_models = []
  @extra_attributes = []
  @new_form = true
end

Instance Attribute Details

#dynamic_modelsObject (readonly)

Returns the value of attribute dynamic_models.



39
40
41
# File 'lib/form_object/base.rb', line 39

def dynamic_models
  @dynamic_models
end

#extra_attributesObject (readonly)

Returns the value of attribute extra_attributes.



39
40
41
# File 'lib/form_object/base.rb', line 39

def extra_attributes
  @extra_attributes
end

#modelsObject (readonly)

Returns the value of attribute models.



39
40
41
# File 'lib/form_object/base.rb', line 39

def models
  @models
end

#multiple_instance_modelsObject (readonly)

Returns the value of attribute multiple_instance_models.



39
40
41
# File 'lib/form_object/base.rb', line 39

def multiple_instance_models
  @multiple_instance_models
end

Class Method Details

.create_form {|instance| ... } ⇒ Object

Note:

This is how all forms should be instantiated

Creates a new instance of the form object with all models and configuration

Parameters:

  • block (Block)

    this yields to a block with an instance of its self

Yields:

  • (instance)

Returns:

  • form object [FormObjects::Base]



18
19
20
21
22
23
# File 'lib/form_object/base.rb', line 18

def create_form
  instance = new
  yield(instance)
  instance.send(:init_attributes)
  instance
end

.form_stepsObject

Note:

This method needs to be overridden with an array of symbols

Needs to be overridden by child class.

Raises:

  • (NotImplementedError)


28
29
30
# File 'lib/form_object/base.rb', line 28

def form_steps
  raise NotImplementedError
end

Instance Method Details

#add_dynamic_model(prefix:, model:) ⇒ Object

Note:

model can be an instance or the class

This method should be used when instantiating a new object. It is used to add dynamic models to the form object. Dynamic models are models that share a base class and are of the same family but can vary depending on child class Example: A Truck model, Racecar model, and a Semi model who all have a base class of Vehicle This method allows your form to recieve any of these models and keep the UI and method calls the same.

Parameters:

  • prefix

    is a string that you want to hcae in front of all your extra attributes [String]

  • model

    class or model instance [ActiveRecord] this is the class that you want these extra attributes to be related to

Returns:

  • array of all the dynamic models [Array]

Raises:

  • (ArgumentError)


231
232
233
234
235
236
# File 'lib/form_object/base.rb', line 231

def add_dynamic_model(prefix:, model:)
  raise ArgumentError, 'Prefix must be a String' unless prefix.is_a?(String)
  model_is_activerecord?(model)

  @dynamic_models << { prefix: prefix, model: instance_of_model(model) }
end

#add_extra_attributes(prefix: nil, attributes:, model: nil) ⇒ Object

Note:

to have these attributes validated using the #validate_attributes method you must pass in a model

Note:

model can be an instance or the class

Note:

attributes should be an array of symbols

This method should be used when instantiating a new object. It is used to add extra attributes to the form object that may not be accessible from the models passed in.

Parameters:

  • prefix (defaults to: nil)

    is a string that you want to hcae in front of all your extra attributes [String]

  • attributes

    should be an array of symbols [Array]

  • model (defaults to: nil)

    class or model instance [ActiveRecord] this is the class that you want these extra attributes to be related to

Returns:

  • array of all the extra attributes [Array]

Raises:

  • (ArgumentError)


207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/form_object/base.rb', line 207

def add_extra_attributes(prefix: nil, attributes:, model: nil )
  if prefix.present?
    raise ArgumentError, 'Prefix must be a String' unless prefix.is_a?(String)
  end
  raise ArgumentError, 'All attributes must be Symbols' unless attributes.all? { |x| x.is_a?(Symbol) }
  model_is_activerecord?(model)

  hash = { 
          prefix: prefix || model_prefix(model), 
          attributes: attributes,
          model: model 
        } 
  extra_attributes << hash
end

#add_model(model, prefix: nil) ⇒ Object

The add_model is an instance method that is used for adding ActiveRecord models

Parameters:

  • prefix (defaults to: nil)

    is optional and is used to change the prefix of the models attributes [String] the prefix defaults to the model name

  • model

    class or instance [ActiveRecord] this is the same model that the instances should be

Returns:

  • array of all the models [Array]



262
263
264
265
266
267
268
269
270
271
# File 'lib/form_object/base.rb', line 262

def add_model(model, prefix: nil)
  if prefix.present?
    raise ArgumentError, 'Prefix must be a String' unless prefix.is_a?(String)
  end
  model_is_activerecord?(model)

  hash = { prefix: prefix || model_prefix(model), model: instance_of_model(model) }

  @models << hash
end

#add_multiple_instance_model(attribute_name: nil, model:, instances: []) ⇒ Object

The add_multiple_instance_model is an instance method that is used for adding ActiveRecord models multiple instance models are models that would be child models in a has_many belongs_to relationship EXAMPLE: Car has_many parts In this example the multiple instance would be parts because a car can have an infinte number of parts

Parameters:

  • name

    of the form object atrribute to retrieve these multiple instances of a model [String]

  • model

    class or instance [ActiveRecord] this is the same model that the instances should be

  • instances (defaults to: [])

    is an array of ActiveRecord models [Array] these are usually the has_many relation instances

Returns:

  • array of all the multiple instance models models [Array]



246
247
248
249
250
251
252
253
254
255
256
# File 'lib/form_object/base.rb', line 246

def add_multiple_instance_model(attribute_name: nil, model:, instances: [])
  if attribute_name.present? 
    raise ArgumentError, 'Attribute name must be a String' unless attribute_name.is_a?(String)
  end
  model_is_activerecord?(model)

  attribute_name = attribute_name || model_prefix(model, pluralize: true)
  hash = { attribute_name: attribute_name, model: instance_of_model(model), instances: instances }

  @multiple_instance_models << hash
end

#as_jsonObject

This method is used to turn the attributes of the form into a stringified object that resembles json

Returns:

  • form atttrubytes as a strigified hash [Hash]



275
276
277
278
279
# File 'lib/form_object/base.rb', line 275

def as_json
  instance_variables.each_with_object({}) do |var, obj|
      obj[var.to_s.gsub('@','')] = instance_variable_get(var)
  end.stringify_keys
end

#attribute_keysObject

Returns an list of all attribute names as symbols



99
100
101
# File 'lib/form_object/base.rb', line 99

def attribute_keys
  ATTRIBUTES
end

#attributesObject

Note:

This method will only return a key value pair for attributes that are not nil

Note:

It ignores the models arrays, errors, etc.

Gets all of the form objects present attributes and returns them in a hash



84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/form_object/base.rb', line 84

def attributes
  hash = ActiveSupport::HashWithIndifferentAccess.new
  instance_variables.each_with_object(hash) do |attribute, object|
    next if %i[@errors @validation_context 
              @models @dynamic_models
              @multiple_instance_models
              @extra_attributes].include?(attribute)

    key = attribute.to_s.gsub('@','').to_sym
    object[key] = self.instance_variable_get(attribute)
  end
end

#attributes_for(model) ⇒ Object

Note:

If you dont pass a model to extra attributes they will not show up here

Note:

attributes for model does not work for multiple_instance_models

Returns all of the attributes for a model

Parameters:

  • model

    class [ActiveRecord]



108
109
110
111
112
113
114
115
116
117
118
# File 'lib/form_object/base.rb', line 108

def attributes_for(model)
  model_is_activerecord?(model)

  hash = ActiveSupport::HashWithIndifferentAccess.new
  
  attribute_lookup.each_with_object(hash) do |value, object|
    lookup = value[1]
    form_attribute = value[0]
    object[lookup[:original_method]] = attributes[form_attribute] if lookup[:model] == model.name
  end
end

#createObject

Note:

this should be done in an ActiveRecord transaction block

Create all of the models from the form object and their realations EXAMPLE: def create

created = false
begin
  ActiveRecord::Base.transaction do
    car = Car.new(attributes_for(Car))
    car.parts = car_parts
    car.save!
  end
  created = true
rescue StandardError => err
  return created       
end
created

end

Returns:

  • returns true if the transaction was successfule and false if not



317
318
319
# File 'lib/form_object/base.rb', line 317

def create
  true
end

#first_step?Boolean

Boolean method returns if the object is on the first step or not

Returns:

  • (Boolean)


73
74
75
76
77
78
# File 'lib/form_object/base.rb', line 73

def first_step?
  return false if current_step.nil?
  return true unless current_step.to_sym

  form_steps.first == current_step.to_sym
end

#invalidate!(error_msg = nil) ⇒ Object

Add a custom error message and makes the object invalid



66
67
68
69
# File 'lib/form_object/base.rb', line 66

def invalidate!(error_msg = nil)
  errors.add(:associated_model, error_msg) unless error_msg.nil?
  errors.add(:associated_model, 'could not be properly save')
end

#persist!Object

Note:

the create method and update method that this method use will have to manually implemented by the child class

Note:

the create and update need to return a boolean based on their success or failure to udpate

Persist is used to update or create all of the models from the form object

Returns:

  • the output of the create or update method [Sybmbol]



295
296
297
# File 'lib/form_object/base.rb', line 295

def persist!
  new_form ? create : update
end

#required_for_step?(step) ⇒ Boolean

This method is used to help validate the form object. Use required for step to do step contional validations of attributes

Parameters:

  • step

    is used to compare the current step [Symbol]

Returns:

  • (Boolean)

    a true or false value if the step give is equal to or smaller in the form_steps [Boolean]



284
285
286
287
288
289
# File 'lib/form_object/base.rb', line 284

def required_for_step?(step)
  # note: this line is specific if using the wicked gem
  return true if current_step == 'wicked_finish' || current_step.nil?

  form_steps.index(step.to_sym) <= form_steps.index(current_step.to_sym)
end

#saveObject

Note:

This method is here because the Wicked gem automagically runs this methdo to move to the next step

Note:

This method needs return a boolean after attempting to create the records

Checks if the form and its attributes are valid



60
61
62
# File 'lib/form_object/base.rb', line 60

def save
  valid?
end

#set_attributes(attributes_hash) ⇒ Object

Note:

If you give it a key that is not a defined method of the class it will simply move on to the next

Takes a hash of key value pairs and assigns those attributes values to its corresponding methods/instance variables

Parameters:

  • hash (Hash)


125
126
127
128
129
130
131
132
133
134
# File 'lib/form_object/base.rb', line 125

def set_attributes(attributes_hash)
  attributes_hash.each do |pair|
    key = "#{pair[0].to_s}="
    value = pair[1]
    self.send(key, value)
  rescue NoMethodError
    next 
  end
  self
end

#updateObject

Note:

this should be done in an ActiveRecord transaction block

Update all of the models from the form object and their realations EXAMPLE: def update

updated = false
begin
  ActiveRecord::Base.transaction do
    car = Car.find(car_id)
    car.attributes = attributes_for(Car)
    car.parts = car_parts
    car.save!
  end
  updated = true
rescue StandardError => err
  return updated       
end
updated

end

Returns:

  • returns true if the transaction was successfule and false if not [Boolean]



340
341
342
# File 'lib/form_object/base.rb', line 340

def update
  true
end

#validate_attributes(*attributes) ⇒ Object

Note:

this method uses a special method #validate_attribute_with_message this method comes from

Note:

this method will add the model errors to your object instance and invalidate it.

Given n number of atrribute names this method will iterate over each attribute and validate that attribute using the model that the attribute orignated from to validate it the an DynamicValidation module and is not built in with ActiveRecord The model errors are using the original attribute names

Parameters:

  • symbol

    names of instance methods [Symbol]

Raises:

  • (ArgumentError)


145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/form_object/base.rb', line 145

def validate_attributes(*attributes)
  raise ArgumentError, 'Attributes must be a Symbol' unless attributes.all? { |x| x.is_a?(Symbol) }

  attributes.map do |single_attr|
    unless respond_to?(single_attr)
      raise FormObject::AttributeNameError, "#{single_attr.to_s} is not a valid attribute of this form object"
    end

    original_attribute = attribute_lookup.dig(single_attr.to_sym, :original_method)
    attribute_hash = { "#{original_attribute}": send(single_attr) }
    instance = attribute_lookup.dig(single_attr.to_sym, :model)&.constantize&.new
    instance&.send("#{original_attribute}=", send(single_attr))
    next if instance.nil?

    validation = validate_attribute_with_message(attribute_hash, model_instance: instance)
    if validation.valid.eql?(false)
      validation.messages.each { |err| errors.add(single_attr.to_sym, err) }
    end

    validation.valid
  end.compact.all?(true)
end

#validate_multiple_instance_model(attribute) ⇒ Object

Note:

this method uses a special method #validate_attribute_with_message this method comes from

Note:

this method will add the model errors to your object instance and invalidate it.

Much like #validate_attributes this method will validate the attributes of a instance model using the original model the an DynamicValidation module and is not built in with ActiveRecord The model errors are using the original attribute names

Parameters:

  • symbol

    name of instance method [Symbol]

Raises:

  • (ArgumentError)


176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/form_object/base.rb', line 176

def validate_multiple_instance_model(attribute)
  raise ArgumentError, 'Attribute must be a Symbol' unless attribute.is_a?(Symbol)
  unless respond_to?(attribute)
    raise FormObject::AttributeNameError, "#{attribute.to_s} is not a valid attribute of this form object"
  end

  model_instance = attribute_lookup.dig(attribute.to_sym, :model)&.constantize&.new
  return nil if model_instance.nil?

  send(attribute).map do |hash_instance|
    hash_instance.map do |key, value|
      model_instance&.send("#{key}=", value)

      validation = validate_attribute_with_message({ "#{key}": value }, model_instance: model_instance )
      if validation.valid.eql?(false)
        validation.messages.each { |err| errors.add(attribute.to_sym, err) }
      end
      validation.valid
    end
  end.compact.all?(true)
end