2

Before getting into details I have read through these posts to try to find the solution without success : one, two, three

That being said: I am [new and] building an ecomm site for selling secondhand clothing, shoes and decor items.

My structure has only one Product model and associated controller and table. Each 'product' has one of three different main categories, which is what I am using to differentiate and create 3 different URLs.

My routes look like this:

Rails.application.routes.draw do

  root to: 'pages#home'

  get 'clothing', to: 'products#clothing'
  get 'clothing/:id', to: 'products#show'

  get 'shoes', to: 'products#shoes'
  get 'shoes/:id', to: 'products#show'

  get 'home', to: 'products#home'
  get 'home/:id', to: 'products#show'

  get 'products/new', to: 'products#new'
  post 'products', to: 'products#create'

end

My products_controller looks like this:

class ProductsController < ApplicationController
  before_action :set_all_products
  before_action :set_one_product, only: [:show]

  def shoes
    @all_shoe_products = @all_products.where(main_category_id: MainCategory.find_by_name("shoes").id)
  end

  def clothing
    @all_clothing_products = @all_products.where(main_category: MainCategory.find_by_name("clothes").id)
  end

  def home
    @all_home_products = @all_products.where(main_category: MainCategory.find_by_name("housewares").id)
  end

  def show
  end

  def new
    @new_product = Product.new
  end

  private

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

  def set_all_products
    @all_products = Product.all
  end
end

And when writing <%= link_to clothing_path(product) %> ('product' being the placeholder in an .each loop), I get a path: root/clothing.[:id] and not root/clothing/[:id]

I know I am making a convention error, and trying to have 3 different URLs within the same controller may be where I am gong wrong.

Note: manually entering root/clothing/[:id] in the address bar does return a product correctly.

brian-welch
  • 441
  • 1
  • 7
  • 19

5 Answers5

2

When you do this:

get 'clothing', to: 'products#clothing'
get 'clothing/:id', to: 'products#show'

in your routes.rb, it creates these routes (which you can see by doing rake routes in your console):

clothing GET    /clothing(.:format)         products#clothing
         GET    /clothing/:id(.:format)     products#show

As you can see, clothing_path routes to /clothing, not /clothing/:id. So, when you do:

<%= link_to clothing_path(product) %>

rails appends the id as .id (which is what you're experiencing).

jvillian
  • 19,953
  • 5
  • 31
  • 44
  • 1
    Hi and thanks for your time in responding. This I understand completely, but I don't know how to fix it? I know the normal convention in _index_ and _show_ in the controller is _plural_ and _singular_ paths respectively - though I am not sure how to crack that nut here – brian-welch Dec 14 '18 at 09:16
  • Glad this clarified things. BTW, your question was "Why is my Rails URL route rendering a URL with a dot and not a slash?". So, I answered that question. "How do I fix it?", "How do I keep rails from...", etc. are different questions. – jvillian Dec 14 '18 at 19:15
1

@jvillian explains the cause of the issue well here, though I'd like to propose a slight refactor as a solution.

This might be a little more work, though you'd likely be better off with seperate controllers for shoes, clothing and home, and following a RESTful design. That would allow you to use resources in your routes file.

For example, your shoes_controller.rb would be like the following:

class ShoesController < ApplicationController
  before_action :set_all_products
  before_action :set_one_product, only: [:show]

  def index
    @all_shoe_products = @all_products.where(main_category_id: MainCategory.find_by_name("shoes").id)
  end

  def show
  end

  private

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

  def set_all_products
    @all_products = Product.all
  end
end

And then the routes to define them would be:

resources :shoes, only: [:index, :show]

You follow this pattern for the other resources and you'll have nicely segregated code be following good Rails conventions.

This will generate the routes as you're after:

shoes   GET    /shoes(.:format)        shoes#index
shoe    GET    /shoe/:id(.:format)     shoes#show

That will resolve your issue and give you a nicely designed app - there's also opportunity to extrapolate some of the code shared between the new controllers, though that sounds like a follow up task :)

Hope this helps - let me know if you've any questions or feedback.

SRack
  • 11,495
  • 5
  • 47
  • 60
1

I found a solution, though seems a bit of a logic mystery to me why it's working.

In routes.....

  get 'clothing', to: 'products#clothing'
  get 'clothing/:id', to: 'products#show', as: 'clothing/item'

In the index page....

  <%= link_to clothing_item_path(product) do %>

This yields the right URL structure: root/clothing/[:id]

While testing this I was expecting: root/clothing/item/[:id] ...though I prefer the result over my expectation

brian-welch
  • 441
  • 1
  • 7
  • 19
  • 1
    Glad you've got it working. I imagine that's a result of Rails sanitising the `/` in the `as: 'clothing/item'` to an underscore: you'd achieve the same result with `as: 'clothing_item'`, which is perhaps a little more readable. I'd recommend having a read of my answer - it's worth the time at the early stages to structure an app correctly, which can avoid a fair bit of pain further down the line :) Hope the rest of the project goes well. – SRack Dec 14 '18 at 09:44
0

I think what you want is parameterized routes, like this:

get ':product_line', to: 'products#index'
get ':product_line/:id', to: 'products#show'

This would allow you to create any number of custom product lines without ever having to define new methods in your controller. Assuming there is a product_line attribute on your Product model, the controller would look like this:

class ProductsController < ApplicationController
  def index
    @product_line = params[:product_line]
    @products = Product.where(product_line: @product_line)
  end

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

And your views/products/index.html.erb would look like this:

<p id="notice"><%= notice %></p>

<h1><%= @product_line %></h1>

<table>
  <thead>
    <tr>
      <th>Description</th>
      <th>Price</th>
      <th></th>
    </tr>
  </thead>

  <tbody>
    <% @products.each do |product| %>
      <tr>
        <td><%= product.description %></td>
        <td><%= product.price %></td>
        <td><%= link_to 'Show', "#{@product_line}/#{product.id}" %></td>
      </tr>
    <% end %>
  </tbody>
</table>

Note that the link_to can no longer use a Rails helper method to generate the url. You'd have to do that yourself.

The beauty of this approach is that users could type in ANY product line in the URL. If you had that product line (like say 'sporting_goods'), go ahead and display it. If not, render a page thanking them for their interest and log the fact that someone requested that product line so you can guage interest as you expand your offerings.

Plus, it's RESTful! Yay!

aridlehoover
  • 3,139
  • 1
  • 26
  • 24
0

The Rails way of solving this is by creating a nested resource:

resources :categories do
  resources :products, shallow: true
end

This nests the collection routes so that you get GET /categories/:category_id/products.

While this might not be as short as your vanity routes it is much more versatile as it will let you show the products for any potential category without bloating your codebase.

You would setup the controller as so:

class ProductsController < ApplicationController
  before_action :set_category, only: [:new, :index, :create]

  # GET /categories/:category_id/products
  def index
    @products = @category.products
  end

  # GET /categories/:category_id/products/new
  def new
    @product = @category.products.new
  end

  # POST /categories/:category_id/products
  def new
    @product = @category.products.new(product_params)
    # ...
  end

  # ...

  private
    def set_category
      @category =  MainCategory.includes(:products)
                               .find_by!('id = :x OR name = :x', x: params[:id])
    end
end

You can link to products of any category by using the category_products_path named path helper:

link_to "#{@category.name} products", category_products_path(category: @category)

You can also use the polymorphic path helpers:

link_to "#{@category.name} products", [@category, :products]
form_for [@category, @product]
redirect_to [@category, :products]

If you want to route the unnested GET /products and nested GET /categories/:category_id/products to different controllers a neat trick is to use the module option:

resources :products

resources :categories do
  resources :products, only: [:new, :index, :create], module: :categories
end

This will route the nested routes to Categories::ProductsController.

max
  • 96,212
  • 14
  • 104
  • 165