Nested Forms
Forms often contain groups of related fields – an address block, a list of line items, a set of emergency contacts. nested_one and nested_many let you define these as nested form objects with their own attributes, validations, and error reporting.
nested_one
Defines a single nested form:
class Order::Form < Dex::Form
attribute :notes, :string
nested_one :shipping_address do
attribute :street, :string
attribute :city, :string
attribute :zip, :string
validates :street, :city, :zip, presence: true
end
endform = Order::Form.new(
notes: "Leave at door",
shipping_address: { street: "123 Main", city: "NYC", zip: "10001" }
)
form.shipping_address.street # => "123 Main"
form.shipping_address.class # => Order::Form::ShippingAddressHash coercion
Pass a Hash and it gets automatically converted to the nested form:
form.shipping_address = { street: "456 Oak", city: "LA", zip: "90001" }
form.shipping_address.city # => "LA"Default initialization
When you don't provide a nested_one value, it initializes as an empty form:
form = Order::Form.new
form.shipping_address # => an empty ShippingAddress (not nil)
form.shipping_address.street # => nilBuilding nested forms
form.build_shipping_address(street: "789 Pine", city: "SF")
form.shipping_address.city # => "SF"nested_many
Defines a collection of nested forms:
class Order::InvoiceForm < Dex::Form
attribute :number, :string
nested_many :line_items do
attribute :description, :string
attribute :quantity, :integer
attribute :price, :decimal
validates :description, :quantity, :price, presence: true
end
endform = Order::InvoiceForm.new(line_items: [
{ description: "Widget", quantity: 2, price: "9.99" },
{ description: "Gadget", quantity: 1, price: "24.99" }
])
form.line_items.size # => 2
form.line_items[0].description # => "Widget"
form.line_items[0].price # => #<BigDecimal: 9.99>Default initialization
When you don't provide a nested_many value, it initializes as an empty array:
form = Order::InvoiceForm.new
form.line_items # => []Building items
form.build_line_item(description: "New item", quantity: 1, price: "5.00")
form.line_items.size # => 1Rails numbered hash format
Rails form builders submit nested collections as numbered hashes. This is handled automatically:
form = Order::InvoiceForm.new(line_items: {
"0" => { description: "Widget", quantity: "2", price: "9.99" },
"1" => { description: "Gadget", quantity: "1", price: "24.99" }
})
form.line_items.size # => 2_destroy support
Items with _destroy set to a truthy value are filtered out during coercion:
form = Order::InvoiceForm.new(line_items: [
{ description: "Keep this", quantity: 1, price: "10.00" },
{ description: "Remove this", quantity: 1, price: "5.00", _destroy: "1" }
])
form.line_items.size # => 1Truthy values include "1", "true", and true – the same values Rails considers truthy for _destroy.
Validation propagation
Invalid nested forms bubble their errors up to the parent with prefixed attribute names:
form = Order::Form.new(shipping_address: { street: "", city: "", zip: "" })
form.valid? # => false
form.errors[:"shipping_address.street"] # => ["can't be blank"]
form.errors[:"shipping_address.city"] # => ["can't be blank"]For nested_many, errors include the index:
form = Order::InvoiceForm.new(line_items: [
{ description: "Good", quantity: 1, price: "10.00" },
{ description: "", quantity: nil, price: nil }
])
form.valid? # => false
form.errors[:"line_items[1].description"] # => ["can't be blank"]
form.errors[:"line_items[1].quantity"] # => ["can't be blank"]Constant naming
nested_one :address creates a constant Address on the parent form class. nested_many :line_items creates LineItem (singularized). Override with class_name::
nested_one :address, class_name: "HomeAddress" do
attribute :street, :string
end
# Creates Order::Form::HomeAddress instead of Order::Form::AddressSerialization
to_h recursively serializes nested forms:
form.to_h
# => {
# number: "INV-001",
# line_items: [
# { description: "Widget", quantity: 2, price: #<BigDecimal: 9.99> },
# { description: "Gadget", quantity: 1, price: #<BigDecimal: 24.99> }
# ]
# }Inheritance
Nested definitions are safely inherited. Adding nested forms to a child class doesn't affect the parent:
class BaseForm < Dex::Form
nested_one :address do
attribute :street, :string
end
end
class ExtendedForm < BaseForm
nested_one :billing do
attribute :card_number, :string
end
end
BaseForm._nested_ones.keys # => [:address]
ExtendedForm._nested_ones.keys # => [:address, :billing]