Search code examples
ruby-on-railsmodelruby-on-rails-3.2categories

Rails: categories and sub-categories model rails


Without using any gems how do I do this in rails?

Main Category
 Sub Category
 Sub Category
 Sub Category

Main Category
 Sub Category
 Sub Category
 Sub Category

Main Category
 Sub Category
 Sub Category
 Sub Category

I have a table that consists of | id | level1 | level2 |

Level 1 being the main category and Level 2 being the subcategory

I would like it displayed in the view like above.

After looking around on the internet everyone seems to recommend using acts-like-a-tree gem, but i want to avoid using them as I'm fairly new to rails and I would like to understand how to do things rather than turn to gems.

Your help is much apreciated

Model:

class Category < ActiveRecord::Base
belongs_to :catalogue
    has_many :subcategories, :class_name => "Category", :foreign_key => "parent_id", :dependent => :destroy
belongs_to :parent_category, :class_name => "Category"
end

Controller:

class CataloguesController < ApplicationController
  layout 'main'
  def index
   @cats = Catalogue.all
  end

  def categories
   @cat = Catalogue.find(params[:id])
  end

end

View:

<ul class="unstyled-list">

    <% @cat.categories.order([:level1]).each do |cat|%>
        <li><%=  cat.level1 %></li>
        <li><%= cat.level2 %></li>
    <% end %>
</ul>

Solution

  • Create a model that has references to itself for a sub-category (or a sub-sub-category, etc):

    class Category < ActiveRecord::Base
      has_many :subcategories, :class_name => "Category", :foreign_key => "parent_id", :dependent => :destroy
      belongs_to :parent_category, :class_name => "Category", :optional => true
    end
    
    • the has_many defines a subcategories association of the model type Category. Ie it uses the same table.
    • the belongs_to defines a relation back to the parent category, accessible via @category.parent_category

    For more information on model associations, has_many or belongs_to, read the Associations Basics Guide.

    To create the table use this migration:

    class CreateCategories < ActiveRecord::Migration
      def self.up
        create_table :category do |t|
          t.string      :text
          # table name should be in plural 
          t.references  :parent_category, foreign_key: { to_table: :categories }
          t.timestamps
        end
      end
    end
    

    Note: this table format is (slightly) different than you proposed, but I suppose that this is not a real problem.

    The Migrations Guide contains more information on database migrations.

    In your controller use

    def index
      @category = nil
      @categories = Category.find(:all, :conditions => {:parent_id => nil } )
    end
    

    to find all categories without a parent, ie the main categories

    To find all sub-categories of any given category use:

    # Show subcategory
    def show
      # Find the category belonging to the given id
      @category = Category.find(params[:id])
      # Grab all sub-categories
      @categories = @category.parent_category
      # We want to reuse the index renderer:
      render :action => :index
    end
    

    To add a new category use:

    def new
      @category = Category.new
      @category.parent_category = Category.find(params[:id]) unless params[:id].nil?
    end 
    

    It creates a new category and sets the parent category, if it is provided (otherwise it becomes a Main Category)

    Note: I used the old rails syntax (due to laziness), but for modern versions of Rails the principle is the same.

    In your categories/index.html.erb you can use something like this:

    <h1><%= @category.nil? ? 'Main categories' : category.text %></h1>
    <table>
    <% @categories.each do |category| %>
    <tr>
      <td><%= link_to category.text, category_path(category) %></td>
      <td><%= link_to 'Edit', edit_category_path(category) unless category.parent.nil? %></td>
      <td><%= link_to 'Destroy', category_path(category), :confirm => 'Are you sure?', :method => :delete unless category.parent.nil? %></td>
    </tr>
    <% end %>
    </table>
    <p>
      <%= link_to 'Back', @category.parent_category.nil? ? categories_path : category_path(@category.parent_category) unless @category.nil? %>
      <%= link_to 'New (sub-category', new_category_path(@category) unless @category.nil? %>
    </p>
    

    It shows the name of the selected category (or Main Category) and all of its sub-categories (in a nice table). It links to all sub-categories, showing a similar layout, but for the sub-category. In the end it adds a 'new sub-category' link and a 'back' link.