Search code examples
ruby-on-railsactiverecordparent-childnested-form-foraccepts-nested-attributes

How to create a Child before associating it to its Parent in rails?


I have Categories (Parents) within which are listed Products (Children).

I want to be able to create a new Product directly from the navbar, anywhere in the app and then, during the creation, assign it to a Category.

However, I get the present error:

NoMethodError in Products#new
Showing /Users/istvanlazar/Mobily/app/views/products/new.html.erb where line #9 raised:

undefined method `products_path' for #<#<Class:0x00007febaa5aec98>:0x00007febae0f9e38>
Did you mean?  product_show_path 

## product_show_path is a custom controller that has nothing to do with this one, 
enabling show and clean redirection from newest_products index bypassing categories nesting.

Extracted source (around line #9):
9  <%= form_for [@product] do |f| %>
10   <div class="form-styling">
11     <div>
12       <%= f.label :name %>

My App works as such:

Models

class Category < ApplicationRecord
  has_many :products, inverse_of: :category
  accepts_nested_attributes_for :products

  validates :name, presence: true
end


class Product < ApplicationRecord
  belongs_to :user, optional: true
  belongs_to :category, inverse_of: :products

  validates :category, presence: true
end

Routes

get 'products/new', to: 'products#new', as: 'new_product'

resources :categories, only: [:index, :show, :new, :edit] do
  resources :products, only: [:index, :show, :edit]
  # resources :products, only: [:index, :show, :new, :edit]
end

Controllers

class ProductsController < ApplicationController
  before_action :set_category, except: :new

  def index
    @products = Product.all
    @products = policy_scope(@category.products).order(created_at: :desc)
  end

  def show
    @product = Product.find(params[:id])
  end

  def new
    @product = Product.new
    @product.user = current_user
  end

  def create
    @product = Product.new(product_params)
    @product.user = current_user
    if @product.save!
      redirect_to category_product_path(@category, @product), notice: "Product has been successfully added to our database"
    else
      render :new
    end
  end

  private

  def set_category
    @category = Category.find(params[:category_id])
  end

  def product_params
    params.require(:product).permit(:name, :price, :description, :category_id, :user, :id)
  end
end


class CategoriesController < ApplicationController
  def index
    @categories = Category.all    
  end

  def show
    @category = Category.find(params[:id])
  end

  def new
    # Non-existant created in seeds.rb
  end

  def create
    # idem
  end

  def edit
    # idem
  end

  def update
    # idem
  end

  def destroy
    # idem
  end

  private

  def category_params
    params.require(:category).permit(:name, :id)
  end
end

Views

# In shared/navbar.html.erb:

<nav>
  <ul>
    <li>Some Link</li>
    <li>Another Link</li>
    <li><%= link_to "Create", new_product_path %></li>
  </ul>
</nav>

# In products/new.html.erb:

<%= form_for [@product] do |f| %>
    <div class="form-styling">
      <div>
        <%= f.label :name %>
        <%= f.text_field :name, required: true, placeholder: "Enter product name" %>

        <%= f.label :price %>
        <%= f.number_field :price, required: true %>
      </div>
      <div>
        <%= f.label :description %>
        <%= f.text_field :description %>
      </div>
      <div>
        <%= f.label :category %>
        <%= f.collection_select :category_id, Category.order(:name), :id, :name, {prompt: 'Select a Category'}, required: true %>
      </div>
      <div>
        <%= f.submit %>
      </div>
    </div>
  <% end %>

Do you have any idea of where it went wrong? Or is it impossible to Create a Child before assigning it to a Parent..?

Thanks in advance.

Best, Ist


Solution

  • You haven't defined any route to handle your new product form's POST. You've defined the new_product path, but this arrangement is breaking Rails' conventions and you're not providing a work-around.

    You could define another custom route, e.g. post 'products', to: 'products#create', as: 'create_new_product' and then set that in your form like form_for @product, url: create_new_product_path do |f|.

    However, you should consider changing the structure so that product routes are not nested under categories. Think twice before breaking conventions this way.