Jakub Kuchar
Jakub Kuchar

Reputation: 1665

Multiple (n) identical nested forms generated square-times(n*n) when validation fails

User has two addresses shipping(:address_type=0) and billing(:address_type=1) User form with 2 classic nested forms for each address type are generated square times every submit and failed validation.

Models:

class User < ActiveRecord::Base
    has_many :addresses, :dependent => :destroy
    accepts_nested_attributes_for :addresses
    validates_associated :addresses
end

class Address < ActiveRecord::Base
    belongs_to :user  
    validates :user, :address_type, :first_name, :last_name, :street
end

Controller

class UsersController < ApplicationController

public
    def new
        @user = User.new
        @shipping_address = @user.addresses.build({:address_type => 0})
        @billing_address = @user.addresses.build({:address_type => 1})
    end

    def create
        @user = User.new(params[:user])
        if @user.save
            #fine
        else
            render => :new
        end
    end

Uncomplete Form

=form_for @user, :html => { :multipart => true } do |ff|
    =ff.fields_for :addresses, @shipping_address do |f|
        =f.hidden_field :address_type, :value => 0

    =ff.fields_for :addresses, @billing_address do |f|
        =f.hidden_field :address_type, :value => 1

    =ff.submit

Upvotes: 1

Views: 316

Answers (2)

Dalibor Filus
Dalibor Filus

Reputation: 1254

The form should look like this:

=form_for @user, :html => { :multipart => true } do |ff|
    =ff.fields_for :addresses do |f|

Nothing else. Addressess is already a collection, so you should have just one rendering of it. Also that ":addresses, @shipping_address" makes it to render addresses AND shipping address, even if it's included in @user.addresses.

The addressess built in new action will show there because they are in the addresses collection.

EDIT:

If you need only these two addresses, you can sort it and pass it to fields_for directly:

=form_for @user, :html => { :multipart => true } do |ff|
    =ff.fields_for ff.object.addresses.sort{|a,b| a.address_type <=> b.address_type } do |f|

That should do it.

Upvotes: 2

Jakub Kuchar
Jakub Kuchar

Reputation: 1665

Surprised? I guess not but I was. I found it am I correct? And its stupid and simple.

There is no @shipping_address nor @billing_address when validation fails and rendering the new action (the form) again. But @user has already 2 addresses builded and nested form behave correctly to render each twice for first time failed validation.

    def create
        @user = User.new(params[:user])
        if @user.save
            #fine
        else
          @user.addresses.clear
          @user_address = @user.addresses.build({:address_type => 0})
          @user_address.attributes = params[:user][:addresses_attributes]["0"]

          @billing_address = @user.addresses.build({:address_type => 1})
          @billing_address.attributes = params[:user][:addresses_attributes]["1"]
          render => :new
        end
    end

Upvotes: 0

Related Questions