Home > ruby on rails > Rails - Why does my nested model form not require 'accepts_nested_attributes_for'

Rails - Why does my nested model form not require 'accepts_nested_attributes_for'

February 9Hits:2
Advertisement

I have two models item and user_item. Item has many user_items and user_items belongs to item. I have a form where a user can create a new item. In the form a user should include a picture. The name, description, and tags get saved to a new item object. The picture should be saved as an attribute on a user_item object which gets created at the same time.

I've been reading about nested model forms and strong parameters and using accepts_nested_attributes_for. My form seems to be working but I don't understand why I don't need accepts_nested_attributes_for. Although it seems to be working, is there a problem with the way I'm doing it?

Form

<%= simple_form_for @item, url: items_path, method: :post 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: "Describe item" %>   <%= item_builder.input :tag_list, required: false, label: "Tags" %>   <%= item_builder.simple_fields_for @item, @item.user_items.build 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', class: "btn btn-primary pull-right inherit-width" %>   </div> <% end %> 

Items_controller

def create     Item.transaction do        @item = Item.create(name: item_params[:name],                                         description: item_params[:description],                                         tag_list: item_params[:tag_list],                                          created_by: current_user.id,                                         status: Item::STATUS[:pending])        if item_params[:item] == nil         @item.errors.add(:picture, "is required")       end        if @item.errors.empty?         @user_item = @item.user_items.build(user_id: current_user.id, picture: item_params[:item][:picture])          if @user_item.save           flash[:notice] = "Thank you for your item submission."           redirect_to items_path         else           render :new           raise ActiveRecord::Rollback, "Useritem create failed"         end        else         render :new         raise ActiveRecord::Rollback, "no picture"       end     end   end    private      def item_params       params.require(:item).permit(:name, :description, :tag_list,                                                                 item: :picture)     end 

EDIT:

I have made changes going along with Richard's answer below but I am now running into a couple problems. This is my current create action in the items_controller

  def create     @item= Item.new item_params     @item.status = Item::STATUS[:pending]     @item.created_by = current_user.id     @item.user_items.first.user_id = current_user.id     if @item.save       redirect_to items_path, notice: "Thank you for your item request!     else       render :new     end   end 
  1. On validation fail I was getting a missing template error so I added an else and render :new.
  2. Now on validation fail the new template is rendered but the fields_for input for picture is not shown.
  3. I have validates_presence_of :picture in user_item.rb and I have validates_associated :user_items in item.rb but I am still able to submit the form without a picture.
  4. As you can see on valid submission I need the item's status attribute to be pending and a created_by attribute to be the current user id. I also needed the user_item's user_id attribute to be set to the current user id. I set this in the controller. I am wondering if setting this like I am in the create action is the correct way.

Answers

You don't need it because your pattern is wrong:

@user_item = @item.user_items.build(user_id: current_user.id, picture: item_params[:item][:picture])

With your above code, you're creating individual user_items from each @item. This does not require accepts_nested_attributes_for, but is cumbersome, against convention and very hacky.



Here's how it should be done:

Models

#app/models/item.rb
class Item < ActiveRecord::Base
  has_many :user_items
  has_many :users, 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
end

Controllers

#app/controllers/items_controller.rb
class ItemsController < ApplicationController
  def new
    @item = Item.new
    @item.user_items.build
  end

  def create
    @item = Item.new item_params
    redirect_to @item, notice: "Thank you for your item submission." if @item.save
  end

  private

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

Views

#app/views/items/new.html.erb
<%= simple_form_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: "Describe item" %>
  <%= item_builder.input :tag_list, required: false, label: "Tags" %>
  <%= 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 %>
  <%= item_builder.submit 'Submit new item', class: "btn btn-primary pull-right inherit-width" %>
<% end %>


is there a problem with the way I'm doing it

Technically, no.

However, if you aspire to be a professional, or on a similar level, you'll not get very far with the code you wrote.

Using a framework like Rails gives you access to an unprecedented array of pre-baked functionality. The best code is not the code you wrote, but the library code that's been compiled & tested over years of production use.

Whilst your code works, it's not efficient nor extensible.

--

The ultimate key of whether what you've written is "right" is whether the future you would be proud of looking at it again. If not, you're probably best refactoring.



Update

If you want to give Item a status, you'll want to look at the enum module for ActiveRecord models:

#app/models/item.rb
class Item < ActiveRecord::Base
  enum status: [:active, :pending, :declined]
end

This is a very interesting method, as it provides a series of class methods (scopes), and instance methods:

@item = Item.find x

@item.active?   #-> true
@item.pending?  #-> false
@item.declined? #-> false

Item.active     #-> collection of "active" items
Item.pending    #-> collection of "pending" items
Item.declined   #-> collection of "declined" items

To save an Item's status, you could use the collection_select as so:

<%= form_for @item do |f| %>
  <%= f.collection_select :status, Item.statuses, :first, :first %>
  <%= f.submit %>
<% end %>



Update

Your code can be improved massively:

def create
    @item= Item.new item_params #-> this line should do ALL the heavy lifting.

    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

The file field won't be populated again if you have validation issues; it's an OS problem, not Rails (how does the OS know your file is in the same location as it was when you first submitted?).

You need to make your create code as succinct as possible; explicit declarations for attributes is generally a bad idea. You should put as much of it as possible into the Item model (enum etc).

Although it seems to be working, is there a problem with the way I'm doing it?

Its great that it works - however it does have a code smell. Your controller has way to much business logic which belongs on the model layer and you would be forced to repeat that whole mess with the param checking in your update action.

So lets look at the Rails way:

class Item
  enum status: [:pending, :approved, :awesome] # or whatever statuses you have
  has_many :user_items
  belongs_to :creator, class_name: 'User' # or author or whatever
  validates_associated :user_items
  accepts_nested_attributes_for :user_items, reject_if: :all_blank?
end

class UserItem
  belongs_to :item
  validates :picture, presence: true
end

class User
  has_many :items, source: :creator, inverse_of: :creator
end

Here we setup item to accepts_nested_attributes_for :user_items and we setup a validations for UserItem, validates_associated will cause the Item validation to fail unless all the associated UserItems are valid.

ActiveRecord::Enum is a less hacky way of doing what I believe you are doing with item.status.

With our models taken care of we can proceed to with the controller:

class ItemsController

  before_action :set_item, only: [:show, :edit, :update, :destroy]

  def new
    @item = Item.new
    @item.user_items.new # seeds the form
  end

  def edit
    @item.user_items.new unless @item.user_items.any? # seeds the form
  end

  def create
    @item = Item.new(item_params) do |item|
      item.status = :pending
      item.creator = current_user
    end
    if @item.save
      redirect_to items_path, success: 'Oh yes'
    else
      render :new, error: 'Oh noes'
    end
  end

  def update
    if @item.update(item_params)
      redirect_to @item, success: 'Item updated'
    else
      render :edit, error: 'Oh noes'
    end
  end

  private
    def set_item
      @item = Item.includes(:user_items).find(params[:id])
    end

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

This is MVC the Rails way - models take care of validations and associations. We use strong parameters to map form input to models in a reusable way.

Lastly lets clean up the form. We will start by extracting the form into a partial so that it can be re-used.

<% # views/items/_form.html.erb %>
<%= simple_form_for @item do |item_builder| %>
  <div class="well">
    <%= item_builder.input :name, require: false, label: "Item name" %>
    <%= item_builder.input :description, require: false, as: :text, label: "Describe item" %>
    <%= item_builder.input :tag_list, label: "Tags", require: false %>
    <%= item_builder.simple_fields_for :user_items do |user_item_builder| %>
    <%= user_item_builder.input :picture, as: :file, label: "Picture of you with this item" %>
  <% end %>
  </div>
  <div class="clearfix">
    <%= item_builder.submit %>
  </div>
<% end %>

We can then use this form for both new and edit.

<h1>Create a new item</h1>
<%= render partial: '_form' %>


<h1>Edit <%= @item.name %></h1>
<%= render partial: '_form' %>

Related Articles

  • Rails - Why does my nested model form not require 'accepts_nested_attributes_for'February 9

    I have two models item and user_item. Item has many user_items and user_items belongs to item. I have a form where a user can create a new item. In the form a user should include a picture. The name, description, and tags get saved to a new item obje

  • Rails routes for self nested modelsJanuary 15

    In my rails app I have a Task model. In my app, my tasks can have substasks and so on. Since my subtasks are tasks with only a parent task, I did a self-nested model such as class Task < ActiveRecord::Base ## Self Join has_many :subtasks, class_name:

  • rails and faker with nested model attributesFebruary 17

    I'm trying to populate my development postgres database with some objects that accepts nested attributes. I can't figure out how I should pass in the nested params. I tried with hash and array as well, but couldn't pull it off. Different error messag

  • Rails nested models and virtual attribute initializationJanuary 27

    I have a problem understanding how are attributes "sent" to nested model(s), and if is possible to do this for model with virtual attrubute too. I have three models: class User < ActiveRecord::Base ... has_and_belongs_to_many :clearancegoods

  • rails with most nested models and cocoon gem;

    rails with most nested models and cocoon gem;February 16

    I have a rails4 app with nested model form. There is a product model that has_many product_features, has_many product_usecases and has_many product_competitors. Everything works fine except when I load the page only the first nested model (product_fe

  • Nested Iteration with model builder - using nested models

    Nested Iteration with model builder - using nested modelsSeptember 7

    I'm new to using model builder and have to do a nested iteration (for every month I have to iterate through 6 parameters). I am thinking of doing that by using nested models. One model doing the interation through monthes and then providing the curre

  • PHP MVC class with controller and nested model

    PHP MVC class with controller and nested modelNovember 26

    I had asked a first question about this class a while ago and got a few answers here which made me rewrite it completely. I removed all statics and globals, added my variables as arguments for the constructor, and pass them again as reference through

  • Problem validating parent model with nested models in modelbuilder

    Problem validating parent model with nested models in modelbuilderMarch 24

    I have a parent model with several nested models. I'm having trouble getting the parent model to validate. The issue is with sub model 4. The two screenshots are of the parent model and sub model 4. The output from sub model 3 feeds into parameter "M

  • Using global parameters in nested models of ModelBuilder?October 22

    I have a model that consists of two nested models and various parameters. There are two parameters that are common to all of the models (including the overall "grand" model). The common parameters are in-line variables and serve to assign unique

  • Creating Instance of Nested model inside parent controllerJanuary 18

    I would like to know if it is possible to create an instance of a nested model inside the parents controller. Let me explain. I have a model called Museum and another model called Museumimage. Museumimage has only one field which is an attachment, ma

  • Knockout JS Mapping fromJS nested modelsJanuary 27

    I am having trouble understanding how to work with Knockout JS Mapping Plugin. I have some nested models (as seen below) and what I am currently doing is just using the ko.mapping.fromJS() in the parent model. What I am noticing is that computed valu

  • Trying to update nested model first on before_saveFebruary 11

    I am calculating :price in my nested item model in order to map it in my invoice model. The problem is that the before_save in the invoice model is being called first and I want it to be called after the before_save in the nesteditem model Any ideas

  • Before validation on nested modelJanuary 9

    I have a nested model items and I am trying to multiply two columns together cost and quantity to set the last column price. I need to set the column price before the form is saved and i need to also validate the model. The before_validation call bac

  • Creating nested models in Rails 4 forum appJanuary 31

    Hello I am making a Forum application in Rails 4. It can have numerous forums, each with numerous topics. Each topic can have many posts. When creating a new topic, one must also create the initial post, much like Stack Overflow itself. Therefore, I

  • Rails how to create select dropdown with double nested model?January 19

    I'm new to Rails and I'm trying to create a select dropdown for menu items. The complicated part is each Restaurant has many Categories which have many menu items. My goal is to have a dropdown that prints something like the following: <optgroup labe

  • form validation with nested models in Rails

    form validation with nested models in RailsJanuary 22

    I have this problem. I need to validate the attributes of two models in the same form in Rails. One is the parent of the other. The form is like this: <%= semantic_form_for @professional do |pro| %> <%= pro.inputs :id => "information"

  • Rails: nested models disappearing after validationJanuary 26

    I have a model: class Invoice < ActiveRecord::Base belongs_to :case has_many :invoice_positions accepts_nested_attributes_for :invoice_positions, allow_destroy: true And i have a form createn with https://github.com/nathanvda/cocoon but i have one sm

  • Ruby on rails. Form for a nested model in a different viewFebruary 13

    I've been battling this for a while now and I can't figure it out. I have a user model using devise. Users can upload songs, and add youtube videos etc.. I'm trying to let users add/delete songs and videos from the devise edit registrations view. Vid

  • Rails: How to have a nested form save to different nested models?

    Rails: How to have a nested form save to different nested models?February 17

    Context: I have a Company model that has many projects. Each project has many tasks. The company has many employees. Employee belongs to Company and has many tasks. It is one employee per task and an employee will have only one task per project. Sche

  • Rails / jQuery sortable = drag Item on top of each other for nesting ModelJanuary 29

    Well, I have Notecards listed as a <div> (not with <ul> and <li>) inside a wrapper <div id="notecards">. Sorting them works great with jQueryUI sortable(). I would like the notes to be nestable: class Note < ActiveReco

Copyright (C) 2017 ceus-now.com, All Rights Reserved. webmaster#ceus-now.com 14 q. 0.298 s.