0

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

Istvanlaz
  • 3
  • 3
  • where are you calling products_path (the error that is thrown?) – Joel Blum Apr 09 '20 at 15:25
  • Litterally nowhere... It seems that form_for does it automatically – Istvanlaz Apr 09 '20 at 15:26
  • Searching 232 files for "products_path" (whole word) 0 matches – Istvanlaz Apr 09 '20 at 15:28
  • is @product being set correctly? and also why the array in form_for [@product] ? – Joel Blum Apr 09 '20 at 15:29
  • @product should be Product.new , set somewhere in your controller new action – Joel Blum Apr 09 '20 at 15:29
  • Yeah, `products_path` is being called by the `form_for [@product]`, as the form is trying to figure out where to POST to (i.e. the `action` attribute of the form tag). By default it wants to POST to `/products`, but there is no such path with the way your routes are set up. The only path for Product creation is `/categories/:category_id/product`. Try running `rake routes` in your terminal to see more about which paths are generated by your `routes.rb` file. – pcrglennon Apr 09 '20 at 15:34
  • Correction, it looks like you don't actually currently have any routes set up for Product creation. You'll definitely need to set one of those up first, either nested under `/categories` or at the top level. – pcrglennon Apr 09 '20 at 15:36
  • the array [@product] in form_for used to be [@category, @product], but the result is the same with (@product) – Istvanlaz Apr 09 '20 at 15:37
  • I have this route created: get 'products/new', to: 'products#new', as: 'new_product' – Istvanlaz Apr 09 '20 at 15:38
  • `new` routes are not the same as `create` routes. `new` is a GET route to show the form for creation, `create` is a POST route for taking in data and actually creating the resource. Try this: add the `[@category, @product]` back to the `form_for`, and then add `:create` to the `:only` array for the product resources nested under categories in your routes file, e.g. `resources :products, only: [:index, :show, :create, :edit]` – pcrglennon Apr 09 '20 at 15:41
  • should I create: get 'categories/products/new', to: 'products#new', as: 'new_product'? As I want to be able to assign the category only on product creation (however, I recall trying it but it sent some error) – Istvanlaz Apr 09 '20 at 15:42
  • I've done so, and it still bring the same error ```` undefined method `products_path' for #<#:0x00007fcdc627ed18>```` – Istvanlaz Apr 09 '20 at 15:45
  • I've restarted my server too – Istvanlaz Apr 09 '20 at 15:45
  • No, because you already have a similar route in place, one that will look like `/categories/:category_id/products/new`. It can be difficult to tell what actual routes are auto-generated by the magic in `routes.rb`, that's why I suggest running `rake routes` in your terminal. BTW I'm writing out an actual answer at the moment with some more detail. – pcrglennon Apr 09 '20 at 15:45

2 Answers2

1

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.

Jeremy Weathers
  • 2,556
  • 1
  • 16
  • 24
  • This seems to function!! – Istvanlaz Apr 09 '20 at 16:00
  • Thanks. However, being new to rails, what is the danger of breaking convention like this? – Istvanlaz Apr 09 '20 at 16:01
  • I hesitated to change my routes so that Products are not nested under Categories, but the rest of my app has been built upon that, so I wanted to give it a shot – Istvanlaz Apr 09 '20 at 16:02
  • When you find yourself fighting against a pattern, take it as either a warning sign that the pattern you've chosen isn't a good fit, or a sign that you haven't fully internalized how the pattern should be used. – Jeremy Weathers Apr 09 '20 at 16:33
  • But that pattern simplifies a related JS implementation.. I'll check how I can un-nest. Thanks anyways a lot for your answer @Jeremy_Weathers, it works like this so far – Istvanlaz Apr 09 '20 at 16:42
0

Edit: I misread the intention, ignore this. Go with Jeremy Weather's answer.

Or is it impossible to Create a Child before assigning it to a Parent..?

With the way you have your relationships and validations set up: yes, it is. A Product requires a Category, through the validates :category, presence: true. And if you're on Rails 5+, the association will already be required by default (meaning that validates is actually redundant). Meaning if you try to create a Product without a Category, it will fail, and the Product will not be saved to the database.

With that constraint in place, here's what I recommend trying:

  • Remove the custom /products/new route
    • remove get 'products/new', to: 'products#new', as: 'new_product'
  • Add :new and :create routes to allow product creation nested under categories
    • Add :create and :new, to the :only array to the resources :products nested under resources :categories
  • Move the file views/products/new.html.erb to views/categories/products/new.html.erb (creating a new categories directory in views if necessary).
  • In that file, alter the form_for so that the form is POSTing to the nested :create route:
    • form_for [@category, @product] do |f|
  • Visit the URL /categories/[insert some Category ID here]/products/new, filling out some data, and see if that works.

Your routes.rb should look like this:

resources :categories, only: [:index, :show, :new, :edit] do
  resources :products, only: [:index, :show, :new, :create, :edit]
end
pcrglennon
  • 457
  • 4
  • 9
  • Sorry, I missed a step, to edit the `form_for`. Editing my answer now. – pcrglennon Apr 09 '20 at 16:02
  • Answer has been edited to include missing step(s), try that. – pcrglennon Apr 09 '20 at 16:06
  • I tried it, however the link /categories/2/products/new brings me to views/products/new.html.erb and not to views/categories/products/new.html.erb – Istvanlaz Apr 09 '20 at 16:15
  • And no routes exist for categories/products/new – Istvanlaz Apr 09 '20 at 16:16
  • Sorry, I think I misread your original intent. Going back and reading the question, I see you're trying to create a Product with a `category_id` chosen from a dropdown. My mistake, I've been leading you down a different path. I got confused by how you already had some nested routes in place. I'd suggest you go with Jeremy Weather's answer. As he says, it isn't exactly Rails convention, but I think in your use case it makes sense. – pcrglennon Apr 09 '20 at 16:26