7
13

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】ancestryを用いた多階層カテゴリー機能の実装『作成フォーム編』

Last updated at Posted at 2020-06-20

目標

ezgif.com-video-to-gif.gif

開発環境

・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

前提

下記実装済み。

Slim導入
Bootstrap3導入
Font Awesome導入
ログイン機能実装
投稿機能実装
多対多のカテゴリー機能実装
多階層カテゴリー機能実装(準備編)
多階層カテゴリー機能実装(seed編)

実装

1.category.rbにメソッドを作成

category.rb
def self.category_parent_array_create
  category_parent_array = ['---']
  Category.where(ancestry: nil).each do |parent|
    category_parent_array << [parent.name, parent.id]
  end
  return category_parent_array
end

【解説】

① 親カテゴリーのセレクトボックスを作成する為の配列を用意し、初期値を格納する。

category_parent_array = ['---']

② ancestryの値がnil、つまり親カテゴリーを全て抽出し、①で作成した配列にカテゴリー名とIDを格納する。

Category.where(ancestry: nil).each do |parent|
  category_parent_array << [parent.name, parent.id]
end

※重要ポイント
[parent.name, parent.id]
➡︎ ビューで親カテゴリーをセレクトボックスに表示する際、
第1引数(parent.name)がブラウザに表示される値になり、
第2引数(parent.id)がパラメーターとして送信する値になる。

で作成した配列を戻り値として返す。

return category_parent_array

2.book_category.rbにメソッドを作成

book_category.rb
  def self.maltilevel_category_create(book, parent_id, children_id, grandchildren_id)
    if parent_id.present? && parent_id != '---'
      category = Category.find(parent_id)
      BookCategory.create(book_id: book.id, category_id: category.id)
    end

    if children_id.present? && children_id != '---'
      category = Category.find(children_id)
      BookCategory.create(book_id: book.id, category_id: category.id)
    end

    if grandchildren_id.present? && grandchildren_id != '---'
      category = Category.find(grandchildren_id)
      BookCategory.create(book_id: book.id, category_id: category.id)
    end
  end

【解説】

① コントローラーから引数を4つ受け取る。

(book, parent_id, children_id, grandchildren_id)

book : 作成する本のデータ
parent_id : 親カテゴリーのID
children_id : 子カテゴリーのID
grandchildren_id : 孫カテゴリーのID

② 親カテゴリーのIDを引数として受け取れた場合かつ、初期値でない場合処理を実行する。

if parent_id.present? && parent_id != '---'

③ Categoryモデルから引数で受け取った親カテゴリーIDに対応するレコードを抽出し、変数に代入する。

category = Category.find(parent_id)

④ BookCategory(中間テーブル)にレコードを作成する。**

BookCategory.create(book_id: book.id, category_id: category.id)

3.コントローラーを編集

books_controller.rb
def create
  @book = Book.new(book_params)
  @book.user_id = current_user.id
  if @book.save
    BookCategory.maltilevel_category_create(
      @book,
      params[:parent_id],
      params[:children_id],
      params[:grandchildren_id]
    )
    redirect_to books_path
  else
    @books = Book.all
    @category_parent_array = Category.category_parent_array_create
    render 'index'
  end
end

def index
  @book = Book.new
  @books = Book.all
  @category_parent_array = Category.category_parent_array_create
end

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

def get_category_grandchildren
  @category_grandchildren = Category.find(params[:children_id]).children
end

【解説】

maltilevel_category_createメソッドに引数を4つ渡して実行する。

BookCategory.maltilevel_category_create(
  @book,
  params[:parent_id],
  params[:children_id],
  params[:grandchildren_id]
)

◎ Ajax通信で送られてきたパラメーターを受け取る。

params[:parent_id],
params[:children_id],
params[:grandchildren_id]

category_parent_array_createメソッドの戻り値として受け取った配列をインスタンス変数に代入する。

@category_parent_array = Category.category_parent_array_create

③ 選択された親カテゴリーに紐付く子カテゴリーを抽出する。

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

④ 選択された子カテゴリーに紐付く孫カテゴリーを抽出する。

def get_category_grandchildren
  @category_grandchildren = Category.find(params[:children_id]).children
end

4.json.jbuilderファイルを作成・編集

ターミナル
$ touch app/views/books/get_category_children.json.jbuilder
books/get_category_children.json.jbuilder
json.array! @category_children do |children|
  json.id children.id
  json.name children.name
end
ターミナル
$ touch app/views/books/get_category_grandchildren.json.jbuilder
books/get_category_grandchildren.json.jbuilder
json.array! @category_grandchildren do |grandchildren|
  json.id grandchildren.id
  json.name grandchildren.name
end

【解説】

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

json.array! @category_children do |children|

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

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": "税金"
  },
]

5.ルーティングを追加

routes.rb
# 追記
get 'get_category/children', to: 'books#get_category_children', defaults: { format: 'json' }
get 'get_category/grandchildren', to: 'books#get_category_grandchildren', defaults: { format: 'json' }

6.ビューを編集

books/index.html.slim
.category-form
  = label_tag 'ジャンル'
  = select_tag 'parent_id', options_for_select(@category_parent_array), id: 'parent-category', class: 'form-control'
  i.fas.fa-chevron-down
br

【解説】

① コントローラーで定義したインスタンス変数内のデータをセレクトボックスに表示し、プロパティ(パラメーター名)をparent_idに設定する。

= select_tag 'parent_id', options_for_select(@category_parent_array), id: 'parent-category', class: 'form-control'

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

ターミナル
$ touch app/assets/javascripts/category_form.js
category_form.js
$(function() {
  function appendOption(category) {
    let html = `<option value='${category.id}' data-category='${category.id}'>${category.name}</option>`;
    return html;
  }

  function appendChidrenBox(insertHTML) {
    let childrenSelectHtml = '';
    childrenSelectHtml = `
      <div id='children-wrapper'>
        <select id='children-category' class='form-control' name='[children_id]'>
          <option value='---' data-category='---'>---</option>
          ${insertHTML}
        </select>
        <i class='fas fa-chevron-down'></i>
      </div>
    `;
    $('.category-form').append(childrenSelectHtml);
  }

  function appendGrandchidrenBox(insertHTML) {
    let grandchildrenSelectHtml = '';
    grandchildrenSelectHtml = `
      <div id='grandchildren-wrapper'>
        <select id='grandchildren-category' class='form-control' name='[grandchildren_id]'>
          <option value='---' data-category='---'>---</option>
          ${insertHTML}
        </select>
        <i class='fas fa-chevron-down'></i>
      </div>
    `;
    $('.category-form').append(grandchildrenSelectHtml);
  }

  $('#parent-category').on('change', function() {
    let parentId = document.getElementById('parent-category').value;
    if (parentId != '---') {
      $.ajax({
        url: '/get_category/children',
        type: 'GET',
        data: {
          parent_id: parentId,
        },
        dataType: 'json',
      })
        .done(function(children) {
          $('#children-wrapper').remove();
          $('#grandchildren-wrapper').remove();
          let insertHTML = '';
          children.forEach(function(children) {
            insertHTML += appendOption(children);
          });
          appendChidrenBox(insertHTML);
        })
        .fail(function() {
          alert('ジャンル取得に失敗しました');
        });
    } else {
      $('#children-wrapper').remove();
      $('#grandchildren-wrapper').remove();
    }
  });

  $('.category-form').on('change', '#children-category', function() {
    let childrenId = $('#children-category option:selected').data('category');
    if (childrenId != '---') {
      $.ajax({
        url: '/get_category/grandchildren',
        type: 'GET',
        data: {
          children_id: childrenId,
        },
        dataType: 'json',
      })
        .done(function(grandchildren) {
          if (grandchildren.length != 0) {
            $('#grandchildren-wrapper').remove();
            let insertHTML = '';
            grandchildren.forEach(function(grandchildren) {
              insertHTML += appendOption(grandchildren);
            });
            appendGrandchidrenBox(insertHTML);
          }
        })
        .fail(function() {
          alert('ジャンル取得に失敗しました');
        });
    } else {
      $('#grandchildren-wrapper').remove();
    }
  });
});

【解説】

① セレクトボックスのオプションを設定する。

$(function() {
  function appendOption(category) {
    let html = `<option value='${category.id}' data-category='${category.id}'>${category.name}</option>`;
    return html;
  }

◎ パラメーターとして送信する値を設定する。

<option value='${category.id}' data-category='${category.id}'>

※重要ポイント
3.コントローラーを編集 で受け取るパラメーターに該当する。

◎ セレクトボックスに表示する値を設定する。

${category.name}

② 子ジャンルのセレクトボックスを作成する。

function appendChidrenBox(insertHTML) {
  let childrenSelectHtml = '';
  childrenSelectHtml = `
    <div id='children-wrapper'>
      <select id='children-category' class='form-control' name='[children_id]'>
        <option value='---' data-category='---'>---</option>
        ${insertHTML}
      </select>
      <i class='fas fa-chevron-down'></i>
    </div>
  `;
  $('.category-form').append(childrenSelectHtml);
}

で作成したパラメーターの、パラメーター名を設定する。

name='[children_id]

で設定したオプションを元に、子カテゴリーのセレクトボックスを作成する。

${insertHTML}

◎ 子カテゴリーのセレクトボックスを表示する。

$('.category-form').append(childrenSelectHtml);

③ 孫ジャンルのセレクトボックスを作成する。( とほぼ同じなので説明は省略)

function appendGrandchidrenBox(insertHTML) {
  let grandchildrenSelectHtml = '';
  grandchildrenSelectHtml = `
    <div id='grandchildren-wrapper'>
      <select id='grandchildren-category' class='form-control' name='[grandchildren_id]'>
        <option value='---' data-category='---'>---</option>
        ${insertHTML}
      </select>
      <i class='fas fa-chevron-down'></i>
    </div>
  `;
  $('.category-form').append(grandchildrenSelectHtml);
}

④ 親カテゴリーが選択された時に発火するイベントを作成する。

$('#parent-category').on('change', function() {
  let parentId = document.getElementById('parent-category').value;
  if (parentId != '---') {
    $.ajax({
      url: '/get_category/children',
      type: 'GET',
      data: {
        parent_id: parentId,
      },
      dataType: 'json',
    })
      .done(function(children) {
        $('#children-wrapper').remove();
        $('#grandchildren-wrapper').remove();
        let insertHTML = '';
        children.forEach(function(children) {
          insertHTML += appendOption(children);
        });
        appendChidrenBox(insertHTML);
      })
      .fail(function() {
        alert('ジャンル取得に失敗しました');
      });
  } else {
    $('#children-wrapper').remove();
    $('#grandchildren-wrapper').remove();
  }
});

◎ 親カテゴリーが選択された時に発火する。

$('#parent-category').on('change', function() {});

◎ 選択された親カテゴリーのIDを取得し、変数に代入する。

let parentId = document.getElementById('parent-category').value;

◎ 親カテゴリーが初期値でない場合、
パラメーター(parent_id)に先ほど取得した親カテゴリーのIDを設定して、
get_category_childrenアクションを非同期で実行する。

if (parentId != '---') {
  $.ajax({
    url: '/get_category/children',
    type: 'GET',
    data: {
      parent_id: parentId,
    },
    dataType: 'json',
  })

◎ Ajax通信が成功した場合は、子カテゴリーのセレクトボックスを作成する。
また、既に子カテゴリー以下のセレクトボックスが表示されている状態で親カテゴリーが変更された場合は、
子カテゴリー以下のセレクトボックスを削除し、子カテゴリーのセレクトボックスを作成し直す。

.done(function(children) {
  $('#children-wrapper').remove();
  $('#grandchildren-wrapper').remove();
  let insertHTML = '';
  children.forEach(function(children) {
    insertHTML += appendOption(children);
  });
  appendChidrenBox(insertHTML);
})

◎ 非同期通信に失敗した場合は、アラートを表示する。

.fail(function() {
  alert('ジャンル取得に失敗しました');
});

◎ 親カテゴリーが初期値の場合は、子カテゴリー以下を削除する。

} else {
  $('#children-wrapper').remove();
  $('#grandchildren-wrapper').remove();
}

④ 子カテゴリーが選択された時に発火するイベントを作成する。( とほぼ同じ)

$('.category-form').on('change', '#children-category', function() {
  let childrenId = $('#children-category option:selected').data('category');
  if (childrenId != '---') {
    $.ajax({
      url: '/get_category/grandchildren',
      type: 'GET',
      data: {
        children_id: childrenId,
      },
      dataType: 'json',
    })
      .done(function(grandchildren) {
        if (grandchildren.length != 0) {
          $('#grandchildren-wrapper').remove();
          let insertHTML = '';
          grandchildren.forEach(function(grandchildren) {
            insertHTML += appendOption(grandchildren);
          });
          appendGrandchidrenBox(insertHTML);
        }
      })
      .fail(function() {
        alert('ジャンル取得に失敗しました');
      });
  } else {
    $('#grandchildren-wrapper').remove();
  }
});

◎ 孫カテゴリーが無いカテゴリーもあるので条件を付与する。(必要に応じて子カテゴリーにも条件を付与する)

if (grandchildren.length != 0)

注意

turbolinksを無効化しないとセレクトボックスが非同期で動作しないので、必ず無効化しておきましょう。

turbolinksを無効化する方法

続編

多階層カテゴリー機能実装(編集フォーム編)

7
13
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
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?