accepts_nested_attributes
is a great tool when working with ActiveRecord
objects. But what if you’re managing complex signup logic with multiple models, like User
and Account
? Enter the PORO (Plain Old Ruby Object): a clean, Rails-friendly solution for maintaining separation of concerns while keeping signup logic in one place.
For instance, you might be looking to create a signup form that manages User
and Account
objects:
class SignUp
def user
@user ||= User.new
end
def account
@account ||= Account.new
end
end
This provides basic support for handling the data. The way we want to use this in our views would be:
<%= form_with model: @sign_up do |form| %>
<%= form.fields_for :user do |user_fields| %>
<%= user_fields.email_field :email_address %>
<%= user_fields.password_field :password %>
<% end %>
<%= form.fields_for :account do |account_fields| %>
<%= account_fields.text_field :name %>
<% end %>
<%= form.submit %>
<% end %>
And in our controllers:
class SignUpsController < ApplicationController
def new
@sign_up = SignUp.new
end
def create
@sign_up = SignUp.new(sign_up_params)
@sign_up.save!
redirect_to welcome_path
end
def sign_up_params
params.require(:sign_up).permit(
user_attributes: [:email_address, :password],
account_attributes: [:name]
)
end
end
To make this work, we need to make some small changes to the SignUp
class:
class SignUp
include ActiveModel::Model
def user
@user ||= User.new
end
def user_attributes=(attributes)
user.assign_attribute(attributes)
end
def account
@account ||= Account.new
end
def account_attributes=(attributes)
account.assign_attribute(attributes)
end
def save!
ActiveRecord::Base.transaction do
user.save!
account.user = user
account.save!
end
end
end
By including ActiveModel::Model
, the SignUp
class gains access to Rails features like form validations and naming conventions. This ensures it integrates seamlessly into the Rails ecosystem, allowing for clean views and controller logic. For instance, it automatically connects to sign_ups_controller
and supports helpers like new_sign_up_path
.
Wrapping the save!
logic in a transaction ensures both the User
and Account
objects are saved together. If either save fails, the entire operation is rolled back, preventing partial updates.
This approach lets you cleanly keep all the signup logic in one place, separate from your general User
and Account
concerns.