[Rails] gem ancestry category function implementation

Introduction

My name is Kusano. This post will be an output after reviewing what I learned at the programming school. The text is poor because it is a memo for myself, but I hope it will help beginners as much as possible. As the title suggests, the content is about the implementation of the category function of the flea market apps by team development conducted at the school. I think there are many immature points. Please point out any deficiencies. I will improve it from time to time.

Finished product

When the parent category is selected, the event will fire and the child, grandchild and select boxes will be displayed.

** Category registration screen when listing products ** Image from Gyazo

** Category information call screen for detailed product information ** qiita_カテゴリー商品情報.png

Implementation procedure

** 1. Creating a model in DB / Association definition **

--Installing gem ancestry --Creating a category model --category Creation of migration file --Association definition with item model --Adding records to DB by reading CSV file

** 2. Category registration function **

--Routing settings for JSON of child and grandchild categories --Items controller instance variable definition of parent category --items Controller method definition for JSON of child and grandchild categories --Conversion description to JSON data to json.jbuilder file --Set behavior when selecting parent, child, grandchild by JavaScript --Call description to html.haml file

** 3. Calling category information **

--Define parent, child, and grandchild instance variables in the show method of the items controller --items Described in a haml file using the instance variable defined in the controller and called to the view

Model creation / association definition to DB

Use the Ruby on Rails “ancestry” gem to add category functionality.

Gemfile 


gem 'ancestry'

Create a category model with the rails g model category command in the terminal. Describe has_ancestory.

app/models/category.rb 


class Category < ApplicationRecord
  has_ancestry
  has_many :items
end

Describe as follows in the categories migration file and execute the rails db: migrate command in the terminal

db/category.rb&nbsp;


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

Describe all categories on 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). I wrote all the categories in a CSV file, but there were 1368 lines, so I had a hard time inputting. (I remember the clerical work I was doing in my previous job.) By the way, there is a category without grandchildren, so be careful not to shift the ancestry value. You can save the data by following the procedure of File → Download → Comma-separated values (.csv current sheet). Image from Gyazo Image from Gyazo Put the downloaded CSV file directly into the db file. Cursor_と_category_csv_—_freemarket_sample_75a-2.png

Write the following in the seeds.rb file and execute the rails db: seed command in the terminal to read the CSV file and automatically generate a DB record. Although it is an explanation about the description content, specify the file you want to read after foreach. For the description below it, 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)

db/seeds.rb&nbsp;


require "csv"

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

Category registration function

Routing settings for JSON in child and grandchild categories The top two in the collection do are the routes for JSON created this time. By writing defaults: {fomat:'json'}, it will be dedicated to JSON.

config/routes.rb&nbsp;


  resources :items do
    resources :comments,  only: [:create, :destroy]
    resources :favorites, only: [:create, :destroy]
    collection do
      get 'get_category_children', defaults: { fomat: 'json'}
      get 'get_category_grandchildren', defaults: { fomat: 'json'}
      get 'search'
      get 'post_done'
      get 'delete_done'
      get 'detail_search'
      get 'update_done'
    end
  end

Instance variable definition of parent category to items controller Define the following description in the new method. (As the implementation progresses, it will be used for other actions as well, so it will be redefined as private later and refactored with before_action.)

app/controllers/items_controller.rb&nbsp;


@category_parent_array = Category.where(ancestry: nil)

Items Controller method definition for JSON of child and grandchild categories The description in parentheses of params [] describes: parent_id and: child_id sent by ajax in the JavaScript file explained later.

app/controllers/items_controller.rb&nbsp;


  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 describe the conversion to JSON data

rb:app/views/items/get_category_children.json.jbuilder&nbsp;


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

rb:app/views/items/get_category_grandchildren.json.jbuilder&nbsp;


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

Set the behavior when selecting parent, child, grandchild by JavaScript

app/assets/javascripts/category.js&nbsp;


$(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="item[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="item[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: '/items/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);
      })
      .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 != "" && childId != 46 && childId != 74 && childId != 134 && childId != 142 && childId != 147 && childId != 150 && childId != 158){
      $.ajax({
        url: '/items/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);
      })
      .fail(function(){
        alert('Failed to get the category');
      })
    }else{
      $('#grandchildren_wrapper').remove();
    }
  })
});

I will explain the above description. The event is set to fire when a category is selected in the parent select box that is displayed first in the following description in the middle row. The description on the second line gets the id of the selected category and defines the variables.


$('#item_category_id').on('change',function(){
    var parentId = document.getElementById('item_category_id').value;

Next, put the id of the category acquired in ① by ajax into parent_id, pass the parent_id to the controller through the route for JSON routed earlier, and acquire the record of the child category.


      $.ajax({
        url: '/items/get_category_children/',
        type: 'GET',
        data: { parent_id: parentId },
        dataType: 'json'
      })

If the ajax communication is successful, the record of the child category acquired in ② is expanded by the forEach method on the 5th line. The remove method on the 2nd and 3rd lines is written to remove the child and grandchild select boxes when another category is selected again in the parent select box.


.done(function(children){
        $('#children_wrapper').remove();
        $('#grandchildren_wrapper').remove();
        var insertHTML = '';
        children.forEach(function(child){
          insertHTML += appendOption(child);
        });
        appendChildrenBox(insertHTML);
      })

The appendOption described in the upper row passes the child record obtained earlier with the argument of appendOption (child) on the 6th line of ③, and embeds id and name (category name) in the option tag, respectively.


  function appendOption(category){
    var html = `<option value="${category.id}">${category.name}</option>`;
    return html;
  }

For appendChildrenBox, put the contents of ④ in the insertHTML described in the 6th line of ③, and pass the option tag of ④ in the argument of appendChildrenBox (insertHTML) described in the 8th line of ③. Embed the option tag with the description of \ $ {insertHTML} Finally, the description of $ ('.append__category'). Append (childSelectHtml); causes the browser to display the child select box asynchronously. The flow of the grandchild select box is the same.


  function appendChildrenBox(insertHTML){
    var childSelectHtml = "";
    childSelectHtml = `<div class="category__child" id="children_wrapper">
                        <select id="child__category" name="item[category_id]" class="serect_field">
                          <option value="">---</option>
                          ${insertHTML}
                        </select>
                      </div>`;
    $('.append__category').append(childSelectHtml);
  }

If ajax communication fails, an alert will be displayed.


      .fail(function(){
        alert('Failed to get the category');
      })

For the if statement described above ajax, the initial value is set to nil using the include_blank option of collection_select in the haml file, and when this selects the option with id from nil, ajax communication starts. On the contrary, if you return to the initial value of "Please select", it becomes else and the grandchild select box is deleted.


 if (parentId != ""){
#The description in the middle is omitted
    }else{
      $('#grandchildren_wrapper').remove();

Regarding the difference between the description of the child and the description of the grandchild, in the description of the grandchild, if the one added by javaScript is targeted for event firing, it is necessary to describe as follows. \ $ (Investigation scope) .on (event name, place where the event occurs, function () { In addition, conditional branching is set so that ajax communication starts when the category is not a grandchildless category in the if statement.


$('.append__category').on('change','#child__category',function(){
    var childId = document.getElementById('child__category').value;
    if(childId != "" && childId != 46 && childId != 74 && childId != 134 && childId != 142 && childId != 147 && childId != 150 && childId != 158){

There is one caveat when displaying the select box asynchronously on the browser. It is about the part where "item [category_id]" is set in the select tag for both children and grandchildren with the name option. The name option specifies which category_id of parent, child, or grandchild should be stored in the items table.

** Add name option only to children and enter grandchild category ** → In DB, child id is saved ** Add name option only to grandchildren, enter category without grandchildren ** → In the DB, the parent category_id is saved ** Add name option for both children and grandchildren, and enter up to grandchild category ** → In the DB, the select tag with the name option displayed asynchronously at the end has priority and is saved.

The reason why it is necessary to save the grandchild category_id is that the parent and child records can be called based on the grandchild category_id when calling the category information.


  function appendChildrenBox(insertHTML){
    var childSelectHtml = "";
    childSelectHtml = `<div class="category__child" id="children_wrapper">
                        <select id="child__category" name="item[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="item[category_id]" class="serect_field">
                                <option value="">---</option>
                                ${insertHTML}
                                </select>
                            </div>`;
    $('.append__category').append(grandchildSelectHtml);
  }

Display the parent select box in the haml file.

rb:app/views/items/_form.html.haml&nbsp;


    .append__category
      .category
        .form__label
          .lavel__name 
Category
          .lavel__Required
            [Mandatory]
        =f.collection_select :category_id, @category_parent_array, :id, :name,{ include_blank: "Please select"},class:"serect_field"

Calling category information

Define parent, child, and grandchild instance variables in the show method of the items controller

app/controllers/items_controller.rb&nbsp;


@category_id = @item.category_id
@category_parent = Category.find(@category_id).parent.parent
@category_child = Category.find(@category_id).parent
@category_grandchild = Category.find(@category_id)

Call the category registered using the instance variable defined earlier in the items controller to the view with a haml file. The if statement conditionally branches the display with no grandchildren or with grandchildren. (Since the path is not specified for the link this time, it is set as #.)

rb:app/views/items/_main_show.html.haml&nbsp;


            %table 
              %tr 
                %th seller
                %td= @user.nickname
              %tr 
                %th category
                - if [46, 74, 134, 142, 147, 150, 158].include?(@category_id)
                  %td
                    = link_to "#{@category_child.name}","#"
                    %br= link_to "#{@category_grandchild.name}","#" 
                -else
                  %td
                    = link_to "#{@category_parent.name}","#"
                    %br= link_to "#{@category_child.name}","#"
                    = link_to "#{@category_grandchild.name}","#"
              %tr
                %th brand
                %td= @item.brand_name
              %tr
                %th product size
                %td
              %tr
                %th Product status
                %td= @item.item_status
              %tr
                %th Shipping charges
                %td= @item.delivery_fee
              %tr
                %th Shipment area
                %td= link_to "#{@item.shipping_origin}","#"
              %tr
                %th Estimated shipping date
                %td= @item.days_until_shipping

Articles that I used as a reference

Realize dynamic category select box using multi-layered data by ancestry ~ Ajax ~ [Translation] Gem Ancestry Official Document

Recommended Posts

[Rails] gem ancestry category function implementation
[Rails] Implementation of category function
[Rails] Implementation of multi-layer category function using ancestry "Preparation"
[Rails] Category function
[Rails] Implementation of multi-layer category function using ancestry "seed"
[Rails] Implementation of multi-layer category function using ancestry "Editing form"
[Rails] Implementation of multi-layer category function using ancestry "Creation form"
Rails search function implementation
Rails fuzzy search function implementation
Implementation of category pull-down function
Login function implementation with rails
[Rails] Implementation of tutorial function
[Rails] Implementation of like function
[Rails 6] Pagination function implementation (kaminari)
[Rails] Implementation of CSV import function
[Rails] Implementation of image preview function
[Rails] About implementation of like function
[Rails] Implementation of user withdrawal function
[Rails] Implementation of CSV export function
[Rails] Implementation of tagging function using intermediate table (without Gem)
[Rails] Implementation of many-to-many category functions
[Ruby on Rails] Comment function implementation
[Rails 6] Like function (synchronous → asynchronous) implementation
[Rails] Comment function implementation procedure memo
[Ruby on Rails] Follow function implementation: Bidirectional
Rails [For beginners] Implementation of comment function
Rails Basic CRUD function implementation procedure scaffold
[Rails 6] Implementation of SNS (Twitter) sharing function
[Vue.js] Implementation of menu function Implementation version rails6
[Ruby on rails] Implementation of like function
[Vue.js] Implementation of menu function Vue.js introduction rails6
[Rails] Implementation of multi-layer category function using ancestry "I tried to make a window with Bootstrap 3"
DM function implementation
[Rails 6] Ranking function
Rails follow function
[Rails] Notification function
Implementation of Ruby on Rails login function (Session)
[Rails 6] Implementation of inquiry function using Action Mailer
[Rails] Implementation of image enlargement function using lightbox2
Implement the product category function using ancestry ① (Preparation)
[Rails] Implementation of retweet function in SNS application
Ruby on Rails Email automatic sending function implementation
[Rails] Implementation of batch processing using whenever (gem)
Rails hashtag search implementation
Ruby on Rails <2021> Implementation of simple login function (form_with)
Rails6 countdown timer implementation
[rails] gem'payjp' implementation procedure
Implementation of Ruby on Rails login function (devise edition)
Rails CRUD function implementation ② (edited and detailed this time)
Comment function (Ajax) implementation
Rails Action Text implementation
Follow function (Ajax) implementation
[Rails] Implement search function
Implementation of search function
[Rails] Implemented hashtag function
[For Rails beginners] Implemented multiple search function without Gem
[Ruby on Rails] Implementation of tagging function/tag filtering function
Image preview function implementation
Implementation of pagination function
Search function [copype implementation]
[Rails] Implementation of SNS authentication (Twitter, Facebook, Google) function