7
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Rails]カテゴリー機能

Posted at

はじめに

アプリ開発において、ancestryというgemを用いてカテゴリー機能を加えたのでまとめました。

目次

  1. カテゴリー選択・保存
  2. カテゴリー検索表示
  3. カテゴリー検索結果表示

1. カテゴリー選択・保存

カテゴリー選択・保存

categoriesテーブルの作成

ancestryをインストールします。

gemfile
gem 'ancestry'

次にcategoryモデルを作成します。

ターミナル
rails g model category

has_ancestryを記述します。

app/models/category.rb
class Category < ApplicationRecord
  has_many :posts
  has_ancestry
end

以下のようにマイグレーションファイルに記述します。
indexについてはこちら

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

googleスプレッドシートにカテゴリーを記述していきます。
Aの列がid、Bの列がname(カテゴリー名)、Cの列がancestry(親子孫を見分ける数値)となります。
データの保存方法は、ファイル → ダウンロード → カンマ区切りの値(.csv 現在のシート) の手順で保存できます。

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

ダウンロードしたcsvファイルはdbフォルダに配置します。

seeds.rbファイル内へ以下の通り記述します。

db/seeds.rb 
require "csv"

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

ターミナルでrails db:seedコマンドを実行するとcsvファイルを読み込み自動でDBのレコードが生成されます。
foreachの後に読み込みたいファイルの指定を行います。
その下の記述については、モデル名.create(カラム名 => 読み込みたい列)となります。
row[0] → Aの列がid
row[1] → Bの列がname(カテゴリー名)
row[2] → Cの列がancestry(親子孫を見分ける数値)

ルーティング

子、孫カテゴリーをjson形式でルーティングを設定します。

config/routes.rb
Rails.application.routes.draw do
  ~~
  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
  ~~
end

コントローラー

postsコントローラーに親カテゴリーを定義します。
複数箇所で使用するためbefore_actionを使って定義します。

app/controllers/posts_controller.rb
def set_parents
  @parents = Category.where(ancestry: nil)
end

postsコントローラーに子、孫カテゴリーのメソッドを定義します。

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

json.jbuilderファイルを作成し、jsonデータへ変換します。

app/views/posts/get_category_children.json.jbuilder 
json.array! @category_children do |child|
  json.id child.id
  json.name child.name
end
app/views/posts/get_category_grandchildren.json.jbuilder 
json.array! @category_grandchildren do |grandchild|
  json.id grandchild.id
  json.name grandchild.name
end

ビュー

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('カテゴリー取得に失敗しました');
      })
    }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('カテゴリー取得に失敗しました');
      })
    }else{
      $('#grandchildren_wrapper').remove();
    }
  })
});

新規投稿ページにカテゴリーセレクトボックスを表示させます。

app/views/posts/new.html.erb
<div class="append__category">
  <div class="category">
    <div class="form__label">
      <div class="weight-bold-text lavel__name ">
        カテゴリー
      </div>
      <div class="lavel__Required">
        <%= f.collection_select :category_id, @parents, :id, :name,{ include_blank: "選択してください"},class:"serect_field", id:"item_category_id" %>
      </div>
    </div>
  </div>
</div>

2. カテゴリー検索表示

カテゴリー検索表示

コントローラー

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

ビュー

javascriptでどの親カテゴリーにの上にマウスがいるのか、それに属する子カテゴリーや孫カテゴリーを取得しています。

:app/javascript/category.js
$(document).ready(function () {
  // 親カテゴリーを表示
  $('#categoBtn').hover(function (e) {
    e.preventDefault();
    e.stopPropagation();
    $('#tree_menu').show();
    $('.categoryTree').show();
  }, function () {
    // あえて何も記述しない
  });

  // 非同期にてヘッダーのカテゴリーを表示
  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;
  }

  // 親カテゴリーを表示
  $('#categoBtn').hover(function (e) {
    e.preventDefault();
    e.stopPropagation();
    timeOut = setTimeout(function () {
      $('#tree_menu').show();
      $('.categoryTree').show();
    }, 500)
  }, function () {
    clearTimeout(timeOut)
  });

  // 子カテゴリーを表示
  $('.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("カテゴリーを選択してください");
        });
    }, 400)
  }, function () {
    clearTimeout(timeParent);
  });

  // 孫カテゴリーを表示
  $(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("カテゴリーを選択してください");
          });
      }, 400)
    },
    mouseleave: function () {
      clearTimeout(timeChild);
    }
  }, '.child_btn');

  // 孫カテゴリーを選択時
  $(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("カテゴリーを選択してください");
          });
      }, 400)
    },
    mouseleave: function () {
      clearTimeout(timeGc);
    }
  }, '.gc_btn');


  // カテゴリー一覧ページのボタン
  $('#all_btn').hover(function (e) {
    e.preventDefault();
    e.stopPropagation();
    $(".categoryTree-grandchild").hide();
    $(".categoryTree-child").hide();
    $(".category_grandchild").remove();
    $(".category_child").remove();
  }, function () {
    // あえて何も記述しないことで親要素に外れた際のアクションだけを伝搬する
  });

  // カテゴリーを非表示(カテゴリーメニュから0.8秒以上カーソルを外したら消える)
  $(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');

  // カテゴリーボタンの処理
  $(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');
});

トップ画面にカテゴリー選択ウィンドウをセットします。

app/views/posts/top.html.erb
  <div class="item-categories">
    <h2>
      カテゴリー一覧
    </h2>
    <%= link_to  posts_path, class: "category-button", id: 'categoBtn' do %>
      カテゴリーから探す
    <% 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. カテゴリー検索結果表示

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

ルーティング

カテゴリーをidで区別するため、memberを用いてsearchアクションを定義しています。

config/routes.rb
resources :posts do
    ~~
    member do
      get 'search'
    end
   ~~
end

コントローラー

クリックしたカテゴリーが、親カテゴリー、子カテゴリー、孫カテゴリーのどれなのかで条件分岐しています。

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

ビュー

app/views/posts/search.html.erb
  <div class="item-categories">
    <h2>
      カテゴリー一覧
    </h2>
    <%= link_to  posts_path, class: "category-button", id: 'categoBtn' do %>
      カテゴリーから探す
    <% 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>

参考リンク

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

7
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?