FormObj
Form Object allows to define a complicated data structure (using nesting, arrays) and use it with Rails-compatible form builders. A Form Object can be serialized and deserialized to a model and/or a hash.
Compatibility and Dependency Requirements
Ruby: 2.2.8+ ActiveSupport: 3.2+ ActiveModel: 3.2+
The gem is tested against all ruby versions and all versions of its dependencies except ActiveSupport and ActiveModel version 4.0.x because they requires Minitest 4 which is not compatible with Minitest 5.
This gem follows Semantic Versioning.
Installation
Add this line to your application's Gemfile:
gem 'form_obj'And then execute:
$ bundle
Or install it yourself as:
$ gem install form_obj
Usage
Class FormObj::Struct allows to describe complicated data structure, to update it with update_attributes method and to get its hash representation with to_hash method.
Class FormObj::Form inherits from FormObj::Struct and adds form builder compatibility and includes ActiveModel validations.
Module FormObj::ModelMapper being included into FormObj::Form descendants allows to map a form object to a model
in order to be able to exchange attributes value between them:
- load attributes value from the model,
- sync attributes value to the model,
- represent a form object as a model hash (similar to the
to_hashmethod but using the model attributes name) and - copy errors from the model(s) to the from object.
Table of Contents
-
FormObj::Struct- Nesting
FormObj::Struct - Array of
FormObj::Struct - Serialize
FormObj::Structto Hash
- Nesting
-
FormObj::Form-
FormObj::FormValidation -
FormObj::FormPersistence - Non-Existent Attributes in
FormObj::Formupdate_attributesDo Not Raise By Default - Delete from Array of
FormObj::Formviaupdate_attributesmethod - Using
FormObj::Formin Form Builder
-
-
FormObj::ModelMapper-
load_from_model- Initialize Form Object from Model -
load_from_models- Initialize Form Object from Few Models - Do Not Map Certain Attribute
- Do Not Map Certain Attribute For Reading From Model
- Do Not Map Certain Attribute For Writing to Model
- Map Nested Form Objects
- Map Nested Form Object to Parent Level Model
- Map Nested Form Objects to A Hash Model
- Map Array of Nested Form Objects
- Map Array of Nested Form Objects to Nested Array of Nested Models
- Default Implementation of Loading of Array of Models
- Custom Implementation of Loading of Array of Models
- Sync Form Object to Model(s)
- Sync Array of Nested Form Objects to Model(s)
- Sync Array of Nested Form Objects to
ActiveRecord-like Models - Customize Sync to Array of Models
- Model Validation and Persistence
- Copy Model Validation Errors into Form Object
- Serialize Form Object to Model Hash
-
- Rails Example
- Reference Guide:
attribute's paremeters-
FormObj::Struct- Parameter
array - Parameter
class - Parameter
default - Parameter
primary_key
- Parameter
FormObj::Form-
FormObj::Formwith includedFormObj::ModelMapper- Parameter
model - Parameter
model_attribute - Parameter
model_class - Parameter
model_hash - Parameter
model_nesting - Parameter
read_from_model - Parameter
write_to_model
- Parameter
-
1. FormObj::Struct
Inherit your class from FormObj::Struct and define its attributes.
class Team < FormObj::Struct
attribute :name
attribute :year
endRead and write attribute values using dot-notation.
team = Team.new # => #<Team name: nil, year: nil>
team.name = 'Ferrari' # => "Ferrari"
team.year = 1950 # => 1950
team.name # => "Ferrari"
team.year # => 1950Initialize attributes in constructor.
team = Team.new(
name: 'Ferrari',
year: 1950
) # => #<Team name: "Ferrari", year: 1950>
team.name # => "Ferrari"
team.year # => 1950Update attributes using update_attributes method.
team.update_attributes(
name: 'McLaren',
year: 1966
) # => #<Team name: "McLaren", year: 1966>
team.name # => "McLaren"
team.year # => 1966In both cases (initialization or update_attributes) hash is transformed to HashWithIndifferentAccess before applying its values
so it doesn't matter whether keys are symbols or strings.
team.update_attributes(
'name' => 'Ferrari',
'year' => 1950
) # => #<Team name: "Ferrari", year: 1950>Attribute value stays unchanged if hash doesn't have corresponding key.
team = Team.new(name: 'Ferrari') # => #<Team name: "Ferrari", year: nil>
team.update_attributes(year: 1950) # => #<Team name: "Ferrari", year: 1950>Exception UnknownAttributeError is raised if there is key that doesn't correspond to any attribute.
Team.new(name: 'Ferrari', a: 1) # => FormObj::UnknownAttributeError: a
Team.new.update_attributes(a: 1) # => FormObj::UnknownAttributeError: aUse parameter raise_if_not_found: false in order to avoid exception and silently skip unknown key in the hash.
team = Team.new({
name: 'Ferrari',
a: 1
}, raise_if_not_found: false) # => #<Team name: "Ferrari", year: nil>
team.update_attributes({
name: 'McLaren',
a: 1
}, raise_if_not_found: false) # => #<Team name: "McLaren", year: nil>Define default attribute value using default parameter.
Use Proc to calculate default value dynamically.
Proc is calculated only once at the moment of first access to attribute.
Proc receives two arguments:
-
struct_class- class (!!! not an instance) where attribute is defined -
attribute- internal representation of attribute
class Team < FormObj::Struct
attribute :name, default: 'Ferrari'
attribute :year, default: ->(struct_class, attribute) { struct_class.default_year(attribute) }
def self.default_year(attribute)
"#{attribute.name} = 1950"
end
end
team = Team.new # => #<Team name: "Ferrari", year: "year = 1950">
team.name # => "Ferrari"
team.year # => "year = 1950" 1.1. Nesting FormObj::Struct
Use blocks to define nested structs.
class Team < FormObj::Struct
attribute :name
attribute :year
attribute :car do
attribute :code
attribute :driver
attribute :engine do
attribute :power
attribute :volume
end
end
endOr explicitly define nested struct classes.
class Engine < FormObj::Struct
attribute :power
attribute :volume
end
class Car < FormObj::Struct
attribute :code
attribute :driver
attribute :engine, class: Engine
end
class Team < FormObj::Struct
attribute :name
attribute :year
attribute :car, class: Car
endRead and write attribute values using dot-notation.
team = Team.new # => #<Team name: nil, year: nil, car: #< code: nil, driver: nil, engine: #< power: nil, volume: nil>>>
team.name = 'Ferrari' # => "Ferrari"
team.year = 1950 # => 1950
team.car.code = '340 F1' # => "340 F1"
team.car.driver = 'Ascari' # => "Ascari"
team.car.engine.power = 335 # => 335
team.car.engine.volume = 4.1 # => 4.1
team.name # => "Ferrari"
team.year # => 1950
team.car.code # => "340 F1"
team.car.driver # => "Ascari"
team.car.engine.power # => 335
team.car.engine.volume # => 4.1Initialize nested struct using nested hash.
team = Team.new(
name: 'Ferrari',
year: 1950,
car: {
code: '340 F1',
driver: 'Ascari',
engine: {
power: 335,
volume: 4.1,
}
}
) # => #<Team name: "Ferrari", year: 1950, car: #< code: "340 F1", driver: "Ascari", engine: #< power: 335, volume: 4.1>>>
team.name # => "Ferrari"
team.year # => 1950
team.car.code # => "340 F1"
team.car.driver # => "Ascari"
team.car.engine.power # => 335
team.car.engine.volume # => 4.1Update nested struct using nested hash.
team.update_attributes(
name: 'McLaren',
year: 1966,
car: {
code: 'M2B',
driver: 'Bruce McLaren',
engine: {
power: 300,
volume: 3.0
}
}
) # => #<Team name: "McLaren", year: 1966, car: #< code: "M2B", driver: "Bruce McLaren", engine: #< power: 300, volume: 3.0>>>
team.name # => "McLaren"
team.year # => 1966
team.car.code # => "M2B"
team.car.driver # => "Bruce McLaren"
team.car.engine.power # => 300
team.car.engine.volume # => 3.0Use hash to define default value of nested struct defined with block.
class Team < FormObj::Struct
attribute :car, default: { code: '340 F1', driver: 'Ascari' } do
attribute :code
attribute :driver
end
end
team = Team.new # => #<Team car: #< code: "340 F1", driver: "Ascari">>
team.car.code # => "340 F1"
team.car.driver # => "Ascari" Use hash or struct instance to define default value of nested struct defined with class.
class Car < FormObj::Struct
attribute :code
attribute :driver
end
class Team < FormObj::Struct
attribute :car, class: Car, default: Car.new(code: '340 F1', driver: 'Ascari')
end
team = Team.new # => #<Team car: #<Car code: "340 F1", driver: "Ascari">>
team.car.code # => "340 F1"
team.car.driver # => "Ascari" The struct instance class should correspond to nested attribute class!
class Team < FormObj::Struct
attribute :car, class: Car, default: 36
end
Team.new # => FormObj::WrongDefaultValueClassError: FormObj::WrongDefaultValueClassError 1.2. Array of FormObj::Struct
Use parameter array: true in order to define an array of nested structs.
Define primary_key so that update_attribute method be able to distinguish
whether to update existing array element or create a new one.
By default attribute id is considered to be a primary key.
class Team < FormObj::Struct
attribute :name
attribute :year
attribute :cars, array: true do
attribute :code, primary_key: true # <- primary key is specified on attribute level
attribute :driver
attribute :engine do
attribute :power
attribute :volume
end
end
endor
class Team < FormObj::Struct
attribute :name
attribute :year
attribute :cars, array: true, primary_key: :code do # <- primary key is specified on struct level
attribute :code
attribute :driver
attribute :engine do
attribute :power
attribute :volume
end
end
endor
class Engine < FormObj::Struct
attribute :power
attribute :volume
end
class Car < FormObj::Struct
attribute :code, primary_key: true # <- primary key is specified on attribute level
attribute :driver
attribute :engine, class: Engine
end
class Team < FormObj::Struct
attribute :name
attribute :year
attribute :cars, array: true, class: Car
endor
class Engine < FormObj::Struct
attribute :power
attribute :volume
end
class Car < FormObj::Struct
attribute :code
attribute :driver
attribute :engine, class: Engine
end
class Team < FormObj::Struct
attribute :name
attribute :year
attribute :cars, array: true, class: Car, primary_key: :code # <- primary key is specified on struct level
endRead and write attribute values using dot-notation.
Add new elements in the array using method create.
team = Team.new # => #<Team name: nil, year: nil, cars: []>
team.name = 'Ferrari' # => "Ferrari"
team.year = 1950 # => 1950
team.cars.size # => 0
car1 = team.cars.create # => #< code: nil, driver: nil, engine: #< power: nil, volume: nil>>
team.cars.size # => 1
car1.code = '340 F1' # => "340 F1"
car1.driver = 'Ascari' # => "Ascari"
car1.engine.power = 335 # => 335
car1.engine.volume = 4.1 # => 4.1
car2 = team.cars.create # => #< code: nil, driver: nil, engine: #< power: nil, volume: nil>>
team.cars.size # => 2
car2.code = '275 F1' # => "275 F1"
car2.driver = 'Villoresi' # => "Villoresi"
car2.engine.power = 330 # => 330
car2.engine.volume = 3.3 # => 3.3
team.name # => "Ferrari"
team.year # => 1950
team.cars[0].code # => "340 F1"
team.cars[0].driver # => "Ascari"
team.cars[0].engine.power # => 335
team.cars[0].engine.volume # => 4.1
team.cars[1].code # => "275 F1"
team.cars[1].driver # => "Villoresi"
team.cars[1].engine.power # => 330
team.cars[1].engine.volume # => 3.3Initialize attributes using hash with array of hashes.
team = Team.new(
name: 'Ferrari',
year: 1950,
cars: [
{
code: '340 F1',
driver: 'Ascari',
engine: {
power: 335,
volume: 4.1,
}
}, {
code: '275 F1',
driver: 'Villoresi',
engine: {
power: 330,
volume: 3.3,
}
}
],
) # => #<Team name: "Ferrari", year: 1950, cars: [#< code: "340 F1", driver: "Ascari", engine: #< power: 335, volume: 4.1>>, #< code: "275 F1", driver: "Villoresi", engine: #< power: 330, volume: 3.3>>]>
team.name # => "Ferrari"
team.year # => 1950
team.cars[0].code # => "340 F1"
team.cars[0].driver # => "Ascari"
team.cars[0].engine.power # => 335
team.cars[0].engine.volume # => 4.1
team.cars[1].code # => "275 F1"
team.cars[1].driver # => "Villoresi"
team.cars[1].engine.power # => 330
team.cars[1].engine.volume # => 3.3Update attributes using hash with array of hashes.
team.update_attributes(
name: 'McLaren',
year: 1966,
cars: [
{
code: '275 F1',
driver: 'Bruce McLaren',
engine: {
volume: 3.0
}
}, {
code: 'M7A',
driver: 'Denis Hulme',
engine: {
power: 415,
}
}
],
) # => #<Team name: "McLaren", year: 1966, cars: [#< code: "M2B", driver: "Bruce McLaren", engine: #< power: nil, volume: 3.0>>, #< code: "M7A", driver: "Denis Hulme", engine: #< power: 415, volume: nil>>]>
team.name # => "McLaren"
team.year # => 1966
team.cars[0].code # => "275 F1"
team.cars[0].driver # => "Bruce McLaren"
team.cars[0].engine.power # => 330 - this value was not updated in :update_attributes method
team.cars[0].engine.volume # => 3.0
team.cars[1].code # => "M7A"
team.cars[1].driver # => "Denis Hulme"
team.cars[1].engine.power # => 415
team.cars[1].engine.volume # => nil - this value is nil because this car was created in :updated_attributes methodUse primary_key method on class to get primary key attribute name.
Use primary_key and primary_key= method on instance to get and set primary key attribute value.
Team.primary_key # => :id - By default primary key is :id even if there is no such attribute
Car.primary_key # => :code
team.cars.first.primary_key # => "275 F1"
team.cars.last.primary_key # => "M7A"update_attributes compares present elements in the array with new elements in hash by using primary key.
By default update_attributes:
- calls attribute setter under hood to update attribute value of present elements,
- calls
FormObj::Structconstructor to create all new elements (that exists in the hash but absent in the present array), - calls
delete_ifto delete all removed elements (that exists in the present array but absent in the hash).
Default behaviour could be easily redefined by overwriting corresponding methods.
class MyStruct < FormObj::Struct
class Array < FormObj::Struct::Array
private
def create_item(hash, raise_if_not_found:)
puts "Create new element from #{hash}"
super
end
def delete_items(ids)
each do |item|
if ids.include? item.primary_key
item._destroy = true
puts "Mark item #{item.primary_key} for deletion"
end
end
end
end
def self.array_class
MyStruct::Array
end
def self.nested_class
MyStruct
end
private
def update_attribute(attribute, new_value)
puts "Update attribute :#{attribute.name} value from #{send(attribute.name)} to #{new_value}"
super
end
end
class Team < MyStruct
attribute :name
attribute :year
attribute :cars, array: true, primary_key: :code do
attribute :code
attribute :driver
attribute :engine do
attribute :power
attribute :volume
end
attr_accessor :_destroy
end
end
team = Team.new(name: 'Ferrari', cars: [{ code: '340 F1' }, { code: '275 F1' }])
# => Update attribute :name value from to Ferrari
# => Create new element from {"code"=>"340 F1"}
# => Update attribute :code value from to 340 F1
# => Create new element from {"code"=>"275 F1"}
# => Update attribute :code value from to 275 F1
# => => #<Team name: "Ferrari", year: nil, cars: [#< code: "340 F1", driver: nil, engine: #< power: nil, volume: nil>>, #< code: "275 F1", driver: nil, engine: #< power: nil, volume: nil>>]>
team.update_attributes(cars: [{ code: '275 F1' }])
# => Update attribute :code value from 275 F1 to 275 F1
# => Mark item 340 F1 for deletion
# => => #<Team name: "Ferrari", year: nil, cars: [#< code: "340 F1", driver: nil, engine: #< power: nil, volume: nil>>, #< code: "275 F1", driver: nil, engine: #< power: nil, volume: nil>>]> Use array of hashes to define default array of nested structs defined with block.
class Team < FormObj::Struct
attribute :cars, array: true, default: [{ code: '340 F1', driver: 'Ascari' }, { code: '275 F1', driver: 'Villoresi' }] do
attribute :code
attribute :driver
end
end
team = Team.new # => #<Team cars: [#< code: "340 F1", driver: "Ascari">, #< code: "275 F1", driver: "Villoresi">]>
team.cars.size # => 2
team.cars[0].code # => "340 F1"
team.cars[0].driver # => "Ascari"
team.cars[1].code # => "275 F1"
team.cars[1].driver # => "Villoresi" Use array of hashes or struct instances to define default array of nested structs defined with class.
class Car < FormObj::Struct
attribute :code
attribute :driver
end
class Team < FormObj::Struct
attribute :cars, class: Car, array: true, default: [Car.new(code: '340 F1', driver: 'Ascari'), { code: '275 F1', driver: 'Villoresi' }]
end
team = Team.new # => #<Team cars: [#<Car code: "340 F1", driver: "Ascari">, #<Car code: "275 F1", driver: "Villoresi">]>
team.cars.size # => 2
team.cars[0].code # => "340 F1"
team.cars[0].driver # => "Ascari"
team.cars[1].code # => "275 F1"
team.cars[1].driver # => "Villoresi" The struct instance class should correspond to nested attribute class!
class Team < FormObj::Struct
attribute :cars, class: Car, array: true, default: [36]
end
Team.new # => FormObj::WrongDefaultValueClassError: FormObj::WrongDefaultValueClassError 1.3. Serialize FormObj::Struct to Hash
Call to_hash() method in order to get a hash representation of FormObj::Struct
class Team < FormObj::Struct
attribute :name
attribute :year
attribute :cars, array: true do
attribute :code, primary_key: true
attribute :driver
attribute :engine do
attribute :power
attribute :volume
end
end
end
team = Team.new(
name: 'Ferrari',
year: 1950,
cars: [
{
code: '340 F1',
driver: 'Ascari',
engine: {
power: 335,
volume: 4.1,
}
}, {
code: '275 F1',
driver: 'Villoresi',
engine: {
power: 330,
volume: 3.3,
}
}
],
) # => #<Team name: "Ferrari", year: 1950, cars: [#< code: "340 F1", driver: "Ascari", engine: #< power: 335, volume: 4.1>>, #< code: "275 F1", driver: "Villoresi", engine: #< power: 330, volume: 3.3>>]>
team.to_hash # => {
# => :name => "Ferrari",
# => :year => 1950,
# => :cars => [{
# => :code => "340 F1",
# => :driver => "Ascari",
# => :engine => {
# => :power => 335,
# => :volume => 4.1
# => }
# => }, {
# => :code => "275 F1",
# => :driver => "Villoresi",
# => :engine => {
# => :power => 330,
# => :volume => 3.3
# => }
# => }]
# => }2. FormObj::Form
FormObj::Form is inherited from FormObj::Struct and adds support for Rails compatible form builders and ActiveModel validations.
2.1. FormObj::Form Validation
class Team < FormObj::Form
attribute :name
attribute :year
validates :name, length: { minimum: 10 }
end
team = Team.new(name: 'Ferrari') # => #<Team name: "Ferrari", year: nil>
team.valid? # => false
team.errors.messages # => {:name=>["is too short (minimum is 10 characters)"]} 2.2. FormObj::Form Persistence
In order to make FormObj::Form compatible with form builder it has to respond to :persisted? message.
Initial form is not persisted.
It can be marked as persisted by assigning persisted = true which marks as persisted only form itself or
by calling mark_as_persisted method which marks as persisted the form itself and all nested forms and arrays.
class Team < FormObj::Form
attribute :name
attribute :year
attribute :cars, array: true do
attribute :code, primary_key: true
attribute :driver
attribute :engine do
attribute :power
attribute :volume
end
end
end
team = Team.new(cars: [{code: 1}])
team.persisted? # => false
team.cars[0].persisted? # => false
team.cars[0].engine.persisted? # => false
team.persisted = true
team.persisted? # => false - because nested forms are not persisted
team.cars[0].persisted? # => false
team.cars[0].engine.persisted? # => false
team.cars[0].engine.persisted = true
team.persisted? # => false - because nested forms are not persisted
team.cars[0].persisted? # => false
team.cars[0].engine.persisted? # => true
team.mark_as_persisted
team.persisted? # => true
team.cars[0].persisted? # => true
team.cars[0].engine.persisted? # => trueChange of attribute value (directly or by update_attributes call) will change persistence status to false.
team.name = 'Ferrari'
team.persisted? # => false
team.mark_as_persisted
team.persisted? # => true
team.update_attributes(name: 'McLaren')
team.persisted? # => false2.3. Non-Existent Attributes in FormObj::Form update_attributes Do Not Raise By Default
FormObj::Form update_attributes method has raise_if_not_found parameter default false value.
In order to have the same behaviour as FormObj::Struct update_attributes explicitly specify this parameter equal to true
class TeamStruct < FormObj::Struct
attribute :name
attribute :year
end
TeamStruct.new(name: 'Ferrari', a: 1) # => FormObj::UnknownAttributeError: a
TeamStruct.new.update_attributes(a: 1) # => FormObj::UnknownAttributeError: a
TeamStruct.new({ name: 'Ferrari', a: 1 }, raise_if_not_found: false) # => #<Team name: "Ferrari", year: nil>
TeamStruct.new.update_attributes({ a: 1 }, raise_if_not_found: false) # => #<Team name: nil, year: nil>class TeamForm < FormObj::Form
attribute :name
attribute :year
end
TeamForm.new(name: 'Ferrari', a: 1) # => #<Team name: "Ferrari", year: nil>
TeamForm.new.update_attributes(a: 1) # => #<Team name: nil, year: nil>
TeamForm.new({ name: 'Ferrari', a: 1 }, raise_if_not_found: true) # => FormObj::UnknownAttributeError: a
TeamForm.new.update_attributes({ a: 1 }, raise_if_not_found: true) # => FormObj::UnknownAttributeError: a 2.4. Delete from Array of FormObj::Form via update_attributes method
FormObj::Struct update_attributes method by default deletes all array elements that are not present in the new hash.
class Team < FormObj::Struct
attribute :cars, array: true, primary_key: :code do
attribute :code
attribute :driver
end
end
team = Team.new(cars: [{code: 1, driver: 'Ascari'}, {code: 2, driver: 'Villoresi'}])
team.update_attributes(cars: [{code: 1}])
team.cars # => [#< code: 1, driver: "Ascari">] In oppose to this FormObj::Form update_attributes method ignores elements that are absent in the hash but
marks for destruction those elements that has _destroy: true key in the hash.
New elements with _destroy: true are not created at all.
class Team < FormObj::Form
attribute :cars, array: true, primary_key: :code do
attribute :code
attribute :driver
end
end
team = Team.new(cars: [{code: 1, driver: 'Ascari'}, {code: 2, driver: 'Villoresi'}])
team.update_attributes(cars: [{code: 2, driver: 'James Hunt'}])
team.cars[0].code # => 1
team.cars[0].driver # => 'Ascari'
team.cars[0].marked_for_destruction? # => false
team.cars[1].code # => 2
team.cars[1].driver # => 'James Hunt'
team.cars[1].marked_for_destruction? # => false
team.update_attributes(cars: [{code: 1, _destroy: true}, {code: 3, _destroy: true}])
team.cars.size # => 2
team.cars[0].code # => 2
team.cars[0].driver # => 'James Hunt'
team.cars[0].marked_for_destruction? # => false
team.cars[1].code # => 1
team.cars[1].driver # => 'Ascari'
team.cars[1].marked_for_destruction? # => true Use mark_for_destruction in order to forcefully mark an array element for destruction.
team.cars[0].marked_for_destruction? # => false
team.cars[0].mark_for_destruction
team.cars[0].marked_for_destruction? # => true2.5. Using FormObj::Form in Form Builder
class Team < FormObj::Form
attribute :name
attribute :year
attribute :cars, array: true, primary_key: :code do
attribute :code
attribute :driver
attribute :engine do
attribute :power
attribute :volume
end
end
end
@team = Team.new<%= form_for(@team) do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.label :year %>
<%= f.text_field :year %>
<% f.cars.each do |car| %>
<%= f.fields_for(:cars, car, index: '') do |fc| %>
<%= fc.label :code %>
<%= fc.text_field :code %>
<%= fc.label :driver %>
<%= fc.text_field :driver %>
<%= fc.field_for(:engine) do |fce| %>
<%= fce.label :power %>
<%= fce.text_field :power %>
<%= fce.label :volume %>
<%= fce.text_field :volume %>
<% end %>
<% end %>
<% end %>
<% end %>3. FormObj::ModelMapper
Include FormObj::ModelMapper module and map form object attributes to one or more models by using :model and :model_attribute parameters.
Use dot notation to map model attribute to a nested model. Use colon to specify a "hash's attribute".
3.1. load_from_model - Initialize Form Object from Model
Use load_from_model(model) method to initialize form object from the model.
This method available both as class method and as instance method.
class Team < FormObj::Form
include FormObj::ModelMapper
attribute :name, model_attribute: :team_name
attribute :year
attribute :engine_power, model_attribute: 'car.:engine.power'
end
car_model = { engine: Struct.new(:power).new(335) }
team_model = Struct.new(:team_name, :year, :car).new('Ferrari', 1950, car_model)
team = Team.load_from_model(team_model)
team.to_hash # => {
# => :name => "Ferrari"
# => :year => 1950
# => :engine_power => 335
# => }
team.load_from_model(team_model) So attributes are mapped as follows:
| Form Object attribute | Model attribute |
|---|---|
team.name |
team_model.team_name |
team.year |
team_model.year |
team.engine_power |
team_model.car[:engine].power |
3.2. load_from_models - Initialize Form Object from Few Models
Use load_from_models(models) method to initialize form object from few models.
This method available both as class method and as instance method.
models parameter is a hash where keys are the name of models and values are models themselves.
By default each form object attribute is mapped to :default model.
Use parameter :model to map it to another model.
class Team < FormObj::Form
include FormObj::ModelMapper
attribute :name, model_attribute: :team_name
attribute :year
attribute :engine_power, model: :car, model_attribute: ':engine.power'
end
car_model = { engine: Struct.new(:power).new(335) }
team_model = Struct.new(:team_name, :year).new('Ferrari', 1950) # <- doesn't have car attribute !!!
team = Team.load_from_models(default: team_model, car: car_model)
team.to_hash # => {
# => :name => "Ferrari"
# => :year => 1950
# => :engine_power => 335
# => }
team.load_from_models(default: team_model, car: car_model) So attributes are mapped as follows:
| Form Object attribute | Model attribute |
|---|---|
team.name |
team_model.team_name |
team.year |
team_model.year |
team.engine_power |
car_model[:engine].power |
3.3. Do Not Map Certain Attribute
Use model_attribute: false in order to avoid mapping of this attribute.
class Team < FormObj::Form
include ModelMapper
attribute :name, model_attribute: :team_name
attribute :year
attribute :engine_power, model_attribute: false
end
team_model = Struct.new(:team_name, :year, :engine_power).new('Ferrari', 1950, 335)
team = Team.load_from_model(team_model)
team.to_hash # => {
# => :name => "Ferrari"
# => :year => 1950
# => :engine_power => nil
# => }So attributes are mapped as follows:
| Form Object attribute | Model attribute |
|---|---|
form.name |
team_model.team_name |
form.year |
team_model.year |
form.engine_power |
- |
It also works for other methods: sync_to_model(s), to_model(s)_hash, copy_errors_from_model(s).
3.4. Do Not Map Certain Attribute For Reading From Model
Use read_from_model: false in order to avoid mapping of the attribute only in load_from_model(s) methods.
class Team < FormObj::Form
include FormObj::ModelMapper
attribute :name
attribute :year, read_from_model: false
end
team_model = Struct.new(:name, :year).new('Ferrari', 1950)
team = Team.new(name: 'McLaren', year: 1966)
team.load_from_model(team_model)
team.name # => "Ferrari"
team.year # => 19663.5. Do Not Map Certain Attribute For Writing to Model
Use write_to_model: false in order to avoid mapping of the attribute only in sync_to_model(s), to_model(s)_hash, copy_errors_from_model(s) methods.
class Team < FormObj::Form
include FormObj::ModelMapper
attribute :name
attribute :year, write_to_model: false
end
team_model = Struct.new(:name, :year).new('Ferrari', 1950)
team = Team.new(name: 'McLaren', year: 1966)
team.sync_to_model(team_model)
team_model.name # => "McLaren"
team_model.year # => 1950
team.to_model_hash # => {:name => "McLaren"}3.6. Map Nested Form Objects
Nested forms are mapped by default to corresponding nested models.
class Team < FormObj::Form
include ModelMapper
attribute :name, model_attribute: :team_name
attribute :year
attribute :car do
attribute :code
attribute :driver
end
end
car_model = Struct.new(:code, :driver).new('340 F1', 'Ascari')
team_model = Struct.new(:team_name, :year, :car).new('Ferrari', 1950, car_model)
team = Team.load_from_model(team_model)
team.to_hash # => {
# => :name => "Ferrari",
# => :year => 1950,
# => :car => {
# => :code => "340 F1",
# => :driver => "Ascari"
# => }
# => }So attributes are mapped as follows:
| Form Object attribute | Model attribute |
|---|---|
team.name |
team_model.team_name |
team.year |
team_model.year |
team.car.code |
team_model.car.code |
team.car.driver |
team_model.car.driver |
It also works for other methods: sync_to_model(s) and to_model(s)_hash
3.7. Map Nested Form Object to Parent Level Model
Use model_nesting: false parameter to map nested form object to parent level model.
class NestedForm < FormObj::Form
include ModelMapper
attribute :name, model_attribute: :team_name
attribute :year
attribute :car, model_nesting: false do # nesting only in form object but not in a model
attribute :code
attribute :driver
end
end
team_model = Struct.new(:team_name, :year, :code, :driver).new('Ferrari', 1950, '340 F1', 'Ascari')
team = Team.load_from_model(team_model)
team.to_hash # => {
# => :name => "Ferrari",
# => :year => 1950,
# => :car => {
# => :code => "340 F1",
# => :driver => "Ascari"
# => }
# => }So attributes are mapped as follows:
| Form Object attribute | Model attribute |
|---|---|
team.name |
team_model.team_name |
team.year |
team_model.year |
team.car.code |
team_model.code |
team.car.driver |
team_model.driver |
It also works for other methods: sync_to_model(s) and to_model(s)_hash
3.8. Map Nested Form Object to A Hash Model
Use model_hash: true in order to map a nested form object to a hash as a model.
class Team < FormObj::Form
include ModelMapper
attribute :name, model_attribute: :team_name
attribute :year
attribute :car, model_hash: true do # nesting only in form object but not in a model
attribute :code
attribute :driver
end
end
car_model = { code: '340 F1', driver: 'Ascari' }
team_model = Struct.new(:team_name, :year, :car).new('Ferrari', 1950, car_model)
team = Team.load_from_model(team_model)
team.to_hash # => {
# => :name => "Ferrari",
# => :year => 1950,
# => :car => {
# => :code => "340 F1",
# => :driver => "Ascari"
# => }
# => }So attributes are mapped as follows:
| Form Object attribute | Model attribute |
|---|---|
team.name |
team_model.team_name |
team.year |
team_model.year |
team.car.code |
team_model.car[:code] |
team.car.driver |
team_model.car[:driver] |
It also works for other methods: sync_to_model(s) and to_model(s)_hash
3.9. Map Array of Nested Form Objects
Array of nested forms is mapped by default to corresponding array (or for example to ActiveRecord::Relation in case of Rails) of nested models.
class Team < FormObj::Form
include FormObj::ModelMapper
attribute :name
attribute :year
attribute :cars, array: true, model_class: CarModel do
attribute :code, primary_key: true
attribute :driver
end
end3.10. Map Array of Nested Form Objects to Nested Array of Nested Models
If corresponding :model_attribute parameter uses dot notations to reference
nested models the value of :model_class parameter should be an array of corresponding model classes.
class ArrayForm < FormObj::Form
include FormObj::ModelMapper
attribute :name
attribute :year
attribute :cars, array: true, model_attribute: 'equipment.cars', model_class: [Equipment, CarModel] do
attribute :code, primary_key: true
attribute :driver
end
end3.11. Default Implementation of Loading of Array of Models
By default load_from_model(s) methods loads all models from arrays.
class Team < FormObj::Form
include ModelMapper
attribute :name, model_attribute: :team_name
attribute :year
attribute :cars, array: true do
attribute :code
attribute :driver
end
attribute :colours, array: true do
attribute :name
attribute :rgb
end
end
CarModel = Struct.new(:code, :driver)
ColourModel = Struct.new(:name, :rgb)
cars_model = [CarModel.new('340 F1', 'Ascari'), CarModel.new('275 F1', 'Villoresi')]
colours_model = [ColourModel.new(:red, 0xFF0000), ColourModel.new(:white, 0xFFFFFF)]
team_model = Struct.new(:team_name, :year, :cars, :colours).new('Ferrari', 1950, cars_model, colours_model)
team = Team.load_from_model(team_model)
team.to_hash # => {
# => :name => "Ferrari",
# => :year => 1950,
# => :cars => [{
# => :code => "340 F1",
# => :driver => "Ascari"
# => }, {
# => :code => "275 F1",
# => :driver => "Villoresi"
# => }],
# => :colours => [{
# => :name => :red,
# => :rgb => 0xFF0000
# => }, {
# => :name => :white,
# => :rgb => 0xFFFFFF
# => }]
# => }3.12. Custom Implementation of Loading of Array of Models
FormObj::ModelMapper::Array class implements method (where *args are additional params passed to load_from_model(s) methods)
def iterate_through_models_to_load_them(models, *args, &block)
models.each { |model| block.call(model) }
endThis method should iterate through all models that has to be loaded and call a block for each of them.
In the example above it will receive cars_model as the value of models parameter.
Overwrite this method in order to implement your own logic.
class ArrayLoadLimit < FormObj::ModelMapper::Array
private
def iterate_through_models_to_load_them(models, params = {}, &block)
models = models.slice(params[:offset] || 0, params[:limit] || 999999999) if model_attribute.names.last == :cars
super(models, &block)
end
end
class LoadLimitForm < FormObj::Form
include FormObj::ModelMapper
def self.array_class
ArrayLoadLimit
end
end
class Team < LoadLimitForm
attribute :name, model_attribute: :team_name
attribute :year
attribute :cars, array: true do
attribute :code
attribute :driver
end
attribute :colours, array: true do
attribute :name
attribute :rgb
end
end
CarModel = Struct.new(:code, :driver)
ColourModel = Struct.new(:name, :rgb)
cars_model = [CarModel.new('340 F1', 'Ascari'), CarModel.new('275 F1', 'Villoresi')]
colours_model = [ColourModel.new(:red, 0xFF0000), ColourModel.new(:white, 0xFFFFFF)]
team_model = Struct.new(:team_name, :year, :cars, :colours).new('Ferrari', 1950, cars_model, colours_model)
team = Team.load_from_model(team_model, offset: 0, limit: 1)
team.to_hash # => {
# => :name => "Ferrari",
# => :year => 1950,
# => :cars => [{
# => :code => "340 F1",
# => :driver => "Ascari"
# => }],
# => :colours => [{
# => :name => :red,
# => :rgb => 0xFF0000
# => }, {
# => :name => :white,
# => :rgb => 0xFFFFFF
# => }]
# => }
team = Team.load_from_model(team_model, offset: 1, limit: 1)
team.to_hash # => {
# => :name => "Ferrari",
# => :year => 1950,
# => :cars => [{
# => :code => "275 F1",
# => :driver => "Villoresi"
# => }],
# => :colours => [{
# => :name => :red,
# => :rgb => 0xFF0000
# => }, {
# => :name => :white,
# => :rgb => 0xFFFFFF
# => }]
# => }Note that our new implementation of iterate_through_models_to_load_them limits only cars but not colours.
It identifies requested model attribute using model_attribute.names which returns
an array of model attribute accessors (in our example [:cars])
In case of ActiveRecord model iterate_through_models_to_load_them will receive an instance of ActiveRecord::Relation as models parameter.
This allows to load in the memory only necessary associated models.
class ArrayLoadLimit < FormObj::ModelMapper::Array
private
def iterate_through_models_to_load_them(models, params = {}, &block)
models = models.offset(params[:offset] || 0).limit(params[:limit] || 999999999) if model_attribute.names.last == :cars
super(models, &block)
end
end3.13. Sync Form Object to Model(s)
Use sync_to_models(models) to sync form object attributes to mapped models.
Method returns self so calls could be chained.
class Team < FormObj::Form
include FormObj::ModelMapper
attribute :name, model_attribute: :team_name
attribute :year
attribute :engine_power, model: :car, model_attribute: ':engine.power'
end
default_model = Struct.new(:team_name, :year).new('Ferrari', 1950)
car_model = { engine: Struct.new(:power).new(335) }
team = Team.new
team.update_attributes(name: 'McLaren', year: 1966, engine_power: 415)
team.sync_to_models(default: default_model, car: car_model)
default_model.team_name # => "McLaren"
default_model.year # => 1966
car_model[:engine].power # => 415 Use sync_to_model(model) if form object is mapped to single model.
3.14. Sync Array of Nested Form Objects to Model(s)
By default FormObj::Form with included FormObj::ModelMapper will try to match Form Objects and Models by primary key.
Therefore Form Object primary key attribute has to be mapped to top level Model attribute.
TeamModel = Struct.new(:cars)
CarModel = Struct.new(:code, :driver)
class Team < FormObj::Form
include FormObj::ModelMapper
attribute :cars, array: true, model_class: CarModel do
attribute :code, primary_key: true
attribute :driver
end
endAttributes of successfully matched models will be updated from corresponding form objects.
team_model = TeamModel.new([CarModel.new('275 F1', 'Ascari')])
team = Team.new(cars: [{ code: '275 F1', driver: 'Villoresi' }])
team_model.cars # => [#<struct CarModel code="275 F1", driver="Ascari">]
team.sync_to_model(team_model)
team_model.cars # => [#<struct CarModel code="275 F1", driver="Villoresi">]New models will be created for form object that doesn't have corresponding models.
In order to create a new model the model class has to be known.
It can be specified by :model_class parameter.
Otherwise form object will try to guess it from the attribute name.
team_model = TeamModel.new([])
team = Team.new(cars: [{ code: '275 F1', driver: 'Villoresi' }])
team_model.cars # => []
team.sync_to_model(team_model)
team_model.cars # => [#<struct CarModel code="275 F1", driver="Villoresi">]Models that does not have corresponding objects will stay without changes.
team_model = TeamModel.new([CarModel.new('275 F1', 'Ascari')])
team = Team.new(cars: [{ code: '275 F1', driver: 'Villoresi' }, { code: '340 F1', driver: 'Hunt' }])
team_model.cars # => [#<struct CarModel code="275 F1", driver="Ascari">]
team.sync_to_model(team_model)
team_model.cars # => [#<struct CarModel code="275 F1", driver="Villoresi">, #<struct CarModel code="340 F1", driver="Hunt">]If array does not respond to :where models that correspond to form objects marked for destruction will be destroyed.
team_model = TeamModel.new([CarModel.new('275 F1', 'Ascari')])
team = Team.load_from_model(team_model) # => #<Team cars: [#< code: "275 F1", driver: nil>]>
team.update_attributes(cars: [{ code: '275 F1', _destroy: true }]) # => #<Team cars: [#< code: "275 F1", driver: nil marked_for_destruction>]>
team_model.cars # => [#<struct CarModel code="275 F1", driver="Ascari">]
team.sync_to_model(team_model)
team_model.cars # => []3.15. Sync Array of Nested Form Objects to ActiveRecord-like Models
if array respond to :where (aka ActiveRecord) models that correspond to form objects marked for destruction will be also marked for destruction.
class TeamModel < ApplicationRecord
has_many :cars
end
class Team < FormObj::Form
include FormObj::ModelMapper
attribute :cars, array: true, model_class: CarModel do
attribute :id
attribute :code
attribute :driver
end
end
team_model = TeamModel.find(1)
team_model.cars # => #<ActiveRecord::Associations::CollectionProxy [#<Car id: 1, code: "275 F1", driver: "Ascari">]>
team_model.cars.first.marked_for_destruction? # => false
team = Team.load_from_model(team_model).update_attributes(cars: [{ id: 1, _destroy: true }])
team.sync_to_model(team_model)
team_model.cars # => #<ActiveRecord::Associations::CollectionProxy [#<Car id: 1, code: "275 F1", driver: "Ascari">]>
team_model.cars.first.marked_for_destruction? # => true3.16. Customize Sync to Array of Models
FormObj::ModelMapper::Array has private methods: sync_creation_to_models, sync_update_to_models, sync_destruction_to_models.
They are called during syncing process and could be overwritten.
The new descendant of FormObj::ModelMapper::Array class has to be returned from array_class class method.
As well as the FormObj::Form descendant class itself has to be returned from nested_class class method.
class MyForm < FormObj::Form
class Array < FormObj::ModelMapper::Array
private
def sync_destruction_to_models(models, ids_to_destroy)
if models[:default].respond_to? :where
models[:default].where(model_primary_key.name => ids_to_destroy).each { |model| puts "Mark for deletion model #{model}" }
else
models[:default].select { |model| ids_to_destroy.include? model_primary_key.read_from_model(model) }.each { |model| puts "Delete model #{model}" }
end
super
end
def sync_update_to_models(models, items_to_update)
items_to_update.each_pair do |model, form_object|
puts "Update model #{model} with #{form_object.to_model_hash}"
end
super
end
def sync_creation_to_models(models, form_objects_to_create)
form_objects_to_create.each do |form_object|
puts "Create model from #{form_object.to_model_hash}"
end
super
end
end
include FormObj::ModelMapper
def self.array_class
Array
end
def self.nested_class
MyForm
end
end3.17. Model Validation and Persistence
sync_to_model(s) do not call save method on the model(s).
Also they don't call valid? method on the model(s).
Instead they just assign form object attributes value to mapped model attributes
using <attribute_name>= accessors on the model(s).
It is completely up to developer to do any additional validations on the model(s) and save it(them).
3.18. Copy Model Validation Errors into Form Object
Even though validation could and should happen in the form object it is possible to have (additional) validation(s) in the model(s). In this case it is handy to copy model validation errors to form object in order to be able to present them to the user in a standard way.
Use copy_errors_from_models(models) or copy_errors_from_model(model) in order to do it.
Methods return self so one can chain calls.
team.copy_errors_from_models(default: default_model, car: car_model)In case of single model:
team.copy_errors_from_model(model)For the moment copy_errors_from_model(s) do not support nested form object/model and array of nested form objects/models.
3.19. Serialize Form Object to Model Hash
Use to_model_hash(model = :default) to get hash representation of the model that mapped to the form object.
class Team < FormObj::Form
include FormObj::ModelMapper
attribute :name, model_attribute: :team_name
attribute :year
attribute :engine_power, model: :car, model_attribute: ':engine.power'
end
team = Team.new(name: 'McLaren', year: 1966, engine_power: 415)
team.to_model_hash # => { :team_name => "McLaren", :year => 1966 }
team.to_model_hash(:default) # => { :team_name => "McLaren", :year => 1966 }
team.to_model_hash(:car) # => { :engine => { :power => 415 } }Use to_models_hash() to get hash representation of all models that mapped to the form object.
team.to_models_hash # => {
# => default: { :team_name => "McLaren", :year => 1966 }
# => car: { :engine => { :power => 415 } }
# => } If array of form objects mapped to the parent model (model_nesting: false) it is serialized to :self key.
class Team < FormObj::Form
include FormObj::ModelMapper
attribute :name
attribute :year
attribute :cars, array: true, model_nesting: false do
attribute :code, primary_key: true
attribute :driver
end
end
team = Team.new(
name: 'McLaren',
year: 1966,
cars: [{
code: 'M2B',
driver: 'Bruce McLaren'
}, {
code: 'M7A',
driver: 'Denis Hulme'
}]
)
team.to_model_hash # => {
# => :team_name => "McLaren",
# => :year => 1966,
# => :self => {
# => :code => "M2B",
# => :driver => "Bruce McLaren"
# => }, {
# => :code => "M7A",
# => :driver => "Denis Hulme"
# => }
# => }4. Rails Example
# db/migrate/yyyymmddhhmiss_create_team.rb
class CreateTeam < ActiveRecord::Migration
def change
create_table :teams do |t|
t.string :team_name
t.integer :year
end
end
end# app/models/team.rb
class Team < ApplicationRecord
has_many :cars, autosave: true
validates :year, numericality: { greater_than_or_equal_to: 1950 }
end# db/migrate/yyyymmddhhmiss_create_car.rb
class CreateCar < ActiveRecord::Migration
def change
create_table :cars do |t|
t.references :team
t.string :code
t.text :engine
end
end
end# app/models/car.rb
class Car < ApplicationRecord
belongs_to :team
serialize :engine, Hash
end# app/form_objects/team_form.rb
class TeamForm < FormObj::Form
include FormObj::ModelMapper
attribute :id
attribute :name, model_attribute: :team_name
attribute :year
attribute :cars, array: true do
attribute :id
attribute :code
attribute :engine_power, model_attribute: 'engine.:power'
validates :code, presence: true
end
validates :name, :year, presence: true
end# app/controllers/teams_controller.rb
class TeamsController < ApplicationController
def show
@team = TeamForm.new.load_from_model(Team.find(params[:id]))
end
def new
@team = TeamForm.new
end
def edit
@team = TeamForm.new.load_from_model(Team.find(params[:id]))
end
def create
@team = TeamForm.new.update_attributes(params[:team])
if @team.valid?
@team.sync_to_model(model = Team.new)
if model.save
return redirect_to team_path(model), notice: 'Team has been created'
else
@team.copy_errors_from_model(model)
end
end
render :new
end
def update
@team = TeamForm.new.load_from_model(model = Team.find(params[:id]))
@team.update_attributes(params[:team])
if @team.valid?
@team.sync_to_model(model)
if model.save
return redirect_to team_path(model), notice: 'Team has been updated'
else
@team.copy_errors_from_model(model)
end
end
render :edit
end
end# app/views/teams/show.erb.erb
<p>Name: <%= @team.name %></p>
<p>Year: <%= @team.year %></p>
<p>Cars:</p>
<ul>
<% @team.cars.each do |car| %>
<li><%= car.code %> (<%= car.engine[:power] %> hp)</li>
<% end %>
</ul># app/views/teams/new.erb.erb
<%= nested_form_for @team do |f| %>
<%= f.text_field :name %>
<%= f.text_field :year %>
<%= f.link_to_add 'Add a Car', :cars %>
<% end %># app/views/teams/edit.erb.erb
<%= nested_form_for @team do |f| %>
<%= f.text_field :name %>
<%= f.text_field :year %>
<%= f.fields_for :cars do |cf| %>
<%= cf.text_field :code %>
<%= cf.link_to_remove 'Remove the Car' %>
<% end %>
<%= f.link_to_add 'Add a Car', :cars %>
<% end %>5. Reference Guide: attribute's parameters
5.1 FormObj::Struct
5.1.1. Parameter array
Default value: false
Specifies attribute as an array of nested FormObj::Struct.
The attribute shuld have either a block which describes the structure of array item
or class parameter which refers to another FormObj::Struct which describes the structure of array item.
class Team < FormObj::Struct
attribute :cars, array: true do
attribute :id
attribute :driver
end
end5.1.2. Parameter class
Specifies the class of nested FormObj::Struct. Cannot be used if there is block definition of nested structure.
Could be either class constant itself or the name of the class.
class Car < FormObj::Struct
attribute :id
attribute :driver
end
class Team < FormObj::Struct
attribute :cars, array: true, class: Car
end5.1.3. Parameter default
Specifies the default value of an attribute.
For nested FormObj::Struct could be specified either by its instance or by its hash representation.
class Team < FormObj::Struct
attribute :name, default: 'Ferrari'
attribute :cars, array: true, default: [{ id: 1, driver: 'Ascari' }] do
attribute :id
attribute :driver
end
endor
class Car < FormObj::Struct
attribute :id
attribute :driver
end
class Team < FormObj::Struct
attribute :name, default: 'Ferrari'
attribute :cars, array: true, class: 'Car', default: [Car.new(id: 1, driver: 'Ascari')]
end5.1.4. Parameter primary_key
Default value: :id
Specifies the primary key of nested FormObj::Struct for the array attribute.
Could be specified either on the primary key attribute itself (primary_key: true)
class Team < FormObj::Struct
attribute :cars, array: true do
attribute :code, primary_key: true
attribute :driver
end
endor on the array attribute.
In latter case the value of the parameter should the name of the primary key attribute (e.g. primary_key: :team_name).
class Team < FormObj::Struct
attribute :cars, array: true, primary_key: :code do
attribute :code
attribute :driver
end
endIf both ways are mixed, than parameter specified on the array attribute will take precedence.
Composite primary key is not supported.
5.2. FormObj::Form
All FormObj::Struct parameters can be used with FormObj::Form
5.3. FormObj::Form with included FormObj::ModelMapper
All FormObj::Form parameters can be together with following.
5.3.1. Parameter model
Default value: :default
Specifies the name of the model which this attribute is mapped on to.
By default each attribute is mapped on to the :default model.
class TeamForm < FormObj::Form
include FormObj::ModelMapper
attribute :name
attribute :engine, model: :car
end
Car = Struct.new(:engine)
Team = Struct.new(:name)
team = Team.new('McLaren')
car = Car.new('Ford')
team.name # => "McLaren"
car.engine # => "Ford"
team_form = TeamForm.load_from_models(default: team, car: car)
team_form.name # => "McLaren"
team_form.engine # => "Ford"5.3.2. Parameter model_attribute
Default value: <attribute name>
Specifies the name of the model attribute which this attribute is mapped on. It supports dot-notation for mapping on the nested model attribute.
class TeamForm < FormObj::Form
include FormObj::ModelMapper
attribute :car_power, model_attribute: 'car.engine_power'
end
Car = Struct.new(:engine_power)
Team = Struct.new(:car)
team = Team.new(Car.new(350))
team.car.engine_power # => 350
team_form = TeamForm.load_from_model(team)
team_form.car_power # => 350Colon has to be used in front of corresponding model_attribute element if the nested model is a hash.
class TeamForm < FormObj::Form
include FormObj::ModelMapper
attribute :car_power, model_attribute: 'car.:engine_power'
end
Team = Struct.new(:car)
team = Team.new(engine_power: 350)
team.car[:engine_power] # => 350
team_form = TeamForm.load_from_model(team)
team_form.car_power # => 3505.3.3. Parameter model_class
Default value: <attribute name>.to_s.classify
This parameter can be used only for nested form objects. Specifies the class of the model which the nested form object is mapped on.
class TeamForm < FormObj::Form
include FormObj::ModelMapper
attribute :name
attribute :engine, model: :car
end
Car = Struct.new(:engine)
Team = Struct.new(:name)
team = Team.new('McLaren')
car = Car.new('Ford')
team.name # => "McLaren"
car.engine # => "Ford"
team_form = TeamForm.load_from_models(default: team, car: car)
team_form.name # => "McLaren"
team_form.engine # => "Ford"5.3.4. Parameter model_hash
Default value: false
If nested model is hash it could be specified by means of this parameter.
class TeamForm < FormObj::Form
include FormObj::ModelMapper
attribute :car, model_hash: true do
attribute :power
end
end
Team = Struct.new(:car)
team = Team.new(power: 350)
team.car[:power] # => 350
team_form = TeamForm.load_from_model(team)
team_form.car.power # => 350The same result could be achieved by using :-notation in model_attribute parameter.
class TeamForm < FormObj::Form
include FormObj::ModelMapper
attribute :car do
attribute :power, model_attribute: ':power'
end
end
Team = Struct.new(:car)
team = Team.new(power: 350)
team.car[:power] # => 350
team_form = TeamForm.load_from_model(team)
team_form.car.power # => 3505.3.5. Parameter model_nesting
Default value: true
This parameter can be used only for nested form objects.
By default nested form object is mapped to nested model.
If this parameter has value false the nested form object will be mapped to the same model as the parent form object is.
class TeamForm < FormObj::Form
include FormObj::ModelMapper
attribute :car, model_nesting: false do
attribute :power
end
end
Team = Struct.new(:power)
team = Team.new(350)
team.power # => 350
team_form = TeamForm.load_from_model(team)
team_form.car.power # => 350Compare with example where model_nesting: true.
class TeamForm < FormObj::Form
include FormObj::ModelMapper
attribute :car, model_nesting: true do
attribute :power
end
end
Car = Struct.new(:power)
Team = Struct.new(:car)
team = Team.new(Car.new(350))
team.car.power # => 350
team_form = TeamForm.load_from_model(team)
team_form.car.power # => 350model_nesting: true can be omitted since it is its default value.
5.3.6. Parameter read_from_model
Default value: true
false value of this parameter prevents from reading attribute value from the model in
load_from_model(s) methods.
class TeamForm < FormObj::Form
include FormObj::ModelMapper
attribute :name
attribute :year, read_from_model: false
end
Team = Struct.new(:name, :year)
team = Team.new('Ferrari', 1950)
team_form = TeamForm.new(name: 'McLaren', year: 1966)
team_form.load_from_model(team)
team_form.name # => "Ferrari"
team_form.year # => 1966 5.3.7. Parameter write_to_model
Default value: true
false value of this parameter
- will prevent from writing attribute value to the model in
sync_to_model(s)methods, - attribute will not be present in the hash generated by
to_model(s)_hashmethods, - attribute errors will not be copied from the model by
copy_errors_from_model(s)methods.
class TeamForm < FormObj::Form
include FormObj::ModelMapper
attribute :name
attribute :year, write_to_model: false
end
Team = Struct.new(:name, :year)
team = Team.new('Ferrari', 1950)
team_form = TeamForm.new(name: 'McLaren', year: 1966)
team_form.sync_to_model(team)
team.name # => "McLaren"
team.year # => 1950
team_form.to_model_hash # => {:name=>"McLaren"} Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/akoltun/form_obj.
License
The gem is available as open source under the terms of the MIT License.