[Rails] Category function

Introduction

In the application development, I added a category function using a gem called ancestry, so I summarized it.

table of contents

  1. Select / save category
  2. Category search display
  3. Category search result display

1. Select / save category

カテゴリー選択・保存

Creating a categories table

Install ancestry.

gemfile


gem 'ancestry'

Next, create a category model.

Terminal
rails g model category

Describe has_ancestry.

app/models/category.rb


class Category < ApplicationRecord
  has_many :posts
  has_ancestry
end

Describe it in the migration file as follows. About index here

db/migrate/20XXXXXXXXXXXX_create_categories.rb


class CreateCategories < ActiveRecord::Migration[6.0]
  def change
    create_table :categories do |t|
      t.string :name,     index: true, null: false
      t.string :ancestry, index: true
      t.timestamps
    end
  end
end

I will describe the category in the google spreadsheet. The A column is id, the B column is name (category name), and the C column is ancestry (numerical value that distinguishes parents and grandchildren). You can save the data by following the procedure of File → Download → Comma-separated values (.csv current sheet).

カテゴリー記述例 カテゴリー記述例

Place the downloaded csv file in the db folder.

Describe as follows in the seeds.rb file.

db/seeds.rb 


require "csv"

CSV.foreach('db/category.csv') do |row|
  Category.create(:id => row[0], :name => row[1], :ancestry => row[2])
end 

When you execute the rails db: seed command in the terminal, the csv file is read and the DB record is automatically generated. Specify the file you want to read after foreach. The description below it will be model name.create (column name => column you want to load). row [0] → column A is id row [1] → B column is name (category name) row [2] → C column is ancestry (a number that distinguishes parents and grandchildren)

routing

Set the routing of child and grandchild categories in json format.

config/routes.rb


Rails.application.routes.draw do
  ~Abbreviation~
  resources :posts do
    collection do
      get 'top'
      get 'get_category_children', defaults: { format: 'json' }
      get 'get_category_grandchildren', defaults: { format: 'json' }
      get 'name_search'
    end
  ~Abbreviation~
end

controller

Define a parent category for the posts controller. Since it is used in multiple places, it is defined using before_action.

app/controllers/posts_controller.rb


def set_parents
  @parents = Category.where(ancestry: nil)
end

Define methods for child and grandchild categories in the posts controller.

app/controllers/posts_controller.rb


def get_category_children
  @category_children = Category.find("#{params[:parent_id]}").children
end

def get_category_grandchildren
  @category_grandchildren = Category.find("#{params[:child_id]}").children
end

Create a json.jbuilder file and convert it to json data.

ruby:app/views/posts/get_category_children.json.jbuilder 


json.array! @category_children do |child|
  json.id child.id
  json.name child.name
end

ruby:app/views/posts/get_category_grandchildren.json.jbuilder 


json.array! @category_grandchildren do |grandchild|
  json.id grandchild.id
  json.name grandchild.name
end

View

Set the operation when selecting a category with javascript.

:app/javascript/category_post.js
$(function(){
  function appendOption(category){
    var html = `<option value="${category.id}">${category.name}</option>`;
    return html;
  }
  function appendChildrenBox(insertHTML){
    var childSelectHtml = "";
    childSelectHtml = `<div class="category__child" id="children_wrapper">
                        <select id="child__category" name="post[category_id]" class="serect_field">
                          <option value="">---</option>
                          ${insertHTML}
                        </select>
                      </div>`;
    $('.append__category').append(childSelectHtml);
  }
  function appendGrandchildrenBox(insertHTML){
    var grandchildSelectHtml = "";
    grandchildSelectHtml = `<div class="category__child" id="grandchildren_wrapper">
                              <select id="grandchild__category" name="post[category_id]" class="serect_field">
                                <option value="">---</option>
                                ${insertHTML}
                                </select>
                            </div>`;
    $('.append__category').append(grandchildSelectHtml);
  }

  $('#item_category_id').on('change',function(){
    var parentId = document.getElementById('item_category_id').value;
    if (parentId != ""){
      $.ajax({
        url: '/posts/get_category_children/',
        type: 'GET',
        data: { parent_id: parentId },
        dataType: 'json'
      })
      .done(function(children){
        $('#children_wrapper').remove();
        $('#grandchildren_wrapper').remove();
        var insertHTML = '';
        children.forEach(function(child){
          insertHTML += appendOption(child);
        });
        appendChildrenBox(insertHTML);
        if (insertHTML == "") {
          $('#children_wrapper').remove();
        }
      })
      .fail(function(){
        alert('Failed to get the category');
      })
    }else{
      $('#children_wrapper').remove();
      $('#grandchildren_wrapper').remove();
    }
  });
  $('.append__category').on('change','#child__category',function(){
    var childId = document.getElementById('child__category').value;
    if(childId != ""){
      $.ajax({
        url: '/posts/get_category_grandchildren',
        type: 'GET',
        data: { child_id: childId },
        dataType: 'json'
      })
      .done(function(grandchildren){
        $('#grandchildren_wrapper').remove();
        var insertHTML = '';
        grandchildren.forEach(function(grandchild){
          insertHTML += appendOption(grandchild);
        });
        appendGrandchildrenBox(insertHTML);
        if (insertHTML == "") {
          $('#grandchildren_wrapper').remove();
        }
      })
      .fail(function(){
        alert('Failed to get the category');
      })
    }else{
      $('#grandchildren_wrapper').remove();
    }
  })
});

Display the category select box on the new post page.

ruby:app/views/posts/new.html.erb


<div class="append__category">
  <div class="category">
    <div class="form__label">
      <div class="weight-bold-text lavel__name ">
Category
      </div>
      <div class="lavel__Required">
        <%= f.collection_select :category_id, @parents, :id, :name,{ include_blank: "Please select"},class:"serect_field", id:"item_category_id" %>
      </div>
    </div>
  </div>
</div>

2. Category search display

カテゴリー検索表示

controller

app/controllers/posts_controller.rb


def top
  respond_to do |format|
    format.html
    format.json do
      if params[:parent_id]
        @childrens = Category.find(params[:parent_id]).children
      elsif params[:children_id]
        @grandChilds = Category.find(params[:children_id]).children
      elsif params[:gcchildren_id]
        @parents = Category.where(id: params[:gcchildren_id])
      end
    end
  end
end

View

In javascript, I get the child category and grandchild category that belong to which parent category the mouse is on.

:app/javascript/category.js
$(document).ready(function () {
  //Show parent category
  $('#categoBtn').hover(function (e) {
    e.preventDefault();
    e.stopPropagation();
    $('#tree_menu').show();
    $('.categoryTree').show();
  }, function () {
    //I dare not write anything
  });

  //Asynchronously display header categories
  function childBuild(children) {
    let child_category = `
                        <li class="category_child">
                          <a href="/posts/${children.id}/search"><input class="child_btn" type="button" value="${children.name}" name= "${children.id}">
                          </a>
                        </li>
                        `
    return child_category;
  }

  function gcBuild(children) {
    let gc_category = `
                        <li class="category_grandchild">
                          <a href="/posts/${children.id}/search"><input class="gc_btn" type="button" value="${children.name}" name= "${children.id}">
                          </a>
                        </li>
                        `
    return gc_category;
  }

  //Show parent category
  $('#categoBtn').hover(function (e) {
    e.preventDefault();
    e.stopPropagation();
    timeOut = setTimeout(function () {
      $('#tree_menu').show();
      $('.categoryTree').show();
    }, 500)
  }, function () {
    clearTimeout(timeOut)
  });

  //Show child categories
  $('.parent_btn').hover(function () {
    $('.parent_btn').css('color', '');
    $('.parent_btn').css('background-color', '');
    let categoryParent = $(this).attr('name');
    timeParent = setTimeout(function () {
      $.ajax({
          url: '/posts/top',
          type: 'GET',
          data: {
            parent_id: categoryParent
          },
          dataType: 'json'
        })
        .done(function (data) {
          $(".categoryTree-grandchild").hide();
          $(".category_child").remove();
          $(".category_grandchild").remove();
          $('.categoryTree-child').show();
          data.forEach(function (child) {
            let child_html = childBuild(child)
            $(".categoryTree-child").append(child_html);
          });
          $('#tree_menu').css('max-height', '490px');
        })
        .fail(function () {
          alert("Please select a category");
        });
    }, 400)
  }, function () {
    clearTimeout(timeParent);
  });

  //Show grandchild categories
  $(document).on({
    mouseenter: function () {
      $('.child_btn').css('color', '');
      $('.child_btn').css('background-color', '');
      let categoryChild = $(this).attr('name');
      timeChild = setTimeout(function () {
        $.ajax({
            url: '/posts/top',
            type: 'GET',
            data: {
              children_id: categoryChild
            },
            dataType: 'json'
          })
          .done(function (gc_data) {
            $(".category_grandchild").remove();
            $('.categoryTree-grandchild').show();
            gc_data.forEach(function (gc) {
              let gc_html = gcBuild(gc)
              $(".categoryTree-grandchild").append(gc_html);
              let parcol = $('.categoryTree').find(`input[name="${gc.root}"]`);
              $(parcol).css('color', 'white');
              $(parcol).css('background-color', '#b1e9eb');
            });
            $('#tree_menu').css('max-height', '490px');
          })
          .fail(function () {
            alert("Please select a category");
          });
      }, 400)
    },
    mouseleave: function () {
      clearTimeout(timeChild);
    }
  }, '.child_btn');

  //When selecting a grandchild category
  $(document).on({
    mouseenter: function () {
      let categoryGc = $(this).attr('name');
      timeGc = setTimeout(function () {
        $.ajax({
            url: '/posts/top',
            type: 'GET',
            data: {
              gcchildren_id: categoryGc
            },
            dataType: 'json'
          })
          .done(function (gc_result) {
            let childcol = $('.categoryTree-child').find(`input[name="${gc_result[0].parent}"]`);
            $(childcol).css('color', 'white');
            $(childcol).css('background-color', '#b1e9eb');
            $('#tree_menu').css('max-height', '490px');
          })
          .fail(function () {
            alert("Please select a category");
          });
      }, 400)
    },
    mouseleave: function () {
      clearTimeout(timeGc);
    }
  }, '.gc_btn');


  //Buttons on the category list page
  $('#all_btn').hover(function (e) {
    e.preventDefault();
    e.stopPropagation();
    $(".categoryTree-grandchild").hide();
    $(".categoryTree-child").hide();
    $(".category_grandchild").remove();
    $(".category_child").remove();
  }, function () {
    //By not writing anything, only the action when it deviates from the parent element is propagated.
  });

  //Hide categories(0 from category menu.It disappears when the cursor is removed for 8 seconds or more)
  $(document).on({
    mouseleave: function (e) {
      e.stopPropagation();
      e.preventDefault();
      timeChosed = setTimeout(function () {
        $(".categoryTree-grandchild").hide();
        $(".categoryTree-child").hide();
        $(".categoryTree").hide();
        $(this).hide();
        $('.parent_btn').css('color', '');
        $('.parent_btn').css('background-color', '');
        $(".category_child").remove();
        $(".category_grandchild").remove();
      }, 800);
    },
    mouseenter: function () {
      timeChosed = setTimeout(function () {
        $(".categoryTree-grandchild").hide();
        $(".categoryTree-child").hide();
        $(".categoryTree").hide();
        $(this).hide();
        $('.parent_btn').css('color', '');
        $('.parent_btn').css('background-color', '');
        $(".category_child").remove();
        $(".category_grandchild").remove();
      }, 800);
      clearTimeout(timeChosed);
    }
  }, '#tree_menu');

  //Category button processing
  $(document).on({
    mouseenter: function (e) {
      e.stopPropagation();
      e.preventDefault();
      timeOpened = setTimeout(function () {
        $('#tree_menu').show();
        $('.categoryTree').show();
      }, 500);
    },
    mouseleave: function (e) {
      e.stopPropagation();
      e.preventDefault();
      clearTimeout(timeOpened);
      $(".categoryTree-grandchild").hide();
      $(".categoryTree-child").hide();
      $(".categoryTree").hide();
      $("#tree_menu").hide();
      $(".category_child").remove();
      $(".category_grandchild").remove();
    }
  }, '.header__headerInner__nav__listsLeft__item');
});

Set the category selection window on the top screen.

ruby:app/views/posts/top.html.erb


  <div class="item-categories">
    <h2>
Category list
    </h2>
    <%= link_to  posts_path, class: "category-button", id: 'categoBtn' do %>
Search by category
    <% end %>
    <div id="tree_menu">
      <ul class="categoryTree">
        <% @parents.each do |parent| %>
          <li class="category_parent">
            <%= link_to search_post_path(parent) do %>
              <input type="button" value="<%= parent.name %>" name="<%= parent.id %>" class="parent_btn">
            <% end %>
          </li>
        <% end %>
      </ul>
      <ul class="categoryTree-child">
      </ul>
      <ul class="categoryTree-grandchild">
      </ul>
    </div>
  </div>

3. Category search result display

カテゴリいー検索結果表示

routing

In order to distinguish the categories by id, the search action is defined using member.

config/routes.rb


resources :posts do
    ~Abbreviation~
    member do
      get 'search'
    end
   ~Abbreviation~
end

controller

The category you clicked is conditional depending on whether it is a parent category, a child category, or a grandchild category.

app/controllers/posts_controller.rb


  def search
    @category = Category.find_by(id: params[:id])

    if @category.ancestry == nil
      category = Category.find_by(id: params[:id]).indirect_ids
      if category.empty?
        @posts = Post.where(category_id: @category.id).order(created_at: :desc)
      else
        @posts = []
        find_item(category)
      end

    elsif @category.ancestry.include?("/")
      @posts = Post.where(category_id: params[:id]).order(created_at: :desc)

    else
      category = Category.find_by(id: params[:id]).child_ids
      @posts = []
      find_item(category)
    end
  end

  def find_item(category)
    category.each do |id|
      post_array = Post.where(category_id: id).order(created_at: :desc)
      if post_array.present?
        post_array.each do |post|
          if post.present?
            @posts.push(post)
          end
        end
      end
    end
  end

View

ruby:app/views/posts/search.html.erb


  <div class="item-categories">
    <h2>
Category list
    </h2>
    <%= link_to  posts_path, class: "category-button", id: 'categoBtn' do %>
Search by category
    <% end %>
    <div id="tree_menu">
      <ul class="categoryTree">
        <% @parents.each do |parent| %>
          <li class="category_parent">
            <%= link_to search_post_path(parent) do %>
              <input type="button" value="<%= parent.name %>" name="<%= parent.id %>" class="parent_btn">
            <% end %>
          </li>
        <% end %>
      </ul>
      <ul class="categoryTree-child">
      </ul>
      <ul class="categoryTree-grandchild">
      </ul>
    </div>
  </div>

Reference link

https://qiita.com/k_suke_ja/items/aee192b5174402b6e8ca https://qiita.com/Sobue-Yuki/items/9c1b05a66ce6020ff8c1
https://qiita.com/dr_tensyo/items/88e8ddf0f5ce37040dc8 https://qiita.com/ATORA1992/items/bd824f5097caeee09678 https://qiita.com/misioro_missie/items/175af1f1678e76e59dea https://qiita.com/Rubyist_SOTA/items/49383aa7f60c42141871

Recommended Posts

[Rails] Category function
[Rails] Implementation of category function
[Rails 6] Ranking function
Rails follow function
[Rails] gem ancestry category function implementation
[Rails] Notification function
[Rails] Implement search function
[Rails] Implemented hashtag function
[rails] tag ranking function
Rails search function implementation
Implement application function in Rails
Rails fuzzy search function implementation
[Rails] Implement User search function
Introduced graph function with rails
Search function using [rails] ransack
Implement follow function in Rails
[Rails 6] Implementation of search function
[Rails] Implementation of multi-layer category function using ancestry "Preparation"
[Rails] Implementation of multi-layer category function using ancestry "seed"
Rails ~ Understanding the message function ~
Implementation of category pull-down function
Implement category function using ancestory
[Rails] (Supplement) Implemented follow function
Login function implementation with rails
[Rails] EC site cart function
Ajax bookmark function using Rails
[Rails] Implementation of tutorial function
[Rails] Implement image posting function
[Rails] Implementation of like function
[Rails 6] Pagination function implementation (kaminari)
[Rails] Implementation of multi-layer category function using ancestry "Editing form"
[Rails] Implementation of multi-layer category function using ancestry "Creation form"
[Rails] Implementation of CSV import function
[Rails] Asynchronous implementation of like function
[Ruby on Rails] Introduced paging function
[Rails] Implementation of image preview function
[Rails] Tag management function (using acts-as-taggable-on)
Implemented mail sending function with rails
Kaminari --Added pagination function of Rails
[Rails] About implementation of like function
[Rails] Implementation of user withdrawal function
[Rails] Implementation of CSV export function
Create pagination function with Rails Kaminari
Implement simple login function in Rails
[Ruby on Rails] CSV output function
[Rails] Voice posting function ~ Cloudinary, CarrierWave
[Rails] Comment function (registration / display / deletion)
[Rails] Implementation of many-to-many category functions
[Ruby on Rails] Comment function implementation
[Ruby on Rails] DM, chat function
[Rails 6] Like function (synchronous → asynchronous) implementation
Implement CSV download function in Rails
[Rails] Comment function implementation procedure memo
[Ruby on Rails] Search function (not selected)
[Rails] Addition of Ruby On Rails comment function
Rails basics
[Rails] Create an evaluation function using raty.js
Rails Review 1
[Rails] Function restrictions in devise (login / logout)
Rails API
Rails migration