Rails - Validating complex nested form

2019-09-08 08:55发布

I have a form with four input fields where a user creates an item. In the form one of the four input fields is for picture which is used to create a user_item at the same time on form submission. I am running into several problems.

  1. When validation fails on the item I have the controller render :new but when this happens the picture input field is not visible.

  2. Validation on presence of picture is not working.

  3. I need a way to set the user_id attribute to current_user on the user_item when it is created.

item.rb

  validates :name, presence: true, uniqueness: true
  validates :description, presence: true
  has_many :tags
  has_many :user_items
  has_many :users, -> { uniq }, through: :user_items
  belongs_to :user

  accepts_nested_attributes_for :user_items
  validates_associated :user_items

user_item.rb

  belongs_to :user
  belongs_to :item
  mount_uploader :picture, PictureUploader
  validates_presence_of :picture
  validate :picture_size

items_controller.rb

  def new
    @item = Item.new
    @item.user_items.build
  end

  def create
    @item = item.new item_params

    if @item.save
      redirect_to items_path, notice: "Thank you for your item request!"
    else
      render :new
    end
  end

  private

    def item_params
      params.require(:item).permit(:name, :description, :tag_list, user_items_attributes: [:picture]).merge(created_by: current_user.id)
    end

new.html.erb

<%= simple_form_for @item, html: { class: "create-item-form" } do |item_builder| %>
  <div class="well">
  <%= item_builder.input :name, required: false, error: false, label: "Item name" %>
  <%= item_builder.input :description, as: :text, required: false, error: false, label: "Description of item" %>
  <%= item_builder.input :tag_list, required: false, label: "Tags (these will help users find your item)" %>
  <%= item_builder.simple_fields_for :user_items do |user_item_builder| %>
    <%= user_item_builder.input :picture, as: :file, required: false, label: "Picture of you with this item" %>
  <% end %>
  </div>
  <div class="clearfix">
    <%= item_builder.submit 'Submit new item request', class: "btn btn-primary pull-right inherit-width" %>
  </div>
<% end %>

1条回答
萌系小妹纸
2楼-- · 2019-09-08 09:47

I need a way to set the user_id

The simplest way to add the user_id to the user_item is to include a hidden_field in your fields_for. Not the most secure, but should work:

#app/views/items/new.html.erb
...
<%= item_builder.simple_fields_for :user_items do |user_item_builder| %>
    <%= user_item_builder.input :picture, as: :file, required: false, label: "Picture of you with this item" %>
    <%= user_item_builder.input :user_id, as: :hidden, input_html: { value: current_user.id } %>
<% end %>

#app/controllers/items_controller.rb
...
def item_params
   params.require(:item).permit(:name, :description, :tag_list, user_items_attributes: [:picture, :user_id]).merge(created_by: current_user.id)
end

the picture input field is not visible

According to this answer: Nested Input Disappears When Form Reloads, you need to rebuild your picture objects:

def create
   if @item.save
     ...
   else
     @item.user_items.build
     render :new
   end
end

file_field inputs are particularly interesting. Because your OS cannot guarantee your files will be exactly the same as they were, so the file_field is not populated.


Validation on presence of picture is not working.

You should use inverse_of to make sure the two objects can talk to each other:

#app/models/user.rb
class User < ActiveRecord::Base
  has_many :user_items, inverse_of: :user
end

#app/models/user_item.rb
class UserItem < ActiveRecord::Base
  belongs_to :user, inverse_of: :user_items
  validates :picture, presence: true
end

Update

If you wanted to pass the user_id through the backend, not having a hidden field, you'd be able to do something like this:

#app/controllers/items_controller.rb
class ItemsController < ApplicationController
  def new
    @user = current_user
    @user.user_items.build.build_item
  end

  def create
    @user = current_user.update user_params
  end 

  private

  def user_params
     params.require(:user).permit(user_items_attributes: [:picture, item_attributes: [:name, :description, :tag_list])
  end
end

This would have to be accompanied with the following change to your items#new view:

<%= simple_form_for @user, url: items_path, html: { class: "create-item-form" } do |f| %>
  <%= f.fields_for :user_items do |user_item_builder|
    <%= user_item_builder.input :picture, as: :file, required: false, label: "Picture of you with this item" %>
    <%= user_item_builder.fields_for :item do |item_builder| %>
        <%= item_builder.input :name, required: false, error: false, label: "Item name" %>
        <%= item_builder.input :description, as: :text, required: false, error: false, label: "Description of item" %>
        <%= item_builder.input :tag_list, required: false, label: "Tags (these will help users find your item)" %>
    <% end %>
  <% end %>
  <%= f.submit 'Submit new item request', class: "btn btn-primary pull-right inherit-width" %>
<% end %>

You'll also need to pass the attributes through the respective models:

#app/models/user.rb
class User < ActiveRecord::Base
  has_many :user_items
  has_many :items, through: :user_items

  accepts_nested_attributes_for :user_items
end

#app/models/user_item.rb
class UserItem < ActiveRecord::Base
  belongs_to :user
  belongs_to :item

  accepts_nested_attributes_for :item
end
查看更多
登录 后发表回答