【Rails】ancestryを用いた多階層カテゴリー機能の実装『Bootstrap3でウィンドウ作ってみた編』

# 目標

![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/579893/d3eb6d46-4fee-961a-18f6-22203b583d19.gif)

# 開発環境
・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

# 前提

下記実装済み。

・[Slim導入](https://qiita.com/matsubishi5/items/b639ffddecc626856039)
・[Bootstrap3導入](https://qiita.com/matsubishi5/items/b117c95f4dd71340cc80)
・[Font Awesome導入](https://qiita.com/matsubishi5/items/a4c72f436e64ad106ee8)
・[ログイン機能実装](https://qiita.com/matsubishi5/items/5bd8fdd45af955cf137d)
・[投稿機能実装](https://qiita.com/matsubishi5/items/1a5aeab651b5822d1f2c)
・[多対多のカテゴリー機能実装](https://qiita.com/matsubishi5/items/a805fbcfd96f768dd622)
・[多階層カテゴリー機能実装(準備編)](https://qiita.com/matsubishi5/items/77aadc97a2e63c98cd91)
・[多階層カテゴリー機能実装(seed編)](https://qiita.com/matsubishi5/items/91248fce37b4b0d2e0f1)
・[多階層カテゴリー機能実装(作成フォーム編)]
(https://qiita.com/matsubishi5/items/4afb4a4f307023126c66)
・[多階層カテゴリー機能実装(編集フォーム編)]
(https://qiita.com/matsubishi5/items/10e61f314f6c56b8690d)

## 1.コントローラーを編集

```ruby:homes_controller.rb
# 追記
def category_window
  @children = Category.find(params[:parent_id]).children
end
```

### 【解説】

#### ① Ajax通信で送られてきたパラメーターに対応するカテゴリーの、子カテゴリーを抽出し、インスタンス変数に代入する。

```ruby
@children = Category.find(params[:parent_id]).children
```

## 2.`json.jbuilderファイル`を作成・編集

```terminal:ターミナル
$ touch app/views/homes/category_window.json.jbuilder
```

```ruby:category_window.json.jbuilder
json.array! @children do |children|
  json.id children.id
  json.name children.name
end
```

### 【解説】

#### ① `get_category_children`アクションで抽出したレコードを繰り返し処理し、配列を作成する。

```ruby
json.array! @children do |children|
```

#### ② 各IDと名前を `①` で作成した配列に格納する。

```ruby
json.id children.id
json.name children.name
```

**◎ 親カテゴリー(ビジネス)にマウスが乗っている場合の返り値**

```
[
  {
    "id": 2, 
    "name": "金融"
  },
  {
    "id": 6, 
    "name": "経済"
  },
  {
    "id": 9, 
    "name": "経営"
  },
  {
    "id": 13, 
    "name": "マーケティング"
  },
]
```

**◎ 子カテゴリー(金融)にマウスが乗っている場合の返り値**

```
[
  {
    "id": 3, 
    "name": "株"
  },
  {
    "id": 4, 
    "name": "為替"
  },
  {
    "id": 5, 
    "name": "税金"
  },
]
```

## 3.ルーティングを追加

```ruby:routes.rb
# 追記
get 'get_category/new', to: 'homes#category_window', defaults: { format: 'json' }
```

## 4.ビューを編集

```slim:application.html.slim
body
  header
    nav.navbar.navbar-default.navbar-fixed-top
      .container-fluid
        ul.nav.navbar-nav.navbar-right
          li.dropdown role='presentation'
            a.dropdown-toggle data-toggle='dropdown' href='#' role='button' aria-expanded='false'
              i.fas.fa-list-ul
              span
                |  カテゴリーから探す
              span.caret
            ul.dropdown-menu role='menu'
              li role='presentation'
                - Category.where(ancestry: nil).each do |parent|
                  = link_to parent.name, root_path, id: "#{parent.id}", class: 'parent-category'
              br
              li role='presentation' class='children-list'
              br
              li role='presentation' class='grandchildren-list'
```

### 【解説】

**※Bootstrapの書き方については省略します。**

#### ① ancestryの値が`nil`、つまり親カテゴリーを全て抽出し、プルダウンメニューに表示する。

```slim
- Category.where(ancestry: nil).each do |parent|
  = link_to parent.name, root_path, id: "#{parent.id}", class: 'parent-category'
```

#### ② 子カテゴリーを表示する場所を用意する。

```slim
li role='presentation' class='children-list'
```

#### ③ 孫カテゴリーを表示する場所を用意する。

```slim
li role='presentation' class='grandchildren-list'
```

## 5.JavaScriptファイルを作成・編集

```terminal:ターミナル
$ touch app/assets/javascripts/category_window.js
```

```js:category_window.js
$(function() {
  function buildChildHTML(children) {
    let html = `
      <a class="children-category" id="${children.id}" href="/">
        ${children.name}
      </a>
    `;
    return html;
  }

  $('.parent-category').on('mouseover', function() {
    let id = this.id;
    $('.children-category').remove();
    $('.grandchildren-category').remove();
    $.ajax({
      type: 'GET',
      url: '/get_category/new',
      data: {
        parent_id: id,
      },
      dataType: 'json',
    }).done(function(children) {
      children.forEach(function(child) {
        let html = buildChildHTML(child);
        $('.children-list').append(html);
      });
    });
  });

  function buildGrandChildHTML(children) {
    let html = `
      <a class="grandchildren-category" id="${children.id}" href="/">
        ${children.name}
      </a>
    `;
    return html;
  }

  $(document).on('mouseover', '.children-category', function() {
    let id = this.id;
    $.ajax({
      type: 'GET',
      url: '/get_category/new',
      data: {
        parent_id: id,
      },
      dataType: 'json',
    }).done(function(children) {
      children.forEach(function(child) {
        let html = buildGrandChildHTML(child);
        $('.grandchildren-list').append(html);
      });
      $(document).on('mouseover', '.children-category', function() {
        $('.grandchildren-category').remove();
      });
    });
  });
});
```

### 【解説】

#### ① 子カテゴリーのHTMLを作成する。

```js
function buildChildHTML(children) {
  let html = `
    <a class="children-category" id="${children.id}" href="/">
      ${children.name}
    </a>
  `;
  return html;
}
```

#### ② どの親カテゴリーにマウスが乗っているかによって、子カテゴリーの表示内容を変更する。

```js
  $('.parent-category').on('mouseover', function() {
    let id = this.id;
    $('.children-category').remove();
    $('.grandchildren-category').remove();
    $.ajax({
      type: 'GET',
      url: '/get_category/new',
      data: {
        parent_id: id,
      },
      dataType: 'json',
    }).done(function(children) {
      children.forEach(function(child) {
        let html = buildChildHTML(child);
        $('.children-list').append(html);
      });
    });
  });
```

**◎ 親カテゴリーにマウスが乗った時に発火するイベントを作成する。**

```js
$('.parent-category').on('mouseover', function() {});
```

**◎ `category_window.json.jbuilder`から送られてきたIDを、変数へ代入する。**

```js
let id = this.id;
```

**◎ とりあえず子カテゴリー以下を削除しておく。**

```js
$('.children-category').remove();
$('.grandchildren-category').remove();
```

**◎ パラメーター(parent_id)に先ほど作成した変数を設定して、`category_window`アクションを非同期で実行する。**

```js
  $.ajax({
    type: 'GET',
    url: '/get_category/new',
    data: {
      parent_id: id,
    },
    dataType: 'json',
  })
```

**◎ Ajax通信が成功した場合は対応する子カテゴリーのHTMLを作成し、表示する。**

```js
.done(function(children) {
  children.forEach(function(child) {
    var html = buildChildHTML(child);
    $('.children-list').append(html);
  });
});
```

#### ③孫カテゴリーのHTMLを作成する。

```js
function buildGrandChildHTML(children) {
  var html = `
    <a class="grandchildren-category" id="${children.id}" href="/">
      ${children.name}
    </a>
  `;
  return html;
}
```

#### ④ どの子カテゴリーにマウスが乗っているかによって、孫カテゴリーの表示内容を変更する。(`③` とほぼ同じなので説明は省略)

```js
$(document).on('mouseover', '.children-category', function() {
  var id = this.id;
  $.ajax({
    type: 'GET',
    url: '/get_category/new',
    data: {
      parent_id: id,
    },
    dataType: 'json',
  }).done(function(children) {
    children.forEach(function(child) {
      var html = buildGrandChildHTML(child);
      $('.grandchildren-list').append(html);
    });
    $(document).on('mouseover', '.children-category', function() {
      $('.grandchildren-category').remove();
    });
  });
});
```

# 注意

`turbolinks`を無効化しないとプルダウンメニューが非同期で動作しないので、必ず無効化しておきましょう。

[turbolinksを無効化する方法](https://qiita.com/matsubishi5/items/c4c8a5df03ae630ae534)