Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails forms - Should I build `accepts_nested_attributes_for` associations in the Controller, Model, or View?

The Question

I have a parent that accepts_nested_attributes_for a child. So, when I have a form for the parent, I need to build the child so I can display form fields for it as well. What I want to know is: where should I build the child? In the Model, View, or Controller?

Why I Am Asking This

You may be shaking your head and thinking I'm a madman for asking a question like this, but here's the line of thinking that got me here.

I have a Customer model that accepts_nested_attributes_for a billing_address, like so:

class Customer
  belongs_to :billing_address, class_name: 'Address'
  accepts_nested_attributes_for :billing_address
end

When I present a form for a new Customer to the user, I want to make sure there is a blank billing_address, so that the user actually sees fields for the billing_address. So I have something like this in my controller:

def new
  @customer = Customer.new
  @customer.build_billing_address
end

However, if the user doesn't fill out any of the billing_address fields, but tries to submit an invalid form, they will be presented with a form that no longer has fields for the billing_address, unless I put something like this in the create action of my controller:

def create
  @customer = Customer.new(params[:customer])
  @customer.build_billing_address if @customer.billing_address.nil?
end

There is another issue, which is that if a user tries to edit a Customer, but that Customer doesn't have an associated billing_address already, they won't see fields for the billing_address. So I have to add somethign like this to the controller:

def edit
  @customer = Customer.find(params[:id])
  @customer.build_billing_address if @customer.billing_address.nil?
end

And something similar needs to happen in the controller's update method.

Anyway, this is highly repetitive, so I thought about doing something in the model. My initial thinking was to add a callback to the model's after_initialize event, like so:

class CustomerModel
  after_initialize :build_billing_address, if: 'billing_address.nil?'
end

But my spidey sense started tingling. Who's to say I won't instantiate a Customer in some other part of my code in the future and have this wreak havoc in some unexpected ways.

So my current thinking is that the best place to do this is in the form view itself, since what I'm trying to accomplish is to have a blank billing_address for the form and the form itself is the only place in the code where I know for sure that I'm about to show a form for the billing_address.

But, you know, I'm just some guy on the Internet. Where should I build_billing_address?

like image 303
Richard Jones Avatar asked Jul 04 '13 18:07

Richard Jones


3 Answers

Though this advice by Xavier Shay is from 2011, he suggests putting it in the view, "since this is a view problem (do we display fields or not?)":

app/helpers/form_helper.rb:

module FormHelper
  def setup_user(user)
    user.address ||= Address.new
    user
  end
end

app/views/users/_form.html.erb:

<%= form_for setup_user(@user) do |f| %>

Note that I had to change the helper method to the following:

  def setup_user(user)
    user.addresses.build if user.addresses.empty?
    user
  end

The controller remains completely unchanged.

like image 123
user664833 Avatar answered Oct 17 '22 05:10

user664833


If you know your model should always have a billing address, you can override the getter for this attribute in your model class as described in the docs:

def billing_address
    super || build_billing_address
end

Optionally pass in any attributes to build_billing_address as required by your particular needs.

like image 23
jxpx777 Avatar answered Oct 17 '22 07:10

jxpx777


You would use build if you want to build up something and save it later. I would say, use it in nested routes.

def create
 @address = @customer.billing_addresses.build(params[:billing_address])
 if @address.save
   redirect_to @customer.billing_addresses
 else
   render "create"
 end
end

Something like that. I also use the build when I'm in the console.

like image 1
Allen Avatar answered Oct 17 '22 07:10

Allen