Whenever:
- Nested forms
- Virtual model attributes
- Multiple varying forms for one resource
- Extract responsibility from the model objects
- Decouple models from forms
- Simplify forms
- Flatten parameter hashes and therefore simplify parameter check
- Simplify form handling when there are multiple different forms attached to one resource
For example a user form with address data has to be saved at once. First the Address model (address.rb):
class Address < ActiveRecord::Base validates :street, :number, presence: true endStarting from the original model object user.rb:
class User < ActiveRecord::Base belongs_to :address validates :name, presence: true accepts_nested_attributes_for :address enda new form object class (user_address.rb) has to be created to achieve the goal:
class UserAddress include ActiveModel::Model validates :name, :street, :number, presence: true delegate :name, :save, to: :user delegate :street, :number, to: :address def attributes= new_attributes user.attributes = new_attributes.slice :name address.attributes = new_attributes.slice :street, :number end def user @user ||= User.new end def address @address ||= user.build_address end endThe UserAddress is a lightweight plain old Ruby object including the ActiveModel::Model, which just means including validation and conversion stuff. To marry User and Address there is a reader acessor for each object and also delegators to their required attribute acessor methods. At least there is an writing accessor for all attributes which just fills both objects.
At first sight the form object seems to cost more effort, than just using accepts_nested_attributes_for, but it offers more flexibility especially when the objects get more complex. Furthermore form logic baggage is extracted from the User model object, which is NOT responsible for it. And the good thing is, the controller and the view stuff is way cleaner through the form object.
The original working but awful nested form (users/_form.html.haml)
= form_for @user do |user_form| .text = user_form.label :name = user_form.text_field :name = user_form.fields_for :address do |address_fields| .text = address_fields.label :street = address_fields.text_field :street .text = address_fields.label :number = address_fields.text_field :number = user_form.submit 'Save'could be way cleaner, if it looked like:
= form_for @user_address do |f| .text = f.label :name = f.text_field :name .text = f.label :street = f.text_field :street .text = f.label :number = f.text_field :number = f.submit 'Save'Please note the @user_address representing the form object and the straightforward form without any nesting.
Also compare the parameter hash from the nested:
"user" => { "name" => "Chris", "address_attributes" => { "street" => "Main street", "number" => "1" } }to the flattened:
"user_address" => { "name" => "Chris", "street" => "Main street", "number" => "1" }Finally even the original controller (controllers/users_controller.rb):
class UsersController < ApplicationController def new @user = User.new @user.build_address end def create @user = User.new @user.attributes = user_params @user.save end private def user_params params.require(:user) .permit(:name, address_attributes: [:street, :number]) end endcan be moved to the new resource (controllers/user_addresses_controller.rb):
class UserAddressesController < ApplicationController def new @user_address = UserAddress.new end def create @user_address = UserAddress.new @user_address.attributes = user_address_params @user_address.save end private def user_address_params params.require(:user_address) .permit(:name, :street, :number) end endhaving an easier parameter check.
Even that small example illustrates how nested forms can be simplified with lots of nice side effects.
Further articles of interest:
Supported by Ruby 2.2.1 and Ruby on Rails 4.2.1
Keine Kommentare:
Kommentar veröffentlichen